spec : add adaptive MTP draft depth (draft-mtp-adaptive) - #27210
Conversation
|
Hi! I've also created a fork with adaptive length for mtp and dflash with a rolling window based heuristic. https://github.com/Us5rName/llama.cpp/tree/draft-adaptive-length-rolling-window and this PR #25726 with the groundwork. Would you like to colab? |
I corrected a different MTP issue in this commit to my personal fork's working branch here: stew675@b2655d3 I really should file a PR for that, but its exposure range is pretty narrow. My fix targets which custom kernel runs for F32-activation small batches. It fixes a corner case for the handling of BF16 KV caches though (most of the work in my branch there targets adding BF16 KV to the ROCm backend), and my code exposed that latent bug. It appears there is a handful of inconsistencies in the mainline MTP code for certain corner cases and you have found another. I do see that your PR got closed. Did you ever work on a more targetted fix as per the reviewer's suggestion? |
Thank you for bringing your PR up. I searched for similar PR's and I don't know why yours didn't show up. I've looked over your branch and groundwork PR. There's a number of conceptual similarities, both being counting based heuristics. Both are sliding window approach, with yours being explicit, and mine being implicit. Both employ a form of "depth stickiness". Yours has a lot more knobs where I made a conscious effort to keep the knobs to a minimum. I do like your Bias knob though. That could be a genuinely useful knob for users to tweak for certain models to adjust the level of depth stickiness. The falling back approaches do differ significantly in concept. I spent quite a bit of time trying difference approaches, and then dialing in the one I settled on. Your personal branch seems to target DFlash too, right? I'm fine with us combining efforts, but we'd need to settle on what the UX approach should be. I'm a big fan of KISS. Years ago I would've had knobs for everything, but nowadays I'm a firm believer of use as few knobs as possible. Do you have any performance comparisons of your implementation and how it affects both prose and code? I tried pretty hard to ensure that prose performance was affected as little as possible, so I'm curious to see results on how you solved that issue. At the end of the day though, we both need one of the maintainers to offer some guidance on what they'd like to see. That will ultimately set the direction on what the next steps will be here. Edited to correct my earlier statement of both being implicit sliding windows. That was my mistake earlier. |
Chained MTP drafting (PR ggml-org#27173 backport): - All N draft tokens produced in one fused GPU decode via in-graph argmax - Deferred catch-up rows merged into first draft decode - --spec-chain N flag enables chain mode and sets depth (default: off) - New llama_set_mtp_chain() API for graph mode switching - Per-shape scheduler pool (LLAMA_SPEC_CHAIN env var still works) Adaptive MTP draft depth (PR ggml-org#27210 backport): - --spec-type draft-mtp-adaptive with hysteresis state machine - Depth climbs after consecutive full accepts, drops on misses - --spec-draft-n-min-adaptive for floor depth (default: 3) Results on RTX 5090, Qwen3.8-27B Q4_K_P: - Code: 206 t/s (chain n=8) vs 156 t/s (MTP n=3) vs 69 t/s (no MTP) - Chain delivers 3.0x over no-MTP, +31% over standard MTP on code Assisted-by: Claude
|
Numbers from our side supporting adaptive draft depth, measured on a Strix Halo APU (gfx1151, Ryzen AI Max+ 395) serving Qwen3.8-27B with draft-mtp + ngram-mod speculative decoding (datapost: #27154): At
The second lane replicates independently on a different Strix Halo system: acceptance "way below 50%" under xhigh-reasoning coding traffic, with n-max 12 regressing generation (peak ~26 t/s, dips into the ~5-6 t/s range) versus lower n-max values — that user is now moving down to n-max 2 (comment: #27154 (comment)). So a fixed n-max serves one lane and starves the other: deep enough for the ~0.96-acceptance lane, or shallow enough to cap wasted verification in the ~0.5-acceptance lane, but not both. Acceptance-driven draft depth is the knob that spans both, which is exactly what this PR adds. |
This comment was marked as abuse.
This comment was marked as abuse.
|
I went over all the check failures and it appears that they are all pre-existing/known flaky tests unrelated to my code changes. Now, I do have another commit that's almost ready which actually solves the 3% performance regression on hard prose, and actually boosts normal prose. The question I have is shall I wait for this PR to go ahead and file a followup, or shall I attach that change to this same PR. I'm fine either way. |
How does this compare with draft-mtp and spec-draft-p-min? I've been playing with pretty deep n-max values (8-16) and p-min>0.5 with good results. |
This comment was marked as low quality.
This comment was marked as low quality.
Yeah, I'll cover it in a followup PR. That will give me more time to refine it further. |
|
Getting this error with plain draft-mtp: It occurs whenever the effective n_max < 3 (e.g. --spec-draft-n-max 2). The range check added in |
Thank you for catching this. I'll be pushing a fix shortly. |
This comment was marked as low quality.
This comment was marked as low quality.
You've provided both a broad range (8-16) and an ambiguous range of p-min > 0.5 I'm guessing you meant --spec-draft-p-min, and not --min-p? Just to be clear, what's your ask here? For me to test all depths from 8-16 and vaguely a number of --spec-draft-p-min values as a matrix? Wouldn't that be highly dependent on what exactly it is you're doing? It's kind of the point of this PR that you don't need to be fiddling about trying to dial in the perfect depth and p-value each time. It's also been my experience that raising --spec-draft-p-min, whether it be normal or adaptive MTP, absolutely destroys prose performance. With the rise of deep thinking models (like our good friend Qwen3.8 here) there's likely to be significant amounts of time spent generating prose before code gets written. This is why I've been focusing on preventing/minimising harm to prose performance. We don't want trash that thinking performance just to get some more t/s on code generation and end up slower overall. |
No ask, just curious because I had your exact though this weekend "man, we need an adaptive n-max or something." and then found spec-draft-p-min and it gave me the sort of results I was looking for, with Qwen3.8.
Your PR is on my list to test. |
|
Tested the fixed-depth equivalent of this on the target hardware/backend this PR doesn't cover yet — Intel Arc Pro B70 (BMG G31, 32GB), llama.cpp SYCL via LocalAI
Draft acceptance 75–90% (mean accepted length 2.5–4.7) across runs. Two observations that support the adaptive approach directly:
Happy to run the adaptive build against the same 700-token workload on this B70 once it's testable and post the side-by-side. |
I ran my heuristic against static length in speedbench with qwen 3.6 35B and mtp roleplay category was used for prose *note: on my hardware (rtx 4080 super + rx 7900 xtx, both using vulkan backend), I noticed the maximum draft length that doesn't hurt my performance was 5, and the minimum draft length that didn't hurt it was 2. draft length 2 was used for prose, and draft length 5 was used for coding. got very similar results in both the non adaptive case and the adaptive case. Speedbench, qwen 3.6 35B UD-Q4_K_XL, using mtp: coding, draft length 5: 157.70 tps roleplay, draft length 2: 147.71 tps In my personal experience with my fork, the draft length of my heuristic usually stabilizes at 2 tokens for prose, Tell me if there are additional files/logs you would like to see.
Yes. By targeting Dflash I meant I copy pasted the code from my mtp implementation into Dflash - the heuristic works exactly the same in both cases. In the code, Because the draft length update is checked on every accept(), which every draft spec implementation has, it can absolutely be ported to draft-simple and draft-eagle3, I don't use them so I didn't apply the heuristic to them. I now also deduplicated code in my fork by using your approach of putting the heuristic in a header, and added a short explanation on the algorithm I used, so it should be easier to read and trivial to port to other draft implementations. I looked at your code, and it seems like adding the same heuristic you implemented in mtp to Dflash (and other spec types) should be simple, and I think adding your heuristic to Dflash as well is a good experiment because even with Dflash generating all tokens at once, the target model can still try to verify "useless tokens" (In my experiments, my heuristic worked well on Dflash)
I agree I that have too many knobs, and when I use my fork I always use --spec-adaptive-length-default (sets my knobs to params that worked well for me) However, I still think that 1-3 knobs of configuration is still good to have, to let people experiment. I think a good approach would be like with n-gram speculative decoding - configurable but includes a parameter that sets sane defaults (--spec-default). |
This comment was marked as low quality.
This comment was marked as low quality.
Ooh, good catch! Thank you for that. I see that another work-flow hasn't been approved yet, so now's a good time to merge that in. |
This comment was marked as low quality.
This comment was marked as low quality.
|
Thank you for the detailed response. You're drawing the correct distinction here and To clarify: my "~50–200 tokens" was about the controller's own state. The depth and The data supports your conflation point. The same model and backend show a floor of 3 Regarding the Oracle: it's an offline perfect-foresight replay for ranking controller With regards to controller memory though it predicts an even shorter algorithmic optimum. On the topic of robustness, I do agree. It's just one model, one backend, and one So your framing is correct. Adaptive MTP learns an effective speculative memory horizon |
|
It looks like the 5 failed checks are all known flakes independent of this PR's changes. I don't intend to work on this PR further. Barring blocking issues being found, if any followup work is needed they should be handled in follow-up PR's. |
|
Forced pushed in order to rebase against current master tip and resolve a handful of merge conflicts due to drift. |
Evaluate the algorithmic DFlash2 selector boundary without importing an untrained convolution layer. Prior-art: ggml-org/llama.cpp#27210; ggml-org/llama.cpp#27342 Signed-off-by: Codex <codex@openai.com> (fak model)
|
@am17an sorry to bug you again as I know the upstream developers are all busy working on exciting new model support, but I'd like to know if this PR here of any interest to the llama.cpp maintainers? I'm reaching out to you since you were responsible for most of the excellent MTP work that this is building upon. I'd just like to know if I should keep holding out hope for this PR, or if the maintainers have other plans instead? A good number of people have independently proven that this PR does exactly what it says on the tin, so to speak, across a number of architectures. It just seems like a free 10% boost and an improved user configuration experience is a lot to sleep on. |
|
I've been testing this Adaptive MTP implementation for a week, and the results are excellent. |
|
Long-context data point: on a single RTX 3090 (Qwen3.6-27B UD-Q3_K_XL, GPU KV q4_0, FA on) we compared fixed draft depths across 20K-120K context on a diverse corpus. n-max=4 beat n-max=3 at every level above 55K (+19% at 65K, +23% at 75K) and only lost at short context. So the drop-pressure side of the state machine should be conservative at long context: in our measurements the deeper draft keeps paying off as context grows. |
Hysteresis state machine with a climb counter and a weighted drop-pressure accumulator. The depth climbs one step after 5 consecutive verifies that accepted every drafted token; any miss adds (N - acceptance) to the drop pressure and the depth drops one step once it exceeds 30. High depths fall quickly (a total miss adds N), low depths hold, and at the floor no pressure accumulates at all. The floor is max(1, n_min) and the ceiling is n_max, so --spec-draft-n-min/--spec-draft-n-max bound the adaptive range and the cold-start depth is 3. Assisted-by: pi
Adaptive MTP starts at the floor of --spec-draft-n-min-adaptive (default 3) and adjusts its own depth: consecutive full accepts climb one step, with a cost table that rises fast to depth 3, blocks 3->4 (where marginal content collapses), and climbs fast at depth; a drop-pressure accumulator of n_draft - n_accepted, with a budget of max(depth * 5, 20), lowers it. Fully accepted but truncated drafts count as full accepts, and only drafts this implementation actually produced update the controller. The depth is independent of --spec-draft-n-min, which keeps its usual meaning of a minimum draft length to verify for the non-adaptive spec types. Extracted the controller into a standalone struct and added unit test cases for adaptive MTP. Assisted-by: pi
n_min_adaptive only applies to draft-mtp-adaptive, but the range check ran unconditionally in the shared MTP ctor, so plain draft-mtp aborted whenever the effective n_max was below the default floor of 3 (e.g. --spec-draft-n-max 2). Gate the check on adaptive mode, and when the chain_heads clamp capped n_max at the model MTP layer count, say so in the abort message. Assisted-by: pi
Assisted-by: pi
climb_threshold: the 3->4 barrier is hardened to 10 consecutive full accepts so prose/reasoning stay pinned at the floor; 4->5 raised to 6; 5->6 and 6->7 lowered to 3 and 2 so code accelerates to the deep hold without over-drafting into the marginal depths. recurrent snapshot fix: the conv-state loop wrote n_rs_seq + 1 copies per layer per round, but a rollback can only reach n_seq_tokens - 1 slots back, so copies beyond the batch were dead work (~2.6% per-round overhead at n_rs_seq=10 on shallow verifies). Start the loop at max(1, K - n_seq_tokens + 1). tests: move the #undef NDEBUG before <cassert> so the asserts actually run in Release builds (they were silent no-ops), and re-derive the climb expectations for the new table. Assisted-by: Pi
I rebased the code against the latest master tip and corrected a handful of outdated comments
b26c775 to
45e3d26
Compare
|
I merged PR #27210 into the latest master branch. Adaptive MTP works correctly on models like Gemma-4, but Qwen3.8-27B fails to load with an access violation crash. I'd like to report this issue. Build steps Configuration Error log when loading Qwen3.8-27B The crash occurs at ggml_mul_mat() + 0x6, which suggests a null or invalid tensor pointer is being passed — possibly the MTP tensor is not being resolved correctly during common_fit_params for this model architecture. |
|
Does this still happen with The backtrace indicates that this is happening even before the model weights have loaded. Another thing to try is to lower Basically the backtrace has failed at a point before the Adaptive MTP stuff even really kicks in. This is the pre-allocation part of the setup, so my immediate guess is a memory buffer overflow from a too large n-max value, or the good old: "You must use --fit off with MTP" issue. If it still persists after these changes then let me know. |
|
Thanks for the quick response. I tried both --fit on and lowering --spec-draft-n-max to 5, but unfortunately the crash persists with the same backtrace. Here's the error log: |
Qwen3.8-Flash-Next ships an MTP block in the checkpoint that the converter was dropping, so the model had no speculative path at all. Three pieces: - ggml-org#27836: the qwen4exp NextN/MTP draft head. Converter export (fc_embedding|fc_hidden fuse into the single eh_proj the shared NextN code expects), the nextn.hc_head_* tensors that stand in for the output norm qwen4exp does not have, and the LLM_GRAPH_TYPE_DECODER_MTP graph. Resolved against the fork's per-layer n_ff_exp accessor. - ggml-org#27210: adaptive draft depth, --spec-type draft-mtp-adaptive. Carries a delta-net fix that matters well beyond the adaptive path: build_conv_state was emitting a snapshot slot for every one of the n_rs_seq + 1 rollback depths, including the ones no rollback inside the batch can reach. Decode is one token, so all but one slot repeated the pre-batch state; the bound turns n_rs_seq into free headroom instead of a per-layer kernel-launch tax. - [fork] mtp_only/trunk_only probing in qwen4exp's load_arch_tensors, following the bailingmoe3/deepseek2 pattern. The draft head can now ship as its own GGUF: quantized apart from the trunk and pinned to the head GPU with --model-draft + --device-draft, rather than riding the trunk's tensor-split out onto the RPC fabric. Trunk-only tensors (hc_head_*, the PLE table, blk.0..n-1) become NOT_REQUIRED when the file has no blk.0, and the nextn block likewise when the file has no eh_proj. token_embd and output stay required in both halves, since qwen4exp sets mtp_use_dedicated_embeddings=false and the draft graph reuses them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U3H5motr51eTWujztSXykc
|
Sorry, my mistake — I had --fit on when I tested earlier. With --fit off, the model loads without any issues. |
--spec-type draft-mtp-adaptive with a separate MTP-head GGUF segfaulted in ggml_is_empty before the model finished loading. common_init_result runs the --fit estimation pass (on by default) before the real load, and it fits the draft model alongside the main one. It decided whether the draft context is an MTP context by matching only COMMON_SPECULATIVE_TYPE_DRAFT_MTP, so the adaptive type left ctx_type at LLAMA_CONTEXT_TYPE_DEFAULT. The fit pass then built the *trunk* graph for a draft-only file, where the trunk tensors are deliberately absent, and build_hc_mix passed a null hc_attn_norm to ggml_mul -> ggml_can_repeat -> ggml_is_empty. The fault address is tensor->ne[0], and the crash lands before tensor loading, which is why the log tail is empty: the threaded logger never flushed. ggml-org#27210 added the adaptive type and updated four copies of this predicate but not the fifth. Collapse all of them onto one common_spec_has_mtp() so the next type cannot drift again. That also fixes a second live instance of the same gap: the mtp_dev pin in common_model_params_to_llama was skipped for adaptive, so an embedded-MTP adaptive run would silently lose its --device-draft placement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U3H5motr51eTWujztSXykc
All good. I'm just glad we got you sorted out in the end. |
|
Follow-up from our August 18 datapost — same Strix Halo box, but this time at the PR head (45e3d26) with the embedded-MTP draft and the adaptive controller, not ngram-mod. Qwen3.8-27B UD-Q4_K_XL from unsloth,
Code is a wash on this run — the edge adaptive shows there is smaller than the run-to-run spread we see with this setup, so I'm not claiming anything from it. Prose is where it shows: adaptive is ~6% faster than fixed-4 and accepts 6.5 points more of what it drafts. The fixed-12 row is the one I keep looking at. 13.2% acceptance means most of those deep drafts were verification work that went nowhere, and on this chip drafts share memory bandwidth with the target so that work isn't free. This is the lane I called out in the August post — deep drafting into low-acceptance traffic — showing up in one run. We ran Happy to share the raw per-request jsonl, or run whatever config anyone wants on this box. |
|
Been using --spec-type draft-mtp-adaptive --spec-draft-n-max 12 for about 2 weeks now with Qwen3.8-27B-UD-Q4_K_XL on the R9700 on Linux/ROCm. It's one of the few methods that speed up both coding and prose in a single setting without sacrificing either. Getting roughly 34 tok/s on prose, 72 tok/s on code. As a comparison, before that i used a static n-max of 3 as a middle ground which gave about also 34 tok/s for prose yet 53 tok/s for code. Would be great to see this land in main. @CISC @ggerganov |
Small precision note on my post above. Throughput is the median wall-clock tok/s across the n=10 runs; acceptance is the aggregate share of drafted tokens the target model accepted across those runs. The adaptive-vs-fixed-4 prose throughput delta is +6.4%. The code comparison is descriptive of this single run only; I should not have characterized it beyond that. Finally, the hardware explanation in that post was not measured and should not be treated as a result. |
|
I tried this on two AMD MI50 cards (gfx906) running Qwen3.8-27B at 8-bit, split across both cards, on the mxxm gfx906 fork of llama.cpp (https://github.com/mxxm-t/mx-llama.cpp) at its b10811 build. Sampling at temperature 0.8, reasoning off, compared against the fixed 3-token draft. Letting the draft length move between 2 and 6: a file-rewrite prompt went from 75.7 to 79.8 tok/s, fresh code went from 68-71 down to 66.3, and fresh prose from 42.4 down to 41.3, with the length changing 36 times over the run. Between 2 and 4: 79.4 / 69.8 / 41.5 on the same three prompts. I think the loss on code and prose comes from how this backend caches its GPU work: it keeps a separate recorded version per draft length, every new length costs a few slow rounds to set up, and lengths that are only used now and then get their recorded version dropped as unstable. Both showed up in the logs during these runs. The other change in this PR, starting the recurrent-state copy at the first slot a rollback can reach, helped on its own: with the fixed 3-token draft, prose went from 41.3 to 42.4 tok/s with it. It might be worth splitting that part out so it can go in separately. Happy to rerun with other settings if that helps. |
|
Limiting the depth range from 2-6 isn't really expected to make a huge difference from, say, setting a fixed MTP depth of 3. There are always short bursts, even for predictable content, where the acceptance rates collapse for short periods, and it's in those periods that the algorithm needs enough of a buffer to ride over those short collapses. When the algorithm is prevented from scaling to the higher depths where the biggest speedups are found for predictable content, then those short periods of collapsed acceptance means that it will fall back to assuming that it's working with prose again far too quickly, and so it's not being given the chance to ride those sweet deep predictive highs for long enough for it to provide a real speedup. It should be noted that for llama.cpp, adjusting the depth does come with a small temporary performance hit, and the algorithm aims to amortise that cost through sustaining the high depths long enough to hide it. When the peak depth is set too low, it cannot do that. In my belief therefore that the algorithm working given the constraints you've set for it to work under. If, for memory reasons, that you'd like to reduce the upper range, then 8 would be about the lower end of where that should be. As the max-gain is dropped much below 8, then the algorithm essentially collapses back to not being a whole lot different to simply running a fixed depth of 2, 3, or 4. In a car analogy sense, you've basically put a speed governor on a Ferrari that limits it from going faster than the regular speed limit, and are then wondering why the car can't reach its destination any faster than a Toyota. I would guess, if anything, what probably needs to change here is to have the adaptive MTP code emit a warning at startup that if the maximum depth has been set to a value of 6 or lower that it's probably best to just stick with normal fixed MTP instead. Even a max depth of 7 is going to be kind of borderline. A maximum depth of 8 is the lowest I would set the maximum at to be sure of starting to see what the algorithm aims to deliver. I'll add a note to the PR description to point this out. |
|
Thanks, that is a fair point about the cap. I reran with the ranges you suggested, same setup as before: two AMD MI50 cards, Qwen3.8-27B at 8-bit split across both, the mxxm gfx906 fork at b10811, temperature 0.8, reasoning off. Three prompts (a file rewrite, fresh code, fresh prose), code and prose twice each. Decode speed in tokens per second; the fixed 3-token draft was run twice as a control.
The controller did what you describe. On code it climbed to depth 9 to 12 and stayed there for most of the run (58 depth changes, 41 of them between 9 and 12). Acceptance per position held up out to the deep positions, for example 0.93, 0.87, 0.79, 0.64, 0.51, 0.41, 0.36, 0.29, 0.21, 0.14, 0.09, 0.05 on one code prompt, and the mean accepted length per round went from 3.7 tokens to about 6.3. It still lost 30 percent on code because each round got much more expensive. From the server's own timings, one round at fixed depth 3 took about 53 ms, of which 9 ms was drafting. At depth 12 a round took about 131 ms, of which 24 ms was drafting. So verifying 13 tokens cost about 2.5 times as much as verifying 4 on this hardware, while the round produced 1.7 times as many tokens. I since measured the forward-pass cost against batch size directly on this build: 31 ms at 1 token, 42 at 4, 69 at 8, then a jump to 107 at 9 and flat to 32, which is the backend's kernel switching from a narrow mat-vec to a 32-column tile. Details in mxxm-t#10. On a card where a 13-token batch is still bandwidth-bound that cost would not show, which would explain why your numbers and ours differ. Prose was a wash at every setting. Acceptance at position 1 is under 50 percent on prose here, so the controller sat at the floor. The file rewrite gained 2 percent at 3..12 and lost 15 percent at 2..8, where the depth moved 45 times over the run. I agree a startup warning for low caps makes sense. It may also be worth capping on measured round time rather than on depth alone, since on compute-limited cards the best depth is low even when acceptance is high. Raw logs are available if useful. |
|
At this stage it sounds like the algorithm just isn't a good fit for your hardware. The cost of the drafting at high depth is high enough that it offers no gains for you. In your scenario, it may just be best to stick with the normal MTP use case. It's interesting because yours is the first scenario reported where it hasn't provided the proposed gains. Can't win 'em all I guess, and this is EXACTLY why I made this feature an explicitly optional configuration. |
Overview
Implements adaptive mtp with a new option
--spec-type draft-mtp-adaptiveSuggested configuration to use is:
--spec-type draft-mtp-adaptive --spec-draft-n-max 12Note: It is not recommended to set
spec-draft-n-maxto values lower than 7 with the Adaptive MTP algorithm. In this instance the algorithm isn't able to reach depths high enough to provide the benefits it aims to achieve. If you find yourself in a situation where insufficient VRAM prevents you from setting a depth of at least 7, or more preferably 8+, then it's generally going to be best to just stick with regular MTP.The algorithm is a counting based state machine with a climb counter and a weighted drop-pressure accumulator. The depth climbs one step after a number consecutive verifies that accepted every drafted token; any miss adds (N - acceptance) to the drop pressure and the depth drops one step once it exceeds a certain amount. High depths fall quickly (a total miss adds N), low depths hold, and at the floor no pressure accumulates at all. The specific climb difficulty and drop pressure values were empirically determined over a wide range of tests.
--spec-draft-n-maxbounds the upper adaptive range and both the floor and the cold-start depth default to 3. The default floor of 3 may be adjusted with--spec-draft-n-min-adaptive, although in testing 3 seems to be the best value to choose here.Overview of the climb-cost algorithm
It was experimentally determined that a draft MTP depth of 2 or 3 is close to optimal for reasoning and prose.
More typically an MTP depth of 3 was found to be optimal, and so this was chosen as the baseline default.
When the depth is less than three the algorithm allows for an easy transition from a depth of 1 to 2.
A slightly higher cost from 2 to 3 is used to minimise the depth oscillating between two and three.
To climb past a depth of three requires passing a hardened barrier. The hardened barrier is in place to
minimise easily reaching a depth of four which is known to be harmful to performance for reasoning and prose.
Beyond a depth of four the barrier to climb higher is gradually reduced.
This allows for predictive runs to quickly ascend as the admission rates indicate that this is useful.
Latest Testing and Performance Results
(Updated 23 Aug 2026)
Latest performance results can be seen here: #27210 (comment)
Extended discussion of results can be seen here: #27210 (comment)
Test Setup
Qwen3.8-27B-Q8_0.gguf9950X3D2, ROCm,
HIP_VISIBLE_DEVICES=0,2,GGML_CUDA_DISABLE_GRAPHS=0llama-serverfrom branchadaptive-mtp(build 10459, 0c6426b,freshly rebuilt)
ctx 262144, f16 KV,
--parallel 1, mlock, reasoning on(
--reasoning-budget 65536 --reasoning-preserve)timings.predicted_per_second(generation only, >= 4000 tokens per request)
Configurations Tested
--spec-draft-n-max 3--spec-draft-n-max 12 --spec-draft-p-min 0.75--spec-draft-n-min-adaptive 3 --spec-draft-n-max 12--spec-draft-p-min 0.75--spec-ngram-mod-n-match 24 --spec-ngram-mod-n-min 48 --spec-ngram-mod-n-max 64Results
Additional information
The total diff size is +451/-26
This PR touches on 10 files which seems large at first, but this mostly arises from integrating the new command line options. The main functional changes take place within a single file (speculative.cpp).
218 lines are for the unit test file. 92 lines are for the header file that contains the algorithm itself.
The rest of the line changes are the wiring of the algorithm into the llama.cpp code base, and the command line option handling.
I tried to keep the size of the functional changes to llama.cpp itself as small as possible, and I tried to keep this diff as independent as I could so that unless it's specifically activated, it won't interfere with normal llama.cpp operations.
"Horses for Courses"
While Adaptive MTP generally works well for most hardware and models, it can make performance worse on older hardware that cannot run the deep drafting and acceptance steps fast enough to realise the potential gains it offers. In these instances it is generally better to not enable Adaptive MTP and just stick with regular MTP with a low fixed depth.
Dense Models generally receive the most benefit, but gains on MoE models are generally weaker. For MoE models it's generally preferable to lower the maximum depth to no more than 8 and then assess. If the performance is no better than using a fixed MTP depth, then that's fine. Just use a fixed MTP depth instead.
Related Issues/PRs
I searched known issues and open PRs and did not find anything precisely mentioning this sort of feature at first.
@Us5rName pointed out in the comments below that they have a similar feature PR here: https://github.com/Us5rName/llama.cpp/tree/draft-adaptive-length-rolling-window
Requirements