Repository navigation
Support Step3.5/3.7 flash mtp3 - #24340
Merged
ggerganov merged 16 commits intoJun 21, 2026
Merged
Conversation
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.
Overview
follow-up to #23274.(cc @pwilkin )
📜 Full data-flow trace — couldn't think of a good way to draw this, so I wrote it all down instead. It's long, but every byte is load-bearing.
Notation:
token@pos/h(pos)— positions are explicit (0-indexed)h_tgt(p)— target NextN hidden at p (before the output norm)h45(p)/h46(p)— head 45/46 output hidden, chained between heads while draftingpending_h = h_tgt(pos of id_last − 1)— always the trunk h, regardless of chainingExample: a 4-token prompt at positions 0–3.
Core strategy
Each MTP head is its own decoder layer with its own KV, and the driver runs one head per
llama_decode. Aseq_rmbefore each head clears the range it re-decodes, so it reuses the same slots (find_slotis deterministic) instead of stacking duplicate positions;find_slot/apply_ubatchare untouched. The two phases differ only in what feeds the heads:process()h_tgt, right-shifted by one (not the inter-head hidden)draft()pending_h)Only the trunk
h_tgtcrosses rounds, sopending_h/verify_hstay single-layer.MTP Block Selection Strategy
cparams.mtp_layer_offset(src/llama-cparams.h) — picks which appended MTP block theDECODER_MTPgraph runs:il = n_layer() + offset. Default 0.graph_mtpselects the head by offset (il = n_layer() + cparams.mtp_layer_offset, was a hardcodedn_layer()).graph_mtpnow gathers its output rows viabuild_inp_out_ids(), like the trunk graph. The fix that makes chaining work: from step 1 on, the output is the last batch row, not row 0, so without it heads 46/47 read the wrong row. Identity gather whenn_outputs == n_tokens, so the single-head path is unchanged.n_layer_nextnMTP blocks.n_maxis clamped to the head count when chaining (each head used once).Results
The command is identical on both machines; only
--spec-draft-n-maxand the build change. Before = single-block MTP on master (one head, looped when n-max > 1); after = the three-layer chain.DGX Spark GB10
Before (single-block MTP, master)
--spec-draft-n-max 2--spec-draft-n-max 3After (three-layer MTP, this PR)
--spec-draft-n-max 2--spec-draft-n-max 3Mac Studio M4 Max
Before (single-block MTP, master)
--spec-draft-n-max 2--spec-draft-n-max 3After (three-layer MTP, this PR)
--spec-draft-n-max 2--spec-draft-n-max 3Requirements