qwen4exp: load the MTP head's hc_head_norm as [n_embd, hc] like the trunk gammas - #219
Conversation
The rebase onto the upstream rms_norm + mul fusion moved the trunk
hyper-connection gammas to { n_embd, hc } with TENSOR_ALLOW_RESHAPE, but left
the MTP head's gamma flat. build_hc_mix then multiplies a [hc_dim] weight into
an [n_embd, hc, n_tokens] stream and ggml_can_repeat aborts while the draft
context is reserved.
|
Independent verification, since this hasn't been built yet: reproduced the crash and confirmed the fix on a from-source CUDA build. Setup: RTX 5090 (32GB), unslothai/llama.cpp mix Before fix: reproduces exactly as described — Fix applied: functionally equivalent patch (reshape 66.1% acceptance matches Unsloth's own published number for For what it's worth, on this setup (Q2_K_XL, most MoE experts CPU-offloaded — i.e. bandwidth-bound, not the fully GPU-resident B200 case in the docs) the real-world speedup was much smaller than 1.3–1.7x: roughly a wash at 1 stream, and prefill measurably regressed (~-22%, 2440→1900 tok/s at 15k context). Matches the "skip MTP for concurrent serving" guidance in the MTP README, just adding a data point for the CPU-offload case specifically. Happy to share full logs/config if useful. |
common/fit.cpp opens the draft model on its own to price it, with
no_alloc set and no target attached. borrow_shared_tensor threw there,
so the fit caught it, warned "failed to measure the memory of the extra
model, fitting without it" and then budgeted nothing for the draft. The
real load ran out of device memory by roughly the head's own size:
error loading model: borrow_shared_tensor: this model is a draft head
without its own 'token_embd.weight'; load it as a draft of its target
model, not on its own
failed to measure the memory of the extra model, fitting without it
allocating 2647.04 MiB on device 0: cudaMalloc failed: out of memory
Refusing there protected nobody, since the caller only writes the draft
out of the budget. On a measurement load the borrowed tensor is reported
to the caller instead, which stands a shape in for it so the measurement
can finish and report the bytes this file does own. The shape goes on the
CPU buffer type: the graph needs the dimensions to reserve correctly, but
those bytes belong to the target and are counted in its own measurement,
so charging them to a device would trade under-budgeting the draft for
over-budgeting it by the size of the embedding.
A real load always has a target by construction and keeps the hard error,
so a borrowing head opened on its own still refuses rather than running on
uninitialised embeddings.
Measured on Qwen3.8-Flash-Next UD-Q4_K_XL with the card held at 30.7 GiB
free, -c 8192, --fit on, --spec-type draft-mtp --spec-draft-n-max 2:
mtp-Qwen3.8-Flash-Next-shared-Q8_0.gguf before: OOM at load
after: 63.6 tok/s
mtp-Qwen3.8-Flash-Next-Q8_0.gguf 61.2 tok/s, unchanged
no drafter 43.8 tok/s
The shared head is the one the model card recommends as fastest, and this
is the first configuration in which a 16 to 32 GB card can load it.
5967b90 to
fafa22f
Compare
ca14269
into
unslothai:mtp/qwen4exp-nextn
* Repin the Qwen3.8-Flash-Next MTP carry onto the hc_head_norm fix #144 is pinned at f40f64a, which aborts at load with GGML_ASSERT(ggml_can_repeat(b, a)) in build_hc_mix whenever the Qwen3.8-Flash-Next MTP head is attached. Every mix build since b10995-mix-3e83366 carries it. #219 is f40f64a plus one line, so this repin merges identically and needs nothing else changed. * Move the pin to #219's head, which now also fixes the fit measurement The branch gained a second commit: a borrowing draft head could not be measured by common/fit.cpp, so the fitter budgeted nothing for it and the recommended shared-* sidecar ran out of device memory on a 16 to 32 GB card. Both fixes ship together or the shared head stays unusable. * Follow #219's amended head after its comment pass * Pin #144 at its own head now that #219 has landed on it #219 merged into mtp/qwen4exp-nextn rather than master, so the carry PR now holds both fixes and the entry goes back to being a single #144 pin. Pinning the merged #219 instead would point the set at a closed PR for work that is already in the open one. ca14269 is a squash of the commits measured earlier and produces a byte-identical tree, so those numbers carry over unchanged. --------- Co-authored-by: danielhanchen <unslothai@gmail.com>
Fixes the
GGML_ASSERT(ggml_can_repeat(b, a))abort that kills the Qwen3.8-Flash-Next drafter on every mix build since b10995-mix-3e83366.Reported in:
What broke
f40f64a81("Fix merge conflicts") rebased this branch onto upstream's rms_norm + mul fusion for qwen4exp (ggml-org#28896). That reshapes the hyper-connection gammas: they now load as{ n_embd, hc }withTENSOR_ALLOW_RESHAPE, andbuild_hc_mixmultiplies the[n_embd, hc, n_tokens]stream by the gamma directly instead of flattening tohc_dimfirst.Three trunk gammas got converted (
qwen4exp.cpp:170,:220,:224), and so did the comment sitting above them:The MTP head's gamma, twenty lines further down at
:280, did not:graph_mtphands that straight tobuild_hc_mix(:643), so the firstggml_multhere sees a[10240]weight against a[2560, 4, n_tokens]input.ggml_can_repeatsays no and the process aborts, ingraph_reservebefore a single draft token is decoded.Nothing about the GGUF changed, which is why the same files work on b10909-mix-bea84f7. The weight was always flat; only the expected shape moved.
The fix
Same treatment the trunk gammas got, and
TENSOR_ALLOW_RESHAPEis what lets the flat[10240]tensor in the file satisfy the 2D declaration.nextn.hnormat:277is still{ hc_dim }and stays that way. It looks like the same oversight but isn't:graph_mtpreshapes to 2D before multiplying by it (:540-542), so that multiply is well formed either way.Testing
I have not built this. The change is a code read against
f40f64a81, the GGUF header and the backtrace; the run below is someone else's.@SvenMeyer applied the same change to the shipped
~/.unsloth/llama.cpptree, rebuilt, and got a working drafter (unslothai/unsloth#11143):The crash itself is reported on macOS/Metal (M4 Max), Windows CUDA (2×5060 Ti + 3060, and a 5080), and Linux CUDA, with both the shared and the standalone head, flash attention on and off, sharded and
gguf-split --merged targets, mmap and--load-mode none. It is not hardware or file specific: any path that reachesgraph_mtphits it.Also worth knowing
Studio catches the exit, retries once without flash attention, then falls back to loading with no speculative decoding at all. The load API still returns 200, so the only user-facing symptom is that decode got slower. That's tracked separately.
Once this lands, #144's pin in
scripts/unsloth/pr-set.jsonneeds a bump to the new head, same shape as #217.Diagnosis by @zachweyland, fix confirmed by @SvenMeyer.