Skip to content

feat(chat-template): render DeepSeek-V4/V4.1 through deepseek-recipe as SGLang does - #4596

Open
guapisolo wants to merge 7 commits into
mainfrom
jiajun/deepseek-recipe-render
Open

guapisolo wants to merge 7 commits into
mainfrom
jiajun/deepseek-recipe-render

Conversation

@guapisolo

@guapisolo guapisolo commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Render DeepSeek-V4 / V4.1 through SGLang's DeepSeekRecipe, as its chat endpoint does.

Motivation

SGLang v0.5.23 renders V4 / V4.1 through deepseek-recipe and removed encoding_dsv4 / encoding_dsv41. #4548 kept miles on vendored copies, so every DeepSeek format change needed a manual copy.

Usage

No new flag; apply_chat_template and the V4 / V4.1 TITO tokenizers keep their interface.

ids = apply_chat_template(messages, tokenizer=tok, tools=tools, tokenize=True, thinking=True)
# == the endpoint's ids for a request ending on a user, tool or system turn, but for V4's effort line

Design Notes

  • Mental model: DeepSeekRecipeFamily builds the chat endpoint's request body, and SGLang's DeepSeekRecipe.convert / encode_conversation turn it into prompt ids. Other families are unchanged.
  • Merge order: after [Feat] deepseek-recipe: [sglang-miles] Expose convert and encode_conversation on DeepSeekRecipe sgl-project/sglang#47808 lands on sglang-miles and dev rebuilds; then drop the ci-sglang-pr line.
  • Lazy import: the renderer loads on first use, so images whose SGLang lacks it still import miles.
  • Effort: V4 keeps its checkpoints' "preview" profile through _DSV4_PREVIEW_EFFORT.
  • Pending tool calls: recipe rejects them, so a TITO prefix awaiting results renders placeholder results, cut after that turn.
  • TITO delta: diffs recipe's ids and re-encodes a plain-text junction up to the next special token.
  • Prompt changes: tool JSON drops strict; developer renders as system; a V4 mid-conversation system turn moves first.
  • Newly rejected: tool calls without id, empty tool_calls, and unanswered tool calls, as at the endpoint, except a trailing turn's calls.
  • Verifier: it answers a pending tool call with a mock result first.
  • Stripped whitespace: recipe's parser strips the whitespace a turn samples before its content, so the V4 TITO tokenizer stores it as content_leading_whitespace and renders it back. The client's message is unchanged.
  • Client replay: preserve_server_message_state restores that key on a strict match, through the SERVER_MESSAGE_KEYS path that Inkling's content_blocks now shares.

Verification

  • test_render_matches_the_chat_endpoint: 9 scenarios × thinking × V4 / V4.1 equal the endpoint's ids at SGLang 83e38f97c7.
  • chat_template_utils CPU tests at e49a15b51: 1653 passed locally. test_template.py is left to stage-a-cpu.
  • The new whitespace round-trip cases fail without the fix.
  • /rerun-test at e7f0b08b4: all eight test_session_server_multi_role/ files passed.
  • /rerun-test at 551e5e303: both V4 4-layer CI files passed.
  • Not run yet: test_deepseekv4.py at e49a15b51, and the ROCm image build.

Review Focus

  • DeepSeekRecipeFamily._encode: request body, placeholder cut, header strip.
  • DeepSeekV4TITOTokenizer.postprocess_completion: which whitespace it stores.
  • No-tools thinking sessions flag assistant_text on every sample, since recipe renders no reasoning there; training tokens are unchanged.

ci-sglang-pr: jiajun/deepseek-recipe-convert-api

…as SGLang does

SGLang v0.5.23 removed its encoding_dsv4 / encoding_dsv41 encoders and
renders DeepSeek-V4 / V4.1 through deepseek-recipe. #4548 kept miles on
vendored copies of the old encoders, so miles and the server each own a
renderer and every DeepSeek format change needs a manual copy.

V4 / V4.1 now render through SGLang's DeepSeekRecipe (convert plus
encode_conversation), so a prompt equals the chat endpoint's render of the
same request. The one deliberate difference is the V4 effort profile: the
V4-Flash / V4-Pro checkpoints ship the "preview" profile, so high renders
no effort line and max renders the line recipe calls high. Text renders
decode the ids. recipe rejects a history whose last assistant turn awaits
tool results, so such a history renders with placeholder results that are
then cut off.

TITO's V4 delta diffs recipe's ids; it falls back to the text diff only at
a plain-text junction, such as a user turn after a tool result, where
tokens merge across the boundary. Encoder-only kwargs (drop_thinking,
context, add_default_bos_token) are now rejected.

V4 / V4.1 prompts change where recipe differs from the vendored encoders:
tool JSON keeps the client's key order without an injected strict, a
developer turn renders as system, a V4 mid-conversation system turn moves
to the front, special-token spellings in content stay text, and without
tools earlier reasoning is not rendered.

deepseek-recipe is now installed for the hosted CPU suite and in the ROCm
image, whose v0.5.22 base predates it. Needs the DeepSeekRecipe API from
sgl-project/sglang branch jiajun/deepseek-recipe-convert-api.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@guapisolo

Copy link
Copy Markdown
Collaborator Author

@claude review always

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings marked 🟡 are optional suggestions and need no follow-up push.

- Import SGLang's recipe renderer on first use, so images whose SGLang
  lacks it (Rubin, TPU) still import miles for every model.
- Cut a pending-call history after its trailing assistant turn: recipe
  drops an empty system turn, which left a placeholder tool result in the
  prefix and broke the next TITO append.
- Resolve "none" and numeric-string efforts as SGLang's chat endpoint does.
- At a TITO plain-text junction, re-encode only the text up to the next
  special token with the BPE alone, so special-token spellings in content
  stay text as in recipe's render.
- Reject an unknown thinking_mode; surface recipe's error for a tool call
  without an id instead of a KeyError.
- Install deepseek-recipe in the ROCm CI job, and fix prose the deleted
  encoders left stale.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-reviewed after the latest commit and found no new bugs. This change replaces the V4/V4.1 render path with an external renderer, so a human look would still be worthwhile.

What was reviewed:

  • The follow-up commit moves the renderer import into _renderer(), so deepseek.py no longer needs the module when it loads. The DeepSeekRecipe annotation stays under TYPE_CHECKING, together with from __future__ import annotations.
  • _placeholder_tool_results now reads call.get("id"), so a pending tool call with no id no longer raises.
  • DeepSeekRecipeFamily._encode, the TITO ids diff with its plain-text fallback, and the deepseek-recipe pins in CI and Docker.
Extended reasoning...

The PR deletes about 1,600 lines of vendored V4/V4.1 encoders. In their place, V4/V4.1 rendering and the V4 TITO incremental tokenization now go through sglang's deepseek-recipe renderer, and deepseek-recipe==0.2.0 is pinned in the ROCm CI workflow, the ROCm Dockerfile and the CPU CI requirements. It touches no security-sensitive surface such as auth, crypto or untrusted input handling. The latest commit addresses both earlier inline points: the renderer is imported lazily, and pending tool calls without an id no longer fail. The change is large and alters rendered prompts by design (tool JSON key order, developer turns, dropped earlier reasoning). The PR says the e2e and ROCm image runs have not been done yet, which argues for a human reviewer.

The check sat in DeepSeekRecipeFamily._encode, which sessions never reach
with a bad value: the TITO tokenizer resolves thinking at construction and
rewrites the kwargs to a boolean, so a typo such as "think" silently ran
in chat mode. Validate in DeepSeekFamily._build_encode_config, which both
paths share; a null thinking_mode keeps meaning chat.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

@guapisolo guapisolo added the run-ci-rollout Run rollout CPU integration tests and performance benchmarks label Nov 2, 2026
@guapisolo

Copy link
Copy Markdown
Collaborator Author

/rerun-test tests/e2e/sglang/test_session_server_multi_role/test_deepseekv4.py

@guapisolo

Copy link
Copy Markdown
Collaborator Author

/rerun-test tests/e2e/megatron/model_scripts/test_deepseek_v4_1_4layer_ci.py

@guapisolo

Copy link
Copy Markdown
Collaborator Author

/rerun-test tests/e2e/megatron/model_scripts/test_deepseek_v4_flash_4layer_ci.py

@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

✅ tests/e2e/megatron/model_scripts/test_deepseek_v4_flash_4layer_ci.py passed on stage-c-4-gpu-h200 in 14m35s — workflow run

@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

✅ tests/e2e/megatron/model_scripts/test_deepseek_v4_1_4layer_ci.py passed on stage-c-8-gpu-h200 in 26m42s — workflow run

@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

❌ tests/e2e/sglang/test_session_server_multi_role/test_deepseekv4.py failed on stage-c-4-gpu-h200 in 16m06s — workflow run

The execution job failed; inspect the workflow run for the failing step.

…swer

DeepSeek V4 / V4.1 now render through deepseek-recipe, which rejects an
assistant tool call that no tool message answers. After a ROLLBACK step
re-sampled a turn that called a tool, the verifier appended FORCE_FINAL to
it, so 12 of 50 V4 session e2e trajectories failed with a 500. USER_FOLLOWUP,
SYSTEM_REMINDER and ASSISTANT_INPUT could do the same.

Before any step other than TOOL_RESULT and ROLLBACK, roll such a call back
the way the tool-call ROLLBACK fallback does, then run the step.
@guapisolo

Copy link
Copy Markdown
Collaborator Author

/rerun-test tests/e2e/sglang/test_session_server_multi_role/test_deepseekv4.py

@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

❌ tests/e2e/sglang/test_session_server_multi_role/test_deepseekv4.py failed on stage-c-4-gpu-h200 in 24m41s — workflow run

The execution job failed; inspect the workflow run for the failing step.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new blocking issues. 1 optional suggestion (a nit or a note on pre-existing code) was found and not posted. Nothing in this review needs a push before merging.

…t back

A harness that receives a tool call runs it before asking for anything
else, and FORCE_FINAL asks the model to summarize all results. Answer the
pending call with the next mock tool result, as TOOL_RESULT does, before a
step that appends another turn, rather than dropping the call.
…e fails

The per-sample sidecar already records the first expected/actual pair of
each assistant_text mismatch, but CI logs only carried the ratio, so a
failed gate could not be read without the preserved sidecar file. Quote
the first three examples in the assertion message.
@guapisolo

Copy link
Copy Markdown
Collaborator Author

/rerun-test tests/e2e/sglang/test_session_server_multi_role/test_deepseekv4.py

@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

✅ tests/e2e/sglang/test_session_server_multi_role/test_deepseekv4.py passed on stage-c-4-gpu-h200 in 1h12m42s — workflow run

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

@guapisolo

Copy link
Copy Markdown
Collaborator Author

/rerun-test tests/e2e/sglang/test_session_server_multi_role/test_glm47.py

@guapisolo

Copy link
Copy Markdown
Collaborator Author

/rerun-test tests/e2e/sglang/test_session_server_multi_role/test_inkling.py

@guapisolo

Copy link
Copy Markdown
Collaborator Author

/rerun-test tests/e2e/sglang/test_session_server_multi_role/test_nemotron3.py

@guapisolo

Copy link
Copy Markdown
Collaborator Author

/rerun-test tests/e2e/sglang/test_session_server_multi_role/test_qwen3.py

@guapisolo

Copy link
Copy Markdown
Collaborator Author

/rerun-test tests/e2e/sglang/test_session_server_multi_role/test_qwen35.py

@guapisolo

Copy link
Copy Markdown
Collaborator Author

/rerun-test tests/e2e/sglang/test_session_server_multi_role/test_qwen36.py

@guapisolo

Copy link
Copy Markdown
Collaborator Author

/rerun-test tests/e2e/sglang/test_session_server_multi_role/test_qwen38small.py

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

✅ tests/e2e/sglang/test_session_server_multi_role/test_glm47.py passed on stage-c-2-gpu-h200 in 1h33m08s — workflow run

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

✅ tests/e2e/sglang/test_session_server_multi_role/test_inkling.py passed on stage-c-2-gpu-h200 in 1h56m21s — workflow run

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

✅ tests/e2e/sglang/test_session_server_multi_role/test_qwen3.py passed on stage-c-2-gpu-h200 in 1h56m03s — workflow run

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

✅ tests/e2e/sglang/test_session_server_multi_role/test_nemotron3.py passed on stage-c-2-gpu-h200 in 2h08m55s — workflow run

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

✅ tests/e2e/sglang/test_session_server_multi_role/test_qwen35.py passed on stage-c-2-gpu-h200 in 2h16m41s — workflow run

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

✅ tests/e2e/sglang/test_session_server_multi_role/test_qwen36.py passed on stage-c-2-gpu-h200 in 2h35m38s — workflow run

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

✅ tests/e2e/sglang/test_session_server_multi_role/test_qwen38small.py passed on stage-c-2-gpu-h200 in 2h33m09s — workflow run

deepseek-recipe's output parser strips the whitespace a V4 turn emits
before its content, such as a blank line after </think>. A re-render of
the stored turn then drops it, and the session comparator reports an
assistant_text mismatch on every sample that sampled one. Store that
whitespace on the server-side message, restore it when a client replays
the turn, and render it back before the content; the client's message
is unchanged. Inkling's content_blocks restore now uses the same
SERVER_MESSAGE_KEYS mechanism.
guapisolo added a commit that referenced this pull request Nov 3, 2026
A real V4 parse failure returns empty content, while the step's reply
keeps the call text in content. The blank line after </think> is not the
reason: the recipe parser strips it in both modes, which #4596 now
handles.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-ci-rollout Run rollout CPU integration tests and performance benchmarks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant