Discovering MTP: How I Confused Configuration With Functionality

I’ve spent a lot of time tuning local LLMs over the last few months, but every now and then I run into an optimization that forces me to stop tweaking settings and actually understand how something works.

Recently that optimization was MTP.

I was running Qwen3.6-35B-A3B on an RTX 3090 and started seeing references to Multi-Token Prediction (MTP) and speculative decoding in llama.cpp. The promise was enticing: generate multiple tokens ahead of time and verify them in batches, potentially producing a significant speedup during inference.

Like many people, I assumed the hard part would be finding the right command-line arguments.

I was wrong.


The Setup

My baseline configuration was:

The model already performed well, but long coding prompts exposed a familiar problem: latency under sustained generation.

Speculative decoding looked like the obvious next step.


The First Mistake: Treating Configuration as Evidence

My first experiments focused on enabling speculative decoding through llama.cpp’s runtime options. I added the appropriate flags, restarted the server, and started watching logs.

At first glance everything looked promising. The server started. Requests completed successfully. I even saw references to speculative decoding in various places throughout the codebase and documentation.

The problem was that I never actually confirmed whether MTP was running.

I was treating the configuration as evidence.

Over the next several hours I went through a familiar optimization cycle: change a setting, run a test, examine logs, repeat. Sometimes performance improved. Sometimes it didn’t. The results were noisy enough that I couldn’t confidently explain what was happening.

The breakthrough came when I stopped looking at throughput numbers and started reading startup logs line by line.

One log entry stood out:

context type MTP requested but model doesn't contain MTP layers
failed to create MTP context

That single message completely changed my understanding of the problem.

Up until that point I had been assuming that enabling:

--spec-type draft-mtp

somehow activated MTP behavior.

What the logs revealed was much simpler.

The flag does not create MTP capability. It merely tells llama.cpp to use MTP if the model already contains MTP layers.

My model didn’t.

I had been asking llama.cpp to use a feature that wasn’t present in the model itself.


The Fix: Switching Models

Once I understood that distinction, everything else fell into place. I located a version of the model that actually contained MTP layers and switched over to it.

The MTP variant comes from bartowski/Qwen_Qwen3.6-35B-A3B-GGUF — the same repo as the standard quantized models, but with a separate file that includes the MTP layers needed for speculative decoding.

The startup logs immediately looked different:

estimated memory usage of MTP context is 232 MiB
creating MTP draft context
speculative decoding context initialized

These were messages I had never seen before. For the first time, llama.cpp was actually creating an MTP context instead of silently falling back to ordinary decoding.


What I Initially Misunderstood About MTP

During early testing, I conflated several distinct parts of the system. Understanding these distinctions is what makes further tuning predictable rather than experimental.

1. draft-mtp is not a full system switch

I initially treated

--spec-type draft-mtp

as if it enabled a complete MTP mode on its own. In reality, it is only a speculative decoding backend selection — a flag that tells llama.cpp to use MTP if the model supports it. The model must already contain MTP layers for anything to happen.

2. The MTP model variant is separate from the algorithm

The model file from bartowski’s repo that includes MTP layers is a build that has been packaged with the additional MTP layers needed for speculative decoding. Using this model enables the algorithm to function, but the two concepts are distinct:

  • The algorithm (draft-mtp) controls how draft tokens are generated and validated during inference
  • The model provides the MTP layers that the algorithm needs to work

This is easy to confuse because llama.cpp exposes them as separate knobs.

3. KV cache behavior is more nuanced than it appears

I previously interpreted internal log messages suggesting

cache_k=f16
cache_v=f16

as meaning the entire system had switched KV cache formats. This was incorrect.

That configuration applies only to the internal speculative draft context, not the main model. In my setup, the main model explicitly uses q4_0 KV cache. The MTP draft context uses f16 for its own internal operations, which is a separate allocation.


What Actually Changed Performance

After enabling draft-mtp and switching to the MTP variant model, the system stabilized and became measurably faster.

Typical results looked like:

  • ~67–70% draft token acceptance rate
  • ~90–100 tokens/sec generation speed
  • consistent performance even at ~30k+ prompt lengths

This confirmed that speculative decoding was actively contributing to throughput, not just enabled in name.


Key Insights from Tuning

Acceptance rate matters most

The most useful signal turned out to be the draft acceptance rate. In my setup, the optimal range was:

~65–70% acceptance

Lower values tend to waste compute on rejected drafts, while higher values often indicate insufficient speculative coverage.

Context size dominates memory behavior

At ~81k tokens, KV cache usage becomes a major factor in performance. This has more impact on stability than most speculative decoding parameters.

MoE models behave differently

Using a mixture-of-experts model changes speculative decoding dynamics:

  • token prediction is less uniform than dense models
  • acceptance rates are naturally lower
  • performance gains are real, but more modest and sensitive to tuning

GPU offloading needs flexibility

An early configuration forcing full GPU offload caused memory pressure:

--n-gpu-layers 999

Switching to:

--n-gpu-layers -1

allowed llama.cpp to automatically balance layer placement, improving stability without reducing performance.


Final Configuration

No external draft model was used.


Conclusion

The most important lesson from this process wasn’t about squeezing out a few extra tokens per second — it was about understanding the structure of the system itself.

Modern llama.cpp speculative decoding is no longer a single, monolithic feature. It operates across multiple layers:

  • a decoding algorithm (draft-mtp)
  • a required MTP model variant
  • and runtime optimizations that adapt dynamically based on workload

Once those layers are separated conceptually, the tuning process becomes much clearer — and far less confusing than the flags initially suggest.

What struck me most about this experience was how easy it is to confuse configuration with functionality. Modern inference servers expose hundreds of knobs and switches, and it’s tempting to assume that enabling a feature means that feature is active.

In reality, the logs were telling the truth the entire time. I just wasn’t asking the right question.

Instead of asking, “Did I set the flag correctly?” I should have been asking, “Can I prove the feature is actually running?”

Once I started looking for proof instead of configuration, the answer became obvious.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *