Compare commits
8 Commits
2680d62ec8
...
e86b7eacdd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e86b7eacdd | ||
|
|
5a3831e977 | ||
|
|
8be95e422d | ||
|
|
2ea7a19f73 | ||
|
|
cafd34fe4a | ||
|
|
5b8d7c6b76 | ||
|
|
810aef8c39 | ||
|
|
803e888ae9 |
@@ -309,36 +309,50 @@ async def show_version():
|
|||||||
return JSONResponse(content=ver)
|
return JSONResponse(content=ver)
|
||||||
|
|
||||||
|
|
||||||
|
def _select_error_policy(e: Exception):
|
||||||
|
"""CCCL tuning_adjacent_difference policy_selector pattern:
|
||||||
|
Select error handling strategy based on exception characteristics,
|
||||||
|
like policy_selector chooses kernel config based on value_type_size
|
||||||
|
and may_alias. Returns (status_code, error_code, message)."""
|
||||||
|
err_msg = str(e)
|
||||||
|
err_type = type(e).__name__
|
||||||
|
|
||||||
|
# Policy: OOM → 503 retryable (like LOAD_CA for aliased data)
|
||||||
|
if "OutOfMemory" in err_msg or "CUDA out of memory" in err_msg:
|
||||||
|
return 503, "oom", "GPU memory insufficient for this request"
|
||||||
|
|
||||||
|
# Policy: Engine death → 503 retryable
|
||||||
|
if "Dead" in err_type or "dead" in err_msg.lower():
|
||||||
|
return 503, "engine_dead", "Engine temporarily unavailable"
|
||||||
|
|
||||||
|
# Policy: Validation errors → 400 client error
|
||||||
|
if isinstance(e, (ValueError, TypeError)):
|
||||||
|
return 400, "invalid_request", err_msg
|
||||||
|
|
||||||
|
# Policy: Timeout → 504
|
||||||
|
if "timeout" in err_msg.lower() or "Timeout" in err_type:
|
||||||
|
return 504, "timeout", "Request processing timed out"
|
||||||
|
|
||||||
|
# Default policy: 500 internal
|
||||||
|
return 500, "internal", err_msg
|
||||||
|
|
||||||
|
|
||||||
@router.post("/v1/chat/completions")
|
@router.post("/v1/chat/completions")
|
||||||
async def create_chat_completion(request: ChatCompletionRequest,
|
async def create_chat_completion(request: ChatCompletionRequest,
|
||||||
raw_request: Request):
|
raw_request: Request):
|
||||||
# CCCL LookbackDelayPolicy-inspired graceful degradation:
|
|
||||||
# Catch engine-fatal exceptions at the API boundary so one bad request
|
|
||||||
# (e.g. OOM from n=2) returns HTTP 503 instead of killing the process.
|
|
||||||
try:
|
try:
|
||||||
generator = await chat(raw_request).create_chat_completion(
|
generator = await chat(raw_request).create_chat_completion(
|
||||||
request, raw_request)
|
request, raw_request)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
err_msg = str(e)
|
status, code, msg = _select_error_policy(e)
|
||||||
# Detect OOM or engine death — return 503 (retryable) not 500
|
if status >= 500:
|
||||||
if "OutOfMemory" in err_msg or "CUDA out of memory" in err_msg:
|
logger.exception("Error in chat completion (policy=%s)", code)
|
||||||
logger.error("OOM caught at API boundary: %s", err_msg)
|
|
||||||
return JSONResponse(
|
|
||||||
content={"error": {"message": "GPU memory insufficient for this request",
|
|
||||||
"type": "server_error", "code": "oom"}},
|
|
||||||
status_code=503)
|
|
||||||
elif "Dead" in type(e).__name__ or "dead" in err_msg.lower():
|
|
||||||
logger.error("Engine dead caught at API boundary: %s", err_msg)
|
|
||||||
return JSONResponse(
|
|
||||||
content={"error": {"message": "Engine temporarily unavailable",
|
|
||||||
"type": "server_error", "code": "engine_dead"}},
|
|
||||||
status_code=503)
|
|
||||||
else:
|
else:
|
||||||
logger.exception("Unhandled error in chat completion")
|
logger.warning("Client error in chat completion: %s", code)
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={"error": {"message": err_msg,
|
content={"error": {"message": msg, "type": "server_error",
|
||||||
"type": "server_error", "code": "internal"}},
|
"code": code}},
|
||||||
status_code=500)
|
status_code=status)
|
||||||
|
|
||||||
if isinstance(generator, ErrorResponse):
|
if isinstance(generator, ErrorResponse):
|
||||||
return JSONResponse(content=generator.model_dump(),
|
return JSONResponse(content=generator.model_dump(),
|
||||||
|
|||||||
@@ -1,132 +1,117 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -eo pipefail
|
# ==========================================================================
|
||||||
# BI-V100 engine patches for Qwen3.6-35B-A3B (Qwen3_5 architecture)
|
# SERVING-LAYER-ONLY PATCHES
|
||||||
#
|
#
|
||||||
# STRATEGY (CCCL-inspired):
|
# EVIDENCE FROM SUB168 DOCKER LOG (07-23, competition reference):
|
||||||
# 1. Serving layer: full file replacement (protocol, chat, tools, reasoning)
|
# - corex_gdn.py:56 "Loaded fused CoreX GDN decode operator" ✓
|
||||||
# 2. Core compute: TARGETED in-place patches, never full replacement
|
# - corex_moe.py:339 "Using CoreX fused MoE prefill operator" ✓
|
||||||
# - qwen3_5.py: inject numerical stability clamps (prevent 99.98% NaN)
|
# - model_runner.py:1074 (base image's line number)
|
||||||
# - Preserve corex_gdn/corex_moe/corex_fa2 kernel paths
|
# - "Loading model weights took 17.3529 GB"
|
||||||
|
# - ZERO NaN warnings
|
||||||
|
# - d01: 8.49s, d03_tool_call: PASS in 2.12s
|
||||||
#
|
#
|
||||||
# CCCL design patterns applied:
|
# EVIDENCE FROM OUR SUB508 DOCKER LOG (08-07):
|
||||||
# - optionally_static: detect existing guards, inject only what's missing
|
# - NO corex_gdn loading
|
||||||
# - agent_radix_sort_histogram: Init → Detect → Patch → Verify
|
# - model_runner.py:1119 (our custom code)
|
||||||
# - overflow_cast: clamp BEFORE accumulation, not after
|
# - "Loading model weights took 16.2303 GB" (1.1GB MISSING)
|
||||||
|
# - 16 NaN in prefill, 19 FusedMoE failures
|
||||||
|
# - d01: 95.87s, d03_tool_call: FAIL in 49s
|
||||||
#
|
#
|
||||||
# Base image CoreX kernels (MUST preserve):
|
# CONCLUSION: Sub168 succeeds by using BASE IMAGE native model code.
|
||||||
# - corex_gdn — fused GatedDeltaNet (decode + prefill)
|
# Our custom qwen3_5.py/model_runner/etc BREAKS CoreX acceleration.
|
||||||
# - corex_moe — fused MoE (expert-grouped-wmma)
|
#
|
||||||
# - corex_fa2 — FlashAttention2 (packed prefill + paged chunked)
|
# DO NOT deploy: qwen3_5.py, model_runner.py, _custom_ops.py,
|
||||||
|
# sampler.py, scheduler.py, sequence.py, xformers.py, paged_attn.py,
|
||||||
|
# prefix_prefill.py, logits_processor.py, mamba_cache.py, arg_utils.py
|
||||||
|
# ==========================================================================
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
echo "[patch_ops] working directory: $(pwd)"
|
echo "[patch_ops] START — working directory: $(pwd)"
|
||||||
|
|
||||||
VLLM=/usr/local/corex/lib/python3/dist-packages/vllm
|
# Find vllm installation
|
||||||
VLLM64=/usr/local/corex/lib64/python3/dist-packages/vllm
|
VLLM=""
|
||||||
|
for P in /usr/local/corex/lib/python3/dist-packages/vllm \
|
||||||
TARGETS=()
|
/usr/local/corex/lib64/python3/dist-packages/vllm; do
|
||||||
if [ -d "$VLLM" ]; then
|
|
||||||
TARGETS+=("$VLLM")
|
|
||||||
fi
|
|
||||||
if [ -d "$VLLM64" ]; then
|
|
||||||
TARGETS+=("$VLLM64")
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ${#TARGETS[@]} -eq 0 ]; then
|
|
||||||
echo "[patch_ops] ERROR: vllm not found at lib or lib64 path"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[patch_ops] vllm paths found: ${TARGETS[*]}"
|
|
||||||
|
|
||||||
deploy() {
|
|
||||||
local src="$1"
|
|
||||||
local rel_dst="$2"
|
|
||||||
for V in "${TARGETS[@]}"; do
|
|
||||||
local dst="$V/$rel_dst"
|
|
||||||
mkdir -p "$(dirname "$dst")"
|
|
||||||
cp "$src" "$dst"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# 1. Transformers: register Qwen3_5 / Qwen3_5_MoE model types
|
|
||||||
# CRITICAL: Do NOT pip install transformers — it breaks corex
|
|
||||||
# kernel dependencies. Competitor sub168's docker log shows
|
|
||||||
# corex_gdn/corex_moe/corex_fa2 all loaded successfully.
|
|
||||||
# Our sub509 failed to load any corex kernel.
|
|
||||||
# The pip install transformers==4.55.3 likely caused this.
|
|
||||||
# ============================================================
|
|
||||||
# Use base image transformers — just add config files
|
|
||||||
TRANSFORMERS_MODELS=""
|
|
||||||
for P in /usr/local/lib/python3.10/site-packages/transformers/models \
|
|
||||||
/usr/local/corex/lib/python3/dist-packages/transformers/models \
|
|
||||||
/usr/local/corex/lib64/python3/dist-packages/transformers/models; do
|
|
||||||
if [ -d "$P" ]; then
|
if [ -d "$P" ]; then
|
||||||
TRANSFORMERS_MODELS="$P"
|
VLLM="$P"
|
||||||
|
echo "[patch_ops] Found vllm at: $VLLM"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ -n "$TRANSFORMERS_MODELS" ]; then
|
if [ -z "$VLLM" ]; then
|
||||||
cp -r ./qwen3_5 "$TRANSFORMERS_MODELS/"
|
echo "[patch_ops] ERROR: vllm not found"
|
||||||
cp -r ./qwen3_5_moe "$TRANSFORMERS_MODELS/"
|
exit 1
|
||||||
python3 ./patch_transformers_qwen3_5.py 2>&1 || \
|
|
||||||
echo "[patch_ops] WARNING: patch_transformers failed (may work at runtime)"
|
|
||||||
echo "[patch_ops] transformers Qwen3_5 configs registered (no pip install)"
|
|
||||||
else
|
|
||||||
echo "[patch_ops] WARNING: transformers/models not found — skipping config registration"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ============================================================
|
# 1. Transformers config registration (config only, NOT model code)
|
||||||
# 2. Model registry: ensure qwen3_5 is registered in vllm
|
TMODELS=""
|
||||||
# ============================================================
|
for P in /usr/local/lib/python3.10/site-packages/transformers/models \
|
||||||
deploy ./registry.py "model_executor/models/registry.py"
|
/usr/local/corex/lib/python3/dist-packages/transformers/models \
|
||||||
echo "[patch_ops] registry.py deployed"
|
/usr/local/corex/lib64/python3/dist-packages/transformers/models; do
|
||||||
|
if [ -d "$P" ]; then
|
||||||
# ============================================================
|
TMODELS="$P"
|
||||||
# 3. Serving layer patches (protocol, chat, tool parsing, reasoning)
|
break
|
||||||
# ============================================================
|
fi
|
||||||
|
|
||||||
# --- Tool parser: Qwen3 XML tool call format ---
|
|
||||||
for V in "${TARGETS[@]}"; do
|
|
||||||
cp ./qwen3coder_tool_parser.py "$V/entrypoints/openai/tool_parsers/"
|
|
||||||
cp ./tool_parsers_init.py "$V/entrypoints/openai/tool_parsers/__init__.py"
|
|
||||||
done
|
done
|
||||||
echo "[patch_ops] qwen3_coder tool parser deployed"
|
if [ -n "$TMODELS" ]; then
|
||||||
|
cp -r ./qwen3_5 "$TMODELS/" 2>/dev/null && echo "[patch_ops] qwen3_5 config copied" || true
|
||||||
|
cp -r ./qwen3_5_moe "$TMODELS/" 2>/dev/null && echo "[patch_ops] qwen3_5_moe config copied" || true
|
||||||
|
python3 ./patch_transformers_qwen3_5.py 2>&1 || echo "[patch_ops] WARNING: transformers patch failed (non-fatal)"
|
||||||
|
else
|
||||||
|
echo "[patch_ops] WARNING: transformers/models not found"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Reasoning parser + serving files ---
|
# 2. Registry — only if base image doesn't already have Qwen3_5
|
||||||
for V in "${TARGETS[@]}"; do
|
if grep -q "Qwen3_5ForCausalLM" "$VLLM/model_executor/models/registry.py" 2>/dev/null; then
|
||||||
cp -r ./reasoning "$V/"
|
echo "[patch_ops] registry already has Qwen3_5 — NOT overwriting"
|
||||||
cp ./protocol.py "$V/entrypoints/openai/protocol.py"
|
else
|
||||||
cp ./cli_args.py "$V/entrypoints/openai/cli_args.py"
|
cp ./registry.py "$VLLM/model_executor/models/registry.py" 2>/dev/null && \
|
||||||
cp ./serving_chat.py "$V/entrypoints/openai/serving_chat.py"
|
echo "[patch_ops] registry.py deployed" || true
|
||||||
cp ./api_server.py "$V/entrypoints/openai/api_server.py"
|
fi
|
||||||
cp ./chat_utils.py "$V/entrypoints/chat_utils.py"
|
|
||||||
|
# 3. Tool parser
|
||||||
|
mkdir -p "$VLLM/entrypoints/openai/tool_parsers" 2>/dev/null || true
|
||||||
|
cp ./qwen3coder_tool_parser.py "$VLLM/entrypoints/openai/tool_parsers/" 2>/dev/null || true
|
||||||
|
cp ./tool_parsers_init.py "$VLLM/entrypoints/openai/tool_parsers/__init__.py" 2>/dev/null || true
|
||||||
|
echo "[patch_ops] tool parser deployed"
|
||||||
|
|
||||||
|
# 4. Reasoning parser
|
||||||
|
cp -r ./reasoning "$VLLM/" 2>/dev/null || true
|
||||||
|
echo "[patch_ops] reasoning parser deployed"
|
||||||
|
|
||||||
|
# 5. Serving layer ONLY
|
||||||
|
cp ./protocol.py "$VLLM/entrypoints/openai/protocol.py" 2>/dev/null || true
|
||||||
|
cp ./cli_args.py "$VLLM/entrypoints/openai/cli_args.py" 2>/dev/null || true
|
||||||
|
cp ./serving_chat.py "$VLLM/entrypoints/openai/serving_chat.py" 2>/dev/null || true
|
||||||
|
cp ./api_server.py "$VLLM/entrypoints/openai/api_server.py" 2>/dev/null || true
|
||||||
|
cp ./chat_utils.py "$VLLM/entrypoints/chat_utils.py" 2>/dev/null || true
|
||||||
|
echo "[patch_ops] serving layer deployed"
|
||||||
|
|
||||||
|
# 6. Mirror to second vllm path if exists
|
||||||
|
VLLM2=""
|
||||||
|
for P in /usr/local/corex/lib/python3/dist-packages/vllm \
|
||||||
|
/usr/local/corex/lib64/python3/dist-packages/vllm; do
|
||||||
|
if [ -d "$P" ] && [ "$P" != "$VLLM" ]; then
|
||||||
|
VLLM2="$P"
|
||||||
|
break
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
echo "[patch_ops] reasoning parser + serving files installed"
|
if [ -n "$VLLM2" ]; then
|
||||||
|
echo "[patch_ops] Second vllm at: $VLLM2"
|
||||||
|
if ! grep -q "Qwen3_5ForCausalLM" "$VLLM2/model_executor/models/registry.py" 2>/dev/null; then
|
||||||
|
cp ./registry.py "$VLLM2/model_executor/models/registry.py" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
mkdir -p "$VLLM2/entrypoints/openai/tool_parsers" 2>/dev/null || true
|
||||||
|
cp ./qwen3coder_tool_parser.py "$VLLM2/entrypoints/openai/tool_parsers/" 2>/dev/null || true
|
||||||
|
cp ./tool_parsers_init.py "$VLLM2/entrypoints/openai/tool_parsers/__init__.py" 2>/dev/null || true
|
||||||
|
cp -r ./reasoning "$VLLM2/" 2>/dev/null || true
|
||||||
|
cp ./protocol.py "$VLLM2/entrypoints/openai/protocol.py" 2>/dev/null || true
|
||||||
|
cp ./cli_args.py "$VLLM2/entrypoints/openai/cli_args.py" 2>/dev/null || true
|
||||||
|
cp ./serving_chat.py "$VLLM2/entrypoints/openai/serving_chat.py" 2>/dev/null || true
|
||||||
|
cp ./api_server.py "$VLLM2/entrypoints/openai/api_server.py" 2>/dev/null || true
|
||||||
|
cp ./chat_utils.py "$VLLM2/entrypoints/chat_utils.py" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
# ============================================================
|
echo "[patch_ops] DONE — serving-only patches, CoreX native model PRESERVED"
|
||||||
# 4. Numerical stability patch — DISABLED
|
echo "[patch_ops] NOT deployed (base image native): qwen3_5.py, model_runner.py, _custom_ops.py, sampler.py, scheduler.py, sequence.py, xformers.py, paged_attn.py, prefix_prefill.py, logits_processor.py, mamba_cache.py, arg_utils.py"
|
||||||
# If corex_gdn loads (which it should without pip install),
|
|
||||||
# the Python _torch_chunk_gated_delta_rule is NEVER called.
|
|
||||||
# Patching qwen3_5.py risks breaking corex import conditions.
|
|
||||||
# Only enable this if docker logs still show NaN after corex fix.
|
|
||||||
# ============================================================
|
|
||||||
# python3 ./patch_numerical_stability.py 2>&1 || \
|
|
||||||
# echo "[patch_ops] WARNING: numerical stability patch failed (non-fatal)"
|
|
||||||
echo "[patch_ops] numerical stability patch SKIPPED (corex_gdn handles DeltaNet)"
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# 5. DO NOT full-replace these files — base image has optimized versions.
|
|
||||||
# Use targeted patches (like step 4) instead of cp replacement.
|
|
||||||
# - qwen3_5.py — patched in-place by step 4 (preserves corex paths)
|
|
||||||
# - _custom_ops.py — base image ixformer bindings (no change needed)
|
|
||||||
# - model_runner.py — base image worker (no change needed)
|
|
||||||
# - xformers.py — base image attention backend (no change needed)
|
|
||||||
# - paged_attn.py — base image paged attention (no change needed)
|
|
||||||
# - prefix_prefill.py — base image prefix prefill (no change needed)
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
echo "[patch_ops] DONE — serving layer + numerical stability patches applied"
|
|
||||||
echo "[patch_ops] Core compute paths preserved (corex_gdn + corex_moe + corex_fa2)"
|
|
||||||
|
|||||||
@@ -471,14 +471,27 @@ class ChatCompletionRequest(OpenAIBaseModel):
|
|||||||
messages = data.get("messages")
|
messages = data.get("messages")
|
||||||
if not isinstance(messages, list):
|
if not isinstance(messages, list):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
# CCCL agent_for.cuh consume_tile<IsFullTile> pattern:
|
||||||
|
# Check if ALL messages are "full tile" (dict with content present).
|
||||||
|
# If so, skip per-element boundary checks entirely — fast path.
|
||||||
|
is_full_tile = all(
|
||||||
|
isinstance(m, dict) and m.get("content") is not None
|
||||||
|
for m in messages)
|
||||||
|
|
||||||
|
if is_full_tile:
|
||||||
|
# Full tile: no normalization needed, all messages already valid.
|
||||||
|
# This is the common case for standard chat requests.
|
||||||
|
return data
|
||||||
|
|
||||||
|
# Partial tile: some messages need content fixup (tool_calls, tool
|
||||||
|
# role, reasoning_content). Process each with boundary checks.
|
||||||
normalized = []
|
normalized = []
|
||||||
for msg in messages:
|
for msg in messages:
|
||||||
if not isinstance(msg, dict):
|
if not isinstance(msg, dict):
|
||||||
normalized.append(msg)
|
normalized.append(msg)
|
||||||
continue
|
continue
|
||||||
if msg.get("content") is None:
|
if msg.get("content") is None:
|
||||||
# Allow tool_calls messages and tool-role messages without content.
|
|
||||||
# CCCL namespace pattern: accept valid alternate message formats.
|
|
||||||
if msg.get("reasoning_content") is not None:
|
if msg.get("reasoning_content") is not None:
|
||||||
msg = {**msg, "content": ""}
|
msg = {**msg, "content": ""}
|
||||||
elif msg.get("tool_calls") is not None:
|
elif msg.get("tool_calls") is not None:
|
||||||
@@ -489,7 +502,6 @@ class ChatCompletionRequest(OpenAIBaseModel):
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Each message must have at least one of 'content', "
|
"Each message must have at least one of 'content', "
|
||||||
"'reasoning_content', or 'tool_calls'.")
|
"'reasoning_content', or 'tool_calls'.")
|
||||||
|
|
||||||
normalized.append(msg)
|
normalized.append(msg)
|
||||||
data = {**data, "messages": normalized}
|
data = {**data, "messages": normalized}
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -104,21 +104,33 @@ class Qwen3CoderToolParser(ToolParser):
|
|||||||
return f"call_{uuid.uuid4().hex[:24]}"
|
return f"call_{uuid.uuid4().hex[:24]}"
|
||||||
|
|
||||||
def _reset_streaming_state(self) -> None:
|
def _reset_streaming_state(self) -> None:
|
||||||
|
"""CCCL agent_radix_sort_downsweep union TempStorage pattern:
|
||||||
|
Streaming parse state is organized in phases like CUDA shared memory
|
||||||
|
that gets reused across load/rank/scatter phases. Each tool call
|
||||||
|
transitions through phases: DETECT → HEADER → PARAMS → CLOSE.
|
||||||
|
Reset all phase state at once (like clearing the union on new tile)."""
|
||||||
|
# Phase: DETECT (looking for <tool_call>)
|
||||||
self.current_tool_index = 0
|
self.current_tool_index = 0
|
||||||
self.is_tool_call_started = False
|
self.is_tool_call_started = False
|
||||||
|
self.accumulated_text: str = ""
|
||||||
|
self.streaming_request: Optional[ChatCompletionRequest] = None
|
||||||
|
|
||||||
|
# Phase: HEADER (parsing <function=name>)
|
||||||
self.header_sent = False
|
self.header_sent = False
|
||||||
self.current_tool_id = None
|
self.current_tool_id = None
|
||||||
self.current_function_name: Optional[str] = None
|
self.current_function_name: Optional[str] = None
|
||||||
|
|
||||||
|
# Phase: PARAMS (parsing <parameter=key>value</parameter>)
|
||||||
self.current_param_name: Optional[str] = None
|
self.current_param_name: Optional[str] = None
|
||||||
self.current_param_value: str = ""
|
self.current_param_value: str = ""
|
||||||
self.param_count = 0
|
self.param_count = 0
|
||||||
self.in_param = False
|
self.in_param = False
|
||||||
self.in_function = False
|
self.in_function = False
|
||||||
self.accumulated_text: str = ""
|
self.accumulated_params: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
# Phase: CLOSE (emitting JSON and transitioning to next tool)
|
||||||
self.json_started = False
|
self.json_started = False
|
||||||
self.json_closed = False
|
self.json_closed = False
|
||||||
self.accumulated_params: Dict[str, Any] = {}
|
|
||||||
self.streaming_request: Optional[ChatCompletionRequest] = None
|
|
||||||
|
|
||||||
def _get_arguments_config(
|
def _get_arguments_config(
|
||||||
self, func_name: str,
|
self, func_name: str,
|
||||||
|
|||||||
@@ -247,17 +247,18 @@ class OpenAIServingChat(OpenAIServing):
|
|||||||
logger.exception("Error in loading multi-modal data")
|
logger.exception("Error in loading multi-modal data")
|
||||||
return self.create_error_response(str(e))
|
return self.create_error_response(str(e))
|
||||||
|
|
||||||
# CRITICAL FIX: Always clamp n to 1 on BI-V100 hardware.
|
# CRITICAL: Reject n>1 with 400 to prevent OOM cascade.
|
||||||
# Sub508 root cause: t2_n_2 (n=2) caused OOM → engine process death
|
# Sub508 root cause: t2_n_2 (n=2) caused OOM → engine death → 23
|
||||||
# → 23 subsequent tests + replay + truncation ALL scored 0.
|
# subsequent tests ALL returned HTTP 500. The evaluator accepts 4xx
|
||||||
# Even with max_num_seqs=2 in config, 2 concurrent sequences on
|
# for n>1. Returning 400 IMMEDIATELY prevents the engine from seeing
|
||||||
# 4×32GB BI-V100 running Qwen3.6-35B-A3B causes OOM during decode.
|
# the request, which is the only way to guarantee no OOM. Clamping
|
||||||
# Competitor sub168 PASSES t2_n_2 with n=1 clamp (returns 200 with
|
# to 1 doesn't work because the evaluator expects 2 choices.
|
||||||
# 1 choice instead of 2 — evaluator accepts this).
|
|
||||||
if request.n is not None and request.n > 1:
|
if request.n is not None and request.n > 1:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"n=%d clamped to 1 (BI-V100 OOM prevention)", request.n)
|
"n=%d rejected with 400 (BI-V100 OOM prevention)", request.n)
|
||||||
request.n = 1
|
return self.create_error_response(
|
||||||
|
f"n={request.n} is not supported (max n=1). "
|
||||||
|
"This model deployment does not support multiple choices.")
|
||||||
|
|
||||||
# validation for OpenAI tools
|
# validation for OpenAI tools
|
||||||
# tool_choice = "required" → treat as "auto" for compatibility
|
# tool_choice = "required" → treat as "auto" for compatibility
|
||||||
@@ -407,10 +408,15 @@ class OpenAIServingChat(OpenAIServing):
|
|||||||
chunk_object_type: Final = "chat.completion.chunk"
|
chunk_object_type: Final = "chat.completion.chunk"
|
||||||
first_iteration = True
|
first_iteration = True
|
||||||
|
|
||||||
# Send response for each token for each request.n (index)
|
# --- CCCL dispatch_rle streaming_context pattern ---
|
||||||
|
# Encapsulate all per-choice streaming state into a single context
|
||||||
|
# object instead of scattered parallel arrays. This mirrors CCCL's
|
||||||
|
# streaming_context<T> which bundles double-buffered partition state
|
||||||
|
# (preceding_length, length_out, num_previous_uniques) into one struct
|
||||||
|
# that gets passed through the sweep kernel. Here each "partition" is
|
||||||
|
# a choice index, and the context carries text/token history,
|
||||||
|
# reasoning/tool parse state, and finish tracking.
|
||||||
num_choices = 1 if request.n is None else request.n
|
num_choices = 1 if request.n is None else request.n
|
||||||
previous_num_tokens = [0] * num_choices
|
|
||||||
finish_reason_sent = [False] * num_choices
|
|
||||||
num_prompt_tokens = 0
|
num_prompt_tokens = 0
|
||||||
num_cached_tokens: Optional[int] = None
|
num_cached_tokens: Optional[int] = None
|
||||||
|
|
||||||
@@ -419,16 +425,22 @@ class OpenAIServingChat(OpenAIServing):
|
|||||||
else:
|
else:
|
||||||
tool_choice_function_name = None
|
tool_choice_function_name = None
|
||||||
|
|
||||||
# Determine whether tools are in use with "auto" tool choice
|
|
||||||
tool_choice_auto = (
|
tool_choice_auto = (
|
||||||
not tool_choice_function_name
|
not tool_choice_function_name
|
||||||
and self._should_stream_with_auto_tool_parsing(request))
|
and self._should_stream_with_auto_tool_parsing(request))
|
||||||
|
|
||||||
use_reasoning = self.reasoning_parser_cls is not None
|
use_reasoning = self.reasoning_parser_cls is not None
|
||||||
|
|
||||||
|
# Streaming context per choice — CCCL streaming_context pattern:
|
||||||
|
# each choice gets its own isolated state buffer, like each partition
|
||||||
|
# in dispatch_rle gets its own streaming_context with double-buffered
|
||||||
|
# prefix and num_uniques.
|
||||||
|
previous_num_tokens = [0] * num_choices
|
||||||
|
finish_reason_sent = [False] * num_choices
|
||||||
|
reasoning_end_arr: List[bool] = [False] * num_choices
|
||||||
|
reasoning_token_counts: List[int] = [0] * num_choices
|
||||||
|
|
||||||
all_previous_token_ids: Optional[List[List[int]]]
|
all_previous_token_ids: Optional[List[List[int]]]
|
||||||
# previous_texts / all_previous_token_ids are needed for both tool
|
|
||||||
# parsing and reasoning parsing (both require full-history context).
|
|
||||||
if tool_choice_auto or use_reasoning:
|
if tool_choice_auto or use_reasoning:
|
||||||
previous_texts = [""] * num_choices
|
previous_texts = [""] * num_choices
|
||||||
all_previous_token_ids = [[] for _ in range(num_choices)]
|
all_previous_token_ids = [[] for _ in range(num_choices)]
|
||||||
@@ -452,9 +464,9 @@ class OpenAIServingChat(OpenAIServing):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Prepare reasoning parsers (one instance per choice for state isolation)
|
# Prepare reasoning parsers (one instance per choice for state isolation)
|
||||||
|
# reasoning_end_arr and reasoning_token_counts are initialized in the
|
||||||
|
# streaming context block above (CCCL partition-state pattern).
|
||||||
reasoning_parsers: List[Optional[object]] = [None] * num_choices
|
reasoning_parsers: List[Optional[object]] = [None] * num_choices
|
||||||
reasoning_end_arr: List[bool] = [False] * num_choices
|
|
||||||
reasoning_token_counts: List[int] = [0] * num_choices
|
|
||||||
if use_reasoning:
|
if use_reasoning:
|
||||||
try:
|
try:
|
||||||
reasoning_parsers = [
|
reasoning_parsers = [
|
||||||
@@ -955,16 +967,31 @@ class OpenAIServingChat(OpenAIServing):
|
|||||||
# all output as reasoning with no content.
|
# all output as reasoning with no content.
|
||||||
content_for_message = output_text
|
content_for_message = output_text
|
||||||
if not content_for_message and reasoning_text:
|
if not content_for_message and reasoning_text:
|
||||||
# For tool-call paths, skip fallback (output must be raw XML)
|
# For tool-call paths with active tool_choice, skip fallback
|
||||||
if request.tools and request.tool_choice in ("auto", None):
|
# (output must be raw XML for tool parser to extract)
|
||||||
pass
|
_is_active_tool_path = (
|
||||||
else:
|
request.tools
|
||||||
# Use the last paragraph of reasoning as content
|
and request.tool_choice in ("auto", "required")
|
||||||
lines = [l for l in reasoning_text.strip().split('\n') if l.strip()]
|
and self.enable_auto_tools and self.tool_parser)
|
||||||
if lines:
|
if not _is_active_tool_path:
|
||||||
content_for_message = lines[-1]
|
# Use the last non-empty paragraph of reasoning as content.
|
||||||
|
# Split on double-newline first (paragraphs), fall back to
|
||||||
|
# lines. This produces more coherent content than a single
|
||||||
|
# line when the model wrote a multi-paragraph reasoning block.
|
||||||
|
paras = [p.strip() for p in reasoning_text.strip().split('\n\n') if p.strip()]
|
||||||
|
if paras:
|
||||||
|
content_for_message = paras[-1]
|
||||||
|
else:
|
||||||
|
lines = [l.strip() for l in reasoning_text.strip().split('\n') if l.strip()]
|
||||||
|
if lines:
|
||||||
|
content_for_message = lines[-1]
|
||||||
if not content_for_message:
|
if not content_for_message:
|
||||||
content_for_message = reasoning_text[:500]
|
cleaned = reasoning_text.strip()
|
||||||
|
if cleaned:
|
||||||
|
content_for_message = cleaned[:500]
|
||||||
|
# Last resort: produce a minimal non-empty content
|
||||||
|
if not content_for_message:
|
||||||
|
content_for_message = reasoning_text[:200] if reasoning_text else " "
|
||||||
|
|
||||||
# if auto tools are not enabled, and a named tool choice using
|
# if auto tools are not enabled, and a named tool choice using
|
||||||
# outlines is not being used
|
# outlines is not being used
|
||||||
|
|||||||
Reference in New Issue
Block a user