Up to 3.09x faster

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.

The two experiments, and why there are two

Experiment 1: thinking on, recommended samplingExperiment 2: the clean-room config
QuestionWhat does MTP do with thinking on and the model card's recommended sampling? With randomness removed, is speed the ONLY thing MTP changes?
ThinkingON, reasoning effort medium (set at server launch)OFF
SamplingOfficial 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 settingsThe 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 legsThe 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.

Experiment 1: thinking ON, model-card sampling

"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.

The Table 60 rows of formula-driven inventory data, every cell checkable

The exact prompt sent (one shot, as-is; the runner strips only the file's title line)
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.
SettingGeneration speedvs offGuesses acceptedAvg guess lengthTotal timeTokensRun completedAudit
MTP off4.17 tok/sbaseline29m 05s72722026-08-23 02:55output ↗ · thinking ↗
depth 210.01 tok/s+140%99%2.9712m 08s72722026-08-23 03:09output ↗ · thinking ↗
depth 311.0 tok/s+164%97%3.9111m 05s73032026-08-23 03:22output ↗ · thinking ↗
depth 412.69 tok/s+204%95%4.789m 37s73032026-08-23 03:33output ↗ · 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).

The Boilerplate a five-resource FastAPI back end, repetitive by design

The exact prompt sent (one shot, as-is; the runner strips only the file's title line)
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.
SettingGeneration speedvs offGuesses acceptedAvg guess lengthTotal timeTokensRun completedAudit
MTP off4.19 tok/sbaseline9m 48s24552026-08-22 23:39output ↗ · thinking ↗
depth 29.11 tok/s+117%94%2.874m 21s23672026-08-23 00:02output ↗ · thinking ↗
depth 310.53 tok/s+151%91%3.733m 46s23672026-08-23 00:19output ↗ · thinking ↗
depth 412.11 tok/s+189%89%4.543m 16s23672026-08-23 00:37output ↗ · 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).

The Essay 800 words of unpredictable prose, the draft head's worst case

The exact prompt sent (one shot, as-is; the runner strips only the file's title line)
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.
SettingGeneration speedvs offGuesses acceptedAvg guess lengthTotal timeTokensRun completedAudit
MTP off4.19 tok/sbaseline9m 54s24852026-08-22 23:49output ↗ · thinking ↗
depth 26.53 tok/s+56%53%2.066m 05s23802026-08-23 00:08output ↗ · thinking ↗
depth 36.31 tok/s+51%41%2.237m 09s27032026-08-23 00:27output ↗ · thinking ↗
depth 48.08 tok/s+93%51%3.0412m 50s62182026-08-23 00:49output ↗ · 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).

Did it change the output?

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.

Score card: 15 comparisons across 3 prompts. 4 byte-identical, 0 same final answer via different thinking, 11 with a different answer. Every one of the "different" cases is shown below with the exact character where it split.
PromptMTP off vs depth 2MTP off vs depth 3MTP off vs depth 4depth 2 vs depth 3depth 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.

PromptAnswer: who matched whomThinking: who matched whomIn plain words
The TableMTP 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 Boilerplatedepth 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 EssayMTP 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.

🟢 Worth staring at:
The Boilerplate: depth 2, depth 3, depth 4 produced the byte-identical answer to each other. The only leg that differs is MTP off, so the fork is between the two decoding paths, not between MTP depths.

Where exactly they split

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.

The Table

✅ 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.

both wrote …et me format the table. The TOTAL row should show the sum of Stock and sum of Line Value.
MTP off
What about the other columns? I'll put dashes or leave them blank. Let me just put "TOTAL" in the Product colu…
depth 3 = depth 4
For other columns in the TOTAL row, I'll leave them blank or put a dash.

Actually, re-reading the instruction…
longer side-by-side

Agreed for the first 6,819 characters (64% of the run), then split in the thinking.

both wrote…e total with a different approach. Let me check a few more: Row 30: 17.50 * 410 = 7175.00 ✓ Row 20: 15.00 * 440 = 6600.00 ✓ Now let me format the table. The TOTAL row should show the sum of Stock and sum of Line Value.
MTP off continued
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 |
|-----|-----…
depth 3 = depth 4 continued
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…

The Boilerplate

🟠 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.

both wrote …nt = 0 customers_store: dict[int, Customer] = {} customers_counter: int = 0 # ── Books
MTP off
Endpoints ───────────────────────────────────────────────────────────

@app.get("/books", response_model=List[…
depth 2 = depth 3 = depth 4
─────────────────────────────────────────────────────────────────────

@app.get("/books", response_model=List[…
longer side-by-side

Agreed for the first 2,350 characters (24% of the run), then split in the answer.

both wrote… 0 publishers_store: dict[int, Publisher] = {} publishers_counter: int = 0 genres_store: dict[int, Genre] = {} genres_counter: int = 0 customers_store: dict[int, Customer] = {} customers_counter: int = 0 # ── Books
MTP off continued
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…
depth 2 = depth 3 = depth 4 continued
─────────────────────────────────────────────────────────────────────

@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…

The Essay

❌ different answer MTP off vs depth 2: agreed for 5,175 chars (46%), split in the thinking.

both wrote …ng prose, reflective tone, about 800 words. Let me aim for a personal, thoughtful voice -
MTP off
someone who has actually kept bees or spent time with hives, reflecting on their engineering work.

Let me dra…
depth 2
like an engineer who has taken a beekeeping class or observed a hive and had a "click" moment.

Let me draft t…
longer side-by-side

Agreed for the first 5,175 characters (46% of the run), then split in the thinking.

both wrote… in the outer rings (less accessed), and ensure the index structure reflects that frequency hierarchy. Let me write this as flowing prose, reflective tone, about 800 words. Let me aim for a personal, thoughtful voice -
MTP off continued
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…
depth 2 continued
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.

both wrote …index is like the comb structure that makes retrieval O(log n) instead of O(n). 2. **The
MTP off
waggle dance / navigation**: Bees use the waggle dance to communicate the location of food sources. The angle …
depth 3
dance language / navigation**: When a forager bee finds a good flower source, it performs the waggle dance to …
longer side-by-side

Agreed for the first 1,213 characters (11% of the run), then split in the thinking.

both wrote…eds of frames - you can go frame by frame (sequential scan) or you can know which box, which frame, which cell contains what. The index is like the comb structure that makes retrieval O(log n) instead of O(n). 2. **The
MTP off continued
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…
depth 3 continued
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.

both wrote …se landmarks, the sun, polarized light to navigate. They don't re-scan the entire meadow e
MTP off
ach time. They build a mental map - an index - of where flowers are. This maps to query plans and how the opti…
depth 4
very time. They build a mental map. This is like a cached index vs. a cold index that needs to be rebuilt.

5.…
longer side-by-side

Agreed for the first 2,499 characters (22% of the run), then split in the thinking.

both wrote…ng two - the spatial arrangement IS the index. 4. **Forager bees and the "index" of the hive**: Forager bees leave the hive and use landmarks, the sun, polarized light to navigate. They don't re-scan the entire meadow e
MTP off continued
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.

…
depth 4 continued
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 …

Full results, leg by leg

Every raw file, one click each, opens in a new tab.

The Table

SettingOutputThinkingOutput sizeThinking sizeCompleted
MTP offoutput ↗thinking ↗3,685 chars7,008 chars2026-08-23 02:55
depth 2output ↗thinking ↗3,685 chars7,008 chars2026-08-23 03:09
depth 3output ↗thinking ↗3,683 chars7,139 chars2026-08-23 03:22
depth 4output ↗thinking ↗3,683 chars7,139 chars2026-08-23 03:33

The Boilerplate

SettingOutputThinkingOutput sizeThinking sizeCompleted
MTP offoutput ↗thinking ↗8,517 chars1,192 chars2026-08-22 23:39
depth 2output ↗thinking ↗8,182 chars1,192 chars2026-08-23 00:02
depth 3output ↗thinking ↗8,182 chars1,192 chars2026-08-23 00:19
depth 4output ↗thinking ↗8,182 chars1,192 chars2026-08-23 00:37

The Essay

SettingOutputThinkingOutput sizeThinking sizeCompleted
MTP offoutput ↗thinking ↗5,838 chars5,411 chars2026-08-22 23:49
depth 2output ↗thinking ↗5,277 chars5,393 chars2026-08-23 00:08
depth 3output ↗thinking ↗5,591 chars6,818 chars2026-08-23 00:27
depth 4output ↗thinking ↗4,801 chars21,448 chars2026-08-23 00:49
One detail from Experiment 1: the model made ONE arithmetic slip in the 60-row table (row 15: it wrote 13.75 × 455 = 6251.25; the real answer is 6256.25), and it made the SAME slip at every setting, off included, while its TOTAL row (418,492.50) matches the true math exactly. It mis-wrote one cell and summed the formulas correctly. MTP reproduced the model faithfully, mistake and all.

Experiment 1, repeated: the baseline run twice

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.

The Table

SettingGeneration speedvs offGuesses acceptedAvg guess lengthTotal timeTokensRun completedAudit
off, run 14.17 tok/s-29m 05s72722026-08-23 02:55output ↗ · thinking ↗
off, run 24.17 tok/s-29m 06s72722026-08-25 01:09output ↗ · thinking ↗

The Boilerplate

SettingGeneration speedvs offGuesses acceptedAvg guess lengthTotal timeTokensRun completedAudit
off, run 14.19 tok/s-9m 48s24552026-08-22 23:39output ↗ · thinking ↗
off, run 24.18 tok/s-9m 48s24552026-08-25 01:19output ↗ · thinking ↗

The Essay

SettingGeneration speedvs offGuesses acceptedAvg guess lengthTotal timeTokensRun completedAudit
off, run 14.19 tok/s-9m 54s24852026-08-22 23:49output ↗ · thinking ↗
off, run 24.18 tok/s-9m 55s24852026-08-25 01:29output ↗ · thinking ↗

Did run 2 reproduce run 1?

What happened:
The Table: run 2 reproduced run 1 byte for byte (answer and thinking).
The Boilerplate: run 2 reproduced run 1 byte for byte (answer and thinking).
The Essay: run 2 reproduced run 1 byte for byte (answer and 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.

Promptrun 1 vs run 2run 2 vs depth 2run 2 vs depth 3run 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
PromptAnswer: who matched whomThinking: who matched whomIn plain words
The Tableoff, 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 Boilerplatedepth 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 Essayoff, 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.

Where run 1 and run 2 differ, if anywhere

The Table

✅ identical (answer + thinking) off, run 1 vs off, run 2

The Boilerplate

✅ identical (answer + thinking) off, run 1 vs off, run 2

The Essay

✅ identical (answer + thinking) off, run 1 vs off, run 2

Full results

The Table

SettingOutputThinkingOutput sizeThinking sizeCompleted
off, run 1output ↗thinking ↗3,685 chars7,008 chars2026-08-23 02:55
off, run 2output ↗thinking ↗3,685 chars7,008 chars2026-08-25 01:09

The Boilerplate

SettingOutputThinkingOutput sizeThinking sizeCompleted
off, run 1output ↗thinking ↗8,517 chars1,192 chars2026-08-22 23:39
off, run 2output ↗thinking ↗8,517 chars1,192 chars2026-08-25 01:19

The Essay

SettingOutputThinkingOutput sizeThinking sizeCompleted
off, run 1output ↗thinking ↗5,838 chars5,411 chars2026-08-22 23:49
off, run 2output ↗thinking ↗5,838 chars5,411 chars2026-08-25 01:29

Experiment 2: thinking OFF, greedy, the clean room

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).

Experiment 2's settings, explained parameter by parameter

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.

ParameterExp 1 (recommended)Exp 2What it actually does, and why we changed it
temperature1.00 (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_p0.95 / 20 / 0inactive 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_thinkingtruefalseQwen 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_effortmedium (server launch)not applicableThe 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.
seed4242The 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 elseidenticalSame 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.

The Table 60 rows of formula-driven inventory data, every cell checkable

The exact prompt sent (one shot, as-is; the runner strips only the file's title line)
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.
SettingGeneration speedvs offGuesses acceptedAvg guess lengthTotal timeTokensRun completedAudit
MTP off4.18 tok/sbaseline9m 45s24432026-08-23 16:25output ↗
depth 210.03 tok/s+140%99%2.974m 05s24432026-08-23 16:56output ↗
depth 311.21 tok/s+168%99%3.963m 39s24432026-08-23 17:12output ↗
depth 412.59 tok/s+201%93%4.733m 15s24432026-08-23 17:27output ↗

Legs of this test completed 2026-08-23 16:25 → 2026-08-23 17:27 (local time; each row above carries its own stamp).

The Boilerplate a five-resource FastAPI back end, repetitive by design

The exact prompt sent (one shot, as-is; the runner strips only the file's title line)
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.
SettingGeneration speedvs offGuesses acceptedAvg guess lengthTotal timeTokensRun completedAudit
MTP off4.18 tok/sbaseline10m 11s25482026-08-23 16:35output ↗
depth 210.03 tok/s+140%99%2.984m 15s25482026-08-23 17:00output ↗
depth 311.06 tok/s+165%97%3.913m 52s25482026-08-23 17:16output ↗
depth 412.91 tok/s+209%96%4.843m 18s25482026-08-23 17:31output ↗

Legs of this test completed 2026-08-23 16:35 → 2026-08-23 17:31 (local time; each row above carries its own stamp).

The Essay 800 words of unpredictable prose, the draft head's worst case

The exact prompt sent (one shot, as-is; the runner strips only the file's title line)
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.
SettingGeneration speedvs offGuesses acceptedAvg guess lengthTotal timeTokensRun completedAudit
MTP off4.18 tok/sbaseline4m 56s12362026-08-23 16:40output ↗
depth 27.17 tok/s+72%56%2.122m 53s12362026-08-23 17:03output ↗
depth 36.49 tok/s+55%43%2.293m 04s11892026-08-23 17:19output ↗
depth 46.13 tok/s+47%33%2.303m 18s12102026-08-23 17:34output ↗

Legs of this test completed 2026-08-23 16:40 → 2026-08-23 17:34 (local time; each row above carries its own stamp).

Did it change the output?

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.

Score card: 15 comparisons across 3 prompts. 11 byte-identical, 4 with a different answer. Every one of the "different" cases is shown below with the exact character where it split.
PromptMTP off vs depth 2MTP off vs depth 3MTP off vs depth 4depth 2 vs depth 3depth 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.

PromptAnswer: who matched whomIn plain words
The TableMTP off = depth 2 = depth 3 = depth 4 (574272a9)All 4 legs wrote the byte-identical answer.
The BoilerplateMTP off = depth 2 = depth 3 = depth 4 (88d512b6)All 4 legs wrote the byte-identical answer.
The EssayMTP 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.

Where exactly they split

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.

The Table

✅ identical MTP off vs depth 2  ·  ✅ identical MTP off vs depth 3  ·  ✅ identical MTP off vs depth 4

The Boilerplate

✅ identical MTP off vs depth 2  ·  ✅ identical MTP off vs depth 3  ·  ✅ identical MTP off vs depth 4

The Essay

✅ identical MTP off vs depth 2

❌ different answer MTP off vs depth 3: agreed for 480 chars (8%), split in the answer.

both wrote …ce for speed, a cold calculation of B-trees and hash maps. Yet, standing before the hive,
MTP off
watching the bees navigate their complex social and physical environment, I realized that the principles gover…
depth 3
I realized that the bees have been solving the problem of efficient retrieval long before we invented SQL. The…
longer side-by-side

Agreed for the first 480 characters (8% of the run), then split in the answer.

both wrote…ure of data structures, particularly the index. We often view indexing as a purely mechanical optimization—a way to trade disk space for speed, a cold calculation of B-trees and hash maps. Yet, standing before the hive,
MTP off continued
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…
depth 3 continued
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.

both wrote …ce for speed, a cold calculation of B-trees and hash maps. Yet, standing before the hive,
MTP off
watching the bees navigate their complex social and physical environment, I realized that the principles gover…
depth 4
I realized that the bees have been solving the problem of efficient retrieval long before we invented SQL. The…
longer side-by-side

Agreed for the first 480 characters (8% of the run), then split in the answer.

both wrote…ure of data structures, particularly the index. We often view indexing as a purely mechanical optimization—a way to trade disk space for speed, a cold calculation of B-trees and hash maps. Yet, standing before the hive,
MTP off continued
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…
depth 4 continued
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…

Full results, leg by leg

Every raw file, one click each, opens in a new tab.

The Table

SettingOutputThinkingOutput sizeThinking sizeCompleted
MTP offoutput ↗3,659 chars2026-08-23 16:25
depth 2output ↗3,659 chars2026-08-23 16:56
depth 3output ↗3,659 chars2026-08-23 17:12
depth 4output ↗3,659 chars2026-08-23 17:27

The Boilerplate

SettingOutputThinkingOutput sizeThinking sizeCompleted
MTP offoutput ↗9,980 chars2026-08-23 16:35
depth 2output ↗9,980 chars2026-08-23 17:00
depth 3output ↗9,980 chars2026-08-23 17:16
depth 4output ↗9,980 chars2026-08-23 17:31

The Essay

SettingOutputThinkingOutput sizeThinking sizeCompleted
MTP offoutput ↗6,398 chars2026-08-23 16:40
depth 2output ↗6,398 chars2026-08-23 17:03
depth 3output ↗5,883 chars2026-08-23 17:19
depth 4output ↗5,939 chars2026-08-23 17:34
Repeatability check, for free: the Table ran twice per setting in this configuration, in completely separate server launches (a pilot control run, then the full cycle). Byte-comparing the pairs:
MTP off: ✅ byte-identical across two separate server launches
depth 2: ✅ byte-identical across two separate server launches
depth 3: ✅ byte-identical across two separate server launches
What thinking bought, measured by accident: in Experiment 1 (thinking on) the model's TOTAL row matched the true math exactly while mis-writing one cell (row 15, off by 5.00). In Experiment 2 (thinking off) it also made exactly one cell slip, a different one (row 27, off by 1.00), but its TOTAL row is just plausible round numbers (18,300 / 427,500 vs the true 24,510 / 418,492.50). Sixty rows of multiplication: fine either way. Summing sixty numbers: thinking mode did it, no-thinking guessed. Same model, one dial.

Direct comparison: thinking vs no thinking

PromptExperiment 1 (thinking, sampled) Experiment 2 (no thinking, greedy)
offbest MTPacceptanceoffbest MTPacceptance
The Table4.17 tok/s12.69 tok/s (depth 4, +204%)95%4.18 tok/s12.59 tok/s (depth 4, +201%)93%
The Boilerplate4.19 tok/s12.11 tok/s (depth 4, +189%)89%4.18 tok/s12.91 tok/s (depth 4, +209%)96%
The Essay4.19 tok/s8.08 tok/s (depth 4, +93%)51%4.18 tok/s7.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.

Bonus experiment: the marathon Experiment 1 side: Qwen's factory-default effort (xhigh), a ~12,000-token thinking workload

The exact prompt sent (one shot, as-is; the runner strips only the file's title line)
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.
SettingGeneration speedvs offGuesses acceptedAvg guess lengthTotal timeTokensRun completedAudit
MTP off4.16 tok/sbaseline47m 45s119032026-08-23 01:39output ↗ · thinking ↗
depth 29.31 tok/s+124%89%2.7720m 01s111712026-08-23 02:00output ↗ · thinking ↗
depth 39.77 tok/s+135%83%3.4822m 07s129522026-08-23 02:24output ↗ · 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.

Did it change the output?

PromptMTP off vs depth 2MTP off vs depth 3depth 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.

🟢 Worth staring at:
The marathon, off vs depth 3: the thinking transcripts diverged, and both runs still arrived at the byte-identical final answer. Different path, same destination.

The side-by-side splits

❌ different answer MTP off vs depth 2: agreed for 7,728 chars (38%), split in the thinking.

both wrote ….00; +77636.25=338931.25; +79561.25=418492.50. Matches. Need format markdown table. Align
MTP off
ment maybe use ---: for numeric? Could use left for text. Output only table. Need no commentary. Ensure exactl…
depth 2
 maybe right numbers. Need exactly 60 product rows plus final TOTAL. No commentary. Could use:
| Row | SKU | P…
longer side-by-side

Agreed for the first 7,728 characters (38% of the run), then split in the thinking.

both wrote….00=63567.00 +7994.25=71561.25 +8000.00=79561.25. Total groups: 54936.25+62861.25=117797.50; +69286.25=187083.75; +74211.25=261295.00; +77636.25=338931.25; +79561.25=418492.50. Matches. Need format markdown table. Align
MTP off continued
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…
depth 2 continued
 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.

both wrote …25, 10.50, 10.75, 11.00, ... increments .25. For N=60: 10+15=25.00. Stock: 497,494,491,...
MTP off
, 500-180=320.
Line: P*S.
Need format numbers with 2 decimals. Stock integer maybe no decimals? Column Stock l…
depth 3
 down to 500-180=320.
Line = P*S. Need two decimals. Since price quarters and stock integer, line can be .00,.…
longer side-by-side

Agreed for the first 1,957 characters (10% of the run), then split in the thinking.

🟢 Despite this split in the thinking, the FINAL ANSWER of both runs is byte-identical. The reasoning took a different route and landed on the same output.
both wrote… individual line values. Could compute table manually or generate via reasoning. Need ensure exact. Let's compute rows. P_N = 10.25, 10.50, 10.75, 11.00, ... increments .25. For N=60: 10+15=25.00. Stock: 497,494,491,...
MTP off continued
, 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 …
depth 3 continued
 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…

Full results, leg by leg

The marathon

SettingOutputThinkingOutput sizeThinking sizeCompleted
MTP offoutput ↗thinking ↗3,648 chars16,744 chars2026-08-23 01:39
depth 2output ↗thinking ↗3,662 chars13,477 chars2026-08-23 02:00
depth 3output ↗thinking ↗3,648 chars18,777 chars2026-08-23 02:24

Why outputs can differ at all (and why that is expected)

Is this a llama.cpp bug? No, and the maintainers have answered it directly. Bug report #23302 ("MTP changed my deterministic output") was closed because the reporter had no fixed seed; the follow-up, #23335, reproduced it WITH a fixed seed, and llama.cpp's creator Georgi Gerganov replied: "This is expected - we use different kernels for different batch sizes." (Same thread: on Q8_0 the reporter got identical output at every depth, and another user measured a strict-JSON pipeline drop from 100% to 97% with MTP on at temperature 0, so near-tie flips can matter for exacting workflows.)

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.

Memory cost

MTP offdepth 2depth 3depth 4
Peak system memory, Experiment 162 GB64 GB64 GB64 GB
Peak system memory, Experiment 264 GB66 GB66 GB66 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.

What is MTP?

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.

So where does the wasted time go when it guesses wrong?

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.

Why doesn't MTP speed up reading my prompt too?

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.

Where MTP came from (a short lineage)

2023, GoogleFast 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, MetaBetter & 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, DeepSeekThe 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.

Worth checking if you run llama.cpp: the flag

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%.

When MTP may not help

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.

Same hardware, other people's numbers

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.

Validation checklist (everything we checked, and how)

Seed42 passed on every request (one shot per leg; no retries on completed responses).
Flags per launchEvery llama-server launch line is preserved in the server logs; the MTP flags for each leg are exactly as labeled.
No harnessRaw /v1/chat/completions calls only. No system prompt, no tools, no agent loop anywhere in the measurement path.
Speed sourcellama.cpp's own per-request timing lines (generation and prefill), not stopwatch math; acceptance from the same log.
Table mathAll 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 validityBoilerplate output passes a Python syntax check at every setting in both experiments.
Output equivalenceByte-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.
TimestampsEach leg's completion time is stamped from its output file and shown in every results table and dropdown.

Conclusion

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.

Credit where it's due

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.

The rig all of this ran on

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.

All sources

Made by Jose (hijoseromero.com) · every number above was measured on the machine described, logs retained. Figuring out tech, together.