Repository navigation
Conversation
5 of 8 tasks
…of the scheduler's ChunkedReq
…d check it after load
This branch has not been deployed
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
Third PR of the speculative decoding plan in #828: the first draft method, MTP, which drafts with the checkpoint's own
mtp.*layer. It covers the Qwen3.5 family (qwen3_5andqwen3_5_moearchitectures, dense and MoE) from HF checkpoints, and Qwen3.8-Flash-Next (qwen4_exp) from its HF checkpoint or from a GGUF target with a Strata MTP pack. It builds on the verify step from #836, the seams from #833, theradix_lookaheadkeys from #834 and the shared GatedDeltaNet layer from #841. The full stack is in the draft PoC #832.Summary
--speculative-algorithm mtp(--spec-algo mtp) turns it on; k defaults to 3 (--speculative-num-steps, up to 8).--speculative-draft-model-path(--spec-draft-model) points at the draft weights when they are not in the target checkpoint, which is the case for Flash-Next GGUF targets: pass a Strata MTP pack, Strata's GGUF that keeps the checkpoint'smtp.*tensors under their HF names. ThenextnMTP blocks llama.cpp writes are not read, for either family. A separate draft checkpoint folder is read with the target's quantization config.docs/cli.mddocuments the three flags.Rejected at config time, before any weights load, so a draft that cannot load does not fail only after the whole target has loaded:
mtp.*.mtp.*tensors, e.g. a Flash-Next GGUF target with no--speculative-draft-model-path; a GGUF draft for a Qwen3.5-family target, whose draft reads safetensors only; a GGUF target with a draft source that is not a GGUF.mtp.*tensors, and an explicit local draft path that does not exist.A checkpoint whose MTP routed experts use a layout the reader does not handle (per-expert NVFP4 such as
nvidia/Qwen3.8-Flash-Next-FP4-Local, or per-tensor FP8) stops at draft load with an error naming the unsupported tensors.Commits:
feat(spec): add the MTP proposer for the qwen3_5 familytest(models): read the qwen3_5 MTP head of bf16 and block-fp8 checkpointsfeat(spec): verify qwen4_exp targets on qsa_sparse graphs with PLE verify handlersfeat(spec): add the qwen4_exp MTP draft from the checkpoint or a Strata packdocs(cli): document the speculative decoding flagsfix(spec): refuse MTP draft sources the draft cannot read before the target loadsdocs(qwen4_exp): describe the verify path in the PLE docstringsDesign
MTP proposer (
spec/mtp/):ModelSpec.spec_drafts["mtp"]; the interface is theMTPDraftprotocol inspec/mtp/proposer.py. This PR adds no import between model families; the routed-expert reader both use ismoe/expert_pieces.iter_extra_layer_expert_pieces.moe_strategy="fused", so its experts are resident on the GPU and never enter the target's expert cache or offload, whatever the target's strategy.radix_lookahead = True: a prefix hit never reuses a draft KV slot written for a different next token.Context.overridewith the target's GDN pool hidden. The draft's per-step state (relayed hidden, first draft token, picked rows) stays insideMTPProposer; nothing method-specific goes on the batch. Host-to-device index uploads are pinned and non-blocking.Flash-Next target verify:
GatedDeltaNetlayer from refactor(layers): share one GatedDeltaNet op in layers/linear_attn #841, whose verify path comes with feat(spec): verify draft tokens on the target side #836; this PR adds nothing GDN-specific.qsa_sparsecaptures multi-row verify graphs (uniform_extend_graph="device"); before this, speculative decoding rejected Flash-Next at startup. The per-request pending ring of index keys is sized with the draft depth (QSAKVCache.ring_capacity_for(index_ratio, k)), so the rows a verify writes and then rejects never overwrite a ring entry an open group still needs. The same applies to the QSA draft pool.ple_conv,ple_ngram_ctx). The pool does not know how to restore them: eachSlotStateSpecnow names aSlotVerifyhandler class (models/config.py). The pool builds one handler per state next to the GDN verify scratch, the owner gets it back withpool.verify_handler(name)at forward time, andcommit_verifyruns the GDN commit and then every handler's commit; the handlers share one pinned, non-blocking upload of the slot and count indices; verify request i's scratch is row i. Handler scratch bytes are counted with the GDN verify scratch. Speculative decoding is rejected at config time, before the weights load, if any slot state has no handler.PLELayer._verify_conv) and stages[live history | verify conv inputs]per row; the n-gram context is not rolled, and the model stages[live context | verify input ids]per row instead. The commit restores both as they stand after the accepted rows.PLEMetadata.is_verifycarries the flag, and a verify takes its PLE slots and indptr frombatch.fla_metadataas decode does.--ple-backend disk(the default): a verify graph replay reads its rows from the graph staging buffer like decode does,rows_per_reqtokens per request read back from the device. The graph buffer holdsmax_running_req * (k + 1)rows, since verify graphs stop atmax_running_reqrequests.Flash-Next MTP draft (
models/qwen4_exp/mtp.py):FeatureSink.final_hiddenfrom the model, with the LM head told not to export again), and the draft maps its output through its own mixer before the target's LM head.mtp.*tensors, or from a Strata MTP pack: a GGUF with the same tensor names, ggml-typed linears and stacked ggml experts, passed with--speculative-draft-model-path.Tests
New or extended:
tests/models/test_qwen3_5_moe_weight.py: the MTP reader fills exactly the draft the engine builds, for bf16 (stacked experts) and block-fp8 (per-expert, quantized linears) checkpoints, and the routed experts keep the form the checkpoint stores.tests/models/qwen4_exp/test_ple.py::test_verify_commit_matches_stepwise_decode: a PLE verify over k+1 rows leaves the conv history and n-gram context untouched; committing n rows (different n per request, verify rows mapped to other slots) leaves exactly what n decode steps leave, and each row's conv output equals its decode step bitwise.tests/models/qwen4_exp/test_qsa_backend.py::test_decode_graph_replay_matches_eager[verify]: a captured 4-row verify replay equals the eager verify bitwise.tests/models/qwen4_exp/test_qsa_backend.py::test_verify_after_rejected_rows_compresses_like_decode: after a verify whose last rows are rejected, the next verify compresses the open index group as decode steps do.tests/models/qwen4_exp/test_ple_disk.py::test_wait_sync_large_graph[verify]: a verify graph replay on the disk PLE backend reads every row it needs, bitwise against the in-memory table.tests/spec/test_verify_contract.py: a tinyqwen4_expmodel (GDN + QSA, PLE, hyper-connections) joins the contract test: speculative decoding with stub drafts must reproduce plain greedy decoding token for token, with and without graphs. The dummy weights are scaled by fan-in and the PLE table gets seeded rows, so the PLE path carries signal and ulp noise stays small; the tie check is now relative to the logit magnitude (the same 0.125 bound as before for logits in [64, 128)).Each new check, run with only the logic it guards reverted:
test_mtp_head_fills_the_draft_and_its_expertstest_verify_commit_matches_stepwise_decodetest_verify_commit_matches_stepwise_decodetest_verify_after_rejected_rows_compresses_like_decodetest_wait_sync_large_graph[verify]host_fill_batchtakes the graph path for decode onlytest_verify_steps_reproduce_greedy_decode[qwen4_exp]Reverting the staged
token_to_reqslice in the QSA graph test makes the capture crash on a shape mismatch rather than fail an assertion. The twotest_verify_commit_matches_stepwise_decoderows ran on 4f8f0cda (this PR on an earlier head of #836); the other rows ran on this branch before its first rebase onto #836.Test environment
kernel/triton/attention.pymoved by 6 lines) besides one sentence indocs/cli.md. Rerun on 8815c0b:tests/spec,tests/layers,tests/models/qwen4_exp,tests/models/test_qwen3_5_moe_weight.py,tests/moeandtests/kernels(758 passed, 244 skipped), and the MTP-on greedy runs of Qwen3.6-35B-A3B and the Flash-Next NVFP4 checkpoint, both token for token identical to the ones on e02a5890; the Flash-Next GGUF with the Strata pack also ran greedy with MTP (846 tokens, answer 70, accept length 3.44).Unit tests
1705 passed, 211 skipped, 1 failed. The failure is
tests/models/test_quant_config.py::test_scheme_for_agrees_with_the_stored_tensors[apodex/Apodex-1.1-mini-NVFP4[index-only]], the same one #836 reports (a local checkpoint withquant_method: modelopt_mixed); this PR does not change the quantization code it checks.Smoke tests, both passed:
FREETOKEN_TEST_MODEL=/path/to/Qwen3.6-35B-A3B FREETOKEN_AIME25_JSONL=/path/to/aime25.jsonl PYTHONPATH=python pytest tests/e2e/test_aime.py FREETOKEN_REBUILD_TEST_MODEL=/path/to/Qwen3.6-35B-A3B FREETOKEN_REBUILD_EXTRA_ARGS="--speculative-algorithm mtp --max-running-requests 4" PYTHONPATH=python pytest tests/e2e/test_cache_rebuild.pyThe second one boots
ft servewith MTP on and grows the KV pool (the draft KV is rebuilt), grows the GDN state pool (the draft KV is kept), flushes the prefix cache and is refused an unfittable size, generating after each step.End-to-end
All runs use AIME25 problem 1 (problems 1-8 for the concurrent run) through
freetoken.llm.LLM(model_path, dtype=torch.bfloat16, max_running_req=4)with the chat template, MTP withspeculative_algorithm="mtp"and the default k = 3. Checkpoints:Qwen/Qwen3.6-35B-A3BandQwen/Qwen3.6-27B(bf16),nvidia/Qwen3.8-Flash-Next-NVFP4(default--ple-backend disk, experts offloaded by the automatic MoE cache), and the Flash-Next GGUFISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF(IQ3_XXS) with the Strata packmtp-q2_0.ggufasspeculative_draft_model_path.Speculative decoding off must match main. Greedy (
temperature=0,max_tokens=16384, natural end) on Qwen3.6-35B-A3B, compared token by token with the same run on main a2ce636 (which #841 and #836 reproduced token for token): both runs hit the 16384-token limit and are identical.MTP off vs on, greedy, same settings:
The accept length is committed tokens per request per verify step (at most k + 1 = 4). The Qwen3.6 divergence points are the ones the PoC run showed.
8 requests at once with MTP (problems 1-8 submitted together, recommended sampling
temperature=1.0, top_p=0.95, top_k=20,max_tokens=4096): every request finished without error and the end of every output reads as coherent reasoning. Where a request finished before the limit, its boxed answer is the reference answer.Decode speed, one request: problem 1, recommended sampling, 1024 decode tokens (
ignore_eos), one warmup then 3 runs, median. The KV column is the pool the engine sized at startup with default memory settings; the draft's weights and KV come out of the same budget.Startup rejections checked on this branch: an FTW checkpoint (
Qwen3.6-27B-NVFP4converted to FTW) and the Flash-Next GGUF without a draft path both stop at config time with the errors above. No Qwen3.5-family GGUF was at hand, so that rejection was checked by callingMTPProposer.adjust_configwith theQwen3_5GGUFForCausalLMregistry entry ("MTP is not supported for Qwen3_5GGUFForCausalLM yet"). The other config-time checks were run through the engine's config resolution on local checkpoints, on CPU: a Strata pack as the draft of Qwen3.6-27B, the Flash-Next GGUF with the Flash-Next HF folder as its draft, Qwen3-30B-A3B (nomtp.*) as the draft of Qwen3.6-27B, and a draft path that does not exist are refused; Qwen3.6-35B-A3B, the Flash-Next NVFP4 checkpoint (alone and with the Strata pack) and the Flash-Next GGUF with the Strata pack pass. The expert-format error was checked against themtp.*expert names of local checkpoints: the bf16 and block-fp8 Qwen3.5-35B-A3B and Qwen3.6-35B-A3B checkpoints, three NVFP4 releases of Qwen3.6-35B-A3B and the Flash-Next NVFP4 checkpoint pass,nvidia/Qwen3.8-Flash-Next-FP4-Local(per-expert NVFP4) is refused.Known gaps
max_running_req=4). The chain graphs' slot-index buffers ((k-1) x max batch x page-table width x 4 B) and the draft extend graph's capture buffers are not budgeted either; all of these come out of thememory_ratioheadroom.MTPProposeritself (the contract test drives a stub proposer): chunked against one-shot prefill, chain-graph against eager drafting, the post-verify relay over padded rows, and detach/attach. A bug there would lower the accept rate, not the output, since the verify is exact.