Просмотр исходного кода

fix(deepseek-v4): use canonical model-card dsv4 encoding over stale bundled copy

The sparkrun-vllm-ds4-gb10:fixed image bundles an OLD revision of
deepseek_v4_encoding.py (vLLM-vendored): single REASONING_EFFORT_MAX wording
with max using the high text, high injecting nothing, and reading the
deprecated 'reasoning' message key. Replacing it with the pristine model-card
reference (deepseek-ai/DeepSeek-V4-Flash-0731 encoding/encoding_dsv4.py) which
has the full low/high/max prompt dict, a correct gate for both high and max,
and uses 'reasoning_content'. The deepseek_v4.py normalization fix (committed
earlier) is retained so low is reachable and unknown values map to None.

Verified on dgx3 via the launcher build: low/None -> no prompt, high -> 527,
max -> 577 tokens; reasoning_content round-trips; imports resolve via the real
vllm module path.
Alexander Korolev 2 месяцев назад
Родитель
Сommit
bd292d7508

+ 7 - 4
docker-images/deepseek-v4-flash-dspark/Dockerfile

@@ -17,10 +17,13 @@ RUN pip install --no-cache-dir --no-deps --force-reinstall instanttensor==0.1.9
 # Fix reasoning_effort handling in the vendored deepseek_v4 tokenizer:
 # Fix reasoning_effort handling in the vendored deepseek_v4 tokenizer:
 # - high was accepted but silently injected nothing (only max triggered text)
 # - high was accepted but silently injected nothing (only max triggered text)
 # - low / unknown values were coerced to high (unreachable low, silent escalation)
 # - low / unknown values were coerced to high (unreachable low, silent escalation)
-# - only a single REASONING_EFFORT_MAX wording existed (max used the "high" text)
-# Replaces the single constant with a low/high/max dict using the full model-card
-# wording and injects for both high and max. See deepseek_v4_encoding.py gate.
-# (Mirrors the local-inference-lab encoding_dsv4.py reference implementation.)
+# - the bundled deepseek_v4_encoding.py was an OLD revision (single REASONING_EFFORT_MAX
+#   wording, max used the high text, read the deprecated `reasoning` key)
+# deepseek_v4_encoding.py is replaced with the canonical model-card reference
+# (deepseek-ai/DeepSeek-V4-Flash-0731 encoding/encoding_dsv4.py): full low/high/max
+# prompt dict, correct gate for both high and max, and reasoning_content key.
+# deepseek_v4.py carries the normalization fix so low is reachable and unknown
+# values map to None. See deepseek_v4.py / deepseek_v4_encoding.py in patches/.
 COPY patches/deepseek_v4.py \
 COPY patches/deepseek_v4.py \
      patches/deepseek_v4_encoding.py \
      patches/deepseek_v4_encoding.py \
      /opt/venv/lib/python3.12/site-packages/vllm/tokenizers/
      /opt/venv/lib/python3.12/site-packages/vllm/tokenizers/

+ 42 - 48
docker-images/deepseek-v4-flash-dspark/patches/deepseek_v4_encoding.py

@@ -1,8 +1,3 @@
-# SPDX-License-Identifier: Apache-2.0
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
-# ruff: noqa
-# fmt: off
-
 """
 """
 DeepSeek-V4 Encoding
 DeepSeek-V4 Encoding
 
 
@@ -13,8 +8,7 @@ with tool calling, thinking mode, and quick instruction task support.
 from typing import Any, Dict, List, Union, Optional, Tuple
 from typing import Any, Dict, List, Union, Optional, Tuple
 import copy
 import copy
 import json
 import json
-
-import regex as re
+import re
 
 
 # ============================================================
 # ============================================================
 # Special Tokens
 # Special Tokens
@@ -50,7 +44,7 @@ user_msg_template: str = "{content}"
 latest_reminder_msg_template: str = "{content}"
 latest_reminder_msg_template: str = "{content}"
 assistant_msg_template: str = "{reasoning}{content}{tool_calls}" + eos_token
 assistant_msg_template: str = "{reasoning}{content}{tool_calls}" + eos_token
 assistant_msg_wo_eos_template: str = "{reasoning}{content}{tool_calls}"
 assistant_msg_wo_eos_template: str = "{reasoning}{content}{tool_calls}"
-thinking_template: str = "{reasoning}"
+thinking_template: str = "{reasoning_content}"
 
 
 response_format_template: str = (
 response_format_template: str = (
     "## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n{schema}"
     "## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n{schema}"
@@ -67,7 +61,10 @@ tool_output_template: str = (
     "<tool_result>{content}</tool_result>"
     "<tool_result>{content}</tool_result>"
 )
 )
 
 
-REASONING_EFFORT_PROMPTS = {
+# Reasoning effort levels. In thinking mode, the prompt for the selected level is
+# prepended at the very beginning of the conversation. `low` is the default and
+# adds nothing.
+REASONING_EFFORT_PROMPTS: Dict[str, str] = {
     "low": "",
     "low": "",
     "high": (
     "high": (
         "Reasoning Effort: Absolute maximum with no shortcuts permitted.\n"
         "Reasoning Effort: Absolute maximum with no shortcuts permitted.\n"
@@ -80,6 +77,7 @@ REASONING_EFFORT_PROMPTS = {
         "Do not stop reasoning until you have independently verified the solution from multiple angles and are certain that no assumption remains unchecked and no error remains undiscovered.\n\n"
         "Do not stop reasoning until you have independently verified the solution from multiple angles and are certain that no assumption remains unchecked and no error remains undiscovered.\n\n"
     ),
     ),
 }
 }
+DEFAULT_REASONING_EFFORT = "low"
 
 
 TOOLS_TEMPLATE = """## Tools
 TOOLS_TEMPLATE = """## Tools
 
 
@@ -116,7 +114,7 @@ def to_json(value: Any) -> str:
     """Serialize a value to JSON string."""
     """Serialize a value to JSON string."""
     try:
     try:
         return json.dumps(value, ensure_ascii=False)
         return json.dumps(value, ensure_ascii=False)
-    except Exception:
+    except:
         return json.dumps(value, ensure_ascii=True)
         return json.dumps(value, ensure_ascii=True)
 
 
 
 
@@ -150,12 +148,12 @@ def tool_calls_to_openai_format(tool_calls):
     ]
     ]
 
 
 
 
-def encode_arguments_to_dsml(tool_call: Dict[str, Any]) -> str:
+def encode_arguments_to_dsml(tool_call: Dict[str, str]) -> str:
     """
     """
     Encode tool call arguments into DSML parameter format.
     Encode tool call arguments into DSML parameter format.
 
 
     Args:
     Args:
-        tool_call: Dict with "name" and "arguments" keys.
+        tool_call: Dict with "name" and "arguments" (JSON string) keys.
 
 
     Returns:
     Returns:
         DSML-formatted parameter string.
         DSML-formatted parameter string.
@@ -163,10 +161,10 @@ def encode_arguments_to_dsml(tool_call: Dict[str, Any]) -> str:
     p_dsml_template = '<{dsml_token}parameter name="{key}" string="{is_str}">{value}</{dsml_token}parameter>'
     p_dsml_template = '<{dsml_token}parameter name="{key}" string="{is_str}">{value}</{dsml_token}parameter>'
     P_dsml_strs = []
     P_dsml_strs = []
 
 
-    if isinstance(tool_call["arguments"], str):
+    try:
         arguments = json.loads(tool_call["arguments"])
         arguments = json.loads(tool_call["arguments"])
-    else:
-        arguments = tool_call["arguments"]
+    except Exception as err:
+        arguments = {"arguments": tool_call["arguments"]}
 
 
     for k, v in arguments.items():
     for k, v in arguments.items():
         p_dsml_str = p_dsml_template.format(
         p_dsml_str = p_dsml_template.format(
@@ -246,7 +244,8 @@ def render_message(index: int, messages: List[Dict[str, Any]], thinking_mode: st
         messages: Full list of messages in the conversation.
         messages: Full list of messages in the conversation.
         thinking_mode: Either "chat" or "thinking".
         thinking_mode: Either "chat" or "thinking".
         drop_thinking: Whether to drop reasoning content from earlier turns.
         drop_thinking: Whether to drop reasoning content from earlier turns.
-        reasoning_effort: Optional reasoning effort level ("max", "high", or None).
+        reasoning_effort: Reasoning effort level, one of "low", "high", "max".
+            None is treated as "low".
 
 
     Returns:
     Returns:
         Encoded string for this message.
         Encoded string for this message.
@@ -263,7 +262,7 @@ def render_message(index: int, messages: List[Dict[str, Any]], thinking_mode: st
     tools = msg.get("tools")
     tools = msg.get("tools")
     response_format = msg.get("response_format")
     response_format = msg.get("response_format")
     tool_calls = msg.get("tool_calls")
     tool_calls = msg.get("tool_calls")
-    reasoning = msg.get("reasoning")
+    reasoning_content = msg.get("reasoning_content")
     wo_eos = msg.get("wo_eos", False)
     wo_eos = msg.get("wo_eos", False)
 
 
     if tools:
     if tools:
@@ -272,9 +271,10 @@ def render_message(index: int, messages: List[Dict[str, Any]], thinking_mode: st
         tool_calls = tool_calls_from_openai_format(tool_calls)
         tool_calls = tool_calls_from_openai_format(tool_calls)
 
 
     # Reasoning effort prefix (only at index 0 in thinking mode; "low" adds nothing)
     # Reasoning effort prefix (only at index 0 in thinking mode; "low" adds nothing)
-    assert reasoning_effort is None or reasoning_effort in REASONING_EFFORT_PROMPTS, \
-        f"Invalid reasoning effort: {reasoning_effort}"
-    if index == 0 and thinking_mode == "thinking" and reasoning_effort in ("high", "max"):
+    reasoning_effort = reasoning_effort or DEFAULT_REASONING_EFFORT
+    assert reasoning_effort in REASONING_EFFORT_PROMPTS, \
+        f"Invalid reasoning effort: {reasoning_effort}, expected one of {list(REASONING_EFFORT_PROMPTS)}"
+    if index == 0 and thinking_mode == "thinking":
         prompt += REASONING_EFFORT_PROMPTS[reasoning_effort]
         prompt += REASONING_EFFORT_PROMPTS[reasoning_effort]
 
 
     if role == "system":
     if role == "system":
@@ -351,14 +351,14 @@ def render_message(index: int, messages: List[Dict[str, Any]], thinking_mode: st
             )
             )
 
 
         summary_content = content or ""
         summary_content = content or ""
-        reasoning = reasoning or ""
+        rc = reasoning_content or ""
 
 
         # Check if previous message has a task - if so, this is a task output (no thinking)
         # Check if previous message has a task - if so, this is a task output (no thinking)
         prev_has_task = index - 1 >= 0 and messages[index - 1].get("task") is not None
         prev_has_task = index - 1 >= 0 and messages[index - 1].get("task") is not None
 
 
         if thinking_mode == "thinking" and not prev_has_task:
         if thinking_mode == "thinking" and not prev_has_task:
             if not drop_thinking or index > last_user_idx:
             if not drop_thinking or index > last_user_idx:
-                thinking_part = thinking_template.format(reasoning=reasoning) + thinking_end_token
+                thinking_part = thinking_template.format(reasoning_content=rc) + thinking_end_token
             else:
             else:
                 thinking_part = ""
                 thinking_part = ""
 
 
@@ -539,10 +539,11 @@ def encode_messages(
         messages: List of message dicts to encode.
         messages: List of message dicts to encode.
         thinking_mode: Either "chat" or "thinking".
         thinking_mode: Either "chat" or "thinking".
         context: Optional preceding context messages (already encoded prefix).
         context: Optional preceding context messages (already encoded prefix).
-        drop_thinking: If True, drop reasoning from earlier assistant turns
+        drop_thinking: If True, drop reasoning_content from earlier assistant turns
                       (only keep reasoning for messages after the last user message).
                       (only keep reasoning for messages after the last user message).
         add_default_bos_token: Whether to prepend BOS token at conversation start.
         add_default_bos_token: Whether to prepend BOS token at conversation start.
-        reasoning_effort: Optional reasoning effort level ("max", "high", or None).
+        reasoning_effort: Reasoning effort level, one of "low", "high", "max".
+            Only takes effect in thinking mode. None is treated as "low".
 
 
     Returns:
     Returns:
         The encoded prompt string.
         The encoded prompt string.
@@ -589,12 +590,12 @@ def encode_messages(
 
 
 def _drop_thinking_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
 def _drop_thinking_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
     """
     """
-    Drop reasoning and non-essential messages before the last user message.
+    Drop reasoning_content and non-essential messages before the last user message.
 
 
     Behavior:
     Behavior:
     - Messages with role in ["user", "system", "tool", "latest_reminder"] are always kept.
     - Messages with role in ["user", "system", "tool", "latest_reminder"] are always kept.
     - Messages at or after the last user index are always kept.
     - Messages at or after the last user index are always kept.
-    - Assistant messages before the last user get reasoning removed.
+    - Assistant messages before the last user get reasoning_content removed.
     - Developer messages before the last user are dropped entirely.
     - Developer messages before the last user are dropped entirely.
     """
     """
     last_user_idx = find_last_user_index(messages)
     last_user_idx = find_last_user_index(messages)
@@ -607,7 +608,7 @@ def _drop_thinking_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, An
             result.append(msg)
             result.append(msg)
         elif role == "assistant":
         elif role == "assistant":
             msg = copy.copy(msg)
             msg = copy.copy(msg)
-            msg.pop("reasoning", None)
+            msg.pop("reasoning_content", None)
             result.append(msg)
             result.append(msg)
         # developer and other roles before last_user_idx are dropped
         # developer and other roles before last_user_idx are dropped
 
 
@@ -659,9 +660,9 @@ def parse_tool_calls(index: int, text: str) -> Tuple[int, Optional[str], List[Di
     tool_calls_end_token = f"</{dsml_token}{tool_calls_block_name}>"
     tool_calls_end_token = f"</{dsml_token}{tool_calls_block_name}>"
 
 
     while index < len(text):
     while index < len(text):
-        index, content_before, stop_token = _read_until_stop(index, text, [f"<{dsml_token}invoke", tool_calls_end_token])
-        if content_before != ">\n":
-            raise ValueError(f"Tool call format error: expected '>\\n' but got '{content_before}'")
+        index, _, stop_token = _read_until_stop(index, text, [f"<{dsml_token}invoke", tool_calls_end_token])
+        if _ != ">\n":
+            raise ValueError(f"Tool call format error: expected '>\\n' but got '{_}'")
 
 
         if stop_token == tool_calls_end_token:
         if stop_token == tool_calls_end_token:
             break
             break
@@ -705,7 +706,7 @@ def parse_message_from_completion_text(text: str, thinking_mode: str) -> Dict[st
 
 
     This function takes the raw text output from the model (a single assistant turn)
     This function takes the raw text output from the model (a single assistant turn)
     and extracts:
     and extracts:
-    - reasoning (thinking block)
+    - reasoning_content (thinking block)
     - content (summary/response)
     - content (summary/response)
     - tool_calls (if any)
     - tool_calls (if any)
 
 
@@ -717,11 +718,10 @@ def parse_message_from_completion_text(text: str, thinking_mode: str) -> Dict[st
         thinking_mode: Either "chat" or "thinking".
         thinking_mode: Either "chat" or "thinking".
 
 
     Returns:
     Returns:
-        Dict with keys: "role", "content", "reasoning", "tool_calls".
+        Dict with keys: "role", "content", "reasoning_content", "tool_calls".
         tool_calls are in OpenAI format.
         tool_calls are in OpenAI format.
     """
     """
-    summary_content, reasoning = "", ""
-    tool_calls: List[Dict[str, str]] = []
+    summary_content, reasoning_content, tool_calls = "", "", []
     index, stop_token = 0, None
     index, stop_token = 0, None
     tool_calls_start_token = f"\n\n<{dsml_token}{tool_calls_block_name}"
     tool_calls_start_token = f"\n\n<{dsml_token}{tool_calls_block_name}"
 
 
@@ -730,37 +730,31 @@ def parse_message_from_completion_text(text: str, thinking_mode: str) -> Dict[st
 
 
     if is_thinking:
     if is_thinking:
         index, content_delta, stop_token = _read_until_stop(index, text, [thinking_end_token, tool_calls_start_token])
         index, content_delta, stop_token = _read_until_stop(index, text, [thinking_end_token, tool_calls_start_token])
-        reasoning = content_delta
-        if stop_token != thinking_end_token:
-            raise ValueError("Invalid thinking format: missing </think>")
+        reasoning_content = content_delta
+        assert stop_token == thinking_end_token, "Invalid thinking format: missing </think>"
 
 
     index, content_delta, stop_token = _read_until_stop(index, text, [eos_token, tool_calls_start_token])
     index, content_delta, stop_token = _read_until_stop(index, text, [eos_token, tool_calls_start_token])
     summary_content = content_delta
     summary_content = content_delta
     if stop_token == tool_calls_start_token:
     if stop_token == tool_calls_start_token:
         is_tool_calling = True
         is_tool_calling = True
     else:
     else:
-        if stop_token != eos_token:
-            raise ValueError("Invalid format: missing EOS token")
+        assert stop_token == eos_token, "Invalid format: missing EOS token"
 
 
     if is_tool_calling:
     if is_tool_calling:
         index, stop_token, tool_calls = parse_tool_calls(index, text)
         index, stop_token, tool_calls = parse_tool_calls(index, text)
 
 
         index, tool_ends_text, stop_token = _read_until_stop(index, text, [eos_token])
         index, tool_ends_text, stop_token = _read_until_stop(index, text, [eos_token])
-        if tool_ends_text:
-            raise ValueError("Unexpected content after tool calls")
+        assert not tool_ends_text, "Unexpected content after tool calls"
 
 
-    if len(text) != index or stop_token not in [eos_token, None]:
-        raise ValueError("Unexpected content at end")
+    assert len(text) == index and stop_token in [eos_token, None], "Unexpected content at end"
 
 
     for sp_token in [bos_token, eos_token, thinking_start_token, thinking_end_token, dsml_token]:
     for sp_token in [bos_token, eos_token, thinking_start_token, thinking_end_token, dsml_token]:
-        if sp_token in summary_content or sp_token in reasoning:
-            raise ValueError(f"Unexpected special token '{sp_token}' in content")
+        assert sp_token not in summary_content and sp_token not in reasoning_content, \
+            f"Unexpected special token '{sp_token}' in content"
 
 
     return {
     return {
         "role": "assistant",
         "role": "assistant",
         "content": summary_content,
         "content": summary_content,
-        "reasoning": reasoning,
+        "reasoning_content": reasoning_content,
         "tool_calls": tool_calls_to_openai_format(tool_calls)
         "tool_calls": tool_calls_to_openai_format(tool_calls)
     }
     }
-
-# fmt: on