Skip to content

qwen4exp MTP: reduced draft vocabulary (d2t + K-row head) - #56

Merged
bong-water-water-bong merged 1 commit into
1bit/vulkan-upstreamfrom
1bit/flash-next-draft-vocab
Oct 1, 2026
Merged

bong-water-water-bong merged 1 commit into
1bit/vulkan-upstreamfrom
1bit/flash-next-draft-vocab

Conversation

@bong-water-water-bong

Copy link
Copy Markdown

Lets a Qwen3.8-Flash-Next (qwen4exp) MTP draft head draft from part of the vocabulary.

  • A draft head GGUF may carry blk.<L>.nextn.shared_head_head.weight with K rows and an I64 d2t [K] (draft row -> token id). Without d2t nothing changes.
  • The draft pass runs the K-row head, then scatters the logits into a full-vocabulary row (-inf elsewhere) with the same ggml_fill + ggml_set_rows pattern dflash.cpp uses.
  • Our code: src/models/qwen4exp-draft-vocab.{h,cpp} (Apache notice); qwen4exp.cpp gets two hooks.
  • The engine side (tool that writes these files, id list, NOTICE) is a separate engine PR.

Measured

Strix Halo, Vulkan, Qwen3.8-Flash-Next UD-Q4_K_XL, Unsloth's shared Q8_0 MTP head, greedy, draft length 3, -lm mmap -lzm on, coder-llm stopped, box.lock held. Median of rounds 2-3 of 3.

Prompt Full head (644 MiB) 65,536-id head (170 MiB)
code, 256 tok 47.8 tok/s, 186/207 accepted 52.1, 186/207
prose, 256 tok 38.6, 169/258 42.2, 169/258
code, 768 tok 45.8, 552/642 48.3, 544/669
French, 26 tok 46.4, 18/21 39.6, 18/27
ms per decode step (code) 76.2 69.9

The id list is English + code (dime-online/qwen3.8-Flash-DGX-UltraFast, Apache-2.0), so French drafts get rejected more often. Without a draft: 23.7 tok/s.

Output

Greedy text compared with decoding without a draft:

  • The full head is not token-identical there either. Prose and long code each flip once at a near-tie, from batched verification.
  • The 65K head gives exactly the full head's text in rounds 2-3. In round 1 it flipped at two other near-ties.

Unsloth's shared Q4_K_M head was also tried: no faster (46.7 code) and more flips, so not used.

Tests

  • Built on strixhalo (llama-server, llama-cli, test-llama-archs).
  • Real-model runs above.
  • The engine PR adds a unit test for the file writer.

🤖 Generated with Claude Code

A draft head GGUF may now carry blk.<L>.nextn.shared_head_head.weight with K rows plus an
I64 d2t [K] (draft row -> token id). The draft pass runs the K-row head and scatters its
logits into a full-vocabulary row that is -inf elsewhere, the same set_rows scatter DFlash
uses. The target still verifies every drafted token. Our code is in
src/models/qwen4exp-draft-vocab.{h,cpp}; qwen4exp.cpp keeps two hooks.

Qwen3.8-Flash-Next UD-Q4_K_XL on Strix Halo (Vulkan, greedy, draft length 3, rounds 2-3 of
3): the 65,536-id English + code list from dime-online/qwen3.8-Flash-DGX-UltraFast turns the
borrowed 644 MiB Q8_0 output matrix into a 170 MiB head. Code 47.8 -> 52.1 tok/s, prose
38.6 -> 42.2, 768-token code 45.8 -> 48.3; step 76 -> 70 ms; acceptance 186/207 both. A
French prompt is slower (46.4 -> 39.6): its tokens are outside the list. Text in rounds 2-3
is the same as with the full head; neither is token-identical to decoding without a draft
(near-tie flips from batched verification, present with the stock head too).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the model label Oct 1, 2026

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review (PR-Agent duty). The approach is good: the K-row head plus a scatter to a full row of -inf reuses DFlash's set_rows path. All the code is in our headed files (qwen4exp-draft-vocab.{h,cpp}), with two hooks in qwen4exp.cpp. And the target still verifies every token.

One fix before merge:

  • Validate the d2t ids. qwen4exp_draft_vocab_rows checks d2t's type, rank and length, but not its values. A d2t id that's negative or >= n_vocab makes ggml_set_rows write outside the full-vocabulary row, and duplicate ids silently drop a row. d2t is tiny (K int64s), so check it once after tensor data is loaded (0 <= id < n_vocab, and no duplicates) and fail the load with a clear message. A malformed or hand-made draft GGUF shouldn't be able to corrupt memory.

Notes:

  1. Non-English. French drops 46.4 -> 39.6 tok/s (-15%) because its tokens fall outside the 65,536-id list. Since the reduced head only exists when the draft GGUF carries it, the default is unaffected. Please keep it that way: 1bit serve shouldn't auto-pick a reduced-head draft, and the docs that mention it should state the non-English cost next to the speedup.
  2. Provenance of the id list. The 65,536-id list comes from dime-online/qwen3.8-Flash-DGX-UltraFast. Before we publish a reduced-head GGUF (HF org) or commit the list, check that repo's license, and credit it in the model card and NOTICE (per-model license checks, as with every HF upload).
  3. Measurements. "Rounds 2-3 of 3" means the warm rounds. Fine, but say so in the docs table too.

Mergeable once the d2t value check is in.

@bong-water-water-bong
bong-water-water-bong merged commit fd16437 into 1bit/vulkan-upstream Oct 1, 2026
9 of 11 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the 1bit/flash-next-draft-vocab branch October 1, 2026 19:53
@bong-water-water-bong

Copy link
Copy Markdown
Author

Post-merge: the d2t value validation from the review is still missing (see engine ggml-org#269). Needed before the engine pins this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant