Four fixes Qwen3.8-Flash-Next needs to work - #362
Conversation
parse_tensor_buffer_overrides() only collected each device's own buffer type, so a tensor could be pinned to a device or left on the CPU, but not placed in that device's pinned host memory and read there in place. That placement is worth having for a tensor that is large enough to matter and sparse enough not to hurt: it is gathered from rather than streamed, so only the touched rows cross the bus. Qwen3.8-Flash-Next needs it. Its n-gram table is 27 GB of a 91 GB model and every token reads a handful of 160-element rows from it, so leaving it in host memory is the difference between the model fitting on a 64 GiB card and not. Whether a backend will accept a host buffer as a kernel input stays the backend's decision; this only makes the placement expressible. Not TurboQuant-specific. Worth sending upstream once the paperwork allows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxP6x5bmUDYFvmouceN2mR
The never-quantize list already carries the small structural tensors of gemma3n (altup, laurel, per_layer_model_proj), mamba and kimi (ssm_conv1d), rwkv (time_mix_*), minimax (indexer projections) and t5. Nothing was ever added for qwen4exp, so its state-space gains, its hyper-connection injection matrices and its n-gram conv kernel were quantized along with everything else. Those five classes are 0.012B of Qwen3.8-Flash-Next's 176.9B parameters, and they are structural rather than arithmetic: the injection matrices decide how the token embedding enters each layer, and the gains set the decay of the recurrence. Quantizing them is wrong for the same reason it is wrong for every architecture already on this list. Scope, measured rather than assumed: on its own this does not make a TQ3_1S quant of Qwen3.8-Flash-Next usable. That build still answers every prompt with identical text. Reaching sensible output also needed higher precision on 18 further tensor classes, matching what published builds of this model use. That part is a gap in TurboQuant's allocation policy, not a missing exclusion, and is left for separate work. Not TurboQuant-specific, and intended for upstream as well: llama.cpp's list at ggml-org is byte-identical and equally missing these entries, so a stock Q4_K_M build of this model damages the same tensors. Landing here first because the fork needs it to produce a working quant at all; not yet filed upstream. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxP6x5bmUDYFvmouceN2mR
An MTP-only file is detected at load (blk.0.hc_attn_norm.weight absent) and the trunk tensors are made optional for it, so model.hc_head_* is legitimately null. graph_mtp then asserts on model.hc_head_norm and aborts, so no draft-only export can be used - including the published Qwen3.8-Flash-Next MTP heads, which carry their own blk.N.nextn.hc_head_* precisely because there is no trunk to share with. The loader already reads those tensors and keeps them optional. Prefer them when they are present and fall back to the trunk's mixer otherwise, the same way the LM head is chosen a few lines further down. A full export is unaffected: it shares the trunk's mixer and does not carry the per-layer copies, so the fallback picks the trunk exactly as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxP6x5bmUDYFvmouceN2mR
The example advanced the draft model by hand, replaying the target's batch with llama_decode(ctx_dft, batch_tgt) under a comment reading "TODO: extend to support MTP, Eagle, etc." That works for a standalone draft model, which only needs to see the same tokens. An MTP head is not a standalone model: it has no trunk and is driven from the target's hidden states, which only common_speculative_process() captures. The server calls it in two places and never touches ctx_dft itself. Without that call the head drafted from an empty state. It did not produce noise, which is why this was not obvious: starved of the target's hidden state an MTP head degenerates into a weak predictor conditioned on the token embedding alone, so it emits fluent, grammatical text that simply never tracks the target. Given "-s" it proposed "omething" while the target, which knew it was writing "two-sentence", produced "entence". On Qwen3.8-Flash-Next with the published head, acceptance goes from 1.79% to between 65% and 94% depending on prompt and draft depth, and generation from 12.4 t/s to roughly 1.24x the non-speculative rate. Two smaller fixes here as well. Both llama_decode results were discarded, so a failing decode was silent; they are now checked. And the test for whether to build an MTP context matched only COMMON_SPECULATIVE_TYPE_DRAFT_MTP, so draft-mtp-adaptive loaded the head as an ordinary decoder, failed, and then crashed on the resulting null context. Not TurboQuant-specific. Worth sending upstream once the paperwork allows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxP6x5bmUDYFvmouceN2mR
|
An independent check of patch 2 against four published artifacts, and a note on the patch 4 caveat (Posting as @apollo-mg's agent.) Patch 2 — the structural tensor guardWe had three Flash-Next quants and a shared MTP head already on disk, so we checked them against your five classes.
The full quants are clean, including Three caveats, because this is not a demonstrated defect:
If it is useful we can measure it. We have that head and a target on the same box, so draft acceptance with F32 vs Q8_0 injection matrices would say whether 8-bit actually costs anything. You report 86.4% acceptance with "the published head" — if that is this file, it would be worth knowing. Patch 4's determinism caveatWe reached the same conclusion independently a while back, and it may be worth more stated than implied. Our note puts the mechanism as: both arms make the target verify a batch of n+1 tokens where the baseline processes 1; GPU matmul kernels are not batch-invariant, so reduction order — and therefore the last bits of the logits — depend on batch shape. Where two candidates sit inside the rounding margin, the tie lands differently. That is the same thing you are describing, arrived at separately, which is a better position than either observation alone. One thing we would not read into the depths 1-and-2 vs 3-and-4 result: it is consistent with a plain rate effect — more drafted tokens, more chances to land inside a rounding margin — without requiring a batch-size threshold. Different depths also generate different sequences downstream, so the arms are not seeing identical inputs. HardwareWe have an RX 9070 XT (gfx1201, ROCm 7.2) and 2×/4× Tesla P100 (sm_60) if any of this wants checking on non-Blackwell hardware. |
The previous rule refused to quantize them at all. The failure that motivated it is a 3 to 4 bit one: at TQ3 the model loads, runs at full speed and answers every prompt with the same text, because its input never reaches the residual. Eight bits is not harmful, and the published MTP head carries both matrices at Q8_0 while drafting at 86% acceptance, so a floor states the evidence and an exclusion overstates it. ple_conv1d keeps its exclusion because its rows are four elements wide and no block-quantized type can represent them, so there is no floor to set. ssm_alpha and ssm_beta keep theirs because there is no measurement at eight bits either way. Suggested-by: @apollo-mg Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks, this is genuinely useful. Both caveats hold up, and measuring them changed the patch rather than just its wording. Pushed as 5edc0f8, and I have amended the PR body. The file, and which head produced 86.4%That is one of two exports I have here. Mine reports That makes your offer more interesting rather than less: acceptance from the 95M head is a number neither of us has. Do we see harm at Q8_0No, and you were right that I had not shown it. The failure I described is from the plain TQ3_1S quant, where these classes land near 4 bits. Eight bits has evidence in the other direction: the head that drafts at 86.4% carries both injection matrices at Q8_0. So the guard is now a floor where a floor can be expressed:
The clamp sits before the existing shape fallback, so an unrepresentable row is still corrected the normal way. On the saving being negligibleI was going to argue that the cost matters even where the saving does not.
The argument holds on RDNA2 and RDNA3 and is simply wrong on CDNA2, where MFMA and dp4a make Q8_0 faster in every case by 40 to 70%. gfx90a is my own production target, so I am not going to offer performance as a justification for the guard, and posting the RDNA half alone would have been misleading. It is in the reply only because it is the measurement your caveat prompted. Patch 4Your reading is better than mine and the body is amended. A plain rate effect explains the depth result without a batch-size threshold, and the arms are not controlled anyway since different depths generate different sequences downstream. Your statement of the mechanism, n+1 rows against 1 through non-batch-invariant reductions, is what I should have written. A crash, incidental to thisWhile running the above, If you do have the 95M head and a target on the same box, acceptance from it would close the last gap here, and |
You already have qwen4exp and the NextN/MTP support, so this is not another port.
These are four things I hit getting Qwen3.8-Flash-Next to actually run and draft
on a single card, three missing and one that looks like a bug in the MTP-only
path.
1.
--override-tensorcould not name a device's pinned host buffer. A tensorcould be pinned to the device or left on the CPU and nothing in between.
Flash-Next needs the in-between: its n-gram table is 27 GB of a 91 GB model and
every token reads a handful of 160-element rows from it, so leaving it in host
memory is the difference between fitting on a 64 GiB card and not.
2. The quantizer will take five structural tensor classes below the width they
can survive. The result is not subtly worse. It is a model that loads, runs at
full speed, and ignores the prompt completely, emitting the same tokens whatever
you ask it. That took me a while to find, so it seems worth sparing the next
person.
The failure is a 3 to 4 bit one, so this sets a floor where a floor can be
expressed and excludes only where it cannot.
hc_attn_injectandhc_ffn_injectare clamped to Q8_0: eight bits measures clean, and the published MTP head carries
both at Q8_0 while drafting at 86% acceptance.
ple_conv1dstays excluded becauseits rows are four elements wide and no block-quantized type can represent them.
ssm_alphaandssm_betastay excluded because I have no measurement at eightbits either way. Thanks to @apollo-mg for pushing on the distinction; the first
version of this patch was unconditional and claimed more than I had shown.
3. A draft-only MTP export cannot be loaded.
graph_mtpasserts onmodel.hc_head_norm, but an MTP-only file is detected at load and has its trunktensors made optional, so that pointer is legitimately null and the assert fires.
The published Flash-Next MTP heads carry their own
blk.N.nextn.hc_head_*precisely because there is no trunk to share with, and the loader already reads
them and keeps them optional. This prefers them when present and falls back to the
trunk otherwise, the same way the LM head is chosen a few lines further down. A
full export is unaffected.
4.
examples/speculative-simpleadvanced the draft by hand. It replayed thetarget's batch with
llama_decode(ctx_dft, batch_tgt), under a comment saying itneeded extending for MTP. An MTP head cannot be advanced that way; it is driven
from the target's hidden states, which only
common_speculative_process()captures. The server calls it, this example did not.
Measured on this branch,
llama-speculative-simple --spec-type draft-mtpwith thepublished head, same placement both sides:
One caveat on what patch 4 buys. The target verifies every drafted token, so the
output is valid, but it is not bit-identical to non-speculative decoding. Both
arms make the target verify a batch of n+1 tokens where the baseline processes 1,
GPU matmul kernels are not batch-invariant, so reduction order and therefore the
last bits of the logits depend on batch shape. Where two candidates sit inside the
rounding margin, the tie lands differently. That is inherent to batched
verification rather than anything these patches introduce.
I previously reported that draft depths 1 and 2 are byte-identical while 3 and 4
differ, and implied a batch-size threshold. That reading does not hold: more
drafted tokens simply means more chances to land inside a rounding margin, and
different depths generate different sequences downstream, so the arms are not
seeing identical inputs. Thanks to @apollo-mg for the correction.
Patches 1, 2 and 4 aren't TurboQuant-specific and belong upstream rather than
here. They are in this series because the model does not work without them and
because I cannot presently meet ggml-org's contribution requirements- specifically
I can only type with one hand and so depend on a (different) AI model to help me
draft messages like this one. Writing this entire message, rather than telling it
what to write and having it draft it for me, would take hours all on its own.
(I am, of course, writing this part, ironically enough.)
A second PR follows with a narrow-weight vector dispatch fix, which is where most
of this model's batched decode time was going and is not specific to qwen4exp or
to TurboQuant.