RESEARCH · #004

MTP in Practice: Benchmarking Gemma's Speculative Decoding on a Real GPU

We ran Gemma-4-12B's MTP drafter on a consumer RTX 3060 and measured, turn by turn, exactly how much faster it gets. Part 2 of 2: the implementation/benchmark edition.

2026.08.17 11 min read Part 2 of 2

In the concept edition, we saw that MTP (Multi-Token Prediction) is a "low-risk, high-reward bet," and that Qwen and Gemma implement this bet in completely different ways.

Fine, the theory makes sense. But how much faster does this actually make things in practice? Is the bet really paying off?

In this installment — the implementation/benchmark edition — we'll actually run Gemma's MTP on llama.cpp and measure, with real numbers on an ordinary consumer PC, just how much of an effect it has.

Running It in llama.cpp

For Qwen Models

Run with the following arguments added. Specifying spec-type activates the model's internal MTP drafter. A dedicated model is provided for MTP.

/opt/llama/bin/llama-server --model /opt/llama/models/Qwen3.5-9B-MTP-UD-Q3_K_XL.gguf \
-t 4 -np 1 --prio 2 --temp 1.0 --top-p 0.95 --top-k 20 --min-p 0.00 --host 0.0.0.0 --port 8001 \
--device CUDA0 -mg 0 -ctv q8_0 -ctk q8_0 --fit off --no-warmup --no-cache-prompt -c 32768 \
--no-warmup --no-cache-prompt --cache-ram 0 \
--spec-type draft-mtp --spec-draft-n-max 4 \
--reasoning on

Argument summary:

For Gemma Models

Run with the following arguments. For Gemma, the drafter-MTP model is bolted on externally, so you enable MTP by pointing the --model-draft argument at that file.

/opt/llama/bin/llama-server --model /opt/llama/models/gemma-4-12B-it-qat-UD-Q4_K_XL.gguf \
--model-draft /opt/llama/models/mtp-gemma-4-12B-it.gguf \
-t 4 --prio 2 --temp 1.0 --top-p 0.95 --top-k 64 --host 0.0.0.0 --port 8001 \
--device CUDA0 -mg 0 -sm layer --fit on -fa on -c 163840 -ctv q8_0 -ctk q8_0 \
--no-warmup --no-cache-prompt --cache-ram 0 \
--spec-type draft-mtp --spec-draft-n-max 4 --spec-draft-device CUDA0 \
--chat-template-kwargs '{"enable_thinking":true}'

Argument summary:

Benchmark: Using MTP on Gemma-4-12B-it

In this section, to investigate how effectively MTP actually works and what kind of benefit it brings to users, we compared performance with and without MTP.

To also demonstrate that this kind of model verification is achievable even on ordinary consumer hardware, the benchmark was run on the author's own personal machine.

Test Environment

The experiment used the following simple test setup.

Test Cases

Goal: measure and compare speed with and without MTP. Only speed is measured — the actual content of the generated output is not evaluated at all. We measure elapsed time, token count, and token speed during generation, taken directly from llama.cpp's own log output.

Method: using the llama.cpp web frontend, we asked the following four turns of questions.

Resource Usage

Memory usage was as follows. The memory consumed specifically for MTP came to 759.68 MiB of VRAM on the GPU side and 324.79 MiB of RAM on the CPU side.

Since we used a 4-bit quantized model for the draft model here, choosing a 16-bit model instead would consume roughly 4x this amount for the weight data. Anyone planning to use a 16-bit model should keep this in mind.

ComponentCUDA0CPU
Weight data6,390.19540.00
KV cache data1,360.00
Sliding window KV cache765.00
Gated DeltaNet compute buffer533.80180.80
MTP (Assistant) model weight data226.90144.00
MTP (Assistant) model Gated DeltaNet compute buffer532.78180.79
Vision Encoder167.00
Audio Encoder167.00
Multimodal Gated DeltaNet compute buffer532.78180.79
Total10,675.451,226.38

Table 1. Memory usage in this test environment. Units: MiB.

Token Speed Comparison Results (MTP On vs. Off)

Comparing token ingestion speed turn by turn, we saw no major variation. With MTP enabled, ingestion speed dropped slightly compared to without MTP, coming in at roughly 70–73% of the baseline performance (see the "Read" rows in the table below).

Token generation speed, on the other hand, was roughly twice as fast overall with MTP enabled (see the "Generate" rows).

Below are the actual measurements. Note that time is measured in ms.

TurnType No MTP: timeNo MTP: tokensNo MTP: tps With MTP: timeWith MTP: tokensWith MTP: tps
1Read3012583.0418125138.34
2Read1,8361,834999.103,0042,135710.71
3Read4,5464,6691,027.066,3614,666733.53
4Read7,0177,1021,012.119,5597,030735.43
1Generate45,3261,78939.4733,1862,08462.80
2Generate80,1902,80234.9440,2812,49962.04
3Generate70,1642,40534.2835,0462,33766.68
4Generate84,3252,85133.8145,5322,85862.77

Table 2. Actual data from the logs.

Without MTP, the whole run took about 293,705 ms in total — just under 5 minutes — while with MTP it completed in about 173,150 ms, just under 3 minutes. That's roughly a 40% reduction in time for this case.

This suggests MTP is a genuinely effective way to boost efficiency in tasks like today's agentic workflows, which repeat many rounds of inference.

To look more closely at the trend, we also checked how speed changed as token output progressed within each turn (again using the logs).

Below is the token output profile without MTP. Speed dips slightly but stays roughly flat overall, hovering in the mid-30s to low-40s tps across all four turns from start to finish.

Generally, with token-by-token generation, output speed tends to gradually decline as context size grows. How much it degrades depends on the model, but this can become a non-trivial problem as more turns accumulate.

By contrast, here is the token output profile with MTP enabled. Speed starts around 35–40 tps early on and climbs to 60–65 tps later, showing that tokens come out faster and faster as the conversation progresses.

A quick note before we go further: I'm a native Japanese speaker, and this model is my daily driver, so the test prompts and responses are in Japanese rather than English. Tokenizer behavior can differ quite a bit across languages, so this also happens to double as a look at how MTP performs outside the English-first benchmarks you usually see.

Next, we examined the actual content of the responses and found that each one opens with a Japanese explanation followed by code. (The original screenshot of that exchange is in Japanese, so it isn't reproduced here — the token breakdown below covers what matters for this analysis.)

Based on that, we checked the token length of the Japanese explanation portion at the start of each turn, with the following results.

TurnToken count of the Japanese portion
1107
2147
3124
4256

Table 3. Token length of the Japanese explanation at the start of each turn.

We can't fully prove causation here, but turns where speed rose sharply (Turn 1, Turn 3) tended to have less Japanese output, while turns where speed rose more gradually (Turn 2, Turn 4) tended to have somewhat more Japanese output. This suggests the code-generation portion is where MTP's effect shows up most strongly.

A likely reason is that, regardless of programming language, code follows fairly strict grammar rules, making it relatively easy to predict what string comes next, and since the characters are essentially alphabetic, predicting the character type is also easier.

Even outside the code-generation portions, we confirmed output speeds of 40–50 tps right from the start — faster than the conventional approach — suggesting MTP was working effectively there too.

The author believes this largely comes down to characteristics of the Gemma tokenizer. The Gemma tokenizer has a vocabulary of roughly 256k entries. Whereas many other tokenizers fall back to character-level tokenization for Japanese, Gemma's tokenizer is able to register most Japanese text as whole "words."

Because of this, the model can predict the next token at the "word" level for Japanese much like it does for English, which we believe is why Japanese output was noticeably faster with MTP than without.

Checking Token Acceptance Rates

llama.cpp is designed to log an "acceptance" value showing what fraction of tokens were accepted during the verification phase when using an MTP model. Here's an example:

prompt eval time = 9559.06 ms / 7030 tokens ( 1.36 ms per token, 735.43 tokens per second)
eval time = 45531.93 ms / 2858 tokens ( 15.93 ms per token, 62.77 tokens per second)
total time = 55090.99 ms / 9888 tokens
graphs reused = 3983
draft acceptance = 0.82931 ( 2196 accepted / 2648 generated)

Using this value, we checked acceptance for each turn, with the following results.

TurnAcceptanceAcceptedGenerated
175.34%1,5642,076
277.71%1,8902,432
388.27%1,8212,064
482.93%2,1962,648

Table 4. Token acceptance rate during the test cases.

AcceptanceAccepted TokensGenerated Tokens
Max88.27%2,1962,648
Min75.34%1,5642,064
Avg81.06%1,8682,305

Table 4 (summary). Max / min / average acceptance across the four test-case turns.

As mentioned above, likely because the output contained a lot of code, these tokens showed a high acceptance rate. So what does the acceptance rate look like for everyday use? Based on several days of logs, we recorded the maximum, minimum, and average, shown below.

AcceptanceAccepted TokensGenerated Tokens
Max75.55%2,1194,800
Min33.56%227412
Avg45.11%7761,798

Table 5. Token acceptance rate for everyday interactions with the Gemma-4-QAT-Assistant model.

Acceptance roughly halved, and speed dropped somewhat as a result. The corresponding speeds are shown below.

ReadGenerate
Max1,040.8284.46
Min117.5230.20
Avg774.4540.44

Table 6. Token speeds under the conditions in Table 5.

So while this isn't exactly blazing fast, it's reasonable to say it's still faster than running without MTP at all.

We confirmed that, for the Gemma-4 series, llama.cpp and Gemma-4-Assistant are a very well-matched pair, capable of delivering real benefits even in everyday use.

Summary

In this article, we looked at the new "MTP" technique — what it is, and how it actually changes generation speed in practice.

In a word, MTP is essentially the "speculative execution" of CPUs, brought into the world of LLMs. When it lands, it can be dramatically faster; when it misses, it can be slower — a slightly gambling-like trait.

We found this feature pairs especially well with code generation. Because programming languages follow strict grammar, once you're partway through a line, it's relatively easy to predict what string should come next. This lets MTP compensate for the parts where the tokenizer tends to slow things down, and our benchmark reflected that with solid results.

The current strength of LLMs comes precisely from their sequential nature — deciding the next word based on the words that came before. MTP, because it speeds things up without sacrificing that strength, is likely to play a genuinely important role as this technology continues to develop.

It's still early days, and there are some constraints, but we think this is a technique well worth watching as it matures and spreads.

Even as we speak, other groups are developing and releasing their own versions of this idea:

These, too, are spreading quickly now that inference engines are starting to support them. We hope to take a closer look at these technologies in a future article.

Supplementary Information (llama.cpp Status, Gemma-4 Architecture)

llama.cpp Support Status

Gemma-4 Model Architecture

Below is the architecture of the Gemma-4-12B-it model.

Input strings ─┬─ Vision/Audio encoders (minimal, mostly folded into the model) ─┐
                └─ tok → Emb ──────────────────────────────────────────────────┴─(+)─┐
                                                                                       │
     ┌── RoPE / p-RoPE ───────────────────────────────────────────────────────────────┤
     │                                                                                 ▼
     │      [ LA → LA → LA → LA → LA → GA ]  x6 groups   →  LNR → SoftMax → Output Probabilities
     └──────────────────────────────────────────────────↑
            (48 blocks total = 6 layers x 8 groups)

Gemma-4-12B-Assistant Model Architecture Details

The Gemma-4-12b-it-assistant model works together with the main model to predict tokens as follows.

Main model (Gemma-4-12b-it):
  Input string → tok → Emb → [LA...LA(45) → GA(46) → GA(47)] → LNR → SoftMax → N+1 output token
                                     │ shares KV cache (layers 45-47) with the Assistant's LA/GA
                                     ▼
Assistant model (MTP-Drafter):
  Query (H_N^0 + Emb of N) → LNR → LA → LA → LA → GA → Norm ─┬→ LNR → predicted state vectors
                                                              └→ Masked Embedder → predicted Token IDs (n tokens)
                                     │
                                     ▼
Verification (inside the inference engine):
  compares the Assistant's predicted state vectors against the Main model's own state vectors
  → decides how many tokens to accept → token output correction → final output

The "Assistant" model that adds MTP capability to Gemma-4-12B-it keeps Gemma-4's architecture but trims its structure down to a genuinely minimal design.

Based on the input string, it predicts the next (N+1) token. To do this, it combines the generated state vector hN0 with the embedded vector of token N, and feeds the result in as a query to begin processing.

Gemma-4's attention layers normally operate as self-attention, but the structure inside the Assistant model uses cross-attention instead.

For KV data, it combines the LocalAttention block at layer 46 of the main model with the LocalAttention blocks at layers 0–2 on the Assistant side, and also integrates the GlobalAttention block at layer 47 of the main model with the GlobalAttention block at layer 3 on the Assistant side, via a shared KV cache. Through this, KV information is bound based on the query.

The output consists of two parts: the predicted tokens — the n tokens predicted by the Assistant model — and the state vector used during that prediction process.

These outputs are sent combined, just before the main model's attention blocks. They pass through all of the main model's attention blocks, generating, in parallel, the state vectors needed for verification.

Once the state vectors to be compared are ready, their contents (probability distributions) are compared to decide how many tokens should be accepted. This is handled in parallel by the inference engine (for example, the generate function in the transformers library), which outputs the resulting number of tokens to accept.

Based on this result, the candidate tokens — accounting for the accepted token count — are combined with token N+1 and passed on for output. This is how the process achieves faster output compared to conventional step-by-step inference.

Source Notes

  1. Unsloth — unsloth.ai
  2. Unsloth Dynamic 2.0 GGUFs — unsloth.ai/docs
  3. Little Notes on p-RoPE in Gemma-4 — linkedin.com
C
Compute Cluster Research
RESEARCH #004 — August 17, 2026