Skip to content

feat: Native Speculative Decoding & MTP (Multi-Token Prediction) - #1431

Open
zsogitbe wants to merge 5 commits into
SciSharp:masterfrom
zsogitbe:SpeculativeDecodingLLamaSharp
Open

feat: Native Speculative Decoding & MTP (Multi-Token Prediction)#1431
zsogitbe wants to merge 5 commits into
SciSharp:masterfrom
zsogitbe:SpeculativeDecodingLLamaSharp

Conversation

@zsogitbe

@zsogitbe zsogitbe commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

⚠️ DEPENDENCY NOTICE: This Pull Request relies on new native C APIs introduced in the companion llama.cpp PR feat: Expose Multi-Sequence Speculative Decoding & MTP to Public C API- #27788. It will not work with the standard llama.cpp binaries currently shipped in LLamaSharp. To test this PR, you must compile and link the modified llama.cpp binaries from the upstream PR.

Summary

This PR adds native managed support for Speculative Decoding (Draft-Simple) and Multi-Token Prediction (MTP). It integrates seamlessly into both StatelessExecutor and BatchedExecutor, allowing users to achieve significant Tokens-Per-Second (TPS) speedups with a single boolean parameter while preserving the existing IAsyncEnumerable streaming API (cc @martindevans).

Key Architectural Changes

  • Native Bindings (NativeApiSpeculative.cs): Added complete P/Invoke definitions for the new llama_speculative_* API, wrapping the native context in a robust SafeSpeculativeContextHandle.
  • The SpeculativeDecoder Wrapper: Safely initializes the native speculative context and automatically mounts a native greedy sampler to guarantee mathematical verification constraints.
  • StatelessExecutor Integration: Updated constructors to accept optional draft weights or a useMtp flag. Transparently intercepts token generation to route through the SpeculativeDecoder using the standard StreamingTokenDecoder.
  • BatchedExecutor Multiplexing: Introduced a Queue<LLamaToken> _speculativeTokens to the Conversation multiplexer. Because BatchedExecutor natively relies on C# to sample tokens after logits are calculated, the Conversation.Sample() method was overridden to safely intercept and dequeue natively pre-sampled speculative tokens.
  • KV Deduplication (_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.
  • Model Parameter Mapping: Exposed LoadMtp in ModelParams to explicitly command the backend to load nextn tensors into VRAM (strictly required for DeepSeek-R1 / Qwen MTP execution).

Validation

  • Added SpeculativeDecodingTests.cs to LLama.Unittest utilizing Constants.GenerativeModelPath.
  • Added a runnable SpeculativeBenchmark class to isolate and measure Tokens-Per-Second (TPS) gains across Baseline, Dual-Model Speculation, and MTP strategies.
  • Cross-Ecosystem Testing: (Note: This PR is paired with a companion PR submitted to llama.cpp feat: 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

  • Hardware: NVIDIA GeForce RTX 4070 Ti (12GB VRAM)
  • Target Model: gemma-4-12b-it-UD-Q4_K_XL.gguf (11.91B params, Q4_K_M, ~6.85 GiB)
  • Draft Model: mtp-gemma-4-12b-it-F16.gguf (gemma4-assistant, 422.86M params, F16, ~806 MiB)
  • MTP Depth / Draft Budget: 4 tokens per burst (nextn_predict_layers = 4)
  • Context / Max Tokens: 4096 context / 256 generated tokens
  • VRAM Offload: 100% GPU Offloaded (Target + Draft)

Benchmark Results

Strategy Tokens Time (s) Tokens/sec Draft Accept % Speedup
Standard Autoregressive 256 5.06 s 50.64 tps N/A Baseline
MTP Speculative (MTP-4) 259 4.34 s 59.69 tps 100.0% 1.18x (+17.9%)

Key Architectural Takeaways

  • Stateless Multi-Token Drafting: Freezing the draft sequence position (current_pos) in llama-speculative.cpp allows MTP projection layers to evaluate continuous 4-token draft bursts without triggering strict KV cache continuity gap aborts (Y = X + 1).
  • Memory-Mapped Hidden States: Pointers passed via CtxOther permit the standalone 423M assistant model to directly sample hidden embeddings (h_row) from the primary 12B target context without duplicate prompt prefilling.
  • Throughput Scaling: Verifying 4 drafted tokens per burst yielded an immediate +17.9% net speedup on a 12B target model while requiring only ~800 MiB of auxiliary VRAM for the draft checkpoint.

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:

  • Hardware: Hybrid Edge Setup (RTX 4070 Ti + Quadro P2200 + System RAM spillover).
  • Target Model: Qwen3.6-35B-A3B-UD-Q5_K_M.gguf (Requires CPU offloading).
  • Draft Model: Qwen3.5-0.8B-UD-Q5_K_XL.gguf (Fits entirely in VRAM).
  • Constraint: The target model is severely bound by system RAM bandwidth during evaluation.

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.

Strategy Draft Budget Tokens/Sec Accept % Speedup
Standard Autoregressive N/A 8.18 N/A Baseline
Draft-Simple Speculative 6 tokens 16.94 100.0% 2.07x (+107.1%)

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.

Strategy Draft Budget Tokens/Sec Accept % Speedup
Standard Autoregressive N/A 9.43 N/A Baseline
Draft-Simple Speculative 8 tokens 16.67 22.6% 1.77x (+76.8%)

Observation:

  1. Sequential Domino Effect: At an 8-token depth, the drafter's accuracy collapses to 22.6%. The target model frequently rejects the later tokens, triggering the llama-speculative.cpp cache rollback mechanisms perfectly as designed.
  2. Bandwidth Economics: Despite throwing away nearly 80% of the drafted tokens, the net throughput (16.67 t/s) remained nearly identical to the optimal run. This confirms that on CPU-offloaded workloads, the time cost to evaluate 1 token vs 8 tokens in a single llama_decode batch 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 ideal Draft 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

Strategy Required Model Setup Minimum Target Size Ideal Workload Expected Result
Draft-Simple Target + Draft (Same Vocab & Family) >= 8B (100% VRAM) Code, JSON, Structured Tasks 1.4x – 2.2x Speedup
Draft-Simple Target + Draft (Different Vocab) Any Any Immediate Crash
MTP Single Model (nextn_predict_layers >= 1) >= 8B–14B (100% VRAM) Low-Temperature / High-Confidence 1.3x – 1.8x Speedup
Any Speculative Model partially offloaded to CPU Any Any Slowdown
Any Speculative Models <= 4B on High-End GPU <= 4B (100% VRAM) High-Entropy / Creative Prompts Slowdown

ANNEX: Architectural Scope and Speculative Decoding Rationale

This public C API implementation in llama-speculative.cpp intentionally supports only standard autoregressive drafting (draft-simple) and Multi-Token Prediction (draft-mtp) . While common/speculative.cpp contains 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-dspark

  • Tight Checkpoint Coupling: Unlike standard speculation where any smaller model with a matching vocabulary can serve as a draft , these methods require custom-trained draft heads mapped to the exact hidden dimensions and layer indices of a specific base model . They are not universally portable across models.
  • High Architectural & State Complexity:
    • draft-eagle3 requires 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-dflash and draft-dspark require non-causal attention toggles (llama_set_causal_attn), manual KV cache feature injection, and fixed-block noise generation with mask tokens .
  • Memory Movement Overhead: The continuous memory copying of multi-layer embeddings into intermediate staging buffers (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-cache

  • Lack of Semantic Generalization: N-gram heuristics only reproduce verbatim token repetitions from the context . On open-ended generation, reasoning, or creative writing, predictive accuracy is poor, leading to immediate rejection and wasted verification cycles.
  • Application-Level Routing Complexity: Exposing N-gram engines in the public C API forces downstream callers and language bindings to implement guessing heuristics to determine whether a given request is "repetitive enough" for N-gram or requires a neural draft model. Furthermore, pushing these state-heavy mechanics across the P/Invoke boundary into managed C# code would introduce significant marshaling overhead and memory-management complexity.
  • State Machine Bloat: Incorporating hash tables, dynamic cache eviction algorithms, static cache file loaders, and occupancy-reset thresholds into the C API introduces substantial state-tracking overhead for a marginal, task-dependent benefit .

Supported Architecture Matrix

Speculative Method Status in Public C API Primary Rationale
draft-simple Supported Universal compatibility; decoupled draft model; clean, standard batch decoding .
draft-mtp Supported Native support for models with integrated prediction heads (DeepSeek, Qwen) without external draft checkpoints .
draft-eagle3 Excluded High maintenance burden; requires deferred boundary bridging and multi-layer feature extraction .
draft-dflash Excluded Non-causal attention requirement; custom KV injection; requires specialized diffusion draft models .
draft-dspark Excluded Same architectural coupling as DFlash with additional Markov-head decoding logic .
ngram-simple Excluded Ineffective outside exact repetitions; forces application-layer heuristic routing .
ngram-map-k Excluded Hash map state management overhead in public C ABI .
ngram-map-k4v Excluded Specialized key-value lookup tables unnecessary for a general-purpose API .
ngram-mod Excluded Heuristic occupancy checks and streak-reset state machines belong in client code, not C ABI .
ngram-cache Excluded Requires disk I/O, cache persistence, and custom lookup formats .

By constraining llama-speculative.cpp to draft-simple and draft-mtp, the API maintains an explicit, deterministic execution model: one predictable drafting phase, one target verification phase, and zero hidden heuristic branching .

@zsogitbe

Copy link
Copy Markdown
Contributor Author

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

@zsogitbe zsogitbe changed the title feat: Native Speculative Decoding & DeepSeek/Qwen MTP Support feat: Native Speculative Decoding & MTP (Multi-Token Prediction) Aug 27, 2026
Comment thread LLama.Examples/Program.cs Outdated
Comment on lines +34 to +37
// 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")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@martindevans martindevans Aug 27, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread LLama.Unittest/Constants.cs Outdated
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";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread LLama.Unittest/LLama.Unittest.csproj Outdated
Comment on lines +162 to +171
<!-- 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>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<!-- 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>

@martindevans

Copy link
Copy Markdown
Member

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

For now, could we re-run the workflow?

I've triggered it, but aren't we expecting it to fail since te binaries don't include your new upstream code?

Long-term, we might want to either cache this model in the GitHub Actions runner

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!

@zsogitbe

Copy link
Copy Markdown
Contributor Author

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

I've triggered it, but aren't we expecting it to fail since te binaries don't include your new upstream code?

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.
@zsogitbe

zsogitbe commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Pushed a small update to StatelessExecutor and SpeculativeBenchmark:

  • Decoupled draft weight resolution from useMtp (_draftWeights ?? _weights), allowing separate MTP draft files (like Gemma 4's assistant checkpoints) to load their distinct weights while still passing CtxOther to the backend.
  • Updated benchmark prompts to support testing both bundled MTP (Qwen/DeepSeek) and split MTP (Gemma 4).

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.

@aropb

aropb commented Aug 28, 2026

Copy link
Copy Markdown

If it works, that’s really cool!

Please try to see how the speed changes with the Qwen3.6/3.8-27B-MTP model?

@zsogitbe

Copy link
Copy Markdown
Contributor Author

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.
Regarding your question about the speed changes with a Qwen 27B MTP model, there are two key factors that dictate the performance based on my testing:

  1. Hardware / VRAM Limits: I can run the 27B model, but because my GPU only has 12 GB of VRAM, the model partially spills over to my CPU. As I noted in the PR description, MTP requires 100% VRAM offloading. When CPU fallback occurs, parallel batch verification turns into a bottleneck and causes a net slowdown.
  2. Number of MTP Heads: Even on a GPU with enough VRAM (like 24GB+), an MTP model with only 1 MTP head generally won't yield a speedup on a fast GPU. The API and CUDA graph launch overhead outpace the memory bandwidth savings for just one extra token. To actually see a speedup on high-end hardware, you need either a multi-head MTP model (which verifies several tokens in a single roundtrip) or a dual-model setup (pairing a large target model with a very small, fast draft model - like the Gemma4 benchmark that I report in the PR).

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!
Let me know if you have any other questions, I'm happy to help.

@zsogitbe

Copy link
Copy Markdown
Contributor Author

Quick update on performance profiling:

I’ve just added a new Benchmark Report section to the PR description above.

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.

@zsogitbe

Copy link
Copy Markdown
Contributor Author

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:

  • ModelsParamsTests: Added the [JsonIgnore] attribute to the CtxOther native pointer property so the JSON serializer no longer attempts to round-trip an unmanaged memory address.
  • MtmdExecutorTests Crash: The AccessViolationException in the tests was caused by a recent upstream update in llama.cpp. A new ggml_backend_dev_t device; field was added to the native mtmd_context_params struct. Because our C# wrapper was missing this field, the memory layout shifted by 8 bytes, causing the C# marshaler to read garbage memory. Adding the missing device field restores the correct alignment!

2. Why the CI is currently failing (Expected Behavior):
While I am building against the updated llama.cpp binary locally (which is why my tests are green), the GitHub CI runner is still fetching the older published native binaries.

  • Because the CI's older binary does not recognize the new struct layout or the new context types we are passing to it, it fails to initialize the context (Unsupported ctx type).
  • This results in a null pointer being passed back to LLamaSharp, which crashes the test host.

The CI tests will naturally turn green once the upstream llama.cpp updates are processed and the new binaries become available to our GitHub Actions runner.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants