feat(sglang): add token-exact SGLang model server for RL training - #1787
feat(sglang): add token-exact SGLang model server for RL training#1787Kh4L wants to merge 9 commits into
Conversation
|
Pushed SGLang hard-rejects input The splice fix + this guard have now been exercised end-to-end by a 16-node R=32 SGLang async-GRPO run: 0 chat-500s, 15+ training steps with graceful trajectory truncation and no stall. Still draft pending a full convergence-vs-baseline comparison. |
|
Convergence validation complete. The splice fix + over-context guard in this PR (as carried on the fork branch) have now been validated by a full-scale end-to-end run: Qwen3-30B-A3B-Thinking SWE-bench async-GRPO on SGLang, 16 nodes (8 train + 8 gen, 32 generation replicas), 63 training steps across four clean checkpoint+resume segments.
Remaining known caveat: this port of the fix onto refactored Gym main is still not GPU-tested in this exact form (the validated code is the fork's pre-refactor equivalent) — keeping the PR draft until that's run. |
Signed-off-by: Serge Panev <spanev@nvidia.com>
SGLang /generate hard-rejects input tokens >= context_length (HTTP 500), unlike vLLM which tolerates it. In a multi-turn SWE rollout the spliced prompt can already fill the window; the 500 then corrupts the trajectory and trips the downstream contiguity assert, stalling the whole async step. Mirror the vLLM context-length handling: pre-check the prompt length against sglang_max_total_sequence_length, and defensively catch the ClientResponseError, ending the turn cleanly with finish_reason="length" (empty completion) in both cases. Validated by a 16-node R=32 SGLang GRPO run (0 chat-500s; 15+ steps, graceful truncation, no stall). Signed-off-by: Serge Panev <spanev@nvidia.com>
The SGLang engine path re-parses tool calls from raw generated text client-side. The existing parser only handled the hermes JSON format (Qwen3-thinking). Add a dependency-free parser for the qwen3_coder XML-ish format (<tool_call><function=NAME><parameter=KEY>...) used by e.g. Qwen3-Coder-style chat templates, with schema-aware argument type coercion from the request's tools, selectable via a new sglang_tool_format config field (default "hermes" = no behavior change). Unit tests mirror the template's canonical example. Signed-off-by: Serge Panev <spanev@nvidia.com>
Chat templates iterate assistant tool-call arguments as a mapping
(tool_call.arguments|items), but OpenAI-format history carries
function.arguments as a JSON string - exactly what our /generate-path
parsers emit. When a request misses the token-splice cache and falls
back to a full template render of a tool-call-bearing conversation,
jinja raises TypeError('Can only get item pairs from a mapping.') and
the request 500s.
Normalize on the full-render path only: decode string arguments that
parse to a JSON object, leave everything else (and the splice path,
which never re-renders old assistant turns) untouched. Inputs are not
mutated - the splice session cache holds the original messages.
Signed-off-by: Serge Panev <spanev@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>
Mechanical ruff check --fix (I001 import order) + ruff format on the SGLang proxy files; no logic changes. Fixes the failing Lint check on the PR. Signed-off-by: Serge Panev <spanev@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>
|
Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually. Contributors can view more details about this message here. |
The adapter renders the chat template and tokenizes locally before calling the SGLang server, so its transformers version has to agree with the server's. It was pinned to 5.6.0 to match the NeMo-RL sglang extra; that extra moves to 5.8.1 alongside sglang 0.5.13.post1, so this pin follows. A mismatch here does not fail loudly at import. It surfaces as a rollout contiguity failure when the locally rendered prefix stops matching what the server tokenized, which is far harder to attribute, hence the comment on the pin. Signed-off-by: Serge Panev <spanev@nvidia.com>
Problem
With the SGLang generation backend, multi-turn agentic (SWE-bench-style) rollouts hit a fatal prefix-stability assert in
nemo_gym.pyon ~every tool-using turn (48/48 turns failed in our runs). The proxy must guarantee that each turn's freshly-built prompt has the prior accumulated tokens as an exact prefix (seen == prompt[:len(seen)]). Two root causes broke this:</think>.Separately, exact sampled integer token ids are needed for token-level RL. On the SGLang version NeMo-RL main currently pins (0.5.12.post1),
/v1/chat/completionsdoes not expose them (logprobs.tokenis a decoded string; there is noreturn_tokens_as_token_ids), and/tokenizeonly accepts a raw prompt string, so the proxy recovers ids another way.Fix
Adds a standalone
responses_api_models/sglang_model/server.SGLangModelsubclasses the vLLMVLLMModeland overrides only the generation path;SGLangModelConfigextendsVLLMModelConfigwith the SGLang-specific fields (a requiredcontext_length, an optional inline/loaded chat template, and asglang_tool_format). The vLLM model server is left byte-identical and carries zero SGLang references, so the two backends are fully decoupled and selected by which server a recipe points at.prompt_{K-1} + gen_{K-1}(verbatim) + delta_K, splicing the prior assistant turn's exact sampledgeneration_token_idsinstead of re-tokenizing them (_build_sglang_prompt_ids,_update_sglang_session_seq,_sglang_followup_fragment_ids, keyed by session id). Prefix-stable by construction; cache-miss falls back to a full chat-template tokenize. A session whose tools or chat-template inputs change mid-trajectory fails loudly rather than silently splicing a stale prefix./generate. Generate through SGLang's native/generate(return_logprob=True) and read ids+logprobs from the returnedmeta_info. NewNeMoGymAsyncOpenAI.create_generate. The extractor tolerates the field shapes SGLang has shipped (mapping, tuple, and split value/index arrays), cross-checks them againstoutput_ids, and treats missing or mismatched token data as a hard error rather than silently emitting an empty completion that would corrupt the training loss mask.</think>preservation + tool parsing. Decode the sampled ids withskip_special_tokens=Falseso the reasoning close-tag survives, then re-parse into reasoning +tool_callsso the returned object is shaped exactly like the vLLM/v1/chat/completionsresponse; every downstream Responses-API conversion is identical. Supports both hermes-JSON andqwen3_coder-style XML tool blocks.context_lengthreturns a terminalfinish_reason="length"turn instead of letting/generatehard-reject the request and corrupt the trajectory.Local tokenization uses
transformers; the leaf package pins the version that matches NeMo-RL's SGLang worker environment.Relationship to native token-in / token-out (SGLang 0.5.13+)
SGLang 0.5.13 added native token-in / token-out on the chat endpoint (
/v1/chat/completionsreturnsmeta_info.output_token_logprobs). On 0.5.13+ the/generateid-recovery mechanism becomes unnecessary and the server can read ids straight from the chat response. Two reasons this PR is still the right shape today:qwen3_coderparser via--tool-call-parser), so the custom parser is a compatibility fallback for the/generatepath (which returns raw text) and for older or affected server-parser versions, not the primary value of the native chat design.Relationship to #1557
This supersedes the earlier engine-flag prototype that lived inside the vLLM server and converges with the standalone-adapter structure proposed in #1557: a separate
sglang_modelpackage rather than anengineswitch on the vLLM config. It layers the multi-turn token-splice, the over-context guard, reasoning/tool parsing, and the fail-loud metadata validation on top of that structure.Result
In our runs: multi-turn contiguity failures 48 to 0 (8/8 rollouts complete), throughput ~ the vLLM path, and the engine emits training-grade per-token logprobs (validated to within the model's own bf16/MoE numerical noise vs vLLM). The focused CPU test suite for the package (splice, over-context, tool parsing, metadata extraction) passes 36/36.
Status: draft, ported onto current
main