Speculative decoding in vLLM serving
Code version: vLLM @ 3e44c0d · 2026-10-22
In one sentence
A drafter proposes k tokens per request on the GPU. The next forward pass checks them all, so one step can emit up to k + 1 tokens.
The idea#
Speculative decoding lets a cheap drafter guess the next tokens. The large target model then checks all guesses in one forward pass. A rejection test keeps the output distribution of the target model (Speculative decoding: draft, verify, accept/reject). This page shows how vLLM does this inside vllm serve.
You turn it on with one flag, --speculative-config, a JSON object that becomes a SpeculativeConfig. The method names the drafter: an EAGLE head, an MTP layer of the model, a separate draft model, an n-gram lookup, and others. num_speculative_tokens (k) sets how many drafts each request gets per step.
In Model Runner V2 the drafts stay on the GPU until the next step checks them. Only a batch with structured outputs copies them to the CPU for the grammar check (vllm/v1/worker/gpu/spec_decode/utils.py:64-80). The scheduler only reserves room for them, with -1 placeholders. The worker samples, drafts the next k tokens right away on the GPU, and puts them into the next input. The rejection sampler checks them in the next step.
An everyday comparison. Think of a junior writer and an editor. The junior writes the next three words. The editor reads them in one pass, keeps the words up to the first wrong one, and writes one correct word there. If all three are right, the editor adds a fourth word. The comparison stops being true in one place: the editor is not slower per word than the junior. The target model checks four positions for about the price of one decode step, because decode is memory-bound (Prefill and decode).
The picture#
Read the panels from top to bottom. Blue and teal are the target forward pass and the check, amber and violet are the GPU work after sampling, emerald is the engine core. The numbers are one example request; the worked example in the deep dive follows the same numbers.
In the code#
Step 1: SpeculativeConfig and the methods#
--speculative-config (short -sc) takes a JSON string. vLLM keeps it as a dict until it builds the engine config, because SpeculativeConfig needs the target model config (vllm/engine/arg_utils.py:1851-1857). Three short flags exist too: --spec-method, --spec-model and --spec-tokens. A short flag and the same key in the JSON raise ValueError (vllm/engine/arg_utils.py:2098-2110).
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--speculative-config '{"method": "eagle3",
"model": "yuhuili/EAGLE3-LLaMA3.1-Instruct-8B",
"num_speculative_tokens": 3}'
The method names are a Literal type:
NgramGPUTypes = Literal["ngram_gpu"]
DFlashModelTypes = Literal["dflash"]
DSparkModelTypes = Literal["dspark"]
EagleModelTypes = Literal[
"eagle", "eagle3", "extract_hidden_states", MTPModelTypes, DFlashModelTypes
]
SpeculativeMethod = Literal[
"ngram",
"draft_model",
"suffix",
"custom_class",
EagleModelTypes,
NgramGPUTypes,
DSparkModelTypes,
]
RejectionSampleMethod = Literal["standard", "synthetic", "block", "fly"]
DraftSampleMethod = Literal["greedy", "probabilistic"]- Draft heads that read the target's hidden states:
eagle,eagle3,mtp,dflashanddspark.use_eagle()is true for all five (vllm/config/speculative.py:2013-2017).dflashanddsparkalways draft in parallel (vllm/config/speculative.py:1505-1506). - MTP. The 27 model-specific MTP names in
MTPModelTypes(for exampledeepseek_mtp) are deprecated (vllm/config/speculative.py:38-67). vLLM logs a warning and usesmtp(vllm/config/speculative.py:1197-1201). Withoutmodel, MTP uses the target checkpoint itself (vllm/config/speculative.py:1204-1215). - A separate small model:
draft_model. It is also the fallback whenmethodis missing and the model name gives no other hint (vllm/config/speculative.py:1186-1195). - No model at all:
ngramandngram_gpulook up the last tokens in the prompt and the output. Both look-up sizes default to 5 when you set neither (vllm/config/speculative.py:1252-1256). - Others:
suffix(suffix decoding),custom_class(your own proposer class, "experimental") andextract_hidden_states(no drafting; it only exports hidden states) (vllm/config/speculative.py:1286-1293,vllm/config/speculative.py:1298-1304).
With a model but no method, vLLM guesses the method from the model name and architecture. It looks for "eagle-", "eagle3", "dflash", "dspark" or an MTP model_type (vllm/config/speculative.py:1363-1398). num_speculative_tokens has no default. It comes from n_predict in the draft config when that exists (for DSpark, from its block size); otherwise it is required (vllm/config/speculative.py:1508-1542).
Step 2: which methods Model Runner V2 supports#
Model Runner V2 is the default. Some speculative settings switch the whole worker back to Model Runner V1 (The model runner (Model Runner V2)):
if speculative_config is not None:
if speculative_config.method in ("suffix", "custom_class"):
unsupported.append(f"speculative method '{speculative_config.method}'")
# V2 EagleSpeculator does not support parallel_drafting (for P-Eagle).
# DFlash and DSpark use parallel drafting natively in V2 via their
# own speculators.
if (
speculative_config.parallel_drafting
and speculative_config.method not in ("dflash", "dspark")
):
unsupported.append("parallel drafting for EAGLE speculative decoding")
# The V2 draft-model speculator has no token mapping between the
# draft and target vocabularies (TLI is TBD in #47172).
if getattr(speculative_config, "use_heterogeneous_vocab", False):
unsupported.append("heterogeneous-vocabulary draft models")So suffix, custom_class, parallel drafting with EAGLE (P-EAGLE) and draft models with a different vocabulary run on the fallback. vLLM logs "Model Runner V2 does not yet support ..." (vllm/config/vllm.py:740-747). The reverse also exists: dspark, adaptive verification and draft LM-head quantization work only on Model Runner V2 (vllm/config/vllm.py:3056-3063).
In Model Runner V2, init_speculator maps the method to a speculator class. ngram and ngram_gpu both get the GPU n-gram speculator (vllm/v1/worker/gpu/spec_decode/__init__.py:81-86, vllm/config/speculative.py:2048-2049). Only the last pipeline-parallel rank builds a speculator (vllm/v1/worker/gpu/model_runner.py:298-308).
Step 3: drafts in the scheduler#
The scheduler treats drafts as normal tokens of the request. A decode with k drafts schedules 1 + k tokens, and they count against the token budget. After scheduling, it keeps only the drafts that fit and moves them into scheduled_spec_decode_tokens (vllm/v1/core/sched/scheduler.py:916-932). The page The V1 scheduler: one token budget, no phases (Step 6) shows the budget rule.
Two extra reservations exist:
- Draft slots in the input budget: rows that the drafter adds to the batch. They are 0 for EAGLE, MTP, n-gram, DFlash and DSpark, 1 for a separate draft model, and more with parallel drafting (
vllm/config/speculative.py:1955-1995,vllm/v1/core/sched/scheduler.py:644). - Lookahead KV slots. The drafter writes KV past the target's query. So
allocate_slotsgetsnum_lookahead_tokens: k for EAGLE-type and draft-model methods, k + 1 for DFlash, 0 for n-gram (vllm/config/vllm.py:625-650,vllm/v1/core/sched/scheduler.py:834-838).
With async scheduling (the default), the scheduler plans step N+1 before the drafts of step N exist. It puts [-1] * k as placeholder drafts and adds 1 + k output placeholders (vllm/v1/core/sched/async_scheduler.py:28-50). The real drafts are only on the GPU. Async scheduling: overlap the CPU and the GPU explains the placeholders.
Step 4: verify, then draft again, in one sample_tokens call#
In sample(), the model runner chooses the sampler. Without drafts in the batch the normal sampler runs; with drafts the RejectionSampler runs:
sampler_output: SamplerOutput | None
if input_batch.num_reqs == 0:
# This rank owns no requests this step. It contributes an
# all-padding block to the gather below.
sampler_output = None
elif input_batch.num_draft_tokens == 0 or self.rejection_sampler is None:
assert self.sampler is not None
sampler_output = self.sampler(logits, input_batch)
else:
# Rejection sampling for spec decoding.
assert self.rejection_sampler is not None
assert self.speculator is not None
sampler_output = self.rejection_sampler(
logits,
input_batch,
# Draft logits are needed for probabilistic rejection sampling.
self.speculator.draft_logits,
invalid_drafts,
)After sampling, sample_tokens starts the output copy (vllm/v1/worker/gpu/model_runner.py:2209-2218), updates the request state (vllm/v1/worker/gpu/model_runner.py:2249-2255), and then calls the speculator at once:
with use_workspace_lane(self._draft_workspace_lane):
draft_tokens = self.speculator.propose(
input_batch,
attn_metadata,
slot_mappings_by_layer,
spec_hidden_states,
aux_hidden_states,
num_sampled,
num_rejected,
self.req_states.last_sampled_tokens,
self.req_states.next_prefill_tokens,
self.sampler.sampling_states.temperature.gpu,
self.sampler.sampling_states.seeds.gpu,
num_speculative_tokens=num_spec_tokens,
dp_sync_state=dp_sync_state,
mm_inputs=mm_inputs,
)
if num_spec_tokens < self.num_speculative_steps:
draft_tokens[:, num_spec_tokens:] = -1
self.req_states.draft_tokens[input_batch.idx_mapping] = draft_tokens- Line 2271:
propose()gets the hidden states, the number of sampled and rejected tokens, and the last sampled token, all on the GPU. - Lines 2287-2288: with dynamic speculative decoding the step may use fewer drafts; the unused columns become
-1. - Line 2289: the drafts go into
req_states.draft_tokens. The nextprepare_inputscopies them intoinput_ids(vllm/v1/worker/gpu/model_runner.py:1493-1504).
The EAGLE speculator first runs a "draft prefill" over the same token rows as the target. So it can reuse the target's attention metadata and slot mappings. Then it runs k - 1 decode steps of one token per request (vllm/v1/worker/gpu/spec_decode/target_dependent_ar/speculator.py:204-209, vllm/v1/worker/gpu/spec_decode/target_dependent_ar/speculator.py:281-299, vllm/v1/worker/gpu/spec_decode/target_dependent_ar/speculator.py:331-366).
Step 5: the RejectionSampler#
The RejectionSampler wraps the normal sampler. First it applies the sampling parameters of each request (penalties, temperature, top-k, top-p) to all 1 + k logits rows; then it runs the test (vllm/v1/worker/gpu/spec_decode/rejection_sampler.py:264-293). The test depends on rejection_sample_method:
rejection_sample_method: RejectionSampleMethod = "standard"
"""The rejection sampling method to use. 'standard' uses probabilistic
rejection sampling (with or without cached draft logits, controlled by
draft_sample_method). 'synthetic' accepts draft tokens with a decaying
probability calibrated to synthetic_acceptance_rate. 'block' uses block
verification (Sun et al.), which jointly verifies the draft tokens as a
block instead of one at a time. 'fly' applies FLy's lossy, entropy-gated
deferred window to the native per-token acceptance decisions."""| Variant | What it does (high level) | Lossless? |
|---|---|---|
standard (default) | Checks each draft in order. Greedy requests accept a draft only if it equals the target argmax (vllm/v1/worker/gpu/spec_decode/rejection_sampler_utils.py:600-616). Random requests accept x when p(x) > u · q(x) (vllm/v1/worker/gpu/spec_decode/rejection_sampler_utils.py:695-697). | yes |
block | Block verification (Sun et al.): judges the drafts as one block, with the joint probability of the prefix (vllm/v1/worker/gpu/spec_decode/rejection_sampler_utils.py:628-660). | yes (per the paper) |
synthetic | Ignores the logits for the decision and accepts with fixed per-position rates. You must give exactly one of synthetic_acceptance_rates and synthetic_acceptance_length (vllm/config/speculative.py:581-585). For benchmarks. | no |
fly | FLy: a lossy, entropy-gated window over the normal decisions. Needs k ≥ 2; the window defaults to min(6, k - 1); vLLM logs a warning (vllm/config/speculative.py:1913-1931). | no |
The acceptance math. p is the target probability and q the draft probability of the draft token x, both after temperature. u is a uniform random number. The draft is kept when p(x) > u · q(x), that is with probability min(1, p(x) / q(x)). The default draft_sample_method is greedy: the drafter takes the argmax, and q counts as one-hot. So q(x) = 1, and the draft is kept with probability p(x) (vllm/config/speculative.py:608-614, vllm/v1/worker/gpu/spec_decode/rejection_sampler_utils.py:191-194). At the first rejection, vLLM draws a new token from the residual max(p - q, 0). With a one-hot draft, that is the target distribution without the rejected token (vllm/v1/worker/gpu/spec_decode/rejection_sampler_utils.py:937-938, vllm/v1/worker/gpu/spec_decode/rejection_sampler_utils.py:950-951). If all k drafts pass, the last row gives a bonus token.
Step 6: the engine core rolls back rejected drafts#
The model runner already corrected its own GPU counters. num_rejected is the number of logits rows minus the number of sampled tokens, and num_computed_tokens grows by the query length minus that (vllm/v1/worker/gpu/input_batch.py:528-534, vllm/v1/worker/gpu/input_batch.py:612-615). The scheduler does the same on the CPU when the output arrives:
scheduled_spec_token_ids = (
scheduler_output.scheduled_spec_decode_tokens.get(req_id)
)
if scheduled_spec_token_ids and (
generated_token_ids or self.num_sampled_tokens_per_step == 0
):
num_draft_tokens = len(scheduled_spec_token_ids)
num_sampled = self.num_sampled_tokens_per_step
num_accepted = max(len(generated_token_ids) - num_sampled, 0)
num_rejected = num_draft_tokens - num_accepted
# Rejections roll back num_computed_tokens (and, under async
# scheduling, num_output_placeholders, which covers the spec
# tokens). A stale rejection count predates the preemption
# rollback and must not apply.
if not output_is_stale:
if request.num_computed_tokens > 0:
request.num_computed_tokens -= num_rejected
if request.num_output_placeholders > 0:
request.num_output_placeholders -= num_rejected
spec_decoding_stats = self.make_spec_decoding_stats(
spec_decoding_stats,
num_draft_tokens=num_draft_tokens,
num_accepted_tokens=num_accepted,
num_invalid_spec_tokens=scheduler_output.num_invalid_spec_tokens,
request_id=req_id,
)The KV entries of rejected drafts are not freed. The blocks stay with the request: the KV manager notes that a request can hold more blocks than it needs after a rejection (vllm/v1/core/single_type_kv_cache_manager.py:233-236). The next step writes the correct token over the stale entry, because num_computed_tokens moved back (derived from the counters above). With async scheduling, post_step does not take draft tokens from the workers (vllm/v1/engine/core.py:689-696). The engine core takes them only for the grammar check of a structured-output step (vllm/v1/engine/core.py:792-803).
Step 7: outputs with more than one token#
One step can give a request up to 1 + k new tokens, and they travel in one EngineCoreOutput. This changes the latency numbers:
- ITL. The front end adds one inter-token latency sample per output, not per token (
vllm/v1/metrics/stats.py:551-557). With 3 tokens per step, there are a third as many ITL samples, and each is longer. - TPOT. The decode time is divided by all output tokens minus one, so TPOT goes down (
vllm/v1/metrics/stats.py:612-616). See Serving metrics: TTFT, TPOT, ITL, throughput, goodput. - Streaming. A stream gets the new tokens of a step together, so one SSE chunk can hold several tokens of text.
Deep dive#
The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.
Deep diveWorked example: EAGLE, k = 3, two drafts accepted
Server: vllm serve meta-llama/Llama-3.1-8B-Instruct with the EAGLE3 command of Step 1, one GPU, block size 16, async scheduling on. One request is in decode. 100 of its tokens are in the KV cache (positions 0 to 99). Its last sampled token x0 (position 100) is not computed yet. The drafter proposed d1, d2, d3 in the previous step.
- Schedule. The scheduler plans
1 + 3 = 4tokens.num_computed_tokensgoes from 100 to 104 at once (vllm/v1/core/sched/scheduler.py:1716).allocate_slotsmust hold 4 tokens plus 3 lookahead slots: positions up to 106, all inside block 6 (positions 96 to 111). The placeholders grow by 4, and the next draft list is[-1, -1, -1]. - Forward. The input is
[x0, d1, d2, d3]at positions[100, 101, 102, 103]. The KV of all four positions is written. Logits come back for all 4 rows:cu_num_logitsgives this request 4 rows, not 1. - Verify. Row 0 checks d1 (accept). Row 1 checks d2 (accept). Row 2 checks d3: reject. A recovered token r is drawn from row 2 without d3. Row 3 (the bonus row) is not used. Result:
sampled = [d1, d2, r],num_sampled = 3,num_rejected = 4 - 3 = 1. - Worker counters.
num_computed_tokenson the GPU grows by4 - 1 = 3: from 100 to 103.last_sampled_tokens = r. The KV at position 103 still holds d3, but position 103 is not computed any more. - Draft again. EAGLE runs the draft prefill over the 4 rows, then 2 draft decode steps, and writes
[e1, e2, e3]intoreq_states.draft_tokens. - Next step. The input is
[r, e1, e2, e3]at positions 103 to 106. The KV of r overwrites the stale d3 entry at 103. - Engine core. The output has 3 tokens.
num_accepted = 3 - 1 = 2,num_rejected = 3 - 2 = 1, sonum_computed_tokensgoes from 104 to 103 and the placeholders drop by 1 (vllm/v1/core/sched/scheduler.py:2209-2221). The 3 new tokens remove the other 3 placeholders (vllm/v1/core/sched/async_scheduler.py:71-73). The client gets d1, d2 and r: 3 tokens for one target forward pass. - Metrics.
num_drafts+1,num_draft_tokens+3,num_accepted_tokens+2, and the per-position counters +1 at positions 0 and 1 (vllm/v1/spec_decode/metrics.py:41-49).
Deep diveMetrics: acceptance rate and mean acceptance length
The scheduler builds SpecDecodingStats per step when stats logging is on (vllm/v1/core/sched/scheduler.py:3061-3078). Prometheus gets four counters (vllm/v1/spec_decode/metrics.py:229-231, vllm/v1/spec_decode/metrics.py:254-258):
| Metric | Counts |
|---|---|
vllm:spec_decode_num_drafts | verification rounds (one per request per step with drafts) |
vllm:spec_decode_num_draft_tokens | draft tokens that were checked |
vllm:spec_decode_num_accepted_tokens | accepted drafts (not the bonus or recovered token) |
vllm:spec_decode_num_accepted_tokens_per_pos | accepted drafts per draft position, label position |
The class docstring gives the PromQL (vllm/v1/spec_decode/metrics.py:177-196):
- acceptance rate =
rate(..._num_accepted_tokens_total) / rate(..._num_draft_tokens_total). - Mean acceptance length =
1 + rate(..._num_accepted_tokens_total) / rate(..._num_drafts_total). The 1 is the bonus or recovered token of each round (vllm/v1/spec_decode/metrics.py:113-114).
In the worked example: acceptance rate 2/3 = 67 %, acceptance length 1 + 2/1 = 3. The log line "SpecDecoding metrics: Mean acceptance length ..." prints the same numbers every log interval (vllm/v1/spec_decode/metrics.py:120-137). --per-request-spec-decode-metrics summary or detailed adds per-request numbers to the response; the default is none, and it needs speculative decoding (vllm/config/observability.py:50-56, vllm/config/vllm.py:1713-1720).
Deep diveCUDA graphs with drafts
A decode with k drafts has 1 + k query tokens. vLLM calls this the uniform decode query length (vllm/config/vllm.py:676-695); Model Runner V2 stores it as decode_query_len (vllm/v1/worker/gpu/model_runner.py:442-445). A FULL CUDA graph replays only for a batch in which every request has exactly that many tokens. Prefill chunks in the batch give a PIECEWISE graph. Without dynamic k, the scheduler pads a waiting 1-token request to 1 + k tokens when no prefill runs, to keep the batch uniform (vllm/v1/core/sched/scheduler.py:1196-1216).
vLLM adds decode capture sizes of 1 + k times 1, 2 or 4 requests, or a multiple of 8 requests (vllm/config/vllm.py:2466-2473, vllm/config/vllm.py:2520-2527). With k = 3, a batch of 3 verifying requests (12 tokens) replays the FULL graph of 16 tokens; torch.compile and CUDA graphs in vLLM has this example. The drafter has its own graphs. The draft prefill can replay a FULL graph when all requests decode. Each draft decode step is a uniform 1-token batch (vllm/v1/worker/gpu/spec_decode/target_dependent_ar/speculator.py:248-258, vllm/v1/worker/gpu/spec_decode/target_dependent_ar/speculator.py:331-345).
Deep diveSpeculativeConfig fields that matter for serving
| Field | Default | Effect |
|---|---|---|
method | guessed, else draft_model | Step 1 (vllm/config/speculative.py:1186-1195). |
num_speculative_tokens | none; from n_predict if present | k (vllm/config/speculative.py:393-395). |
draft_tensor_parallel_size | target TP | Only 1 or the target TP size (vllm/config/speculative.py:407-409). tensor_parallel_size inside the JSON raises ValueError (vllm/config/speculative.py:1837-1841). |
rejection_sample_method | standard | Step 5. |
draft_sample_method | greedy | probabilistic samples drafts and keeps draft logits, at the cost of GPU memory (vllm/config/speculative.py:608-614). |
prompt_lookup_min / max | 5 / 5 (n-gram) | n-gram window (vllm/config/speculative.py:1252-1256). |
num_speculative_tokens_per_batch_size | none | Dynamic k by batch size, as (start, end, k) ranges (vllm/config/speculative.py:498-503). Turned off with data parallelism (vllm/config/vllm.py:1092-1109). |
parallel_drafting | false (true for DFlash, DSpark) | EAGLE with it falls back to Model Runner V1 (Step 2). |
One cross-check applies with LoRA and speculative decoding together. Then max_num_batched_tokens must be at least max_num_seqs × (k + 1), else start-up fails with "Consider increasing max_num_batched_tokens or decreasing num_speculative_tokens" (vllm/engine/arg_utils.py:2700-2713).
Deep diveTuning with the defaults that the code gives
The code does not choose k for you. The only exceptions are opt-in: dynamic k by batch size, and adaptive verification for DSpark (vllm/config/speculative.py:559-561). Use the metrics above to choose:
- Start from the drafter's own k. EAGLE-type and MTP checkpoints often carry
n_predict, which vLLM uses when you omit k (vllm/config/speculative.py:1524-1536). If you set k aboven_predict, k must be a multiple ofn_predict(same lines). - Watch the per-position rates. If position 3 rarely accepts, k = 3 wastes target compute on the third row. Each extra draft costs one more logits row and one more draft step for every request.
- Batch size matters. Verification adds k tokens per request to the token budget. In large batches the step is less memory-bound, so drafts cost more.
num_speculative_tokens_per_batch_sizelowers k for larger batches. - Greedy requests accept only exact argmax matches; random requests use the probability test. The same drafter can show different rates for different temperatures.
The docs give measured speedups only for a few setups, for example FLy against standard verification (docs/features/speculative_decoding/fly.md:55-62). Measure your own workload with vllm bench serve (Benchmarking vLLM: vllm bench serve, throughput, latency and startup).
What can go wrong#
- "num_speculative_tokens was provided but without speculative model." You set k for a method that needs a model, without
model(vllm/config/speculative.py:1243-1246). - "Target and draft model should have the same vocabulary size." A
draft_modelwith another tokenizer (vllm/config/speculative.py:1937-1953). - "The min_p and logit_bias sampling parameters are not yet supported with speculative decoding." Such a request gets a validation error (
vllm/sampling_params.py:1192-1197). - ITL looks worse, TPOT better. This is expected: one output holds several tokens (Step 7).
- Low acceptance with structured outputs. Drafts that the grammar rejects become
-1before verification and count as invalid (Structured outputs: JSON schemas, grammars and token bitmasks in vLLM). - FLy or synthetic in production. Both change the output distribution; FLy logs "FLy verification is a lossy speculative decoding method" (
vllm/config/speculative.py:1928-1931).
Why it matters. Speculative decoding trades extra GPU work per step for fewer steps per answer. It helps most at small batch sizes, where decode leaves the GPU idle. The acceptance metrics tell you whether the trade pays off.
Check yourself#
- 1Which flag turns on speculative decoding, and what is k?easy
Show answer
--speculative-configwith a JSON object; k isnum_speculative_tokens, the number of drafts per request per step (vllm/engine/arg_utils.py:1854-1857,vllm/config/speculative.py:393-395). - 2With async scheduling, what draft tokens does the scheduler put into the SchedulerOutput, and where are the real ones?medium
Show answer
A list of
-1placeholders, k long (vllm/v1/core/sched/async_scheduler.py:28-50). The real drafts are inreq_states.draft_tokenson the GPU, written bysample_tokensright after sampling (vllm/v1/worker/gpu/model_runner.py:2289). - 3With the defaultmedium
draft_sample_method, a random request (temperature 0.8) checks a draft x with target probability 0.3. What is the chance to accept it?Show answer
0.3. Greedy drafting treats q as one-hot, so
q(x) = 1, and the testp(x) > u · q(x)passes with probabilityp(x)(vllm/v1/worker/gpu/spec_decode/rejection_sampler_utils.py:191-194,vllm/v1/worker/gpu/spec_decode/rejection_sampler_utils.py:695-697). - 4k = 4. A request had 200 computed tokens before the step; the target accepts 1 draft. What are num_sampled, num_rejected (worker) and num_accepted (scheduler), and where does num_computed_tokens end?hard
Show answer
The query has 5 tokens and 5 logits rows. Sampled: 1 accepted draft + 1 recovered token, so
num_sampled = 2and the worker'snum_rejected = 5 - 2 = 3(vllm/v1/worker/gpu/input_batch.py:528-534). The scheduler computesnum_accepted = 2 - 1 = 1andnum_rejected = 4 - 1 = 3(vllm/v1/core/sched/scheduler.py:2209-2213).num_computed_tokensgoes 200 → 205 at schedule time and back to 202. - 5Why can a 3-token output make the Prometheus ITL histogram look worse while TPOT improves?hard
Show answer
ITL adds one sample per engine-core output, the gap since the previous output, no matter how many tokens it holds (
vllm/v1/metrics/stats.py:551-557). TPOT divides the decode time by all generated tokens minus one (vllm/v1/metrics/stats.py:612-616). A step with 3 tokens is a little slower than a plain step, so each ITL sample grows, but the time per token falls.