Skip to content

Qwen3.8-27B: field notes from two more stacks (GB10 + gfx1151) - MTP speculative decoding at up to 3.5x, the quant that ships the draft head, and two small template notes #24

Description

@Defilan

Summary

Field notes on the Qwen3.8-27B guide from two stacks it does not cover: a DGX Spark GB10 on CUDA and a Strix Halo gfx1151 on llama.cpp Vulkan. I used the guide as a test plan rather than as background reading, so most of this is confirmation, and the parts that are not are additions rather than disagreements.

Everything in the guide that I probed held up. The xhigh default reproduced, your Trap 2 render behaviour reproduced exactly, and the low-beats-xhigh finding reproduced with a mechanism I can show. I went in expecting to find at least one thing to push back on and did not.

Four things, and the first is the one worth your time:

  1. The MTP head is worth up to 3.5x, and it is not in the quant you tested on. Your tested_on line is the unsloth Q4_K_M, and that repo has no mtp-*.gguf in it. The ggml-org repo does. I think that is why the guide has no MTP section: there was nothing to find.
  2. A gfx1151 row for the throughput table, plus two Vulkan-specific cautions that do not apply to your CUDA and Metal numbers.
  3. reasoning_effort: high raises a 500 on the stock template rather than degrading quietly.
  4. A behavioural addendum to Trap 2, and a second route to the turn-6 signature that an operator with a conservative max_tokens will hit instead of yours.

Environment

  • GPUs: NVIDIA GB10 (DGX Spark, sm_121, 128GB unified) and AMD Radeon 8060S (Strix Halo / gfx1151, RADV, 128GB unified)
  • Serving: llama.cpp via my operator, stock upstream, no fork
  • Models: ggml-org/Qwen3.8-27B-GGUF Q4_K_M and unsloth/Qwen3.8-27B-GGUF Q4_K_M, both pulled 2026-08-14, byte sizes below
  • Flags: --jinja --flash-attn on --cache-type-k/v f16 --ctx-size 16384 --parallel 1 -fit off, stock template, temperature 0, max_tokens 512 for throughput. Strix additionally needs --no-mmap.
  • Method: n=5 per cell, three prompts (code generation, conceptual explanation, debugging). GPU residency verified before every measurement rather than assumed, for a reason I will come back to.

One caveat up front on the throughput numbers. My first cross-platform comparison varied context, KV type, temperature and template all at once and produced a table that looked clean and was not comparable. Everything below is from a re-run where the only flag differing between the two machines is --no-mmap. I mention it because the first version of these numbers reversed one of the conclusions.


1. MTP speculative decoding, and the quant that ships it

Qwen3.8 ships an MTP head as a separate GGUF rather than carrying it in the target model, so drafting is much cheaper than self-speculation and the speedup is large.

The file is mtp-Qwen3.8-27B-Q4_0.gguf, 1.6 GiB, and it exists only in ggml-org/Qwen3.8-27B-GGUF. I searched the unsloth repo including under BF16/ and there is no mtp-*.gguf there.

GB10, ctx 16384, f16 KV, temperature 0, n=5 per cell:

prompt no spec nDraftMax 3 nDraftMax 5 nDraftMax 7
code generation 11.02 29.40 35.89 37.70
conceptual explanation 10.98 22.14 21.00 19.91
debugging 10.98 20.98 19.34 18.08

The optimum depends on the workload, which I did not expect. Code generation keeps improving as you draft further ahead. Prose peaks at 3 and then declines, and by 7 it has given back a third of the gain. The mechanism is draft acceptance falling as nDraftMax rises: 0.469 at 3, 0.308 at 5, 0.229 at 7 on the debugging prompt.

Worth stating plainly because it nearly caught me: my first pass measured only a code prompt, concluded "5 is the winner at 3.13x", and that conclusion is wrong on two of the three workloads. If you add MTP guidance I would suggest framing it as measure-it-on-your-workload, with 3 as the safe general default and higher values only for code-heavy use.

This interacts with your quant-versus-engine section, which I think is the right analysis and this only extends. The two Q4_K_M files are not the same file:

repo bytes size MTP head
unsloth/Qwen3.8-27B-GGUF 17,106,775,008 15.93 GiB no
ggml-org/Qwen3.8-27B-GGUF 18,973,870,432 17.67 GiB yes

Same node, same flags, swapping only the model: unsloth 12.02 tok/s, ggml-org 10.98. A 10.9% smaller file gives 9.5% more throughput, which is what your "batch-1 decode is memory-bandwidth bound" line predicts, so treat this as a datapoint for that conclusion rather than a new one.

The trade is lopsided though. Best code throughput available from each choice on the same GB10:

tok/s
unsloth Q4_K_M, no MTP possible 12.07
ggml-org Q4_K_M + MTP at nDraftMax 7 37.70

Taking unsloth for a 9.5% faster baseline forfeits 3.1x. For anyone optimising single-stream speed the recommendation inverts: take the larger, slower file, because it is the only one that lets you turn the big lever.

For calibration against your 11.3: on the unsloth file I get 12.02 on my GB10. Different build and box, and I would not read anything into the difference.

2. A gfx1151 row, and two Vulkan cautions

Strix Halo, same methodology: 11.26 tok/s baseline, marginally ahead of the GB10's 10.98 on the same file. With MTP it reaches 33.03 at nDraftMax 5, and unlike CUDA it gets worse at 7 rather than better.

Two things specific to the Vulkan path that your CUDA and Metal numbers would not surface, and that I would want to know before quoting a speculated Vulkan figure:

Speculation makes output non-deterministic at temperature 0. On Strix, identical requests at nDraftMax 5 and 7 return different generation lengths run to run. On CUDA every arm was deterministic across all five samples. Same model, same draft head, same flags.

Speculation is unstable there. Worst-case spread across five identical samples:

arm GB10 Strix
no spec 0.68% 0.14%
nDraftMax 3 1.49% 9.27%
nDraftMax 5 3.52% 31.11%
nDraftMax 7 3.64% 26.83%

Its unspeculated baseline is the single most stable measurement I took, so this is speculation introducing the variance rather than the platform being noisy. A single-sample speculated Vulkan number could be off by a third.

3. reasoning_effort: high is a hard 500, not a quiet degrade

Your comparison table notes froggeric adds a working high rung, which implies stock has none. On stock it does not fall back or no-op, it raises:

HTTP 500
Jinja Exception: Unexpected reasoning effort high.
Supported types are xhigh (default), medium, and low.

Small thing, but "the community template adds a high rung" reads like the stock behaviour is a silent downgrade, and anyone wiring high into a client gets a request-time failure instead. The error text is also a tidy independent confirmation of the xhigh default.

4. Trap 2 and the turn-6 trap: one addendum each, both agreeing with you

Trap 2 reproduced exactly. Your /apply-template numbers were 297 / 226 / 297 / 60 chars for none / low / xhigh / medium. My prompt-token counts are 71 / 59 / 71 / 29. Same shape, same conclusion: medium renders no effort line.

The addendum is behavioural. With no instruction rendered, the model does not fall back to something neutral. It reasons more than an explicit low. Reasoning characters at max_tokens 1024, temperature 0:

prompt low medium xhigh
code 347 374 644
explain 1031 1633 1329
debug 1429 2974 4297

So medium is inert as an instruction and not inert in effect. Someone setting it expecting a midpoint gets unguided behaviour, which sat above low on all three prompts and above xhigh on one. If Trap 2 is worth a second line, that might be it: the danger is not that nothing happens, it is that nothing steers.

On the turn-6 failure, your diagnosis is right and I want to be clear I am not disputing it. Your completion counts fall monotonically (2900, 2805, 2714, 2645, 1645) without ever reaching your 32,768 budget, which is exactly the ctx minus prompt signature, and the arithmetic you show lands on it.

I initially thought I had refuted it, and I had not. I was running max_tokens 2048 against the same ctx 16384, and I get empty content with finish_reason: length at turn 3, three times out of three, with prompt_tokens at 3851 of 16384. That is 23% of context, so mine is output-budget exhaustion, a different wall reached first because my budget was small.

Two different causes, one identical signature, and your mitigation (a bigger window) fixes yours and does nothing for mine. They are only distinguishable by checking completion_tokens against both max_tokens and ctx minus prompt. Might be worth a sentence in that section, since an operator running a conservative output budget will hit the other one and reasonably conclude your fix did not work.


What I would most want your read on

The MTP-versus-quant tradeoff, because it cuts against the obvious reading of your serving section. Your quant analysis says take the 4-bit GGUF and do not overthink the engine, which is right. But between two files both called Q4_K_M, the slower one is the better choice by 3x, purely because of a 1.6 GiB side file that only one publisher ships. That feels like the kind of thing the guide exists to say, and I would rather you write it in your own voice than have me guess at the framing.

Happy to package the measurement harness as a scripts/mtp-probes/ contribution in the shape of the thinking and spine probes, standard library only, if that is useful. It does the arm sweep, asserts the running process actually carries the speculation config before each measurement, and refuses to record a number if the model is not resident on the GPU. Both guards exist because I got bitten by their absence.

There is one more thing I left out because it is a container problem rather than a model one, and I did not want to pad a model guide with it. Briefly: I lost a measurement to a silent CPU fallback, where the pod held the GPU, the device node was present, and llama.cpp still served from CPU at half speed with only a single startup warning to show for it. The number it produced looked entirely plausible. If you think a line about verifying GPU residency belongs in the serving notes for container users, say the word and I will write it up properly. If not, it is happily out of scope.

Raw JSON for every cell is available if you want it.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions