feat: Native Speculative Decoding & MTP (Multi-Token Prediction) - #1431
feat: Native Speculative Decoding & MTP (Multi-Token Prediction)#1431zsogitbe wants to merge 2 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.
| <!-- 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. |
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
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)