Skip to content

qwen4exp: run MTP heads that share the target's token_embd and output - #240

Open
danielhanchen wants to merge 2 commits into
base/upstream-23b0202a1from
compat/qwen4exp-shared-mtp
Open

danielhanchen wants to merge 2 commits into
base/upstream-23b0202a1from
compat/qwen4exp-shared-mtp

Conversation

@danielhanchen

Copy link
Copy Markdown
Member

Summary

Upstream merged Qwen4Exp MTP (ggml-org#29761), superseding our #144, but the MTP heads published in unsloth/Qwen3.8-Flash-Next-GGUF/MTP/ still do not run on mainline:

head upstream b11368
mtp-*-shared-*.gguf (recommended, no token_embd / output) fails to load: check_tensor_dims: tensor 'token_embd.weight' not found
mtp-*-{BF16,Q8_0,Q4_K_M}.gguf (self-contained) loads, then aborts on the first decode: GGML_ASSERT(buffer) failed

The shared heads were introduced in #142 (nextn_shared_target_tensors), which borrowed the target's tensors in the loader; that went away with #144.

Changes

  1. Shared heads: when a qwen4exp model has no token_embd / output and is created as an MTP draft (LLAMA_CONTEXT_TYPE_MTP), llama_context records the target model from params.ctx_other in a new cparams.model_tgt, and graph_mtp takes the embedding and LM head (with its scale) from it. cparams.ctx_other is deliberately left unset: for draft-mtp, common/speculative.cpp reads it as "the draft shares the target's memory" (gemma4-assistant), and it is also passed as mem_other to the memory. load_arch_tensors makes token_embd optional only for MTP-only files and no longer duplicates a missing token_embd as output. Any other use of such a head (e.g. --spec-type draft-simple) fails with a clear error instead of a null dereference; the memory-fit pass fails gracefully as before.
  2. Dense MTP block: our heads store compress_ratios[n_layer] = 0 (the MTP block attends densely), while upstream's converter writes the QSA ratio there. graph_mtp built the k-pool input unconditionally, but the draft's indexer cache then holds no layer, so k_idxs was never allocated and set_input asserted. The k-pool input is now built only for a QSA MTP block; build_layer_attn already treats ratio 0 as dense. Heads converted by upstream are unaffected.

Testing

Qwen3.8-Flash-Next UD-IQ1_S + -md <head> --spec-type draft-mtp --spec-draft-n-max 2, llama-server, greedy, 3 prompts x 3 repeats, one exclusive B200:

build head acceptance decode tok/s
this PR none - 93.4
this PR shared Q4_K_M 89.2% 149.3 (1.60x)
this PR self-contained Q4_K_M 89.7% 148.5
last good nightly (b11160 + #144) shared Q4_K_M 89.7% 150.0 / 148.5 (ABBA, same card)
  • Shared and self-contained heads produce byte-identical output on every prompt, including a 3248-token prompt past the indexer budget and two concurrent requests with -np 2.
  • Without a head, output and speed match upstream b11368.
  • test-llama-archs (133/133) passes.

Related: #227 (fit: measure a draft head that borrows the target's embeddings) is complementary; without it the fit pass logs one "failed to measure the memory of the extra model" line for shared heads, as before.

Base is b11368 (base/upstream-1fb7ef3e3). The full nightly pin set merges cleanly with this PR on top.

MTP heads exported with nextn_shared_target_tensors carry no token_embd
or output, which load_arch_tensors required, so mainline refused them.
For such a head created as an MTP draft, take both from the target the
draft context is created against. ctx_other itself stays unset: for
draft-mtp it means the draft shares the target's memory, which this head
does not. Any other use of such a head fails with a clear error rather
than a null dereference.

Heads converted before upstream MTP support also mark the MTP block dense
(compress ratio 0). graph_mtp built the k-pool input regardless, but the
draft's indexer cache then holds no layer, so the input was never
allocated and the first decode asserted in set_input. Build it only for a
QSA MTP block; build_layer_attn already treats ratio 0 as dense.
@danielhanchen
danielhanchen requested a review from CISC as a code owner October 4, 2026 05:35
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 36175a8325

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/models/qwen4exp.cpp
Comment on lines +558 to +559
const llama_model & emb = model.tok_embd ? model : *cparams.model_tgt;
const llama_model & head = model.output ? model : *cparams.model_tgt;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Register target devices before borrowing target weights

When a shared head is run with --spec-draft-device excluding the device that holds the target LM head, this inserts a preallocated target tensor into a graph whose scheduler was created only from the draft model's model.devices. No registered backend can support that buffer, so scheduler assignment aborts instead of decoding. Include the target tensor devices in the draft context's backend set, or reject incompatible device configurations before building the graph.

Useful? React with 👍 / 👎.

Comment thread src/llama-context.cpp
Comment on lines +169 to +171
if (cparams.ctx_type != LLAMA_CONTEXT_TYPE_MTP || !tgt || !tgt->tok_embd || !tgt->output ||
tgt->hparams.n_embd != model.hparams.n_embd || tgt->vocab.n_tokens() != model.vocab.n_tokens()) {
throw std::runtime_error(model.arch_name() + " head without token_embd/output only works as an MTP draft (--spec-type draft-mtp) of a matching target (this warning is normal during memory fitting)");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Measure shared heads during automatic fitting

With a separate shared MTP head and the server's default --fit on, the extra-model measurement creates this context without ctx_other, so this condition rejects it. common_params_fit_impl catches that failure and substitutes zero memory for the entire extra model, omitting the head weights, KV cache, and compute buffers; on memory-constrained systems the fitter can therefore retain an oversized context or offload plan and the real load can OOM. The no-allocation fitting path needs a way to validate/build this context without requiring a live target context.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-04T05:43:41.625021Z 36175a8 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Shreesh-Sree pushed a commit to Shreesh-Sree/llama.cpp that referenced this pull request Oct 6, 2026
…ed heads (unslothai#242)

Drop ggml-org#27754, unslothai#144 and unslothai#137: upstream merged GLM-5-Next (ggml-org#27773),
Qwen4Exp MTP (ggml-org#29761) and row prefetch (ggml-org#29599), and the old pins no
longer merge. Old-GGUF loading moves to unslothai#239, shared MTP heads to unslothai#240,
and what the dropped pins had beyond upstream to unslothai#241.

Repin ggml-org#24423, ggml-org#25731, unslothai#61 and unslothai#176 to their
conflict-fixed heads.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant