Skip to content

Suffix decode - #26283

Open
kashif wants to merge 8 commits into
ggml-org:masterfrom
kashif:suffix-decode
Open

Suffix decode#26283
kashif wants to merge 8 commits into
ggml-org:masterfrom
kashif:suffix-decode

Conversation

@kashif

@kashif kashif commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Overview

Initial implementation of https://suffix-decoding.github.io/, which is a model-free spec. decoding method. We build the tree online over the current request and tokens generated so far, and it's best when the matched suffix is longer.

Additional information

Currently, we build only the online tree only and no global corpus tree. vLLM optionally keeps the global cross-request cache (up to 10k past requests). We implement the per-request/prompt tree only.

Requirements

  • I have read and agree with the contributing guidelines YES
  • AI usage disclosure: YES for tests and docs and fixing code vs. reference code

@kashif
kashif requested a review from a team as a code owner July 29, 2026 15:26
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jul 29, 2026
@ServeurpersoCom

Copy link
Copy Markdown
Contributor

The concept is very elegant! It treats the context and previous outputs as a memory of sequences, proposes the continuation of a previously seen pattern in one block, and then has the LLM verify it, so it mimics the effect of MTP without requiring MTP heads. It becomes extremely effective whenever patterns repeat, especially in code, structured documents, JSON, and agentic loops.

@ggerganov ggerganov self-assigned this Jul 29, 2026
@am17an

am17an commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Can you add some unit-tests to showcase the functionality?

@kashif
kashif requested a review from ggerganov as a code owner July 29, 2026 19:46
@github-actions github-actions Bot added the testing Everything test related label Jul 29, 2026
@ruixiang63
ruixiang63 self-requested a review July 30, 2026 23:35
@Green-Sky

Copy link
Copy Markdown
Collaborator

without looking too deep, sounds just like ngram with tree, like #8648 (tho that pr is probably dead)

@kashif
kashif requested review from a team, CISC, JohannesGaessler, ngxson and pwilkin as code owners August 2, 2026 16:38
@github-actions github-actions Bot added model Model specific Vulkan Issues specific to the Vulkan backend devops improvements to build systems and github actions server ggml changes relating to the ggml tensor library for machine learning SYCL https://en.wikipedia.org/wiki/SYCL - GPU programming language Apple Metal https://en.wikipedia.org/wiki/Metal_(API) OpenCL Issues specific to the OpenCL backend mtmd Related to multimodal functionality (video/image/audio) CUDA Related to the CUDA backend AMD ZenDNN Issues related to the AMD ZenDNN backend WebGPU server/ui conversion vendor labels Aug 2, 2026
@ServeurpersoCom

Copy link
Copy Markdown
Contributor

Launching CI :p

@ServeurpersoCom

Copy link
Copy Markdown
Contributor

Tested on my prod (Qwen 3.6 dense 27B Q8, single slot): default config drops decode from 42 to 31 t/s on normal prompts. The per-request tree has count=1 almost everywhere so min_prob filters nothing, and short junk drafts eat the perf in verification.

Attached commit adds a n_min gate (default 3) like the other draftless impls -> back to 41 t/s worst case, long matches untouched. On a MoE the gap is even bigger: 110 t/s without the gate vs 185 t/s with it. Default can go back down once a global cross-request cache gives real frequency stats.

I'd suggest adding this patch and default values until the global cross-request cache from the paper is implemented:
with real frequency stats accumulated across requests, min_prob becomes meaningful again as the adaptive gate and n_min can go back down to 1 :

ServeurpersoCom@2cfce6c

diff --git a/common/arg.cpp b/common/arg.cpp
index 8f77830..7d6f2d3 100644
--- a/common/arg.cpp
+++ b/common/arg.cpp
@@ -4198,6 +4198,16 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
             params.speculative.ngram_suffix.n_max = value;
         }
     ).set_spec().set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}));
+    add_opt(common_arg(
+        {"--spec-ngram-suffix-n-min"}, "N",
+        string_format("discard drafts shorter than this for ngram-suffix speculative decoding (default: %d)", params.speculative.ngram_suffix.n_min),
+        [](common_params & params, int value) {
+            if (value < 1) {
+                throw std::invalid_argument("ngram-suffix n-min must be at least 1");
+            }
+            params.speculative.ngram_suffix.n_min = value;
+        }
+    ).set_spec().set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}));
     add_opt(common_arg(
         {"--spec-ngram-suffix-max-factor"}, "F",
         string_format("draft up to match_len * F tokens for ngram-suffix speculative decoding (default: %.1f)", params.speculative.ngram_suffix.max_factor),
diff --git a/common/common.h b/common/common.h
index 7065636..3871ce1 100644
--- a/common/common.h
+++ b/common/common.h
@@ -370,6 +370,7 @@ struct common_params_speculative_ngram_cache {
 struct common_params_speculative_ngram_suffix {
     int32_t max_depth = 24;    // suffix-tree depth = max context-match length
     int32_t n_max     = 24;    // maximum number of drafted tokens
+    int32_t n_min     = 3;     // discard drafts shorter than this
     float   max_factor = 1.0f; // draft up to match_len * max_factor tokens
     float   min_prob   = 0.1f; // stop drafting below this frequency probability
 };
diff --git a/common/speculative.cpp b/common/speculative.cpp
index ee98261..85b0a78 100644
--- a/common/speculative.cpp
+++ b/common/speculative.cpp
@@ -1822,6 +1822,11 @@ struct common_speculative_impl_ngram_suffix : public common_speculative_impl {
             common_suffix_draft draft = st.tree.speculate(
                     context, params.n_max, params.max_factor, params.min_prob);

+            // a short draft is unlikely to pay for its verification batch.
+            if ((int32_t) draft.tokens.size() < params.n_min) {
+                continue;
+            }
+
             *dp.result = std::move(draft.tokens);
         }
     }

@ServeurpersoCom

Copy link
Copy Markdown
Contributor

Quick demo on a dense model: chat baseline runs at 41 t/s, then repetitive XML generation ramps up to 122 t/s as the suffix tree warms up :

suffix-decode.mp4

@Green-Sky

Green-Sky commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

@ServeurpersoCom opening a minimal pr in a sec that can improve perf further for all non draft-model speculators for models with recurrent state.

edit: here #26499

@kashif

kashif commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

thanks! cherry picking the commit thanks @ServeurpersoCom

ServeurpersoCom and others added 2 commits August 3, 2026 09:49
Short drafts from low-support matches rarely pay for their
verification batch and degrade decode speed on non-repetitive
prompts. Discard drafts shorter than n_min, configurable with
--spec-ngram-suffix-n-min (default: 3), consistent with the
other draftless implementations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AMD ZenDNN Issues related to the AMD ZenDNN backend Apple Metal https://en.wikipedia.org/wiki/Metal_(API) conversion CUDA Related to the CUDA backend devops improvements to build systems and github actions documentation Improvements or additions to documentation ggml changes relating to the ggml tensor library for machine learning model Model specific mtmd Related to multimodal functionality (video/image/audio) OpenCL Issues specific to the OpenCL backend server/ui server SYCL https://en.wikipedia.org/wiki/SYCL - GPU programming language testing Everything test related vendor Vulkan Issues specific to the Vulkan backend WebGPU

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants