Repository navigation
llama : MTP draft vocabulary (--spec-draft-vocab, llama-draft-vocab) - #36
Conversation
An MTP draft step multiplies its hidden state with the whole output head and samples over the whole vocabulary: for Qwen3.8 27B (248k tokens, Q6_K head) 1.37 ms of GPU time per drafted token, plus ~0.7 ms of logits copy and CPU sampling on the critical path of the draft chain. - llama_model_params::draft_vocab: token ids; after load the model gets output_draft.weight, a copy of their rows of the output head read from the model file (same type, same buffer type, split by rows like the head under split mode tensor) - llama_context_params::draft_vocab: an MTP context projects its draft head onto it (build_lora_mm substitutes the output head in the graphs of that context only) and reports its logits through the sampled-candidates API (llama_get_sampled_logits_ith / _candidates_ith), so common_sampler samples over the subset; backend sampling is not used then. The target still verifies every draft against the whole vocabulary: the output is unchanged, a token outside the list is never drafted. - --spec-draft-vocab FILE (token ids, # comments); llama-draft-vocab builds the list from a text corpus (the most frequent tokens covering --coverage of it, plus control and end-of-generation tokens) - sampler clone/copy copy only the live candidates instead of the n_vocab scratch buffer The idea follows the draft vocabulary of Strata (https://github.com/Niko1221/Strata, MIT). Qwen3.8 27B UD-Q4_K_S, 4070 + 3060 -sm tensor, a 50k-token list from code and English text (99.9% coverage), mean of 4 synthetic tasks: 57.8 -> 64.7 t/s at temperature 1.0 (draft depth 2 -> 3), 62.0 -> 70.9 t/s greedy; code editing 65.2 -> 80.3 t/s. The first 30 draft candidates of a greedy request are identical with and without the list (ids and probabilities). ~200 MiB more VRAM over both GPUs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e n_vocab scratch buffer The server clones the slot's sampler before every speculative verify; cur holds n_vocab entries (3 MB for a 248k vocabulary), of which only those cur_p still lists are read before the next sample refills it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- the draft head takes the input transform (prism.hadamard) and the activation precision of the output head, looked up with the output head (llama_prec_policy::apply(res, key)); LoRA adapters of the output head are not applied to it - a sequence whose recent text is often outside the list (a language the corpus lacks) drafts over the whole vocabulary: the MTP drafter checks the last 64 tokens of the drafting sequences and switches the context with llama_set_draft_vocab() while more than --spec-draft-vocab-max-miss (default 0.10) of them are outside it, back below half of it; the graphs compare their head for reuse. German with a code/English list at depth 3: 38.5 -> 40.6 t/s at temperature 1.0 and 43.0 -> 48.0 greedy, as without a list (40.6 / 48.0); code editing keeps 79.9 / 86.4 t/s - llama_model_draft_vocab(): the sorted ids - only the model the MTP context runs on gets the draft head (a separate MTP draft model, not the target) - a no_alloc load (fit) counts the draft head with a dummy buffer - llama-draft-vocab also adds the user-defined tokens (<think>, <tool_call>, ...) - docs: what happens to a language outside the list Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review fixes (5 findings) in bc24d81:
Docs: the server README rows and the tool README describe what happens to a language outside the list. 🤖 Generated with Claude Code |
- the MTP context reserves its compute buffers for the whole head: a switch to it (llama_set_draft_vocab) reallocated them, GGML_SCHED_DEBUG_REALLOC aborted - backend draft sampling is off only when the model has a draft head, not for every --spec-draft-vocab (a list the model skipped kept it off) - the head goes to the whole vocabulary while most drafting sequences are outside the list, per-sequence hysteresis: one conversation in another language no longer takes the gain of the others - models whose MTP layers have their own head (nextn.shared_head_head) skip the list with a warning instead of allocating an unused copy - fit counts the draft head of a separate MTP draft model - docs: the majority rule, CPU draft sampling with a list, the shared_head_head limit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rman list Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Second review (5 findings, none changed the output) fixed in cda23ab, main merged in 7e6cf41:
The README row and the PR description now show depth 2 (the production choice) with a 66k English + German + code + man-page list: +7% at temperature 1.0 and greedy, German included (46.9 -> 50.0 t/s). The review also found a crash in main with 🤖 Generated with Claude Code |
Problem
An MTP draft step projects onto the whole output head and samples over the whole vocabulary. For Qwen3.8 27B (248k tokens, Q6_K head, 4070 + 3060
-sm tensor) that is 1.37 ms of GPU time per drafted token, at the memory bandwidth limit of both cards, plus ~0.7 ms of logits copy and CPU sampling on the critical path of the draft chain (nsys: the GPU idles 1.07 ms after every draft head). Two drafts per step make that about 10% of a decode step. It also means a third draft token does not pay: depth 3 measured no faster than depth 2.Change
llama_model_params::draft_vocab: a list of token ids. After load, the model getsoutput_draft.weight, a copy of those rows of the output head. The rows are read from the model file, keep the head's type and buffer type, and are split by rows like the head under split mode tensor.llama_context_params::draft_vocab: only for an MTP context.build_lora_mmsubstitutes the output head through the graph params, in that context's graphs only.llama_get_sampled_logits_ith/_candidates_ith), socommon_samplersamples over the subset. Backend sampling is not used then.--spec-draft-vocab FILE: token ids, whitespace separated,#comments.llama-draft-vocab(tools/draft-vocab): builds the list from a text corpus. It takes the most frequent tokens covering--coverageof the corpus, plus the control and end-of-generation tokens.common_sampler_clone/copycopy only the live candidates instead of the n_vocab scratch buffer (the server clones before every verify).The idea follows the draft vocabulary of Strata by @Niko1221 (MIT). It is credited in the README features table.
Results
Depth 2 with an English + German list
The production choice is depth 2, because German drafts worse than code and depth 3 made German slower. The list is 66k tokens from English, German (Wikipedia), code and man pages.
Qwen3.8 27B UD-Q4_K_S, production config (2 slots, q5_1 KV, MTP depth 2), 384 tokens, median of 2 runs, after the second review:
Acceptance is unchanged (German 0.390 vs 0.386 at temp 1.0). VRAM: the 66k list takes ~266 MiB over both GPUs.
Depth sweep (first version, list from code/English only)
Mean of 4 synthetic tasks (write code, edit quoted code, explain, list), 384 tokens, median of 3 runs. The lists come from public text: the llama.cpp sources, the Python stdlib, wikitext test, Strata's docs.
Validation
Notes
nextn.shared_head_head) does not use the list (warning at load).--spec-draft-vocab-max-miss(0.10) of the last 64 tokens of most drafting sequences are outside the list, the context drafts over the whole vocabulary; German with a code/English list then runs as without a list. The tool takes any text; the ids depend only on the tokenizer.--spec-draft-backend-sampling(not available with-sm tensor) the fallback is about 2% slower than no list.🤖 Generated with Claude Code