Merge #1557 chat transport into #1787: dual-transport SGLang server - #1
Open
linnanwang wants to merge 5 commits into
Open
Merge #1557 chat transport into #1787: dual-transport SGLang server#1linnanwang wants to merge 5 commits into
linnanwang wants to merge 5 commits into
Conversation
…nt message
`ResponsesConverterState.flush_assistant()` reset `content_buffer` and
`tool_calls_buffer` but left `token_information` in place. An assistant message
that carried no token ids of its own was therefore stamped with the PREVIOUS
assistant turn's `prompt_token_ids` / `generation_token_ids` /
`generation_log_probs`, attributing one turn's generated tokens to another
turn's text in the training data.
Reachable whenever a harness injects or rewrites an assistant message without
token ids, e.g.
assistant(ids) -> tool_output(flush) -> assistant(no ids) -> tool_output(flush)
where the second flush reuses the first turn's ids.
`token_information` describes exactly one assistant turn, so consume it into a
local and clear it on every path -- including the empty-buffer early return,
since an item can carry ids while producing no content and no tool calls (a
zero-token generation), and that message is dropped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: linnan wang <linnanw@nvidia.com>
(cherry picked from commit 9cf9abf)
(cherry picked from commit 4b0b7e6e70018676f528d25f0f75a13ce301bf65)
… hook Move the body of the `return_token_id_information` block in `chat_completions` into `_attach_token_id_information(choice_dict, body_dict, client)`. Pure code move: the 26 executable lines are byte-identical and the call site is unchanged, so behavior is identical for every existing caller. Splitting it out lets a backend whose server returns token ids natively -- e.g. SGLang via `meta_info` on the chat endpoint -- override this one step instead of reimplementing the whole endpoint. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: linnan wang <linnanw@nvidia.com>
Adds `transport: chat` alongside the existing `/generate` path, and makes it the default. It drives SGLang's OpenAI-compatible /v1/chat/completions, reading the training ids and logprobs from the native `return_meta_info` / `return_prompt_token_ids` extensions. This is the stable token-ID/logprob contract the README was waiting for: it landed with the sglang-miles TITO sync series (sgl-project/sglang#23751) and is in the 0.5.13 release tree, so no patched build or fork is required. As the README anticipated, the transport changes without touching the session-splice or context-overflow rules. On this path the server inherits the whole VLLMModel flow and overrides only `_attach_token_id_information`, so templating, tool-call parsing, sampling params, auth and context-overflow are all handled server-side by SGLang. There is no local tokenizer to drift from the server's and no client-side tool parser to maintain. `transport: generate` is unchanged in substance and remains for builds that predate chat-side TITO (e.g. forks serving diffusion LLMs), where the local render, the splice and the client-side tool parsers are all still required. Fixes carried into the generate path: - `finish_reason="abort"` now raises on both transports rather than being reported as `stop`. An aborted generation is a truncated fragment and must not enter a training batch looking like a completed turn. - The ChatML end-of-turn markers become config (`sglang_eos_markers`, `sglang_turn_suffix`). They were hardcoded to `<|im_end|>`; a model whose template closes turns differently would otherwise get a malformed boundary spliced into every follow-up prompt. - Sampling params /generate cannot honor are reported instead of dropped silently, and the passthrough set is widened. - `context_length` is required only for `generate`, enforced by a config validator. The chat transport does not need it because SGLang applies its own limit and the inherited overflow handling recognizes the error. `make_model` in the tests defaults to `transport="generate"` so the existing generate-path tests keep exercising that path, joined by chat-transport coverage for the preprocess contract, native id extraction, abort, and the pre-0.5.13 error path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: linnan wang <linnanw@nvidia.com>
The transport dispatch replaced the original docstring with inline comments. Restore it, extended to cover both transports. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: linnan wang <linnanw@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hi @Kh4L — this rebuilds NVIDIA-NeMo#1557 on top of your branch rather than merging with it. Deliberately small: 4 commits, 9 files, +512/−80, fast-forwardable onto
sglang-splice-fix(no merge commit).Formally,
linnan_sglang_adaptoris not an ancestor here. Your/generatemachinery is the base, and NVIDIA-NeMo#1557's/generateimplementation is discarded wholesale — after comparing the two properly, yours won nearly every overlap.The framing is your own README:
That contract shipped. Chat-side TITO (sgl-project/sglang#23751, merged Jun 4) is in the stock 0.5.13 tree — v0.5.13 was cut Jun 13;
compare v0.5.13...e03dfa8givesahead_by=0. This is the transport change you anticipated, and exactly as you predicted, your splice and overflow rules carry over untouched.ProRL's
patch_sglang_0513_token_metadata.shis not required — its own header says it exposes the metadata "whenlogprobs=true", i.e. it only makeslogprobsimply flags that already exist. Setting them explicitly needs no patch and no fork.What this PR changes
sglang_model/app.pytransportknob; chat-path preprocess + token extraction; dispatch; abort; markers from configsglang_model/tests/test_app.pymake_modeldefaults totransport="generate"; +8 chat testssglang_model/README.mdvllm_model/app.pynemo_gym/responses_converter.pytests/unit_tests/test_responses_converter.pysglang_model/_logic.pyunsupported_sampling_paramsonly — your extractor untouchedconfigs/...for_training.yamlconfigs/...for_training_generate.yamlDispatch is the only change to your entry point (
app.py:140):Untouched from your branch:
_full_sglang_tokenize,_sglang_followup_fragment_ids,_sglang_msg_sig,_sglang_messages_match,_sglang_rendering_sig,_build_sglang_prompt_ids,_update_sglang_session_seq,_parse_sglang_generation,_sglang_length_finish,_get_sglang_tokenizer,_get_sglang_chat_template, both tool-call regexes, and the rest of_sglang_chat_completion.openai_utils.py,tool_parsers.py,tests/test_tool_parsers.py,tests/test_logic.pyandpyproject.tomlare the same blob as on your branch.vllm_model/app.py— pure extract-methodThe
return_token_id_informationblock moves out ofchat_completionsinto_attach_token_id_information(choice_dict, body_dict, client); the call site is the same guard plus oneawait. After dedent, old and new are 56 lines, textually identical (51 non-blank), and all 13 top-level AST statements match. The only live references in the block areself,choice_dict,body_dict,client— all parameters.(My commit message says "26 executable lines byte-identical". That was under my own normalization — strip blanks/comments/docstrings — and isn't reproducible without stating the rule. The claim above is the checkable one.)
New capabilities
transport: chat(new default, sglang ≥ 0.5.13) — inherits the wholeVLLMModelflow, overriding only token extraction. Templating, tool-call parsing, sampling params, auth and context-overflow all move server-side. No local tokenizer that can drift; no client-side parser to maintain; no/tokenizeround-trip.transport: generate— your path, unchanged in substance. Still required for builds predating chat-side TITO (e.g. forks serving diffusion LLMs), where the local render, the splice and the client-side parsers all earn their keep.Overlapping functionality reused
I compared implementations, not names. Yours won 8 of 11.
if otl:_extract_output_ids,_validate_selected_idsmeta_info.output_ids; typed errors on malformed containers; a legitimate zero-token generation returns([], [])instead of raisingcap_to_context— head-truncate then bail_sglang_length_finishprompt_token_idson the terminalfinish_reason="length", so the turn stays attributable. Also recovers from a server-side 400 by matching the error text — NVIDIA-NeMo#1557 had no equivalent/generateHTTP callng_request+ hand-rolled auth header +hash(sid) % len(urls)create_generate+_resolve_client_raise_for_status, producing exactly theresponse_content-carrying error your overflow branch matches on; strips/v1internally. Myhash()sharding was also unstable across processes underPYTHONHASHSEEDnormalize_token_ids_full_sglang_tokenizechat_template=,tools=,normalize_tool_call_arguments(messages), and handles tensor returns via.tolist()build_sampling_params,default_max_new_tokens=1024cap_to_context(deleted) to clamp; deriving from the window cannot overflow on its owntool_parsers.py+_parse_sglang_generationrequirements.txt(transformers>=4.44,<6)pyproject.toml(==5.8.1)cli/setup_command.pyraises if both exist; and the exact pin matters on the local-render path, where a tokenizer delta is a silent contiguity failure that a range permits_build_sglang_prompt_ids+ prefix validationunsupported_sampling_params_logic.py(temperature, top_p, top_k, stop)_attach_token_id_informationhookVLLMModeltoken_id:NNNand issues acreate_tokenizeround-trip; SGLang does neither. Without the seam,SGLangModelwould override ~120 lines to change ~30transportknob + config layoutuses_reasoning_parser: trueandcontext_length: ???are yours; NVIDIA-NeMo#1557 shippedfalseand a silent4096Verified at 0 hits in this tree:
normalize_token_ids,build_sampling_params,_PASSTHROUGH_SAMPLING_PARAMS,cap_to_context,would_truncate,default_max_new_tokens,add_generation_prompt,_sglang_urls,ng_request,logprob_start_len,_chat_completions_via_generate,requirements.txt, NVIDIA-NeMo#1557's_logic.extract_*, and its entiretest_logic.py.Your splice changed my mind. On NVIDIA-NeMo#1557 I argued a multi-turn splice wasn't safe yet, because nothing validated the carried prefix against the caller's current messages — Gym agents legitimately rewrite history (
browsecomp_agentruns context resets by default). Your_build_sglang_prompt_idsdoes validate it:_sglang_messages_matchcompares role/content/tool_calls signatures,_sglang_rendering_sigcatches tools/template drift, and theNone-returning fragment render falls back to a full render. Building on session state rather than carried message ids sidesteps the failure mode entirely. I owe NVIDIA-NeMo#1557 a correction.Fixes applied to the generate path
finish_reason="abort"raises on both transports. Your ladder special-cased only"length", so an abort — a server-cancelled partial — mapped to"stop"and entered the batch looking like a completed turn.sglang_eos_markers,sglang_turn_suffix), replacing the_SGLANG_EOS_MARKERSClassVar and the literal"<|im_end|>\n"in_sglang_eos_nl. On a Nemotron-style template (content + '\n' + '<extra_id_1>') the splice would otherwise write a malformed boundary into every follow-up prompt. Defaults unchanged, so ChatML models behave identically.context_lengthrequired only forgenerate, via a config validator — your mandatory-???discipline is preserved for the transport that needs it.frequency_penalty,repetition_penalty,min_p) plus a warning for what/generatecan't honor.What is NOT verified
Being explicit, since this is the training-data path:
test_app.pyhas never been executed. Nofastapiin my environment (ModuleNotFoundErroron collection), so its 16 tests — and the 72 invllm_model/tests/test_app.pythat would empirically confirm the extract-method — are compile-checked only. Only the 25 pure tests (test_logic.py,test_tool_parsers.py) actually ran.make_modelnow defaults totransport="generate"so they still exercise your path, but I could not run them to confirm.ruffisn't installed locally; formatting unverified.nemo_gym/responses_converter.pyis shared code, so this stack widens CI scope beyondsglang_model.Happy to reshape any of this — including holding the chat transport as a follow-up if you'd rather land NVIDIA-NeMo#1787 first.
🤖 Generated with Claude Code