Speculative Decoding
Overview
Speculative decoding (also referred to as assisted-generation) is a latency-oriented optimization for autoregressive text generation. A small, cheap drafter proposes one or more candidate continuations, and the larger target (main) model validates them in a single forward pass. Tokens that the target would have produced anyway are accepted; the first divergent token is replaced by the target's own sample, and drafting resumes from there.
Because validation is parallel over the candidate length, each accepted draft token replaces a sequential decode step of the target model with (a fraction of) one batched step. When acceptance is high, this yields end-to-end speedups on memory-bandwidth-bound decoding without changing the model output distribution in theory (the standard verification scheme is statistically equivalent to sampling from the target).
OpenVINO GenAI implements three drafting strategies. They differ only in how candidates are produced — the target-side validation step is shared.
| Strategy | Drafter | Best For | Continuous Batching (CPU/GPU) | Stateful (NPU) |
|---|---|---|---|---|
| Speculative Decoding (Fast Draft) | Smaller off-the-shelf LLM | General-purpose, robust speedup without retraining | ✓ | ✓ |
| Prompt Lookup Decoding | N-gram match against the prompt | Output copies large spans of the input (RAG, summarization, code editing) | ✓ | ✗ |
| EAGLE3 | Custom draft head trained on the target's hidden states | Highest acceptance rate; also supports tree drafting | ✓ | ✓ |
All three strategies are available through the LLMPipeline Python and C++ APIs.
Prompt Lookup Decoding and EAGLE3 are additionally supported in VLMPipeline (e.g. Qwen3-VL).
The Continuous Batching backend supports multi-request batching; the Stateful backend is single-request.
Every strategy is configured in two places: the drafter is selected when the pipeline is constructed, and the drafting behavior is tuned per request through GenerationConfig.
The shared knob is num_assistant_tokens — the number of candidate tokens submitted to the target per iteration, defaulting to 5 when unset.
The remaining knobs are strategy-specific and described in the corresponding sections below.
Inspect result.extended_perf_metrics, which exposes main_model_metrics, draft_model_metrics and get_num_accepted_tokens().
The single most useful diagnostic is get_num_accepted_tokens() divided by the number of target iterations — the average accepted tokens per step.
Aim for this to be well above 1; values close to 1 mean drafting is not paying for itself.
Speculative Decoding (Fast Draft)
Fast Draft is the classic two-model setup: a smaller off-the-shelf LLM that shares the target's tokenizer drafts tokens autoregressively, and the target verifies them. It works with any target/draft pair you already have, without retraining. The speedup is bounded by how often the small model's distribution agrees with the large one.
Model Preparation
Export both models with optimum-cli. The draft model must use the same tokenizer as the target:
optimum-cli export openvino --model Qwen/Qwen3-8B --weight-format int4 --task text-generation-with-past Qwen3-8B-ov-int4
optimum-cli export openvino --model Qwen/Qwen3-0.6B --weight-format int4 --task text-generation-with-past Qwen3-0.6B-ov-int4
Configuration and Usage
In addition to num_assistant_tokens, Fast Draft supports:
assistant_confidence_threshold(float) — a dynamic-length stopping criterion. The drafter keeps proposing while the candidate token probability exceeds this threshold, then hands off to the target. It is mutually exclusive withnum_assistant_tokens.
- Python
- C++
import openvino_genai
main_model_dir = "Qwen3-8B-ov-int4"
draft_model_dir = "Qwen3-0.6B-ov-int4"
draft_model = openvino_genai.draft_model(draft_model_dir, "CPU")
pipe = openvino_genai.LLMPipeline(main_model_dir, "CPU", draft_model=draft_model)
config = openvino_genai.GenerationConfig()
config.max_new_tokens = 100
config.num_assistant_tokens = 5
# Or, on the Continuous Batching backend, dynamic-length drafting:
# config.assistant_confidence_threshold = 0.4
result = pipe.generate("The Sun is yellow because", config)
#include "openvino/genai/llm_pipeline.hpp"
std::string main_model_dir = "Qwen3-8B-ov-int4";
std::string draft_model_dir = "Qwen3-0.6B-ov-int4";
ov::genai::LLMPipeline pipe(
main_model_dir, "CPU",
ov::genai::draft_model(draft_model_dir, "CPU"));
ov::genai::GenerationConfig config;
config.max_new_tokens = 100;
config.num_assistant_tokens = 5;
// Or, on the Continuous Batching backend, dynamic-length drafting:
// config.assistant_confidence_threshold = 0.4f;
pipe.generate("The Sun is yellow because", config);
Increase num_assistant_tokens until the accepted-tokens-per-step figure plateaus, then back off.
Past the plateau, rejected draft tokens are pure overhead.
- Continuous Batching uses
num_assistant_tokensas-is and runs the drafter and target as fully scheduled paged-attention pipelines, each with its own KV cache. The target and draft caches share the configuredSchedulerConfig.cache_size, so increase it when the drafter is large. - Stateful uses
num_assistant_tokensas the initial value and adapts it per step based on recent acceptance, falling back gracefully when acceptance is low.
assistant_confidence_threshold is supported on the Continuous Batching backend only.
The Stateful backend requires NPU to be the execution device for at least one of the two models. For NPU deployments, the recommended configuration places both the target and the draft model on the NPU.
Prompt Lookup Decoding
Prompt Lookup decoding replaces the draft model with a string match: the most recently emitted suffix is searched as an n-gram inside the prompt, and the tokens that follow each match are proposed as candidates. No second model is loaded.
This is highly effective for input-grounded generation — RAG, document QA, summarization, code editing, multi-turn chat — where the output frequently copies entity names, phrases or code chunks verbatim from the input. On these workloads it delivers speculative-decoding-class speedups at near-zero drafter cost. On workloads with low prompt/output overlap it degrades gracefully back to plain decoding, since the proposals are simply rejected.
Configuration and Usage
Prompt Lookup is enabled at pipeline construction time with prompt_lookup=True; it cannot be switched on per request.
In addition to num_assistant_tokens it supports:
max_ngram_size(size_t) — the maximum n-gram length matched against the prompt. Larger values produce longer, more confident continuations when they hit, but match less often.
- Python
- C++
import openvino_genai
main_model_dir = "Qwen3-8B-ov-int4"
pipe = openvino_genai.LLMPipeline(main_model_dir, "CPU", prompt_lookup=True)
config = openvino_genai.GenerationConfig()
config.max_new_tokens = 100
config.num_assistant_tokens = 5
config.max_ngram_size = 3
pipe.generate("The Sun is yellow because", config)
#include "openvino/genai/llm_pipeline.hpp"
std::string main_model_dir = "Qwen3-8B-ov-int4";
ov::genai::LLMPipeline pipe(
main_model_dir, "CPU",
ov::genai::prompt_lookup(true));
ov::genai::GenerationConfig config;
config.max_new_tokens = 100;
config.num_assistant_tokens = 5;
config.max_ngram_size = 3;
pipe.generate("The Sun is yellow because", config);
Prompt Lookup is available on the Continuous Batching backend only.
The draft_model and prompt_lookup constructor options are mutually exclusive — Prompt Lookup uses no draft model.
EAGLE3
EAGLE-3 (Extrapolation Algorithm for Greater Language-model Efficiency) replaces the generic draft model with a small head — typically one transformer layer — trained to mimic the target's distribution conditioned on the target's hidden states, not just on tokens. This much tighter coupling raises the per-token acceptance rate substantially over Fast Draft on the same target, while keeping the drafter cheap.
EAGLE3 can produce candidates either as a linear chain or as a tree.
Model Preparation
Export both the target and the matching EAGLE3 draft head with optimum-cli.
The draft repository must be an EAGLE3 head trained against the same target family — the head exposes a d2t mapping table that OpenVINO GenAI picks up automatically:
optimum-cli export openvino --model Qwen/Qwen3-8B --weight-format int4 --task text-generation-with-past --trust-remote-code Qwen3-8B-ov-int4
optimum-cli export openvino --model AngelSlim/Qwen3-8B_eagle3 --weight-format int4 --task text-generation-with-past --trust-remote-code Qwen3-8B_eagle3-ov-int4
Start with chain drafting: it has a single knob, and on a well-matched EAGLE3 head it already captures most of the available speedup.
Switch to tree drafting when acceptance per step has plateaued but the target still has spare compute per forward pass — typically on GPU at small batch sizes, where the target's step time grows sublinearly with the candidate count. Tree drafting trades a larger validation batch for a higher accepted-token count per step.
Chain Drafting
Chain drafting runs the EAGLE3 head autoregressively for num_assistant_tokens iterations and submits the resulting linear chain to the target for verification.
It needs no configuration beyond num_assistant_tokens.
- Python
- C++
import openvino_genai
main_model_dir = "Qwen3-8B-ov-int4"
draft_model_dir = "Qwen3-8B_eagle3-ov-int4"
draft_model = openvino_genai.draft_model(draft_model_dir, "GPU")
pipe = openvino_genai.LLMPipeline(main_model_dir, "GPU", draft_model=draft_model)
config = openvino_genai.GenerationConfig()
config.max_new_tokens = 100
config.num_assistant_tokens = 5
pipe.generate("What is OpenVINO?", config)
#include "openvino/genai/llm_pipeline.hpp"
std::string main_model_dir = "Qwen3-8B-ov-int4";
std::string draft_model_dir = "Qwen3-8B_eagle3-ov-int4";
ov::genai::LLMPipeline pipe(
main_model_dir, "GPU",
ov::genai::draft_model(draft_model_dir, "GPU"));
ov::genai::GenerationConfig config;
config.max_new_tokens = 100;
config.num_assistant_tokens = 5;
pipe.generate("What is OpenVINO?", config);
Tree Drafting
Instead of a single linear chain, the drafter expands the top branching_factor continuations at each of tree_depth layers, scores the resulting tree, and submits the highest-scoring num_assistant_tokens candidates to the target in one packed verification step.
With a single target forward pass the pipeline can therefore validate multiple alternative continuations and accept the longest matching one, compounding EAGLE3's already-high acceptance rate into longer accepted runs.
Tree drafting adds two GenerationConfig fields:
branching_factor(size_t) — number of top-k expansions retained per tree node and per tree layer.tree_depth(size_t) — number of draft iterations, i.e. tree layers. Settingtree_depth > 0switches the request from chain drafting to tree drafting.
How Tree Drafting Works
- The EAGLE3 head runs
tree_depthiterations. At each layer it expands the topbranching_factorchildren of every surviving node, producing up tobranching_factor^2 * (tree_depth - 1) + branching_factorcandidate tokens organized as a tree. - Candidates are scored cumulatively along their tree paths; the top
num_assistant_tokensare flattened into a single packed input together with a tree attention mask and tree position ids. - The target processes the packed input in one forward pass and verifies all paths simultaneously. The longest path whose tokens match the target's own samples is accepted.
- Accepted tokens become the new prefix, the draft and target KV caches are advanced to match, and the next round begins.
Two constraints follow from this, and both are enforced by GenerationConfig::validate():
- The tree must be able to supply the requested number of candidates:
branching_factor^2 * (tree_depth - 1) + branching_factor >= num_assistant_tokens. num_assistant_tokens >= tree_depth, since an accepted path is at mosttree_depthtokens long.
- Python
- C++
import openvino_genai
main_model_dir = "Qwen3-8B-ov-int4"
draft_model_dir = "Qwen3-8B_eagle3-ov-int4"
draft_model = openvino_genai.draft_model(draft_model_dir, "GPU")
pipe = openvino_genai.LLMPipeline(main_model_dir, "GPU", draft_model=draft_model)
config = openvino_genai.GenerationConfig()
config.max_new_tokens = 100
config.num_assistant_tokens = 15 # top-K candidates verified per step
config.branching_factor = 8 # top-k expansions per tree node
config.tree_depth = 4 # number of draft layers
pipe.generate("What is OpenVINO?", config)
#include "openvino/genai/llm_pipeline.hpp"
std::string main_model_dir = "Qwen3-8B-ov-int4";
std::string draft_model_dir = "Qwen3-8B_eagle3-ov-int4";
ov::genai::LLMPipeline pipe(
main_model_dir, "GPU",
ov::genai::draft_model(draft_model_dir, "GPU"));
ov::genai::GenerationConfig config;
config.max_new_tokens = 100;
config.num_assistant_tokens = 15; // top-K candidates verified per step
config.branching_factor = 8; // top-k expansions per tree node
config.tree_depth = 4; // number of draft layers
pipe.generate("What is OpenVINO?", config);
The trio (branching_factor, tree_depth, num_assistant_tokens) is the main tuning surface.
A reasonable starting point on a high-acceptance target is branching_factor=4..8 and tree_depth=3..4, with num_assistant_tokens chosen to match how many candidate tokens the target can verify per step on your hardware.
Tree drafting is an EAGLE3-only feature; Fast Draft and Prompt Lookup ignore branching_factor and tree_depth.
It cannot be combined with beam search or multinomial sampling — set tree_depth=0, do_sample=false or num_beams=1 accordingly.
Streaming is restricted to batch size 1, with either greedy decoding or tree search. Beam search and multinomial sampling are not supported.
assistant_confidence_threshold must be 0 for EAGLE3 — dynamic-length drafting is a Fast Draft feature.