feat: Native Speculative Decoding & MTP (Multi-Token Prediction) - #1431
feat: Native Speculative Decoding & MTP (Multi-Token Prediction)#1431zsogitbe wants to merge 5 commits into
Conversation
|
The CI failed because the new 3GB Qwen model download from Hugging Face timed out/dropped mid-way (ResponseEnded). Since MSBuild's native download task doesn't have great retry logic for large files, this might become a flaky test. For now, could we re-run the workflow? Long-term, we might want to either cache this model in the GitHub Actions runner,... |
| // enable this for forcing specific version of llama.cpp; disable for standard use | ||
| //.WithLibrary( | ||
| // @"D:\_MTP\LLamaSharp\llama.cpp\_vs\bin\Release\llama.dll", | ||
| // @"D:\_MTP\LLamaSharp\llama.cpp\_vs\bin\Release\mtmd.dll") |
There was a problem hiding this comment.
| // enable this for forcing specific version of llama.cpp; disable for standard use | |
| //.WithLibrary( | |
| // @"D:\_MTP\LLamaSharp\llama.cpp\_vs\bin\Release\llama.dll", | |
| // @"D:\_MTP\LLamaSharp\llama.cpp\_vs\bin\Release\mtmd.dll") |
There was a problem hiding this comment.
I will go ahead and remove these hardcoded local paths so they don't break the build for others. I originally added them as a temporary workaround because it is currently very difficult to test a custom llama.cpp build (which this PR relies on).
I also noticed a potential bug during this: a strange DLL appeared in the cuda12 folder called libmtmd.dll (it should probably be mtmd.dll). Without these custom MSBuild scripts and .WithLibrary() overrides, it felt nearly impossible to force the project to use my custom binaries. Now that I am removing my workaround, what is the recommended/standard way in LLamaSharp to point to local custom backend binaries during development?
There was a problem hiding this comment.
libmtmd.dll
That's odd, I see the same in my local CUDA12 folder. It does look like a bug. The build action outputs mtmd.dll so I'm not sure where that's coming from.
There was a problem hiding this comment.
I think WithLibrary as you had is the right way to do it for development, just don't commit it as part of the PR.
| public static readonly string MtmdMmpPath = "Models/gemma-mmproj-model-f16.gguf"; | ||
| public static readonly string MtmdImage = "Models/extreme-ironing-taxi-610x427.jpg"; | ||
|
|
||
| public static readonly string MtpModelPath = "Models/Qwen3.5-4B-MTP-Q4_K_M.gguf"; |
There was a problem hiding this comment.
Can we get away with https://huggingface.co/unsloth/Qwen3.5-2B-MTP-GGUF here instead? CI runs entirely on (underpowered) CPUs, so we need models to be as lightweight as possible. Even smaller than my suggestion would be better, but I can't find anything smaller that still has MTP head.
There was a problem hiding this comment.
That makes total sense for the CI runners, and I agree we should use something as lightweight as possible to save both cache space and CPU time.
I actually tried using the unsloth MTP quants earlier, but I ran into a bad crash. It turns out some of those specific GGUFs were exported missing the MTP head metadata (like mtp_num_hidden_layers), which causes llama.cpp (and LLamaSharp) to fail when initializing the speculative context.
I will hunt for a working ~1B or 2B MTP quant later.
There was a problem hiding this comment.
I have found the perfect model for us and it works. I have changed the code.
https://huggingface.co/prithivMLmods/Qwen3.5-2B-MTP-GGUF/resolve/main/Qwen3.5-2B.Q4_0.gguf
| <!-- Automatically overwrite Nuget DLLs with custom local builds --> | ||
| <Target Name="CopyCustomNativeLibs" AfterTargets="Build;PostBuildEvent"> | ||
| <ItemGroup> | ||
| <CustomNativeLibs Include="D:\_MTP\LLamaSharp\llama.cpp\_vs\bin\Release\*.dll" /> | ||
| </ItemGroup> | ||
| <Message Text="[Custom Script] Overwriting old LLamaSharp native libraries with custom builds..." Importance="high" /> | ||
| <!-- Overwrite CPU and CUDA folders to guarantee LLamaSharp finds them --> | ||
| <Copy SourceFiles="@(CustomNativeLibs)" DestinationFolder="$(OutDir)runtimes\win-x64\native\avx2" SkipUnchangedFiles="false" /> | ||
| <Copy SourceFiles="@(CustomNativeLibs)" DestinationFolder="$(OutDir)runtimes\win-x64\native\cuda12" SkipUnchangedFiles="false" /> | ||
| </Target> |
There was a problem hiding this comment.
| <!-- Automatically overwrite Nuget DLLs with custom local builds --> | |
| <Target Name="CopyCustomNativeLibs" AfterTargets="Build;PostBuildEvent"> | |
| <ItemGroup> | |
| <CustomNativeLibs Include="D:\_MTP\LLamaSharp\llama.cpp\_vs\bin\Release\*.dll" /> | |
| </ItemGroup> | |
| <Message Text="[Custom Script] Overwriting old LLamaSharp native libraries with custom builds..." Importance="high" /> | |
| <!-- Overwrite CPU and CUDA folders to guarantee LLamaSharp finds them --> | |
| <Copy SourceFiles="@(CustomNativeLibs)" DestinationFolder="$(OutDir)runtimes\win-x64\native\avx2" SkipUnchangedFiles="false" /> | |
| <Copy SourceFiles="@(CustomNativeLibs)" DestinationFolder="$(OutDir)runtimes\win-x64\native\cuda12" SkipUnchangedFiles="false" /> | |
| </Target> |
|
Thanks for putting this together. It's a big chunk of work, so we'll definitely need to break this up into multiple smaller PRs, but we can worry about the details of that once the upstream PR is resolved :)
I've triggered it, but aren't we expecting it to fail since te binaries don't include your new upstream code?
Anything that goes into the test models folder is cached (see here). Actually that's another reason we need a small model - we're running out of cache space! |
I completely understand that a PR of this size is daunting to review, and breaking it down is usually the best approach for large features. However, I am quite hesitant to split this specific PR. The Native Speculative Decoding and MTP implementations are deeply intertwined - they share the same underlying C# native bindings, state management, and CI testing logic. Because they act as a single cohesive unit, trying to untangle them into separate PRs would be a massive amount of work on my end, and I fear it would actually make the review more confusing, as the intermediate PRs would be missing crucial context from each other. Furthermore, I have already thoroughly tested this LLamaSharp code alongside my custom llama.cpp PR, and it works incredibly well together as-is. I’ve actually included the real-world benchmark results in the 'Annex' section of the PR description, which demonstrates the stability and performance gains of this exact setup.
Thanks Martin! You are completely right - the CI will absolutely fail right now because the current LLamaSharp binaries do not contain my upstream llama.cpp PR yet. I opened this PR early so that you can also test the code. |
…head architectures **Fix: Decouple draft weights from MTP flag to support separate assistant checkpoints** - **StatelessExecutor**: Updated draft weights resolution (`_draftWeights ?? _weights`) so that `_useMtp = true` does not forcefully override `_draftWeights` with target weights. This allows split-model MTP architectures (e.g., Gemma 4's separate assistant GGUF) to load distinct draft weights while still injecting `CtxOther` into the draft context parameters. - **SpeculativeBenchmark**: Updated `RunInteractiveAsync` and `RunAsync` to permit selecting a separate draft model when MTP is enabled, loading separate weights only when the target and draft file paths differ.
|
Pushed a small update to
I have updated the PR description above to include the benchmark results for Multi-Token Prediction: Gemma 4 (Split-Weight MTP-4), executed using our new code. |
|
If it works, that’s really cool! Please try to see how the speed changes with the Qwen3.6/3.8-27B-MTP model? |
Thanks aropb! I’ve successfully tested many model combinations, and functionally, they all work well.
So on my specific 12GB setup - and assuming a single-head architecture - the 27B MTP model will run slower. However, with fully offloaded VRAM and a multi-head/dual-model approach, you would see the excellent TPS gains this feature was built for! |
|
Quick update on performance profiling: I’ve just added a new We ran extensive tests on a constrained hybrid edge setup (target model partially offloaded to CPU + draft model fully in VRAM) to stress-test the new C API's batch verification and rollback mechanics. Key takeaway: The engine beautifully handles the CPU memory bandwidth bottlenecks. We identified that a draft budget of 6 tokens is the "sweet spot" for consumer hardware, pushing a 35B MoE to nearly 17 tps (a 2.07x / +107% speedup), on limited GPU memory device, while maintaining flawless cache rollback when the draft model's accuracy degrades at deeper budgets. Everything is looking highly stable. |
…ge) and ignore CtxOther pointer in JSON
Heads-up regarding the CI test failures:Please note that the GitHub Actions CI tests are currently failing, but all tests pass successfully on my local machine. Here is a breakdown of the last fixes and why the CI is currently expected to fail: 1. Last fixes:
2. Why the CI is currently failing (Expected Behavior):
The CI tests will naturally turn green once the upstream |
Summary
This PR adds native managed support for Speculative Decoding (Draft-Simple) and Multi-Token Prediction (MTP). It integrates seamlessly into both
StatelessExecutorandBatchedExecutor, allowing users to achieve significant Tokens-Per-Second (TPS) speedups with a single boolean parameter while preserving the existingIAsyncEnumerablestreaming API (cc @martindevans).Key Architectural Changes
NativeApiSpeculative.cs): Added complete P/Invoke definitions for the newllama_speculative_*API, wrapping the native context in a robustSafeSpeculativeContextHandle.SpeculativeDecoderWrapper: Safely initializes the native speculative context and automatically mounts a native greedy sampler to guarantee mathematical verification constraints.StatelessExecutorIntegration: Updated constructors to accept optional draft weights or auseMtpflag. Transparently intercepts token generation to route through theSpeculativeDecoderusing the standardStreamingTokenDecoder.BatchedExecutorMultiplexing: Introduced aQueue<LLamaToken> _speculativeTokensto theConversationmultiplexer. BecauseBatchedExecutornatively relies on C# to sample tokens after logits are calculated, theConversation.Sample()method was overridden to safely intercept and dequeue natively pre-sampled speculative tokens._speculativeTokensToIgnore): Automatically tracks and ignores user prompt inputs for tokens that the C++ engine has already evaluated and cached during a speculative burst, preventing cache duplication.LoadMtpinModelParamsto explicitly command the backend to loadnextntensors into VRAM (strictly required for DeepSeek-R1 / Qwen MTP execution).Validation
SpeculativeDecodingTests.cstoLLama.UnittestutilizingConstants.GenerativeModelPath.SpeculativeBenchmarkclass to isolate and measure Tokens-Per-Second (TPS) gains across Baseline, Dual-Model Speculation, and MTP strategies.llama.cppfeat: Expose Multi-Sequence Speculative Decoding & MTP to Public C API- #27788 which implements and exposes the standalone native C API. The full architecture has been rigorously tested across both boundaries, from native C++ state-rollback CI validation to managed C# asynchronous streaming, batch queue multiplexing, and interactive benchmarking.)ANNEX: Model Selection & Evaluation
1. Model Compatibility & Selection Rules
Dual-Model Speculation: The target and draft models must share the exact same tokenizer architecture and vocabulary size to prevent immediate cache desynchronization crashes. Crucially, the larger the target model and the smaller/faster the draft model, the higher the resulting speedup.
Multi-Token Prediction (MTP): This requires a single model with pre-trained speculative projection layers (
nextn_predict_layers >= 1). The more draft heads the model has, the higher the potential speedup, as it can verify more tokens per native API call without context-switching overhead. The draft budget must match the available heads.2. Real-World Benchmark Examples
Draft-Simple
Pairing Meta-Llama-3-8B-Instruct-Q8_0.gguf (Target) with Llama-3.2-1B-Instruct-Q4_0.gguf (Draft) yielded a 1.26x speedup (+25.7%). Even on a fast GPU, the 1B draft model was lightweight enough to outpace the compute overhead of the 8B target.
MTP Speculation
Running Qwen3.5-4B-MTP-Q4_K_M.gguf (1 MTP head) resulted in a 0.47x slowdown (-53.2%). Because the 4B base model is computed incredibly fast on high-end hardware, the API overhead and CUDA graph launch latency of evaluating the MTP head completely overshadowed the memory bandwidth savings. To achieve a speedup instead, you must use a much larger model (like 14B or 32B) where the GPU's memory bandwidth becomes a true bottleneck as it fetches massive weight matrices from VRAM for every single token. Alternatively, using an MTP model with multiple prediction heads allows you to verify several tokens in a single API roundtrip, making the overhead worthwhile.
Multi-Token Prediction: Gemma 4 (Split-Weight MTP-4)
Unlike bundled MTP architectures where projection layers reside in the same weight file, Gemma 4 separates its multi-token prediction heads into a standalone assistant checkpoint (
gemma4-assistant). This benchmark demonstrates MTP-4 self-speculation where 4 future tokens are drafted simultaneously via auxiliary projection layers using the target model's hidden states (CtxOther), verified in a single batched verification pass.Test Configuration
gemma-4-12b-it-UD-Q4_K_XL.gguf(11.91B params, Q4_K_M, ~6.85 GiB)mtp-gemma-4-12b-it-F16.gguf(gemma4-assistant, 422.86M params, F16, ~806 MiB)nextn_predict_layers = 4)Benchmark Results
Key Architectural Takeaways
current_pos) inllama-speculative.cppallows MTP projection layers to evaluate continuous 4-token draft bursts without triggering strict KV cache continuity gap aborts (Y = X + 1).CtxOtherpermit the standalone 423M assistant model to directly sample hidden embeddings (h_row) from the primary 12B target context without duplicate prompt prefilling.2.A Benchmark Report: Edge Hardware Behavior & Draft Budget Tuning
As part of the cross-ecosystem validation for native Speculative Decoding (Draft-Simple) integration, we conducted extensive hardware benchmarking to map the performance thresholds of the new API.
The following benchmarks demonstrate how the new native verification loop performs on a highly constrained edge setup where the target model severely bottlenecks the CPU.
Hardware & Model Configuration:
Qwen3.6-35B-A3B-UD-Q5_K_M.gguf(Requires CPU offloading).Qwen3.5-0.8B-UD-Q5_K_XL.gguf(Fits entirely in VRAM).The "Goldilocks Zone" (Optimal Performance)
By keeping the tiny draft model entirely resident on the GPU and relying on the new API's batch verification, we successfully bypassed the PCIe/System RAM bottleneck, crossing the real-time threshold.
Observation: A draft budget of 6 tokens represents the maximum limit this hardware can batch-verify efficiently before the 0.8B draft model begins failing strict sequential prefix validation.
The Penalty of Over-Drafting (Validation Resilience)
To test the engine's failure-recovery mechanisms and batch-processing limits, we intentionally pushed the draft budget past the model's predictive capabilities.
Observation:
llama-speculative.cppcache rollback mechanisms perfectly as designed.llama_decodebatch is virtually identical.Under-Drafting
At a conservative budget of 4 tokens, the drafter maintained 100% acceptance but only achieved 13.27 tps (1.68x speedup), confirming that setting the budget too low leaves available batch-verification compute unutilized.
Summary for Implementers
The new public C API successfully enables massive throughput gains (>2x) on bandwidth-constrained hardware. For implementers exposing this via
LLamaSharp, the idealDraft Budget(burst size) for Draft-Simple setups on consumer hardware consistently sits between 5 and 6 tokens.3. Hardware & Workload Dynamics
100% VRAM Offloading: Both models (or the full MTP model) must fit entirely within GPU VRAM. If layers spill over to the CPU, parallel batch verification turns into serialized matrix multiplication, causing a severe net slowdown.
The Size Threshold: Speculative decoding is an optimization for memory-bandwidth-bound workloads. Speedups reliably appear on mid-to-large models (8B, 14B, 32B, 70B) where reading massive weight matrices per token is the actual bottleneck.
Task Predictability: Deterministic tasks (code completion, JSON extraction) achieve high draft acceptance rates (>70%), maximizing throughput. Creative writing and high-temperature sampling trigger frequent draft rejections, wasting compute cycles.
4. Quick Selection & Viability Matrix
nextn_predict_layers >= 1)ANNEX: Architectural Scope and Speculative Decoding Rationale
This public C API implementation in
llama-speculative.cppintentionally supports only standard autoregressive drafting (draft-simple) and Multi-Token Prediction (draft-mtp) . Whilecommon/speculative.cppcontains research and experimental implementations of several other speculative decoding methods , they have been excluded from the core public C API and foreign-function bindings (such as LLamaSharp) to preserve API stability, avoid brittle model coupling, and minimize runtime complexity.Evaluation of Excluded Experimental Methods
Feature-Conditioned Draft Models:
draft-eagle3,draft-dflash,draft-dsparkdraft-eagle3requires extracting hidden states from multiple target layers, running a separate encoder pass, and managing a "deferred boundary" to reconcile token-feature alignment across ubatch boundaries .draft-dflashanddraft-dsparkrequire non-causal attention toggles (llama_set_causal_attn), manual KV cache feature injection, and fixed-block noise generation with mask tokens .features_buf) creates CPU/GPU bus contention , often diminishing the theoretical FLOP advantage on unified or consumer memory hardware.Prompt-Lookup and Heuristic Methods:
ngram-simple,ngram-map-k,ngram-map-k4v,ngram-mod,ngram-cacheSupported Architecture Matrix
draft-simpledraft-mtpdraft-eagle3draft-dflashdraft-dsparkngram-simplengram-map-kngram-map-k4vngram-modngram-cacheBy constraining
llama-speculative.cpptodraft-simpleanddraft-mtp, the API maintains an explicit, deterministic execution model: one predictable drafting phase, one target verification phase, and zero hidden heuristic branching .