feat(speculative): add SGLang target backend for EAGLE-3 training - #2449
Conversation
Add a third Eagle3TargetBackend implementation that runs the frozen target
through SGLang, alongside the co-located HF and remote backends. SGLang is the
fastest serving path for mainstream architectures, so the remote target server
can hold the target on dedicated GPUs while the draft trains elsewhere.
The backend is split into two layers:
- SGLangEagle3TargetModel (sglang_target.py) owns the supervision contract and
assembles an Eagle3TargetBatch whose shift / aux-concatenation semantics are
byte-for-byte identical to HFEagle3TargetModel, so a SGLang run is
numerically equivalent to a co-located one. It depends only on a small runner
protocol, so it is unit-testable on CPU without SGLang.
- SGLangTargetRunner (sglang_runner.py) owns the SGLang-internal forward
(ModelRunner + CaptureHiddenMode.FULL + a logits-processor wrap that returns
all-position full-vocab logits plus the three concatenated aux hidden
states). It is lazily imported and validated on the GPU server; CPU tests
cover the surface that does not need SGLang.
serve_target gains an --engine {hf,sglang} flag (engines share a builder
signature via a dispatch map). SGLang is declared as an optional spec_sglang
extra pinned to 0.5.9 and kept out of the main training image. Shared
aux-layer-id default / validation helpers are extracted in target.py so all
backends default identically (behavior-preserving refactor).
Signed-off-by: khazic <khazzz1c@gmail.com>
Generalize the SGLang-specific runner protocol and target backend into an engine-agnostic seam so a second engine (vLLM) can plug in without touching the trainer, the remote server, or the supervision contract: - new target_runner.py: TargetRunner protocol + RunnerEagle3TargetModel, which owns the shift / aux-concatenation contract for any runner; - sglang_target.py: SGLangEagle3TargetModel now only adds SGLang construction on top of RunnerEagle3TargetModel (SGLangRunnerProtocol kept as a backwards-compatible alias of TargetRunner); - sglang_runner.py unchanged (still the GPU/SGLang forward). Behavior-preserving: the SGLang supervision is byte-for-byte identical, all existing CPU contract tests pass, plus a test locking that the backend is engine-agnostic. Signed-off-by: khazic <khazzz1c@gmail.com>
sglang>=0.5.9 made moe_ep_rank/moe_ep_size required positional args on ModelRunner.__init__; the target runner is single-process with no expert parallelism, so pass (0, 1). Signed-off-by: khazic <khazzz1c@gmail.com>
ModelRunner.init_torch_distributed already calls initialize_model_parallel in sglang>=0.5.9, so calling it ourselves trips 'tensor model parallel group is already initialized'. Bring up only the world process group here and let ModelRunner build the TP group. Signed-off-by: khazic <khazzz1c@gmail.com>
The sglang engine never loads the HF AutoModel, so importing NeMoAutoModelForCausalLM at module top forced the sglang target server to pull in Automodel's full model stack. Move it into _build_hf_target, matching the lazy sglang import in _build_sglang_target, so the sglang server runs in a minimal sglang-only environment. Signed-off-by: khazic <khazzz1c@gmail.com>
A client without sglang (the disaggregated case: sglang target server + sglang-free training client) cannot join the NCCL group, but _init_nccl still POSTed /init_nccl and let the server block on the rendezvous until its 120s timeout before both fell back to wire. Gate the request on a local nccl_transport_available() check so an sglang-free client goes straight to wire and never stalls the server. Also fix the serve_target test to patch the now lazily-imported NeMoAutoModelForCausalLM at its source. Signed-off-by: khazic <khazzz1c@gmail.com>
Drop the dead SGLangRunnerProtocol alias (the protocol and alias were both introduced on this branch, so there is no prior name to keep resolving), and cache the constant teacher-forcing SamplingParams on the runner instead of rebuilding it every extend. Signed-off-by: khazic <khazzz1c@gmail.com>
|
/ok to test b1f6e19 |
|
/ok to test 02c4a63 |
…argets The packed_sequence_size guard only blocked the remote backend, but the SGLang runner processes each row as one full causal sequence with no per-document masking, so packing + sglang silently leaked supervision across document boundaries. Gate packing on backend != 'colocated' and hoist the backend-name validation ahead of the guard so a misspelled backend still reports the clearer 'unknown backend' error. Also fix the copyright year in the new test (2026 -> 2025) and add coverage for the packing guard. Signed-off-by: khazic <khazzz1c@gmail.com>
GPU validation of the SGLang EAGLE-3 target backendRan the backend on a single A800-80GB GPU at this branch's HEAD ( Setup (dedicated env, sglang installed out of band as the PR documents):
1.
|
|
/ok to test 493a904 |
Is it possible to include the version information in a guide/tutorial in the future? Thank you so much. |
_setup_online_target now reads cfg.get("distributed.cp_size") for the
context-parallelism gate (merged in from NVIDIA-NeMo#2465). The test helper built the
recipe via __new__ without a cfg, so the four tests that dispatch through
_setup_online_target failed with AttributeError. Give the stub an empty
_RecipeCfg so the gate defaults to cp_size=1 (no CP).
Signed-off-by: khazic <khazzz1c@gmail.com>
SGLang 0.5.9 pins transformers==4.57.1 while NeMoAutoModelForCausalLM needs transformers 5.x (AutoModelForMultimodalLM), so the smoke --compare-hf check cannot run as written in one environment. Document this in the EAGLE guide's environment-setup step, per maintainer request on NVIDIA-NeMo#2449. Signed-off-by: khazic <khazzz1c@gmail.com>
|
Done. Added a version-compatibility note to the EAGLE guide's environment-setup step ( |
Closes #2424.
What
Make SGLang accelerate the EAGLE-3 target in both deployment shapes:
target_model_backend: sglangruns the frozen target through SGLang's ModelRunner inside the training process, replacing the HF eager forward (single-process runs).serve_target --engine sglangserves the target on dedicated GPUs while the draft trains elsewhere.Both sit on an engine-agnostic target-backend contract so another engine (e.g. vLLM) can drop in later without touching the trainer or the remote server.
Why
EAGLE-3 trains the draft against the target's auxiliary hidden states plus its distribution. The target forward dominates each training step, and SGLang is the fastest serving path for mainstream architectures. Running the frozen target through SGLang keeps training and inference numerically consistent while cutting target-side latency.
What changed
target_runner.py(new): a narrowTargetRunnerprotocol plusRunnerEagle3TargetModel, which owns the supervision contract (shift / aux concatenation / aux-layer defaulting) for any runner. This is the single engine-agnostic seam.sglang_target.py/sglang_runner.py(new): the SGLang adapter.SGLangEagle3TargetModeladds only SGLang construction on top ofRunnerEagle3TargetModel;SGLangTargetRunnerowns the SGLang-internal forward (wrapsLogitsProcessorto return all-position full-vocab logits plus the three EAGLE-3 aux hidden states) and is imported lazily so the module stays importable without SGLang.ServerArgs.dtypereceives SGLang's string form (sglang_dtype_str), and building inside an initialized process group requiresworld_size == tp_sizewith a clear error.train_eagle3.py:target_model_backend: sglangselects the co-located SGLang path. SGLang's weight + KV pool defaults to half the GPU so the draft trains in the remainder;recipe_args.sglang_argsforwards ServerArgs overrides. Single-process only (SGLang's parallel state must own every rank of the process group); multi-GPU runs use the remote backend. Example config:examples/speculative/eagle3/llama_eagle3_sglang.yaml.target.py: extractdefault_eagle3_aux_layer_ids/validate_eagle3_aux_layer_idsso every backend defaults and validates aux layers identically (behavior-preserving).serve_target.py: add--engine {hf,sglang}; lazy-import the HF AutoModel so the sglang engine runs in a minimal sglang-only environment.remote/client.py+remote/transport.py: gate the NCCL handshake on local availability (nccl_transport_available()). A client without sglang now goes straight to the wire fallback instead of asking the server to start a rendezvous it can never complete (which previously blocked the server for ~120s before falling back).scripts/smoke_sglang_target.py(new): GPU smoke for the server: validates the ported SGLang forward (shapes, finiteness) and its numerical agreement with the HF backend, with and without a pre-initialized process group (the co-located case).Validation
The contract layer and the recipe wiring are fully unit-tested on CPU. The GPU path was validated end-to-end on a server: a standalone SGLang target server (Qwen3-4B) plus a sglang-free training client over HTTP + wire returns the correct supervision shapes (
target_probs [B,S,vocab],aux [B,S,3*hidden]). The co-located path is validated on the server withscripts/smoke_sglang_target.py --compare-hf --init-dist.Tests
tests/unit_tests/speculative/test_eagle3_sglang.py: SGLang supervision is byte-for-byte identical to the co-located HF backend; the backend is engine-agnostic; aux-layer defaulting/validation; runner surface without SGLang; dtype string mapping.tests/unit_tests/recipes/llm/test_eagle3_sglang_backend.py: backend dispatch, CUDA / single-process guards, andsglang_argsflow into the SGLang kwargs.tests/unit_tests/speculative/test_eagle3_remote_coverage.py: an sglang-free client does not contact the server for NCCL.Follow-up
A separate PR will switch the remote data model to ship hidden states and recompute the target distribution trainer-side (lower bandwidth, less target-side compute, and less engine coupling).