Repository navigation
DeepSeek-V4.1-1017: /v1/chat/completions returns 500 "no chat template"; the server never calls the family encoder (serve/template.zig) #1208
Description
What happens
The DeepSeek build of the native server returns HTTP 500 on every /v1/chat/completions request for a DeepSeek-V4.1-Flash-1017 checkpoint:
this checkpoint has no chat template: send a completion prompt instead
/v1/completions serves normally on the same boot. The 1017 release ships its prompt format as Python (encoding/encoding.py). It has no chat_template.jinja, and its tokenizer_config.json has no chat_template key. The server only renders chat through a Jinja template from the checkpoint. The family's own chat encoder, zig/src/families/deepseek_v41/serve/template.zig, is in the tree and its goldens pass, but nothing in zig/src/server calls it.
What we ran
- PR deepseek_v41: four-rank tensor parallel (whole-block shares, padded prefill heads) #1206's head
9e2f85bcon main958efa3b, the DeepSeek build ofzig/src/server/main.zig(tensorfold-dsv41 serve, followerstf-dsv41 follow), CUDA, NCCL, world 4 on four GB10s,--context 4096 --no-thinking. - We added two local commits so our pack loads: fp16 router bias and DSpark confidence head, widened to fp32. Neither touches the server or the text path.
- Between
958efa3b, deepseek_v41: four-rank tensor parallel (whole-block shares, padded prefill heads) #1206's head and current mainc9fab624, there is no diff inhf_text.zig,prompt.zig,model_text.zig,main.zigorzig/build/dsv41_draft.zig. - Checkpoint: a 2.9 bpw EXL3 pack of
deepseek-ai/DeepSeek-V4.1-Flash-1017. Itstokenizer.json,tokenizer_config.jsonandencoding/encoding.pyare byte-identical (sha256) to the release's at1bae0e10.
The failing request is tools/bench_concurrent.py's chat prompt, run as the #1206 description gives it:
{"model": "dsv41", "max_tokens": 256, "temperature": 0, "stream": true, "stream_options": {"include_usage": true},
"ignore_eos": true, "seed": 1234,
"messages": [{"role": "user", "content": "Explain how matrix multiplication uses a GPU in plain English, then give a small numerical example."}],
"chat_template_kwargs": {"enable_thinking": false}}Every chat cell failed: temperatures 0 and 1.0, at 1 and 4 streams. Rank 0's log has 44 "POST /v1/chat/completions HTTP/1.1" 500 lines and no chat 200 in any boot. The code cells (/v1/completions, 44 × 200) all passed, with alone and serial equal.
The code path (main c9fab624)
zig/src/server/main.zig:124always loads the text side throughloadText→hf_text.HfText.load(:162-163), whatever the family. Its only source of a template is--chat-template.zig/src/server/hf_text.zig:46-50: with no--chat-template,readTemplates(:97-130) looks forchat_template.jinja, thenchat_template.json, thentokenizer_config.json'schat_template. The 1017 release has none of these, sot.sourcestays empty, and:58-66compiles nothing.t.compiledstays null and load succeeds.zig/src/server/hf_text.zig:304-308:renderFnfinds no compiled template and returnserror.Templatewith the message above.zig/src/server/prompt.zig:41-43:renderIdsturnserror.Templateintocx.fail(.server, ...), which is 500 (errors.zig:49).docs/api.md:387says a template refusal is a 400. A missing template isn't quite that case, but a 4xx would arguably fit better.- The family's encoder:
zig/src/families/deepseek_v41/serve/template.zig:1is "DeepSeek-V4.1 chat encoding: OpenAI-shaped messages to prompt text".renderis at:365, andserve/root.zig:3exports it.- Its only callers are
serve/golden_test.zig:110-187, which checks it againstencoding.encodeand the V4.1 release Jinja. zig/build/dsv41_draft.zig:63-65addsdsv41_serveto the server module ("the family's text side"), but no file underzig/src/serverimportsdsv41_serve.
zig/src/server/model_text.zig:1says the tokenizer and template side is where "the tok and jinja branches plug in".Text.render(:113) is a vtable slot, andHfTextis the only implementation (hf_text.zig:227).
Other families don't supply a template in a way DeepSeek misses. Qwen, GLM, Nemotron and the rest all get theirs from the checkpoint's Jinja through HfText. No family has a render hook today. DeepSeek is the one family whose release ships no Jinja, and the one family with a native encoder in the tree.
Two more things suggest the wiring is missing rather than never planned:
- The family declares itself chat-only:
serve_engine.zig:23sets.raw_text = false, andbenchmark_model.zig:55-58reads it. - DeepSeek-V4.1: serving host, release tokenizer and the server's family hook #702 describes a server family render/reader hook for
model_type: deepseek_v41(srv.family/family.renderinprompt.zig). Its closing comment says 1.1.1 includes those hooks. We can't find them on main or atv1.1.1/v1.1.2:git log -S'srv.family'andgit log -S'family.render'are empty.
Raw completion is not a substitute. #1019 shows a bare prompt on V4.1 often ends at once with EOS. On #1206 we saw the same thing: concurrent completion prompts with no assistant turn returned one token each.
Host reproduction
This runs without a GPU. We added a test to hf_text.zig (scratch only, not proposed as-is). It loads a directory laid out like the 1017 release:
fixtures/serve/mini_tokenizer.jsonastokenizer.json- a
tokenizer_config.jsonwith onlybos_token/eos_token - an
encoding/encoding.py
Rendering one user message then returns error.Template with exactly this checkpoint has no chat template: send a completion prompt instead. With the test added, zig build test-server-cpu gives 614/614 passed.
A second scratch test passes V4.1's release chat_template.jinja (deepseek-ai/DeepSeek-V4.1-Flash) as the override. It compiles and renders <|begin▁of▁sentence|><|User|>hi<|Assistant|></think> with thinking off. So --chat-template <V4.1's jinja> looks like a workaround on current main. #847 has a Metal run that served 1017 with V4.1's Jinja. Another comment there reports the template compiler rejecting DeepSeek's base template (*args and **kwargs calls are not supported); current main compiled the V4.1 file in our host test. We did not run it on the GPU (see below).
What a fix would touch
Option A, wire the native encoder (our preference, since it is the reference-checked one):
- Let an engine module (or
native_engines) provide amodel_text.Textrender for itsmodel_type, used when the checkpoint has no template and--chat-templateis not given. - The DeepSeek implementation would map
RenderOptions(tools,enable_thinking,reasoning_effort,add_generation_prompt) ontotemplate.Optionsandtemplate.render. - Template-written spans need marking for provenance. Today
template.renderreturns text only, whileHfTextreturnsRenderedwith spans. That matters for Chat prompts: control-token spellings in request text (user, system, tool results) become control tokens #1117's control-token rule. - Files:
main.zig(choose the text),hf_text.zigor a small family text adapter,serve/template.zig(spans), and a server test like the one above. - Tool-call parsing (DSML,
serve/tools.zig) is a separate question. We have not looked at whether the server's existing tool parser reads DeepSeek's output.
Option B, document or ship a Jinja: the DeepSeek recipe tells users to pass V4.1's chat_template.jinja with --chat-template for 1017 checkpoints, or tensorfold pull places one. That needs no code, but chat would then rest on a template the family's goldens only partly cover.
The gate question: is wiring the family's native encoder into the server acceptable, and under what gate (the template goldens plus a server test)? Or do you prefer DeepSeek packs to carry a Jinja template and the server to stay Jinja-only? We'd be glad to do either once you pick.
Not tested
--chat-templatewith V4.1's Jinja on the GPU at any world size. Tool calls,reasoning_effortand thinking-on chat through it.- Whether V4.1's Jinja output equals
template.zig's (orencoding.encode's) for 1017 beyond the one message above. - World 1 or 2, and Metal. The text path does not depend on world size, but we only ran world 4.
- Current main
c9fab624on hardware. We ran deepseek_v41: four-rank tensor parallel (whole-block shares, padded prefill heads) #1206's head; the files above are unchanged between them. - The raw HTTP response body. We have the server's log line (500) and the message from the code path and the host test, not a saved response body.
Activity
flashosophy commentedon Oct 23, 2026
/atc claim
Render no-Jinja DeepSeek chat with request provenance
ashhart commentedon Oct 24, 2026
Thanks @flashosophy. On
main(e8faf00), a DeepSeek-V4.1 checkpoint that ships no Jinja chat template now renders chat through the native DeepSeek encoder (the 178 encoding goldens pass), and a missing template answers HTTP 400 instead of 500. The engine is still experimental (TF_EXPERIMENTAL=deepseek). One check remains on our side: comparing that encoder against the 1017 checkpoint's ownencoding.pyon a fixed prompt set. If you see a prompt rendered differently fromencoding.py, please post the messages and both renders.- Synthema, Ash's AI Assistant.