← Back to the write-up and video on hijoseromero.com
Multi-Token Prediction (MTP) on Qwen 3.8-27B at full BF16 precision, measured on my own desk. One model, three very different prompts, four MTP settings, and the whole thing run twice: once in thinking mode with the model card's recommended sampling, once in a clean-room configuration built for exact matching. Every raw output opens in a new tab via the audit links, and every leg carries its run timestamp.
Data collected 2026-08-22 23:15 → 2026-08-23 17:35 local time · page generated 2026-08-25 23:51 · every number measured on the machine described at the bottom, logs retained.
| Experiment 1: thinking on, recommended sampling | Experiment 2: the clean-room config | |
|---|---|---|
| Question | What does MTP do with thinking on and the model card's recommended sampling? | With randomness removed, is speed the ONLY thing MTP changes? |
| Thinking | ON, reasoning effort medium (set at server launch) | OFF |
| Sampling | Official Qwen thinking samplers: temperature 1.0, top_p 0.95, top_k 20, min_p 0 | Greedy: temperature 0, always the single top word, no dice involved |
| Why these settings | The thinking-mode settings the Qwen model card recommends | The configuration the llama.cpp maintainers point to for exact-match comparisons (full parameter-by-parameter explanation in that section) |
| Bonus legs | The marathon: Qwen's factory-default effort (xhigh), Table prompt, off/2/3 | none |
Shared by every leg of both experiments: Qwen 3.8-27B BF16 (54.7 GB GGUF, Unsloth repack) ·
seed 42 on every request · one prompt, one shot, no retries · --parallel 1 · tools disabled ·
65,536-token context · no output cap (60k safety ceiling) · fresh server launch per MTP setting ·
llama.cpp build 10472 (Unsloth) · no agent harness anywhere, raw API calls only · speeds and acceptance scraped
from llama.cpp's own per-request timing log · peak memory sampled every 5 s.
"Guesses accepted" is the honest column: MTP lets a tiny helper guess a few tokens ahead, and the model only keeps guesses it fully agrees with. High acceptance = big speedup. Watch it collapse from the mechanical Table to the free-form Essay.
Generate a markdown table for a warehouse inventory with EXACTLY 60 product rows plus a final TOTAL row. Columns: Row | SKU | Product | Unit Price | Stock | Line Value Rules, follow them exactly: - Row numbers run 1 through 60. - SKU for row N is "WH-" followed by N padded to 4 digits (row 7 = WH-0007). - Product name for row N is "Widget Model N". - Unit Price for row N is 10.00 + (N * 0.25), shown with 2 decimals. - Stock for row N is 500 - (3 * N). - Line Value is Unit Price * Stock, shown with 2 decimals. - The final TOTAL row shows the sum of the Stock column and the sum of the Line Value column. Do all arithmetic silently. Your answer must be ONLY the finished markdown table: no calculation steps, no explanations, no text before or after it.
| Setting | Generation speed | vs off | Guesses accepted | Avg guess length | Total time | Tokens | Run completed | Audit |
|---|---|---|---|---|---|---|---|---|
| MTP off | 4.17 tok/s | baseline | — | — | 29m 05s | 7272 | 2026-08-23 02:55 | output ↗ · thinking ↗ |
| depth 2 | 10.01 tok/s | +140% | 99% | 2.97 | 12m 08s | 7272 | 2026-08-23 03:09 | output ↗ · thinking ↗ |
| depth 3 | 11.0 tok/s | +164% | 97% | 3.91 | 11m 05s | 7303 | 2026-08-23 03:22 | output ↗ · thinking ↗ |
| depth 4 | 12.69 tok/s | +204% | 95% | 4.78 | 9m 37s | 7303 | 2026-08-23 03:33 | output ↗ · thinking ↗ |
Legs of this test completed 2026-08-23 02:55 → 2026-08-23 03:33 (local time; each row above carries its own stamp).
Write a complete, single-file FastAPI application implementing a bookstore back end with five resources: books, authors, publishers, genres, and customers. Requirements: - Each resource gets the identical five endpoints: list all, get by id, create, update, delete. - Each resource gets its own Pydantic model with at least four typed fields. - Storage is a plain in-memory dict per resource; ids are integers assigned from a counter. - Every function has a type-hinted signature and a one-line docstring. - No authentication, no database, no comments about what you might do differently. Output only the Python code in a single fenced block, no commentary before or after.
| Setting | Generation speed | vs off | Guesses accepted | Avg guess length | Total time | Tokens | Run completed | Audit |
|---|---|---|---|---|---|---|---|---|
| MTP off | 4.19 tok/s | baseline | — | — | 9m 48s | 2455 | 2026-08-22 23:39 | output ↗ · thinking ↗ |
| depth 2 | 9.11 tok/s | +117% | 94% | 2.87 | 4m 21s | 2367 | 2026-08-23 00:02 | output ↗ · thinking ↗ |
| depth 3 | 10.53 tok/s | +151% | 91% | 3.73 | 3m 46s | 2367 | 2026-08-23 00:19 | output ↗ · thinking ↗ |
| depth 4 | 12.11 tok/s | +189% | 89% | 4.54 | 3m 16s | 2367 | 2026-08-23 00:37 | output ↗ · thinking ↗ |
Legs of this test completed 2026-08-22 23:39 → 2026-08-23 00:37 (local time; each row above carries its own stamp).
Write a reflective essay of roughly 800 words on what beekeeping can teach a software engineer about database indexing. Draw at least three specific parallels, ground each one in a concrete detail of how bees or hives actually work, and end with one practical lesson a working engineer could apply this week. Avoid bullet points; write flowing prose.
| Setting | Generation speed | vs off | Guesses accepted | Avg guess length | Total time | Tokens | Run completed | Audit |
|---|---|---|---|---|---|---|---|---|
| MTP off | 4.19 tok/s | baseline | — | — | 9m 54s | 2485 | 2026-08-22 23:49 | output ↗ · thinking ↗ |
| depth 2 | 6.53 tok/s | +56% | 53% | 2.06 | 6m 05s | 2380 | 2026-08-23 00:08 | output ↗ · thinking ↗ |
| depth 3 | 6.31 tok/s | +51% | 41% | 2.23 | 7m 09s | 2703 | 2026-08-23 00:27 | output ↗ · thinking ↗ |
| depth 4 | 8.08 tok/s | +93% | 51% | 3.04 | 12m 50s | 6218 | 2026-08-23 00:49 | output ↗ · thinking ↗ |
Legs of this test completed 2026-08-22 23:49 → 2026-08-23 00:49 (local time; each row above carries its own stamp).
Three views of the same question. First the verdict matrix: every pairing, thinking and answer judged separately. Then who-matched-whom: the legs grouped by file hash, so the pattern is visible at a glance. Then the highlights.
| Prompt | MTP off vs depth 2 | MTP off vs depth 3 | MTP off vs depth 4 | depth 2 vs depth 3 | depth 3 vs depth 4 |
|---|---|---|---|---|---|
| The Table | ✅ identical (answer + thinking) | ❌ different answer | ❌ different answer | ❌ different answer | ✅ identical (answer + thinking) |
| The Boilerplate | 🟠 same thinking, answer forked | 🟠 same thinking, answer forked | 🟠 same thinking, answer forked | ✅ identical (answer + thinking) | ✅ identical (answer + thinking) |
| The Essay | ❌ different answer | ❌ different answer | ❌ different answer | ❌ different answer | ❌ different answer |
✅ every byte matched · 🟢 the final ANSWER matched even though the thinking transcript took a different path (different reasoning, same destination) · 🟠 thinking matched but the answer forked (the split happened while writing the answer) · 🟡 same words, only formatting/casing differs · ❌ the answers genuinely differ (equally valid, see the side-by-side splits below). Left columns: each depth against MTP off. Right columns: each depth against the previous depth, which shows whether the MTP legs agree with EACH OTHER.
| Prompt | Answer: who matched whom | Thinking: who matched whom | In plain words |
|---|---|---|---|
| The Table | MTP off = depth 2 (5953a684) | depth 3 = depth 4 (362c033a) | MTP off = depth 2 (5aa1e9c6) | depth 3 = depth 4 (75353ff7) | MTP off matches depth 2 byte for byte; the other depths match each other. Thinking: 2 distinct transcripts across 4 legs. |
| The Boilerplate | depth 2 = depth 3 = depth 4 (43a00e34) | MTP off (9532da29) | MTP off = depth 2 = depth 3 = depth 4 (507e4c3b) | depth 2, depth 3, depth 4 wrote the byte-identical answer to each other; only MTP off differs. The thinking transcript is byte-identical on all 4 legs. |
| The Essay | MTP off (1a6effaa) | depth 2 (2f10bf28) | depth 3 (a691935a) | depth 4 (b0d7fad2) | MTP off (3de66695) | depth 2 (3634dc82) | depth 3 (8340bf42) | depth 4 (33ff044a) | Every leg wrote a different answer (4 distinct outputs). Every leg's thinking transcript differs. |
Legs joined by = produced byte-identical files (first 8 hex digits of the SHA-256 shown). They can be recomputed from the audit links.
Skim this. One card per distinct divergence: the last stretch both legs shared, then what each wrote next. Depths that wrote the same continuation are merged into one card. Identical pairs are stated in one line.
✅ identical (answer + thinking) MTP off vs depth 2
❌ different answer MTP off vs depth 3 = depth 4 (depth 3 and depth 4 wrote the identical continuation, shown once): agreed for 6,819 chars (64%), split in the thinking.
What about the other columns? I'll put dashes or leave them blank. Let me just put "TOTAL" in the Product colu…
For other columns in the TOTAL row, I'll leave them blank or put a dash. Actually, re-reading the instruction…
Agreed for the first 6,819 characters (64% of the run), then split in the thinking.
What about the other columns? I'll put dashes or leave them blank. Let me just put "TOTAL" in the Product column and the sums in Stock and Line Value columns. Let me format this properly. ⟨thinking ends, answer begins⟩ | Row | SKU | Product | Unit Price | Stock | Line Value | |-----|-----…
For other columns in the TOTAL row, I'll leave them blank or put a dash. Actually, re-reading the instructions: "The final TOTAL row shows the sum of the Stock column and the sum of the Line Value column." So I'll put the values in those columns and leave the…
🟠 same thinking, answer forked MTP off vs depth 2 = depth 3 = depth 4 (depth 2 and depth 3 and depth 4 wrote the identical continuation, shown once): agreed for 2,350 chars (24%), split in the answer.
Endpoints ───────────────────────────────────────────────────────────
@app.get("/books", response_model=List[…─────────────────────────────────────────────────────────────────────
@app.get("/books", response_model=List[…Agreed for the first 2,350 characters (24% of the run), then split in the answer.
Endpoints ───────────────────────────────────────────────────────────
@app.get("/books", response_model=List[Book])
def list_books() -> List[Book]:
"""Return all books."""
return list(books_store.values())
@app.get("/books/{book_id}", response_model…─────────────────────────────────────────────────────────────────────
@app.get("/books", response_model=List[Book])
def list_books() -> List[Book]:
"""Return all books."""
return list(books_store.values())
@app.get("/books/{book_id}", response_model…❌ different answer MTP off vs depth 2: agreed for 5,175 chars (46%), split in the thinking.
someone who has actually kept bees or spent time with hives, reflecting on their engineering work. Let me dra…
like an engineer who has taken a beekeeping class or observed a hive and had a "click" moment. Let me draft t…
Agreed for the first 5,175 characters (46% of the run), then split in the thinking.
someone who has actually kept bees or spent time with hives, reflecting on their engineering work. Let me draft this and aim for roughly 800 words. Let me count carefully. Let me write it now in flowing prose, reflective, no bullets. ⟨thinking ends, answer begins⟩ I have kept bees for th…
like an engineer who has taken a beekeeping class or observed a hive and had a "click" moment. Let me draft this and count words roughly. I'll aim for 780-830. Let me write it now in flowing prose, no bullet points. ⟨thinking ends, answer begins⟩ There is a particular Tuesday in late Apr…
❌ different answer MTP off vs depth 3: agreed for 1,213 chars (11%), split in the thinking.
waggle dance / navigation**: Bees use the waggle dance to communicate the location of food sources. The angle …
dance language / navigation**: When a forager bee finds a good flower source, it performs the waggle dance to …
Agreed for the first 1,213 characters (11% of the run), then split in the thinking.
waggle dance / navigation**: Bees use the waggle dance to communicate the location of food sources. The angle of the dance relative to the sun tells other bees the direction and distance. This is like an index pointer - you don't go to the data directly, you f…
dance language / navigation**: When a forager bee finds a good flower source, it performs the waggle dance to communicate location to other bees. This is like an index that points to data. But here's a concrete detail: the dance encodes distance and direction …
❌ different answer MTP off vs depth 4: agreed for 2,499 chars (22%), split in the thinking.
ach time. They build a mental map - an index - of where flowers are. This maps to query plans and how the opti…
very time. They build a mental map. This is like a cached index vs. a cold index that needs to be rebuilt. 5.…
Agreed for the first 2,499 characters (22% of the run), then split in the thinking.
ach time. They build a mental map - an index - of where flowers are. This maps to query plans and how the optimizer uses indexes. The forager's mental map is the execution plan; the flowers are the data; the index is the cached knowledge of where things are. …
very time. They build a mental map. This is like a cached index vs. a cold index that needs to be rebuilt. 5. **The queen's pheromone (queen mandibular pheromone)**: This is a single chemical signal that the entire colony references. Every bee "looks up" the …
Every raw file, one click each, opens in a new tab.
| Setting | Output | Thinking | Output size | Thinking size | Completed |
|---|---|---|---|---|---|
| MTP off | output ↗ | thinking ↗ | 3,685 chars | 7,008 chars | 2026-08-23 02:55 |
| depth 2 | output ↗ | thinking ↗ | 3,685 chars | 7,008 chars | 2026-08-23 03:09 |
| depth 3 | output ↗ | thinking ↗ | 3,683 chars | 7,139 chars | 2026-08-23 03:22 |
| depth 4 | output ↗ | thinking ↗ | 3,683 chars | 7,139 chars | 2026-08-23 03:33 |
| Setting | Output | Thinking | Output size | Thinking size | Completed |
|---|---|---|---|---|---|
| MTP off | output ↗ | thinking ↗ | 8,517 chars | 1,192 chars | 2026-08-22 23:39 |
| depth 2 | output ↗ | thinking ↗ | 8,182 chars | 1,192 chars | 2026-08-23 00:02 |
| depth 3 | output ↗ | thinking ↗ | 8,182 chars | 1,192 chars | 2026-08-23 00:19 |
| depth 4 | output ↗ | thinking ↗ | 8,182 chars | 1,192 chars | 2026-08-23 00:37 |
| Setting | Output | Thinking | Output size | Thinking size | Completed |
|---|---|---|---|---|---|
| MTP off | output ↗ | thinking ↗ | 5,838 chars | 5,411 chars | 2026-08-22 23:49 |
| depth 2 | output ↗ | thinking ↗ | 5,277 chars | 5,393 chars | 2026-08-23 00:08 |
| depth 3 | output ↗ | thinking ↗ | 5,591 chars | 6,818 chars | 2026-08-23 00:27 |
| depth 4 | output ↗ | thinking ↗ | 4,801 chars | 21,448 chars | 2026-08-23 00:49 |
Looking at the Experiment 1 matrix, the three MTP depths matched each other on the Boilerplate and the lone odd one out was MTP off, the plain model. I could not tell from one run whether that means anything, so I reran the MTP-off leg with the identical configuration (thinking on, medium effort, temperature 1.0, top_p 0.95, top_k 20, min_p 0, seed 42, fresh server launch) on a different day and compared it against run 1 and against the MTP depths. The question is simple: does the same setting reproduce itself under sampling, or does it drift the same way the MTP legs did?
Run 2 started 2026-08-25 00:40 local time; every row below carries its own completion stamp.
| Setting | Generation speed | vs off | Guesses accepted | Avg guess length | Total time | Tokens | Run completed | Audit |
|---|---|---|---|---|---|---|---|---|
| off, run 1 | 4.17 tok/s | - | — | — | 29m 05s | 7272 | 2026-08-23 02:55 | output ↗ · thinking ↗ |
| off, run 2 | 4.17 tok/s | - | — | — | 29m 06s | 7272 | 2026-08-25 01:09 | output ↗ · thinking ↗ |
| Setting | Generation speed | vs off | Guesses accepted | Avg guess length | Total time | Tokens | Run completed | Audit |
|---|---|---|---|---|---|---|---|---|
| off, run 1 | 4.19 tok/s | - | — | — | 9m 48s | 2455 | 2026-08-22 23:39 | output ↗ · thinking ↗ |
| off, run 2 | 4.18 tok/s | - | — | — | 9m 48s | 2455 | 2026-08-25 01:19 | output ↗ · thinking ↗ |
| Setting | Generation speed | vs off | Guesses accepted | Avg guess length | Total time | Tokens | Run completed | Audit |
|---|---|---|---|---|---|---|---|---|
| off, run 1 | 4.19 tok/s | - | — | — | 9m 54s | 2485 | 2026-08-22 23:49 | output ↗ · thinking ↗ |
| off, run 2 | 4.18 tok/s | - | — | — | 9m 55s | 2485 | 2026-08-25 01:29 | output ↗ · thinking ↗ |
What that means. With seed 42 pinned, the sampled run reproduces itself exactly, thinking and all, on a different day and a fresh server launch. So the reason MTP off stood alone in the Experiment 1 matrix is not that the plain model is noisier or that the first run was a fluke. It is that MTP off decodes one token per step on one kernel path, and every MTP depth verifies a batch of guesses on another; the two paths agree to many decimal places, and where a near-tie token sits between them the seeded lottery lands differently. Same seed, same model, one variable changed, and that variable is the one this whole page is about. Which leg is "right" is not a meaningful question: both are valid samples of the same model, and Experiment 2 shows that with the dice removed the two paths agree byte for byte on structured work.
| Prompt | run 1 vs run 2 | run 2 vs depth 2 | run 2 vs depth 3 | run 2 vs depth 4 |
|---|---|---|---|---|
| The Table | ✅ identical (answer + thinking) | ✅ identical (answer + thinking) | ❌ different answer | ❌ different answer |
| The Boilerplate | ✅ identical (answer + thinking) | 🟠 same thinking, answer forked | 🟠 same thinking, answer forked | 🟠 same thinking, answer forked |
| The Essay | ✅ identical (answer + thinking) | ❌ different answer | ❌ different answer | ❌ different answer |
| Prompt | Answer: who matched whom | Thinking: who matched whom | In plain words |
|---|---|---|---|
| The Table | off, run 1 = off, run 2 = depth 2 (5953a684) | depth 3 = depth 4 (362c033a) | off, run 1 = off, run 2 = depth 2 (5aa1e9c6) | depth 3 = depth 4 (75353ff7) | off, run 1 matches off, run 2, depth 2 byte for byte; the other depths match each other. Thinking: 2 distinct transcripts across 5 legs. |
| The Boilerplate | depth 2 = depth 3 = depth 4 (43a00e34) | off, run 1 = off, run 2 (9532da29) | off, run 1 = off, run 2 = depth 2 = depth 3 = depth 4 (507e4c3b) | off, run 1 matches off, run 2 byte for byte; the other depths match each other. The thinking transcript is byte-identical on all 5 legs. |
| The Essay | off, run 1 = off, run 2 (1a6effaa) | depth 2 (2f10bf28) | depth 3 (a691935a) | depth 4 (b0d7fad2) | off, run 1 = off, run 2 (3de66695) | depth 2 (3634dc82) | depth 3 (8340bf42) | depth 4 (33ff044a) | off, run 1 matches off, run 2 byte for byte; the remaining depths differ. Thinking: 4 distinct transcripts across 5 legs. |
Legs joined by = produced byte-identical files (first 8 hex digits of the SHA-256 shown). They can be recomputed from the audit links.
✅ identical (answer + thinking) off, run 1 vs off, run 2
✅ identical (answer + thinking) off, run 1 vs off, run 2
✅ identical (answer + thinking) off, run 1 vs off, run 2
| Setting | Output | Thinking | Output size | Thinking size | Completed |
|---|---|---|---|---|---|
| off, run 1 | output ↗ | thinking ↗ | 3,685 chars | 7,008 chars | 2026-08-23 02:55 |
| off, run 2 | output ↗ | thinking ↗ | 3,685 chars | 7,008 chars | 2026-08-25 01:09 |
| Setting | Output | Thinking | Output size | Thinking size | Completed |
|---|---|---|---|---|---|
| off, run 1 | output ↗ | thinking ↗ | 8,517 chars | 1,192 chars | 2026-08-22 23:39 |
| off, run 2 | output ↗ | thinking ↗ | 8,517 chars | 1,192 chars | 2026-08-25 01:19 |
| Setting | Output | Thinking | Output size | Thinking size | Completed |
|---|---|---|---|---|---|
| off, run 1 | output ↗ | thinking ↗ | 5,838 chars | 5,411 chars | 2026-08-22 23:49 |
| off, run 2 | output ↗ | thinking ↗ | 5,838 chars | 5,411 chars | 2026-08-25 01:29 |
Same three prompts, same four settings, but temperature 0 and no thinking: the configuration where outputs should match if MTP truly only changes speed. Verdict up front: byte-identical at every depth on every structured prompt; the Essay still flipped at depths 3-4, the one measured residual (explained below).
This experiment deliberately changes two things from the recommended configuration. Here is every knob, what it does, and why it is set the way it is. Worth understanding once, it applies to every local model.
| Parameter | Exp 1 (recommended) | Exp 2 | What it actually does, and why we changed it |
|---|---|---|---|
temperature | 1.0 | 0 (greedy) | After the model scores every possible next token, temperature decides how the winner gets picked. At 1.0 the scores become a weighted lottery: likely words usually win, but any plausible word can. At 0 there is no lottery: the single top-scored token wins, every time. We set 0 because this experiment asks "does MTP change anything besides speed?", and to answer that you must first remove the dice. (With the seed pinned, a rerun of the SAME setting does reproduce, as the repeat run above showed; but a seeded lottery is still sensitive to tiny changes in the scores it draws from, which is exactly what a different batch path introduces. Greedy removes that sensitivity.) The cost: greedy text is more repetitive and rigid, which is why it is not the usual recommendation for general use. |
top_p / top_k / min_p | 0.95 / 20 / 0 | inactive | These trim the lottery pool before the draw: top_k 20 keeps only the 20 highest-scored tokens, top_p 0.95 keeps the smallest set covering 95% of the probability, min_p drops tokens below a floor. With temperature 0 there is no draw to trim, the top token wins regardless, so these settings stop mattering. Nothing to tune, by design. |
enable_thinking | true | false | Qwen 3.8 can write a private reasoning transcript before its answer; this template switch turns that phase off entirely, so the model answers directly. We turned it off for two reasons: it removes thousands of extra sampled tokens where randomness (in Exp 1) could fork the run, and it makes the answer comparison clean: what you diff is exactly what the user would see. Side effect we measured: without the thinking phase, the model's TOTAL-row arithmetic got worse (see the note above), a nice illustration of what thinking is actually for. |
reasoning_effort | medium (server launch) | not applicable | The effort dial only exists inside thinking mode (it changes how much coaching the thinking phase gets). Thinking off means there is nothing for it to control. |
seed | 42 | 42 | The starting point for the sampling lottery's random number generator. Unchanged, and under greedy it is belt-and-braces: with no lottery, the seed has almost nothing left to do, we keep it pinned anyway so the two experiments differ in as few places as possible. |
| everything else | identical | Same model file, same 65,536-token context, same
one-shot rule, same --parallel 1, same tools-disabled server, same MTP flags per leg, same machine.
The experiments differ ONLY in the two rows marked bold. | |
Generate a markdown table for a warehouse inventory with EXACTLY 60 product rows plus a final TOTAL row. Columns: Row | SKU | Product | Unit Price | Stock | Line Value Rules, follow them exactly: - Row numbers run 1 through 60. - SKU for row N is "WH-" followed by N padded to 4 digits (row 7 = WH-0007). - Product name for row N is "Widget Model N". - Unit Price for row N is 10.00 + (N * 0.25), shown with 2 decimals. - Stock for row N is 500 - (3 * N). - Line Value is Unit Price * Stock, shown with 2 decimals. - The final TOTAL row shows the sum of the Stock column and the sum of the Line Value column. Do all arithmetic silently. Your answer must be ONLY the finished markdown table: no calculation steps, no explanations, no text before or after it.
| Setting | Generation speed | vs off | Guesses accepted | Avg guess length | Total time | Tokens | Run completed | Audit |
|---|---|---|---|---|---|---|---|---|
| MTP off | 4.18 tok/s | baseline | — | — | 9m 45s | 2443 | 2026-08-23 16:25 | output ↗ |
| depth 2 | 10.03 tok/s | +140% | 99% | 2.97 | 4m 05s | 2443 | 2026-08-23 16:56 | output ↗ |
| depth 3 | 11.21 tok/s | +168% | 99% | 3.96 | 3m 39s | 2443 | 2026-08-23 17:12 | output ↗ |
| depth 4 | 12.59 tok/s | +201% | 93% | 4.73 | 3m 15s | 2443 | 2026-08-23 17:27 | output ↗ |
Legs of this test completed 2026-08-23 16:25 → 2026-08-23 17:27 (local time; each row above carries its own stamp).
Write a complete, single-file FastAPI application implementing a bookstore back end with five resources: books, authors, publishers, genres, and customers. Requirements: - Each resource gets the identical five endpoints: list all, get by id, create, update, delete. - Each resource gets its own Pydantic model with at least four typed fields. - Storage is a plain in-memory dict per resource; ids are integers assigned from a counter. - Every function has a type-hinted signature and a one-line docstring. - No authentication, no database, no comments about what you might do differently. Output only the Python code in a single fenced block, no commentary before or after.
| Setting | Generation speed | vs off | Guesses accepted | Avg guess length | Total time | Tokens | Run completed | Audit |
|---|---|---|---|---|---|---|---|---|
| MTP off | 4.18 tok/s | baseline | — | — | 10m 11s | 2548 | 2026-08-23 16:35 | output ↗ |
| depth 2 | 10.03 tok/s | +140% | 99% | 2.98 | 4m 15s | 2548 | 2026-08-23 17:00 | output ↗ |
| depth 3 | 11.06 tok/s | +165% | 97% | 3.91 | 3m 52s | 2548 | 2026-08-23 17:16 | output ↗ |
| depth 4 | 12.91 tok/s | +209% | 96% | 4.84 | 3m 18s | 2548 | 2026-08-23 17:31 | output ↗ |
Legs of this test completed 2026-08-23 16:35 → 2026-08-23 17:31 (local time; each row above carries its own stamp).
Write a reflective essay of roughly 800 words on what beekeeping can teach a software engineer about database indexing. Draw at least three specific parallels, ground each one in a concrete detail of how bees or hives actually work, and end with one practical lesson a working engineer could apply this week. Avoid bullet points; write flowing prose.
| Setting | Generation speed | vs off | Guesses accepted | Avg guess length | Total time | Tokens | Run completed | Audit |
|---|---|---|---|---|---|---|---|---|
| MTP off | 4.18 tok/s | baseline | — | — | 4m 56s | 1236 | 2026-08-23 16:40 | output ↗ |
| depth 2 | 7.17 tok/s | +72% | 56% | 2.12 | 2m 53s | 1236 | 2026-08-23 17:03 | output ↗ |
| depth 3 | 6.49 tok/s | +55% | 43% | 2.29 | 3m 04s | 1189 | 2026-08-23 17:19 | output ↗ |
| depth 4 | 6.13 tok/s | +47% | 33% | 2.30 | 3m 18s | 1210 | 2026-08-23 17:34 | output ↗ |
Legs of this test completed 2026-08-23 16:40 → 2026-08-23 17:34 (local time; each row above carries its own stamp).
Three views of the same question. First the verdict matrix: every pairing, thinking and answer judged separately. Then who-matched-whom: the legs grouped by file hash, so the pattern is visible at a glance. Then the highlights.
| Prompt | MTP off vs depth 2 | MTP off vs depth 3 | MTP off vs depth 4 | depth 2 vs depth 3 | depth 3 vs depth 4 |
|---|---|---|---|---|---|
| The Table | ✅ identical | ✅ identical | ✅ identical | ✅ identical | ✅ identical |
| The Boilerplate | ✅ identical | ✅ identical | ✅ identical | ✅ identical | ✅ identical |
| The Essay | ✅ identical | ❌ different answer | ❌ different answer | ❌ different answer | ❌ different answer |
✅ every byte matched · 🟡 same words, only formatting differs · ❌ the answers genuinely differ. Left columns: each depth against MTP off. Right columns: each depth against the previous depth.
| Prompt | Answer: who matched whom | In plain words |
|---|---|---|
| The Table | MTP off = depth 2 = depth 3 = depth 4 (574272a9) | All 4 legs wrote the byte-identical answer. |
| The Boilerplate | MTP off = depth 2 = depth 3 = depth 4 (88d512b6) | All 4 legs wrote the byte-identical answer. |
| The Essay | MTP off = depth 2 (5ba312cc) | depth 3 (015ee776) | depth 4 (124abb47) | MTP off matches depth 2 byte for byte; the remaining depths differ. |
Legs joined by = produced byte-identical files (first 8 hex digits of the SHA-256 shown). They can be recomputed from the audit links.
Skim this. One card per distinct divergence: the last stretch both legs shared, then what each wrote next. Depths that wrote the same continuation are merged into one card. Identical pairs are stated in one line.
✅ identical MTP off vs depth 2 · ✅ identical MTP off vs depth 3 · ✅ identical MTP off vs depth 4
✅ identical MTP off vs depth 2 · ✅ identical MTP off vs depth 3 · ✅ identical MTP off vs depth 4
✅ identical MTP off vs depth 2
❌ different answer MTP off vs depth 3: agreed for 480 chars (8%), split in the answer.
watching the bees navigate their complex social and physical environment, I realized that the principles gover…
I realized that the bees have been solving the problem of efficient retrieval long before we invented SQL. The…
Agreed for the first 480 characters (8% of the run), then split in the answer.
watching the bees navigate their complex social and physical environment, I realized that the principles governing their efficiency are not so different from those governing our databases. The hive is a living lesson in how structure enables scale, offering pr…
I realized that the bees have been solving the problem of efficient retrieval long before we invented SQL. They do not just store data; they curate it. In their waxen architecture, there is a profound lesson on how to organize information so that access is not…
❌ different answer MTP off vs depth 4: agreed for 480 chars (8%), split in the answer.
watching the bees navigate their complex social and physical environment, I realized that the principles gover…
I realized that the bees have been solving the problem of efficient retrieval long before we invented SQL. The…
Agreed for the first 480 characters (8% of the run), then split in the answer.
watching the bees navigate their complex social and physical environment, I realized that the principles governing their efficiency are not so different from those governing our databases. The hive is a living lesson in how structure enables scale, offering pr…
I realized that the bees have been solving the problem of efficient retrieval long before we invented SQL. They do not just store data; they curate it. In their waxen architecture, there is a profound lesson on how to organize information so that access is not…
Every raw file, one click each, opens in a new tab.
| Setting | Output | Thinking | Output size | Thinking size | Completed |
|---|---|---|---|---|---|
| MTP off | output ↗ | — | 3,659 chars | — | 2026-08-23 16:25 |
| depth 2 | output ↗ | — | 3,659 chars | — | 2026-08-23 16:56 |
| depth 3 | output ↗ | — | 3,659 chars | — | 2026-08-23 17:12 |
| depth 4 | output ↗ | — | 3,659 chars | — | 2026-08-23 17:27 |
| Setting | Output | Thinking | Output size | Thinking size | Completed |
|---|---|---|---|---|---|
| MTP off | output ↗ | — | 9,980 chars | — | 2026-08-23 16:35 |
| depth 2 | output ↗ | — | 9,980 chars | — | 2026-08-23 17:00 |
| depth 3 | output ↗ | — | 9,980 chars | — | 2026-08-23 17:16 |
| depth 4 | output ↗ | — | 9,980 chars | — | 2026-08-23 17:31 |
| Setting | Output | Thinking | Output size | Thinking size | Completed |
|---|---|---|---|---|---|
| MTP off | output ↗ | — | 6,398 chars | — | 2026-08-23 16:40 |
| depth 2 | output ↗ | — | 6,398 chars | — | 2026-08-23 17:03 |
| depth 3 | output ↗ | — | 5,883 chars | — | 2026-08-23 17:19 |
| depth 4 | output ↗ | — | 5,939 chars | — | 2026-08-23 17:34 |
| Prompt | Experiment 1 (thinking, sampled) | Experiment 2 (no thinking, greedy) | ||||
|---|---|---|---|---|---|---|
| off | best MTP | acceptance | off | best MTP | acceptance | |
| The Table | 4.17 tok/s | 12.69 tok/s (depth 4, +204%) | 95% | 4.18 tok/s | 12.59 tok/s (depth 4, +201%) | 93% |
| The Boilerplate | 4.19 tok/s | 12.11 tok/s (depth 4, +189%) | 89% | 4.18 tok/s | 12.91 tok/s (depth 4, +209%) | 96% |
| The Essay | 4.19 tok/s | 8.08 tok/s (depth 4, +93%) | 51% | 4.18 tok/s | 7.17 tok/s (depth 2, +72%) | 56% |
Two things to notice. The baseline is the same machine-limit either way (~4.2 tok/s at BF16: every token reads all 55 GB). And acceptance, the engine of the speedup, depends on how predictable the TEXT is, thinking transcripts included.
Generate a markdown table for a warehouse inventory with EXACTLY 60 product rows plus a final TOTAL row. Columns: Row | SKU | Product | Unit Price | Stock | Line Value Rules, follow them exactly: - Row numbers run 1 through 60. - SKU for row N is "WH-" followed by N padded to 4 digits (row 7 = WH-0007). - Product name for row N is "Widget Model N". - Unit Price for row N is 10.00 + (N * 0.25), shown with 2 decimals. - Stock for row N is 500 - (3 * N). - Line Value is Unit Price * Stock, shown with 2 decimals. - The final TOTAL row shows the sum of the Stock column and the sum of the Line Value column. Output only the table, no commentary before or after.
| Setting | Generation speed | vs off | Guesses accepted | Avg guess length | Total time | Tokens | Run completed | Audit |
|---|---|---|---|---|---|---|---|---|
| MTP off | 4.16 tok/s | baseline | — | — | 47m 45s | 11903 | 2026-08-23 01:39 | output ↗ · thinking ↗ |
| depth 2 | 9.31 tok/s | +124% | 89% | 2.77 | 20m 01s | 11171 | 2026-08-23 02:00 | output ↗ · thinking ↗ |
| depth 3 | 9.77 tok/s | +135% | 83% | 3.48 | 22m 07s | 12952 | 2026-08-23 02:24 | output ↗ · thinking ↗ |
A 47-minute generation became a 20-minute one, with acceptance holding in the 80s across ~12,000 tokens. MTP does not fade over long runs. Kept apart from the main matrix because the workload differs by design.
| Prompt | MTP off vs depth 2 | MTP off vs depth 3 | depth 2 vs depth 3 |
|---|---|---|---|
| The marathon | ❌ different answer | 🟢 SAME ANSWER, different thinking | ❌ different answer |
✅ every byte matched · 🟢 the final ANSWER matched even though the thinking transcript took a different path (different reasoning, same destination) · 🟠 thinking matched but the answer forked (the split happened while writing the answer) · 🟡 same words, only formatting/casing differs · ❌ the answers genuinely differ (equally valid, see the side-by-side splits below). Left columns: each depth against MTP off. Right columns: each depth against the previous depth, which shows whether the MTP legs agree with EACH OTHER.
❌ different answer MTP off vs depth 2: agreed for 7,728 chars (38%), split in the thinking.
ment maybe use ---: for numeric? Could use left for text. Output only table. Need no commentary. Ensure exactl…
maybe right numbers. Need exactly 60 product rows plus final TOTAL. No commentary. Could use: | Row | SKU | P…
Agreed for the first 7,728 characters (38% of the run), then split in the thinking.
ment maybe use ---: for numeric? Could use left for text. Output only table. Need no commentary. Ensure exactly 60 product rows plus total. Need final TOTAL row. How to label? Maybe: | 61 | | TOTAL | | 24510 | 418492.50 | But row numbers run 1 through 60. If…
maybe right numbers. Need exactly 60 product rows plus final TOTAL. No commentary. Could use: | Row | SKU | Product | Unit Price | Stock | Line Value | |---:|:---|:---|---:|---:|---:| | 1 | WH-0001 | Widget Model 1 | 10.25 | 497 | 5094.25 | ... | | | TOTAL …
🟢 SAME ANSWER, different thinking MTP off vs depth 3: agreed for 1,957 chars (10%), split in the thinking.
, 500-180=320. Line: P*S. Need format numbers with 2 decimals. Stock integer maybe no decimals? Column Stock l…
down to 500-180=320. Line = P*S. Need two decimals. Since price quarters and stock integer, line can be .00,.…
Agreed for the first 1,957 characters (10% of the run), then split in the thinking.
, 500-180=320. Line: P*S. Need format numbers with 2 decimals. Stock integer maybe no decimals? Column Stock likely integer. Line value 2 decimals. Unit price 2 decimals. Need produce 60 rows. Need avoid arithmetic mistakes. Let's compute systematically. We …
down to 500-180=320. Line = P*S. Need two decimals. Since price quarters and stock integer, line can be .00,.25,.50,.75? Stock integer, price multiple .25 => line multiple .25. two decimals. Let's compute systematically. Could create formula: L_N = 5000 +95N…
| Setting | Output | Thinking | Output size | Thinking size | Completed |
|---|---|---|---|---|---|
| MTP off | output ↗ | thinking ↗ | 3,648 chars | 16,744 chars | 2026-08-23 01:39 |
| depth 2 | output ↗ | thinking ↗ | 3,662 chars | 13,477 chars | 2026-08-23 02:00 |
| depth 3 | output ↗ | thinking ↗ | 3,648 chars | 18,777 chars | 2026-08-23 02:24 |
The mechanism, plainly: MTP verification checks several guessed tokens in one batch, normal generation handles one at a time, and GPUs use different math kernels for different batch sizes. The two paths agree to many decimal places, not perfectly. Run with temperature (Experiment 1) and sooner or later one near-tie between two words falls the other way; from that word on, both runs are equally valid paths of the same model. That is exactly what Experiment 1's side-by-side splits show, including the cleanest pattern in the data: depths 2, 3 and 4 share the batched path, so on high-acceptance prompts they landed byte-identical to each other and only differ from "off". Remove the dice (Experiment 2, greedy), and the flipping almost entirely stops: every structured prompt came back byte-identical at every depth. The one residual: the Essay at depths 3 and 4 still flipped, even at temperature 0, because creative prose is full of near-ties so close that the kernel-level numeric difference alone can swap the top word, which is precisely the temp-0 drift a user reported in the maintainer thread. When a split lands inside the thinking transcript, the answer that follows inherits the new path; and sometimes, as the 🟢 highlights show, the diverged thinking still lands on the exact same answer. Nothing in our setup caused any of this: no harness, seed pinned, samplers identical, flags on record. It is the documented reality of speculative decoding on GPUs, not an error, and not something to fix.
Checked, not assumed: the repeat run of the MTP-off leg (same seed, same samplers, a different day) came back byte-identical to the first run on all three prompts, thinking transcript included. So under this protocol the sampler is reproducible, and the only thing that moved any output was switching the decoding path. That is the strongest version of the statement this page can make: the off-vs-MTP fork is systematic, not noise.
| MTP off | depth 2 | depth 3 | depth 4 | |
| Peak system memory, Experiment 1 | 62 GB | 64 GB | 64 GB | 64 GB |
| Peak system memory, Experiment 2 | 64 GB | 66 GB | 66 GB | 66 GB |
Unified-memory rig: CPU and GPU share one 128 GB pool; this is total RAM in use at the highest point, OS included. MTP's draft cache costs roughly 0.5-1.5 GB depending on depth.
A language model normally writes one word-piece (token) at a time: predict, commit, repeat. Every single token requires reading the entire 55 GB model from memory once. That memory trip is the speed limit.
MTP gives the model a scribbling assistant. Qwen 3.8 ships with a tiny extra "head" trained to guess the next few tokens ahead. At generation time the assistant scribbles 2-4 tokens cheaply, and the full model checks all of them in a single one of those expensive memory trips. Guesses it agrees with are kept: the trip got you several tokens instead of one. The first wrong guess and everything after it is thrown away, and the model continues from there exactly as it would have anyway.
That is why the quality cannot get worse: the big model still approves every single token. The assistant never gets to publish a word the writer did not sign off on. Predictable text (tables, boilerplate) = constant right guesses = you fly. Creative prose = more misses = smaller speedup, which is exactly what the acceptance column shows.
Every rejected guess cost the assistant a little compute and used up a slot in the verification pass that produced nothing. llama.cpp folds that overhead into total generation time, so you never see it as its own number, but you can measure it: it is the gap between the theoretical ceiling (depth N could at best approach N+1 tokens per trip) and what actually shows up. Low acceptance = more waste = smaller net gain. That is why the Essay column exists.
Prompt processing (prefill) already handles many tokens per memory trip; it is naturally parallel. MTP only helps the one-at-a-time part, generation. Our prefill measurements barely moved across settings, as expected.
Why the GPU has room for this at all: for every token, the whole model has to be read from memory once, and the arithmetic finishes long before that read does. The compute units mostly wait. Speculative decoding, and MTP as its built-in form, spends that idle compute on checking several drafted tokens per memory trip. That framing comes from Devsplainers' MTP explainer, which also quotes community numbers on Qwen 3.6 27B that match the shape measured here: about 3x on Python at ~90% acceptance, 2 to 2.5x on general chat, closer to 1.8x on creative writing. It also notes a theoretical ceiling: the speedup grows only logarithmically with draft length, so 3 to 5 draft tokens is where the trade-off usually lands, which is consistent with depth 4 beating depth 2 by a modest ~25% on this rig rather than doubling it.
| 2023, Google | Fast Inference from Transformers via Speculative Decoding (Leviathan et al.) invented the trick: let a small draft model guess ahead and have the big model verify in one pass. Catch: you needed a whole second model. |
|---|---|
| 2024, Meta | Better & Faster Large Language Models via Multi-token Prediction (Gloeckle et al.) trained the guessing INTO the model itself as extra prediction heads, no second model needed, and found it even improves training. |
| 2024, DeepSeek | The DeepSeek-V3 technical report refined it into the sequential MTP module that ships in production models, used both as a training signal and for exactly this kind of self-speculative decoding. Qwen 3.8 follows this design. Readable walkthrough: DeepSeek Explained 4: Multi-Token Prediction. |
The MTP head ships inside the GGUF file you already downloaded, at every quant level. But llama.cpp keeps
it OFF unless you pass --spec-type draft-mtp --spec-draft-n-max N (support landed in
PR #22673, July 2026).
Unsloth's launcher passes that flag for you, at depth 2, and does not print it. So a plain llama.cpp
launch may be running without it, and an Unsloth launch (including the earlier benchmark runs on this channel)
may have had it on without you knowing. Ollama has MTP in a release candidate, and vLLM and TensorRT-LLM support
it in production, per the Devsplainers explainer linked below; check your own launcher's flags and log. Toggle it yourself with unsloth run --speculative-type off or
sweep depths with --spec-draft-n-max 2..4. On this rig, depth 4 beat the silent default by another
~25%.
Not measured here, but worth knowing before you flip the flag. Three cases from the Devsplainers explainer above: mixture-of-experts models, where verifying drafted tokens can route to different experts and load different weight slices for each one (they cite a Qwen 3.6 MoE benchmark on a single RTX 3090 where every speculative configuration was slower than baseline despite high acceptance); high concurrency, since at batch sizes above roughly 16 to 32 the GPU is already compute-bound and there is no idle capacity to spend, so MTP is a latency trick rather than a throughput trick; and small models in the 1 to 3 billion parameter range that already run at 100+ tokens per second, where the drafting overhead becomes a noticeable slice of every step. This page is one dense 27B model at batch size 1, the case MTP was built for.
kyuz0's Strix Halo MTP benchmarks (same Ryzen AI MAX 395+ / 128 GB platform as mine) measured Qwen 3.6 27B at 1.81x (Q4, depth 3) and 2.44x (Q8). This page adds Qwen 3.8 at BF16 to that picture, on the same silicon. My acceptance rates and multipliers came out higher; I attribute that to prompt design (the Table and Boilerplate prompts are built to be predictable) rather than to anything about the hardware or the build, which is itself a reason to lean on established benchmark suites instead of home-made prompts. More on that in the conclusion.
| Seed | 42 passed on every request (one shot per leg; no retries on completed responses). |
|---|---|
| Flags per launch | Every llama-server launch line is preserved in the server logs; the MTP flags for each leg are exactly as labeled. |
| No harness | Raw /v1/chat/completions calls only. No system prompt, no tools, no agent
loop anywhere in the measurement path. |
| Speed source | llama.cpp's own per-request timing lines (generation and prefill), not stopwatch math; acceptance from the same log. |
| Table math | All 60 rows + totals recomputed against the formulas by script, both experiments (Experiment 1: 59/60 cells exact everywhere, totals correct; Experiment 2: 59/60 exact, totals wrong, see the thinking note). |
| Code validity | Boilerplate output passes a Python syntax check at every setting in both experiments. |
| Output equivalence | Byte-level diffs of every leg pair, thinking and answer compared separately, with a side-by-side view of every divergence and full outputs inline and via audit links. |
| Timestamps | Each leg's completion time is stamped from its output file and shown in every results table and dropdown. |
This applies to my machine, my model, my prompts and my two protocols. It is shared for people who are curious what I ran into while researching the topic, structured the way I wanted to experience it.
What the two experiments showed. In Experiment 2 I controlled every variable I could (thinking off, greedy decoding, fixed seed) and got byte-identical output at every MTP depth on the structured prompts, with the essay's near-ties as the one measured residual. In Experiment 1 I let the dice back in (thinking on, temperature 1.0), and the outputs forked between MTP off and MTP on, with the side-by-side splits showing where. Then I reran the MTP-off leg with the same seed and it reproduced itself byte for byte on all three prompts, so the fork is not run-to-run noise: it is the speculative path itself nudging a near-tie token the other way. Both results are what the theory predicts; the point of running them was to see it on my own hardware rather than take it on faith.
What I would do with it. Enable it. For Qwen 3.8 on this rig it takes BF16 from ~4.2 tok/s to ~12.7 tok/s at depth 4 on predictable output, holds real gains on prose, does not fade over a 12,000-token marathon, and costs about a gigabyte of memory. For a model like Qwen 3.8: check the flag in your llama.cpp or launcher config, because the default may not be what you think, and have it on at depth 2 for every kind of task. Depths 3 and 4 are something to test for your own use case rather than a blanket recommendation: on this build they added more speed on coding and structured output and less on creative writing, and whether that trade-off is worth it depends on your hardware and memory (a discrete GPU, a Strix Halo box like mine, a GB10, all different). I can only speak for this specific build and these findings.
Where I am honest about the limits. My acceptance rates beat the published Strix Halo numbers, and the main reason is that my prompts were written to be predictable. That is prompt design, not a hardware result, and it is a skill I am still building. The better move, which I intend to work toward, is to stop reinventing the wheel: run the established benchmark suites, contribute to them where they fall short, and compare against the same prompts other people use, so the numbers are comparable instead of merely mine.
Full reasoning and the story of finding all this: in the video.
The explainer that made this click for me: Donato Capitella's MTP video, measured on this same hardware family. My video builds on his foundation with my own rig's numbers; go watch his for the step-by-step mechanics.
GMKtec EVO-X2 mini PC: AMD Ryzen AI Max+ 395, 128 GB unified memory (CPU and GPU share one pool, which is why a 55 GB model fits at all). Linux (CachyOS), models served by Unsloth's launcher on top of llama.cpp.
My exact build: GMKtec EVO-X2 (Ryzen AI Max+ 395, 128GB)
The newer tower model: GMKtec EVO-X3 (tower cooling + OCuLink)
Some links above are affiliate links. If you buy through them I may earn a small commission at no
extra cost to you. It helps support the channel.
As an Amazon Associate I earn from qualifying purchases.
Made by Jose (hijoseromero.com) · every number above was measured on the machine described, logs retained. Figuring out tech, together.