[DSV4] Add official DSV4 reasoning effort support (#33140)
Co-authored-by: Xinyuan Tong <xinyuantong.cs@gmail.com> Co-authored-by: David Orman <ormandj@corenode.com>
This commit is contained in:
co-authored by
Xinyuan Tong
David Orman
parent
198a3bc29b
commit
059269594c
@@ -7,8 +7,103 @@ it here instead of re-deriving it from model architectures themselves.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sglang.srt.entrypoints.openai import encoding_dsv4
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DSV4_REASONING_EFFORT_PROFILE_OVERRIDE = "dsv4_reasoning_effort_profile"
|
||||
_DSV4_REASONING_EFFORT_ENCODER = "encoding/encoding_dsv4.py"
|
||||
_MAX_DSV4_ENCODER_BYTES = 1 << 20
|
||||
|
||||
|
||||
def _detect_dsv4_reasoning_effort_profile(
|
||||
model_path: str, revision: Optional[str] = None
|
||||
) -> Optional[str]:
|
||||
encoder_path = Path(model_path) / _DSV4_REASONING_EFFORT_ENCODER
|
||||
try:
|
||||
if not encoder_path.is_file():
|
||||
from huggingface_hub import hf_hub_download
|
||||
|
||||
encoder_path = Path(
|
||||
hf_hub_download(
|
||||
model_path,
|
||||
_DSV4_REASONING_EFFORT_ENCODER,
|
||||
revision=revision,
|
||||
)
|
||||
)
|
||||
if encoder_path.stat().st_size > _MAX_DSV4_ENCODER_BYTES:
|
||||
return None
|
||||
tree = ast.parse(encoder_path.read_text(encoding="utf-8"))
|
||||
except Exception as error:
|
||||
logger.debug(
|
||||
"Could not inspect DeepSeek-V4 checkpoint encoder at %s: %s",
|
||||
encoder_path,
|
||||
error,
|
||||
)
|
||||
return None
|
||||
|
||||
assignments = {}
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.Assign):
|
||||
targets = node.targets
|
||||
value = node.value
|
||||
elif isinstance(node, ast.AnnAssign):
|
||||
targets = [node.target]
|
||||
value = node.value
|
||||
else:
|
||||
continue
|
||||
|
||||
for target in targets:
|
||||
if not isinstance(target, ast.Name):
|
||||
continue
|
||||
try:
|
||||
assignments[target.id] = ast.literal_eval(value)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
prompts = assignments.get("REASONING_EFFORT_PROMPTS")
|
||||
if (
|
||||
assignments.get("DEFAULT_REASONING_EFFORT") == "low"
|
||||
and isinstance(prompts, dict)
|
||||
and {"low", "high", "max"} <= prompts.keys()
|
||||
):
|
||||
return "official"
|
||||
if "REASONING_EFFORT_MAX" in assignments:
|
||||
return "preview"
|
||||
return None
|
||||
|
||||
|
||||
def _validate_dsv4_reasoning_effort_profile(profile: str) -> str:
|
||||
if profile not in encoding_dsv4.REASONING_EFFORT_PROFILES:
|
||||
raise ValueError(
|
||||
f"Invalid {DSV4_REASONING_EFFORT_PROFILE_OVERRIDE}: {profile!r}; "
|
||||
f"expected one of {list(encoding_dsv4.REASONING_EFFORT_PROFILES)}"
|
||||
)
|
||||
return profile
|
||||
|
||||
|
||||
def resolve_dsv4_reasoning_effort_profile(
|
||||
*,
|
||||
model_path: str,
|
||||
revision: Optional[str] = None,
|
||||
override: Optional[str] = None,
|
||||
) -> str:
|
||||
if override is not None:
|
||||
return _validate_dsv4_reasoning_effort_profile(override)
|
||||
|
||||
return (
|
||||
_detect_dsv4_reasoning_effort_profile(
|
||||
model_path=model_path,
|
||||
revision=revision,
|
||||
)
|
||||
or "preview"
|
||||
)
|
||||
|
||||
|
||||
def resolve_chat_encoding_spec(
|
||||
*,
|
||||
|
||||
@@ -60,12 +60,30 @@ tool_calls_block_name: str = "tool_calls"
|
||||
|
||||
tool_output_template: str = "<tool_result>{content}</tool_result>"
|
||||
|
||||
REASONING_EFFORT_MAX = (
|
||||
REASONING_EFFORT_PREVIEW_MAX = (
|
||||
"Reasoning Effort: Absolute maximum with no shortcuts permitted.\n"
|
||||
"You MUST be very thorough in your thinking and comprehensively decompose the problem to resolve the root cause, rigorously stress-testing your logic against all potential paths, edge cases, and adversarial scenarios.\n"
|
||||
"Explicitly write out your entire deliberation process, documenting every intermediate step, considered alternative, and rejected hypothesis to ensure absolutely no assumption is left unchecked.\n\n"
|
||||
)
|
||||
|
||||
REASONING_EFFORT_OFFICIAL_MAX = (
|
||||
"Reasoning Effort: Beyond maximum — exhaustive, relentless, and uncompromising.\n"
|
||||
"You MUST reason with the utmost depth and rigor, leaving absolutely nothing to chance: exhaustively decompose the problem into its most fundamental components, trace every causal chain to its root, and resolve the underlying cause rather than any surface symptom.\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"
|
||||
)
|
||||
|
||||
REASONING_EFFORT_PROFILES = {
|
||||
"preview": {
|
||||
"high": "",
|
||||
"max": REASONING_EFFORT_PREVIEW_MAX,
|
||||
},
|
||||
"official": {
|
||||
"low": "",
|
||||
"high": REASONING_EFFORT_PREVIEW_MAX,
|
||||
"max": REASONING_EFFORT_OFFICIAL_MAX,
|
||||
},
|
||||
}
|
||||
|
||||
TOOLS_TEMPLATE = """## Tools
|
||||
|
||||
You have access to a set of tools to help answer the user's question. You can invoke tools by writing a "<{dsml_token}tool_calls>" block like the following:
|
||||
@@ -250,6 +268,7 @@ def render_message(
|
||||
thinking_mode: str,
|
||||
drop_thinking: bool = True,
|
||||
reasoning_effort: Optional[str] = None,
|
||||
reasoning_effort_profile: str = "preview",
|
||||
) -> str:
|
||||
"""
|
||||
Render a single message at the given index into its encoded string form.
|
||||
@@ -262,7 +281,9 @@ def render_message(
|
||||
messages: Full list of messages in the conversation.
|
||||
thinking_mode: Either "chat" or "thinking".
|
||||
drop_thinking: Whether to drop reasoning content from earlier turns.
|
||||
reasoning_effort: Optional reasoning effort level ("max", "high", or None).
|
||||
reasoning_effort: Optional reasoning effort level. The preview profile accepts
|
||||
"high" and "max"; the official profile accepts "low", "high", and "max".
|
||||
reasoning_effort_profile: DeepSeek-V4 effort mapping ("preview" or "official").
|
||||
|
||||
Returns:
|
||||
Encoded string for this message.
|
||||
@@ -290,14 +311,21 @@ def render_message(
|
||||
if tool_calls:
|
||||
tool_calls = tool_calls_from_openai_format(tool_calls)
|
||||
|
||||
# Reasoning effort prefix (only at index 0 in thinking mode with max effort)
|
||||
assert reasoning_effort in [
|
||||
"max",
|
||||
None,
|
||||
"high",
|
||||
], f"Invalid reasoning effort: {reasoning_effort}"
|
||||
if index == 0 and thinking_mode == "thinking" and reasoning_effort == "max":
|
||||
prompt += REASONING_EFFORT_MAX
|
||||
if reasoning_effort_profile not in REASONING_EFFORT_PROFILES:
|
||||
raise ValueError(
|
||||
f"Invalid reasoning effort profile: {reasoning_effort_profile!r}; "
|
||||
f"expected one of {list(REASONING_EFFORT_PROFILES)}"
|
||||
)
|
||||
effort_prompts = REASONING_EFFORT_PROFILES[reasoning_effort_profile]
|
||||
if reasoning_effort is None:
|
||||
reasoning_effort = "low" if reasoning_effort_profile == "official" else "high"
|
||||
if reasoning_effort not in effort_prompts:
|
||||
raise ValueError(
|
||||
f"Invalid reasoning effort {reasoning_effort!r} for profile "
|
||||
f"{reasoning_effort_profile!r}; expected one of {list(effort_prompts)}"
|
||||
)
|
||||
if index == 0 and thinking_mode == "thinking":
|
||||
prompt += effort_prompts[reasoning_effort]
|
||||
|
||||
if role == "system":
|
||||
prompt += system_msg_template.format(content=content or "")
|
||||
@@ -583,6 +611,7 @@ def encode_messages(
|
||||
drop_thinking: bool = True,
|
||||
add_default_bos_token: bool = True,
|
||||
reasoning_effort: Optional[str] = None,
|
||||
reasoning_effort_profile: str = "preview",
|
||||
) -> str:
|
||||
"""
|
||||
Encode a list of messages into the DeepSeek-V4 prompt format.
|
||||
@@ -600,7 +629,9 @@ def encode_messages(
|
||||
drop_thinking: If True, drop reasoning_content from earlier assistant turns
|
||||
(only keep reasoning for messages after the last user message).
|
||||
add_default_bos_token: Whether to prepend BOS token at conversation start.
|
||||
reasoning_effort: Optional reasoning effort level ("max", "high", or None).
|
||||
reasoning_effort: Optional reasoning effort level. The preview profile accepts
|
||||
"high" and "max"; the official profile accepts "low", "high", and "max".
|
||||
reasoning_effort_profile: DeepSeek-V4 effort mapping ("preview" or "official").
|
||||
|
||||
Returns:
|
||||
The encoded prompt string.
|
||||
@@ -640,6 +671,7 @@ def encode_messages(
|
||||
thinking_mode=thinking_mode,
|
||||
drop_thinking=effective_drop_thinking,
|
||||
reasoning_effort=reasoning_effort,
|
||||
reasoning_effort_profile=reasoning_effort_profile,
|
||||
)
|
||||
|
||||
return prompt
|
||||
|
||||
@@ -24,7 +24,7 @@ from fastapi import Request
|
||||
from fastapi.responses import ORJSONResponse, StreamingResponse
|
||||
from jsonschema import Draft202012Validator, SchemaError
|
||||
|
||||
from sglang.srt.entrypoints.openai import encoding_dsv4, encoding_dsv32
|
||||
from sglang.srt.entrypoints.openai import chat_encoding, encoding_dsv4, encoding_dsv32
|
||||
from sglang.srt.entrypoints.openai.protocol import (
|
||||
ChatCompletionMessageGenericParam,
|
||||
ChatCompletionRequest,
|
||||
@@ -253,6 +253,17 @@ class OpenAIServingChat(OpenAIServingBase):
|
||||
# Which Python-based chat encoder (if any) bypasses apply_chat_template.
|
||||
# Values: "dsv32", "dsv4", or custom values set by subclass. None for default.
|
||||
self.chat_encoding_spec = self._resolve_chat_encoding_spec()
|
||||
self._dsv4_reasoning_effort_profile = (
|
||||
chat_encoding.resolve_dsv4_reasoning_effort_profile(
|
||||
model_path=self.tokenizer_manager.model_path,
|
||||
revision=self.tokenizer_manager.server_args.revision,
|
||||
override=self.tokenizer_manager.model_config.hf_config.to_dict().get(
|
||||
chat_encoding.DSV4_REASONING_EFFORT_PROFILE_OVERRIDE
|
||||
),
|
||||
)
|
||||
if self.chat_encoding_spec == "dsv4"
|
||||
else None
|
||||
)
|
||||
|
||||
# Resolve the env-configured Inkling effort default once: the env var is
|
||||
# frozen for the server's lifetime, and a misconfigured value should
|
||||
@@ -339,11 +350,7 @@ class OpenAIServingChat(OpenAIServingBase):
|
||||
|
||||
Override in subclass to add custom encoding specs.
|
||||
"""
|
||||
from sglang.srt.entrypoints.openai.chat_encoding import (
|
||||
resolve_chat_encoding_spec,
|
||||
)
|
||||
|
||||
return resolve_chat_encoding_spec(
|
||||
return chat_encoding.resolve_chat_encoding_spec(
|
||||
hf_config=self.tokenizer_manager.model_config.hf_config,
|
||||
tokenizer=self.tokenizer_manager.tokenizer,
|
||||
tool_call_parser=self.tool_call_parser,
|
||||
@@ -1216,16 +1223,18 @@ class OpenAIServingChat(OpenAIServingBase):
|
||||
|
||||
# Default encoding (dsv4/dsv32)
|
||||
if self.chat_encoding_spec == "dsv4":
|
||||
# V4 encoder only accepts "max" / "high" / None.
|
||||
# OpenAI protocol defaults to "medium" which V4 rejects; drop it.
|
||||
# Fallback: if request didn't set it, try env SGLANG_DSV4_REASONING_EFFORT.
|
||||
effort_source = request.reasoning_effort
|
||||
if effort_source is None:
|
||||
env_val = envs.SGLANG_DSV4_REASONING_EFFORT.get()
|
||||
if env_val:
|
||||
effort_source = env_val
|
||||
reasoning_effort_profile = self._dsv4_reasoning_effort_profile
|
||||
assert reasoning_effort_profile is not None
|
||||
accepted_efforts = encoding_dsv4.REASONING_EFFORT_PROFILES[
|
||||
reasoning_effort_profile
|
||||
]
|
||||
v4_reasoning_effort = (
|
||||
effort_source if effort_source in ("max", "high") else None
|
||||
effort_source if effort_source in accepted_efforts else None
|
||||
)
|
||||
if request.task is not None:
|
||||
encoding_dsv4.attach_task_to_last_user_message(
|
||||
@@ -1235,6 +1244,7 @@ class OpenAIServingChat(OpenAIServingBase):
|
||||
messages,
|
||||
thinking_mode=thinking_mode,
|
||||
reasoning_effort=v4_reasoning_effort,
|
||||
reasoning_effort_profile=reasoning_effort_profile,
|
||||
)
|
||||
prompt_ids = self.tokenizer_manager.tokenizer.encode(real_input)
|
||||
else:
|
||||
|
||||
@@ -1156,8 +1156,7 @@ class Envs:
|
||||
# Copy rank-local MoE slices into independent CPU storage before H2D when
|
||||
# they reference a larger mmap-backed checkpoint storage.
|
||||
SGLANG_MOE_COPY_WEIGHT_VIEWS_BEFORE_H2D = EnvBool(False)
|
||||
# Default reasoning_effort for dsv4 chat encoder when request doesn't set it.
|
||||
# Accepts "", "max", "high" (empty string means unset); other values filtered to None.
|
||||
# Flash-0731 also accepts "low"; the active profile is checkpoint-resolved.
|
||||
SGLANG_DSV4_REASONING_EFFORT = EnvStr("")
|
||||
# Quantize the SWA fp8 KV cache from bf16-rounded values (matches
|
||||
# trainer-side QAT and the DSA-CP path) instead of fp32 registers.
|
||||
|
||||
Reference in New Issue
Block a user