Repository navigation
feat(spec): verify draft tokens on the target side - #836
Merged
Merged
Conversation
jason-fxz
force-pushed
the
feat/spec-verify
branch
2 times, most recently
from
October 28, 2026 21:38
70f327d to
4ebebbe
Compare
jason-fxz
force-pushed
the
feat/spec-verify
branch
from
October 29, 2026 03:12
4ebebbe to
fa0fba5
Compare
This was referenced Oct 29, 2026
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.
Refs #828
Second PR of the speculative decoding plan in #828: the target side of a verify step, driven by the scheduler, on top of the multi-row seams from #833, the
LinearAttnBackendfrom #837 and the sharedGatedDeltaNetlayer from #841. No draft method is registered yet (the MTP PR adds the first one), so nothing changes unless a proposer is plugged in; the tests plug in a stub. The full stack is in the draft PoC #832.Summary
With
--speculative-algorithmset, every decode step becomes a verify step: each running request gets k draft tokens from aProposer, the target scores the k+1 rows in one forward, and the scheduler commits the accepted drafts plus one target token.New flags:
--speculative-algorithm(--spec-algo),--speculative-num-steps(--spec-num-steps, k),--speculative-draft-model-path(--spec-draft-model).Design
engine/spec_protocol.py:SpecRuntimeis a Protocol whose one implementation,SpecRunner(spec/runner.py), is loaded by name, so the engine never importsfreetoken.spec.SpecRunneris the target side and the order of a step. A draft method implementsProposer(spec/proposer.py):proposereturns k drafts per request, and the other hooks (feature sink, post-verify, prefill, graph capture, release) default to no-ops. Method code goes underspec/<method>/in later PRs.spec/rejection.py). Each row samples with its request's temperature, top-k, top-p and greedy flag. Presence and frequency penalties are applied per row, with the drafts before a row counted as already generated.LinearVerifyScratchowned by the state pool and handed to the GDN layers throughFLAMetadata.verify_scratch, whichLinearAttnBackendsets for eager and captured verify batches. The backend now holds theLinearStatePoolit is built with, andbuild_fla_metadatatakes the pool as an argument, soattention/linear.pyno longer reads the state pool from the global context (the hybrid-radix track metadata read its conv width from there too). Inlayers/linear_attn/gdn.pya verify goes through the decode branch: the verify conv (causal_conv1d_verify) in place of the decode conv, and the same fused recurrent kernel withdisable_state_updateandinput_stash. The drain (Engine.commit_verify) replays the accepted rows into the live state (gdn_state_replay, a Triton kernel whose result matches stepwise decode bitwise) and restores the conv window, before a finished request gives its slot back./v1/cache/status(geometry.unit_bytes.kv_per_token). The verify scratch is counted with the GDN state pool.Rejected at startup: TP > 1; targets with sliding-window or DSV4 window-tier KV; decode attention backends that cannot capture multi-row graphs. With
--attention-backend X,fi, where X captures multi-row graphs on device, the backend becomesXand the change is logged.Tests
tests/spec/test_verify_contract.py: a tiny hybrid (GDN + attention) model with dummy weights, served end to end through the scheduler with a stub proposer whose drafts come from a plain greedy run and are wrong at a different place each step (so every accept count occurs). The speculative run must reproduce the greedy run token for token, except at a near tie, which the test checks is one. fa and triton, with and without CUDA graphs.tests/layers/test_gdn.py: the GDN layer test now continues its prefill and decode with a verify of k+1 rows, a commit at a different accept count per request (1, 2 and k+1), and another decode step, each checked against the pure-torch reference over the request's whole sequence so far; a verify that wrote the live state or a wrong commit shows up in the next step.tests/spec/test_gdn_verify.py: the stash and replay kernels match decode bitwise for every accept count, also for a launch with more layer x row x head programs than one CUDA grid axis holds.tests/spec/test_rejection.py: greedy prefix match; each verify row samples with its own request's parameters (top-k, greedy); each verify row counts the drafts before it as committed for penalties.tests/scheduler/test_spec_drain.py: what a verify drain commits, which slots it gives back, and that the target state is committed before a finished request frees its GDN slot.tests/kvcache/test_cache_unit_bytes.py:kv_bytes_per_tokencounts the draft KV.tests/models/qwen4_exp/test_ple.pypasses its pool tobuild_fla_metadatainstead of installing a global context for it.Test environment
tests/serverpasses, 640 tests).Unit tests
FREETOKEN_QWEN36_MODELgives the contract test its tokenizer. 1695 passed, 202 skipped, 1 failed. The failure istests/models/test_quant_config.py::test_scheme_for_agrees_with_the_stored_tensors[apodex/Apodex-1.1-mini-NVFP4[index-only]](a local checkpoint withquant_method: modelopt_mixed); it fails the same way on main.End-to-end with speculative decoding off
No draft method is registered here, so serving without the new flags must match main. AIME25 problem 1 through
freetoken.llm.LLM(model_path, dtype=torch.bfloat16, max_running_req=4)with the chat template, greedy (temperature=0,max_tokens=16384, natural end), compared token by token with the same run on main a2ce636 (#841 reproduced these runs token for token, so they stand for main d043fb6 too):Qwen/Qwen3.6-35B-A3B(GDN hybrid, MoE)Qwen3.8-Flash-Next-NVFP4(qwen4_exp)AIME25 problems 1-3 submitted together on
Qwen/Qwen3.6-35B-A3B(max_running_req=4,cache_type=hybrid_radix, greedy, 1024 tokens each withignore_eos): all three identical to main a2ce636 token by token.Smoke test on this branch, passed:
Known gaps
FREETOKEN_MAMBA_SSM_DTYPEset to bfloat16 or float16 (fp32 is the default), the replay keeps the recurrent state in fp32 across the accepted rows and rounds once, while decode rounds after every token, so the two are no longer bitwise equal.