Repository navigation
spec : the MTP draft head as a view of the reordered output head, without memory of its own - #46
Merged
Merged
Conversation
…hout memory of its own The draft head of --spec-draft-vocab was a copy of the list's rows of the output head (168 / 100 MiB on a 4070 / 3060 for 66k tokens of Qwen3.8 27B), which did not fit next to a 95k-token prompt at 262k context. Now the rows of the output head are written again at load in an order where the list's tokens come first in each device's slice (spread over the devices like the head's rows), and the draft head is a view of the first rows of each slice: - ggml-backend-meta: ggml_backend_meta_tensor_n_slices / _slice give the device tensors of a split weight, ggml_backend_meta_tensor_init_row_views makes a weight of views of the first rows of each slice of another one (its split state from the llama callback); without split mode tensor a plain view - llama_context: the target's logits come in the head's row order; a row is put in token order when it is read (llama_get_logits_ith / llama_get_logits), into a buffer of its own, so unread rows cost nothing - only for a head of its own in the plain buffer type of a device (the meta buffer type under -sm tensor), else the copy as before; backend samplers are not used with such a model, LoRA adapters of the output head are refused, the model saver asserts - test-llama-archs: a model loaded with a draft vocabulary gives bitwise the logits of the same model without it Qwen3.8 27B, 4070 + 3060, -sm tensor, MTP 2: generation as with the copy (63.6 vs 63.7 t/s, no list 59.2), greedy outputs and MTP draft candidates identical to the copy, VRAM = no list; a 98.6k prompt next to a generating slot keeps the min free VRAM of the config without the list (291 / 120 MiB). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…thout elements Review of #46: - The view is taken only where device buffers keep a weight's rows as they are (CUDA, ROCm, MUSA, Vulkan, Metal, and a meta device over them). OpenCL, CANN and SYCL store some weights transformed as a whole, so a partial write or a view of the first rows would be wrong there; they keep the copy. - The logits API puts all rows in token order on its first use instead of only the requested one: a row pointer of llama_get_logits_ith is followed by the next rows again, as documented (perplexity and imatrix walk rows from one pointer). The buffer only grows, no zero fill per decode. - Under split mode tensor tokens move between the devices; on GPUs of different types (4070 + 3060) the logits of moved tokens differ in the last bits (up to ~2e-6, argmax unchanged in the probes). Documented; the test compares with a tolerance there and bitwise on one device. - test-llama-archs: a context with draft_vocab checks the draft head's logits against the full head at the list's tokens, also with a one-token list (no draft head rows on the first device under split mode tensor). For that a draft_vocab context no longer has to be an MTP context: its output head projects onto the draft vocabulary. ggml-meta: the slices of a tensor take the strides of the dimensions outside the split one scaled by their share. With one element along the split dimension the next dimension has the same stride and was not scaled, so a slice without elements kept the full stride and a read of it went past its end (the new test: draft logits of a one-token list under split mode tensor). Dimensions after the split one with the same stride are scaled now. ggml: ggml_backend_meta_dev_n_simple / ggml_backend_meta_dev_simple give the simple devices of a meta device. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner
Author
|
Review fixes in 26544da: (1) the -sm tensor rounding on mixed GPUs is documented and the test tolerates it, (2) the logits API orders all rows at once, so a row pointer walks the next rows again, (3) the view only on CUDA/ROCm/MUSA/Vulkan/Metal (and meta over them), (4) the test now multiplies with the draft head itself; it found a ggml-meta stride bug for slices without elements, fixed in the same commit. test-llama-archs llama|qwen35|qwen35moe: 64/64. 🤖 Generated with Claude Code |
This was referenced Oct 20, 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.
The MTP draft head of
--spec-draft-vocab(#36) was a copy of the list's rows of the output head: 168 / 100 MiB on the 4070 / 3060 for the 66k-token list of Qwen3.8 27B. With 262k context in two slots that did not fit: a 95k-token prompt next to a generating slot ran the 3060 out of memory (cudaGraphInstantiate), so the +7% could not be deployed. This makes the draft head a view of the output head, without memory of its own.How
ggml_backend_meta_tensor_n_slices/ggml_backend_meta_tensor_slicegive the device tensors of a split weight, andggml_backend_meta_tensor_init_row_viewsmakes a weight whose slice j is a view of the first rows[j] rows of another weight's slice j (the split state callback reports its rows per device). Without split mode tensor the draft head is a plainggml_view_2d.llama_contextputs them in token order when they are first read (llama_get_logits_ith/llama_get_logits, all rows at once so a row pointer is followed by the next rows as before), into a buffer of its own (~60 us per row of 248k tokens); a decode whose logits nobody reads costs nothing, and the samplers are unchanged.-sm tensor. Tied heads, heads in host memory, row-split buffers and OpenCL/CANN/SYCL (they store some weights transformed as a whole) keep the copy.-sm tensortokens move between the GPUs (the list is spread like the head's rows, so both keep their share of the draft head). On GPUs of different types the logits of moved tokens differ from those without the list in the last bits: up to ~2e-6 on the 4070 + 3060, argmax unchanged; on one GPU they are bitwise equal.llama_context_params::draft_vocabno longer has to be an MTP context (its output head projects onto the draft vocabulary); the test uses that.-sm tensor).Tried and dropped: reordering every row in place when the context synchronizes (~90 us per row, -1.4% against the copy), and reporting the rows in head order with their tokens through the sampled-candidates API (no reorder at all, but -11%: the penalties sampler's fast path needs the candidates in vocabulary order).
Results (Qwen3.8 27B UD-Q4_K_S, 4070 + 3060,
-sm tensor, MTP depth 2, production settings)mtp-bench(single slot, 384 tokens, 2 reps; mean over write-code, edit-code, explain, list), two runs each for view and copy:German letter (production sampling): no list 47.2 -> view 50.0 t/s. Acceptance of view and copy is identical in every task.
test-llama-archs -a '^(llama|qwen35|qwen35moe)$'(CPU, each GPU, the Meta configs): all 64 passed, with a new check: a model loaded with a draft vocabulary gives the logits of the same model without it (bitwise on one device, nmse under-sm tensor), and a draft_vocab context gives the full head's logits at the list's tokens, for a list of a third of the vocabulary and one of a single token.llama_get_logits_ithpointer now match (before: rows 2.. were zeros).🤖 Generated with Claude Code