feat(spec-v2): Option-alpha scaffolding -- FROZEN_KV_MTP via EAGLE V2 (experimental, env-gated, NOT yet functional) - #26
Draft
pyc96 wants to merge 1 commit into
Conversation
…LE V2 (env-gated experimental)
Adds the architectural scaffolding for Option-alpha (mirror vLLM's
approach: thin proposer subclass of generic spec-decode worker,
instead of a custom worker). Default behavior unchanged.
Background: per the vLLM-vs-SGLang MTP comparison
(agent-pod/runs/20260525_mtp_v2/analysis/vllm_vs_sglang_mtp.md),
vLLM bakes frozen-KV into the model and uses a 335-LOC Gemma4Proposer
subclass of the generic SpecDecodeBaseProposer for the scheduler hook.
SGLang's 787-LOC FrozenKVMTPWorker is the entire reason FROZEN_KV_MTP
can't use spec-V2 overlap scheduling -- the worker doesn't implement
the BaseSpecWorker interface.
This PR is the equivalent of vLLM's Gemma4Proposer for SGLang:
* New file: python/sglang/srt/speculative/gemma4_mtp_via_eagle.py
- Gemma4MTPEagleWorker(EAGLEWorkerV2): thin subclass that
- constructs the parent EAGLE V2 worker
- calls bind_frozen_kv_context on the draft model after init
(mirrors what FrozenKVMTPWorker does, but at a different entry
point so EAGLE V2's overlap pipeline can drive it)
- monkey-patches the draft worker's draft_forward to NOT advance
positions between draft iterations (Gemma-4 MTP analog of
vLLM's constant_draft_positions=True)
- Gemma4MTPEagleDraftWorker: subclass placeholder for the
constant-positions variant; currently the monkey-patch approach
is used because EagleDraftWorker is constructed by the parent
EAGLEWorkerV2 ctor.
* spec_info.py: dispatcher routes FROZEN_KV_MTP to Gemma4MTPEagleWorker
when SGLANG_GEMMA4_MTP_VIA_EAGLE=1 is set; otherwise raises (overlap)
or returns the V1 FrozenKVMTPWorker (--disable-overlap-schedule).
supports_spec_v2() also updated to return True for FROZEN_KV_MTP.
* speculative_hook.py: _handle_frozen_kv_mtp still sets
disable_overlap_schedule=True by default; only skips that when the
env var is set. Preserves existing FROZEN_KV_MTP deployments
unchanged.
KNOWN LIMITATION (the EAGLE-V2 path is NOT yet functional):
The Gemma-4 MTP assistant model expects the recurrent ``prev_hidden``
in spec_info.hidden_states to have backbone_hidden_size shape
(target's hidden_size, 5376 for 31B-it), NOT the assistant's internal
hidden_size (1024). EAGLE V2 sizes its hidden_states buffers using
draft_model_config.spec_hidden_size = draft.hidden_size = 1024.
CUDA-graph capture crashes:
ValueError: Frozen-KV MTP forward: token_embed and prev_hidden
must have the same shape (got torch.Size([80, 5376]) vs
torch.Size([80, 1024])).
Fixing this requires either:
1. Override draft ModelConfig.spec_hidden_size BEFORE EagleDraftWorker
constructs the draft model + allocates CUDA-graph buffers
(~10-50 LOC in spec_info + ModelConfig)
2. Modify the assistant model's forward() to return
hidden_states_before_norm in the LogitsProcessorOutput.hidden_states
field, so EAGLE's recurrent ferry gets backbone-size data even
though spec_hidden_size remains 1024 (cleaner but couples model
internals to spec_v2 contract)
3. Add an EAGLE V2 hidden_size override hook
(~3 lines in EAGLE V2, plus draft-side init wiring)
Beyond the hidden-size mismatch, EAGLE V2 also assumes positions
advance by 1 each draft step; the monkey-patched constant-positions
behavior may interact with attention metadata caching and CUDA graph
capture in ways not yet validated.
Verified V1 default path still works after this PR (smoke test
"What is 2+2?" -> "2 + 2 = 4", parity preserved).
To try the experimental EAGLE-V2 path (will crash):
SGLANG_GEMMA4_MTP_VIA_EAGLE=1 python -m sglang.launch_server ...
Stack base: pyc/feat-gemma4-ultimate-v2 (PR #21)
Co-authored-by: Claude
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.
Summary
Scaffolding for Option-α from the vLLM-vs-SGLang MTP comparison
(
agent-pod/runs/20260525_mtp_v2/analysis/vllm_vs_sglang_mtp.md):route
FROZEN_KV_MTPto a thin subclass ofEAGLEWorkerV2insteadof the 787-LOC
FrozenKVMTPWorker, mirroring vLLM's architecturalpattern (
vllm/v1/spec_decode/gemma4.py:31-340— also 335 LOC).Behavior is unchanged by default:
FROZEN_KV_MTPstill uses V1with
disable_overlap_schedule=True. The new EAGLE-V2 path isgated behind
SGLANG_GEMMA4_MTP_VIA_EAGLE=1.The EAGLE-V2 path is NOT yet functional — it crashes during CUDA
graph capture on a hidden-state shape mismatch. This PR ships the
scaffolding + the detailed analysis of what remains; it is
documentation-quality code for the next implementer, not a working
overlap path.
Stack base
pyc/feat-gemma4-ultimate-v2(PR #21).What ships
python/sglang/srt/speculative/gemma4_mtp_via_eagle.py(NEW)Gemma4MTPEagleWorker(subclass ofEAGLEWorkerV2): callsbind_frozen_kv_contexton the draft after init; monkey-patchesdraft_forwardto snapshot+restorepositionsaround each draft loop (Gemma-4 MTP analog of vLLM'sconstant_draft_positions=True).python/sglang/srt/speculative/spec_info.pyFROZEN_KV_MTPtoGemma4MTPEagleWorkerwhenSGLANG_GEMMA4_MTP_VIA_EAGLE=1AND overlap enabled.supports_spec_v2()now returns True forFROZEN_KV_MTP.python/sglang/srt/arg_groups/speculative_hook.py_handle_frozen_kv_mtpstill forcesdisable_overlap_schedule=Trueby default; only skips the override when the env var is set.The remaining work (what stops this from being ship-ready)
Three sequential blockers:
Blocker 1 —
spec_hidden_sizemismatch (the immediate crash)The Gemma-4 MTP assistant model expects
forward_batch.spec_info.hidden_states(the recurrentprev_hiddenferried across draft iterations) to be sized[N, backbone_hidden_size]=[N, 5376]for the 31B-it target. EAGLE V2 sizes its hidden_states buffers viaEagleDraftInput.hidden_size_for(worker)which returnsdraft_runner.model_config.spec_hidden_size= the draft model's hidden_size = 1024.Crash during CUDA graph capture:
Fix options (any one):
draft_runner.model_config.spec_hidden_size = target_backbone_hidden_sizebeforeEagleDraftWorker.__init__runs (currently it runs insideEAGLEWorkerV2.__init__so the subclass can't intercept). Requires touching how the parent constructs the draft worker.Gemma4AssistantForCausalLMto expose aspec_hidden_sizeclass attribute thatModelConfigreads. ~10 LOC in two files.EagleDraftInput.hidden_size_foroverride hook so the model can declare its required hidden state size.Blocker 2 — return value contract
Even with the input shape fixed, the assistant's
forward()returnsLogitsProcessorOutput.hidden_statessized at the internalhidden_size=1024(the inner Gemma4TextModel output, beforepost_projection). EAGLE V2 takes this output and stores it as the next iter'sspec_info.hidden_states. We need the assistant to instead returnpost_projection(hidden_states)=[N, 5376]so the recurrent ferry stays at backbone size.Currently the assistant passes
hidden_states_before_norm=projected_states(=post_projectionoutput) toLogitsProcessor, which preserves it asoutput.hidden_states_before_norm. The fix: swap which one goes intooutput.hidden_statesfor the EAGLE-V2 path.Blocker 3 —
constant_draft_positionscorrectness validationMy current implementation monkey-patches
draft_worker.draft_forwardto snapshot positions before the EAGLE draft loop and restore after. But INSIDE the loop, EAGLE V2 still doesforward_batch.positions.add_(1)at line 493 ofeagle_worker_v2.pybetween draft steps. The attention metadata is built once fordraft_index=0(outside the loop) so each draft step still calls into a kernel that sees incremented positions. Whether this produces correct outputs depends on whether the attention backend readspositionsdirectly or only via the cached metadata. Needs validation once Blocker 1 is past.The full vLLM approach (
SpecDecodeBaseProposer.proposeatllm_base_proposer.py:570-587) skips the position-update entirely whenconstant_draft_positions=True. Equivalent SGLang fix: add aself.constant_draft_positions: bool = Falseflag toEagleDraftWorker.draft_forwardand gate thepositions.add_(1)call (~3 LOC change ineagle_worker_v2.py:493). Low-risk if confined to spec-v2 path with explicit opt-in.Why we shipped scaffolding instead of the full fix
Original time estimate (from the comparison report): 5-10 days for Option-α. Investigation today confirmed this is accurate — the architectural mismatch between EAGLE V2 and Gemma-4 MTP runs three layers deep (init-time buffer sizing, runtime recurrent-state shape, scheduler position-advancement semantics).
Doing this surgery correctly in this session would require:
Decision: ship the scaffolding + analysis as infrastructure for the next implementer, preserve V1 default behavior, and document the three remaining blockers concretely.
Verified V1 default path still works
Server log:
Reference
Gemma4Proposer(the architecture this PR mirrors):vllm/v1/spec_decode/gemma4.py:31-335SpecDecodeBaseProposer.proposewithconstant_draft_positions:vllm/v1/spec_decode/llm_base_proposer.py:421-600agent-pod/runs/20260525_mtp_v2/analysis/vllm_vs_sglang_mtp.mdFor production today
Stick with the recommendation from PR #21: use SGLang no-MTP. The MTP gap to vLLM remains until either:
CI States
Latest PR Test (Base): ❌ Missing
run-cilabel -- add it to run CI tests.Latest PR Test (Extra): ❌ Blocked --
run-ciis required first.