Speculators: the draft-model checkpoint format¶
Scope: the toolchain and checkpoint format layer of speculative decoding, not the algorithm. This page covers vllm-project/speculators: what a draft checkpoint contains (config.json plus one model.safetensors), which fields a serving engine reads, how EAGLE-3 and DFlash differ as configurations inside the same format (which target layers are consumed, chain versus block, what must match the target), and the drift failures that a draft/target pair produces. The accept/reject mathematics and losslessness proof live in speculative decoding; the walltime and cost model in speculative decoding economics; acceptance measurement across workloads in evaluating speculative decoding; DeepSeek's separate DSpark drafter in DSpark speculative decoding.
Evidence: the
speculatorsrepository was cloned and read at commit0faffeb3bd547b4451a978d7aaf26a2f01b83d62(2026-08-25); its two algorithm doc pages were fetched fromdocs.vllm.aion 2026-08-26, and their prose matchesdocs/user_guide/algorithms/{eagle3,dflash}.mdat that commit sentence for sentence (compared after normalising links and list markers, not byte for byte, since one side is rendered HTML). The vLLM loader was read fromvllm-project/vllmmainat61d4f56635e0f2faae4bd60e6da88a53c9d3affb(2026-08-26); it is not pinned to a release. Five published checkpoints on Hugging Face were fetched for real: theirconfig.jsonbodies and, for three of them, the safetensors tensor index (via HTTP range requests on the header). The numpy block is executed and asserted, and its checkpoint numbers are those fetched values. No GPU, no vLLM server, and no training run were involved: every throughput, acceptance, or speedup figure attributed to the project is the project's claim, not a measurement made here.
What it is¶
Speculators is a library and a checkpoint format for producing speculative-decoding draft models and shipping them into vLLM. The README states the purpose directly: "Speculators is a library for training speculative decoding draft models that deploy directly to LLM inference engines like vLLM."1 The format is deliberately thin: a draft checkpoint is an ordinary Hugging Face model directory, and the entire speculative-decoding contract is carried by extra keys inside config.json.
Two keys make a directory a speculators checkpoint:
speculators_model_type: the algorithm discriminator (eagle3,dflash,dspark,peagle,mtp).SpeculatorModelConfig.from_pretrainedraisesNotImplementedErrorif it is absent, andfrom_dictraisesValueError; both refuse to guess.2speculators_config: a nested object holdingalgorithm,proposal_methods,default_proposal_method, andverifier. Theverifierblock recordsname_or_pathandarchitecturesso the engine can load and validate the target the draft was built against.2
Everything else is either standard Transformers (architectures, torch_dtype, transformers_version) or algorithm-specific. The draft's own decoder hyperparameters live in a nested transformer_layer_config, which is a full PretrainedConfig in its own right.
Two checkpoint files carry the model: config.json and model.safetensors. The Hub listings for RedHatAI/Qwen3-8B-speculator.eagle3 and RedHatAI/gemma-4-31B-it-speculator.dflash contain nothing else load-bearing besides a small auto_map module (eagle3.py, config.py) and a README. There is no separate tokenizer, no separate weight index, and no sidecar metadata file: the vocabulary mapping tensors t2d and d2t are registered buffers inside the same safetensors file.3
Why use it¶
- One format, five algorithms. EAGLE-3, P-EAGLE, DFlash, DSpark, and MTP all serialise through
SpeculatorModelConfig, discriminated byspeculators_model_typeand resolved through a Pydantic class registry (auto_package = "speculators.models",schema_discriminator = "speculators_model_type").2 Adding an algorithm means registering a config class and a model class, not changing the training script.25 - The engine reads the checkpoint, not a flag. vLLM's
SpeculatorsConfig.build_vllm_speculative_configderives{"method": speculators_model_type, "num_speculative_tokens": proposal_methods[0].speculative_tokens}straight fromconfig.json, sovllm serve RedHatAI/Qwen3-8B-speculator.eagle3needs no speculative flags at all.21 - Provenance is in the artifact.
speculators_versionis stamped into every config (0.2.0.dev11on the Llama-3.1-8B and Qwen3-8B EAGLE-3 checkpoints,0.5.0on the Qwen3-8B DFlash one), which is the only reliable way to tell which defaults were in force when a checkpoint was written.3 - Third-party checkpoints convert in.
convert_model()ingests EAGLE-3 checkpoints fromSafeAILab/EAGLE, native MTP layers, and z-lab DFlash checkpoints, emitting the speculators format.5
When to use it (and when not)¶
- Use it when a draft model has to be handed to a serving team as an artifact: the verifier binding, draft vocabulary mapping, proposal length, and algorithm are all inside the file, so the deployment cannot silently pair the wrong draft with the wrong target.
- Use it when a pre-trained speculator already exists for the target. The RedHatAI collection covers Llama 3.1/3.3, Qwen3, Llama-4-Maverick, gpt-oss, and gemma-4 verifiers; training from scratch requires extracting hidden states from the target with vLLM first, which is a multi-GPU data-generation job before any training starts.1316
- Do not use it to decide whether to speculate. That is a latency and cost question answered in speculative decoding economics, and a workload question answered in evaluating speculative decoding.
- Do not use it for n-gram or prompt-lookup drafting: those have no draft model and therefore no checkpoint (speculative decoding).
- MTP is a different shape. The decision guide notes MTP "does not train from scratch" and is "only available for models with native MTP support", so it finetunes an existing head rather than producing an independent draft.15
Architecture¶
The draft is not a standalone language model. It consumes hidden states from named layers of the target, which means the target must be instrumented to emit them, in training and in serving alike. Training therefore runs a vLLM server of the target purely as a hidden-state source (scripts/launch_vllm.py), and serving re-establishes the same tap inside the engine.
flowchart TD
T["Target model (verifier)"] --> LV["launch_vllm.py: tap aux layers<br/>default (2, L//2, L-3) + last layer"]
LV --> HS["hs_i.safetensors hidden states<br/>(offline cache) or streamed (online)"]
PD["prepare_data.py: tokenize,<br/>loss mask, token_freq.pt"] --> HS
PD --> VM["t2d / d2t vocab mapping<br/>(draft_vocab_size most frequent tokens)"]
HS --> TR["train.py: --speculator-type eagle3 | dflash | dspark | peagle | mtp"]
VM --> TR
EXT["External checkpoint<br/>(EAGLE-3, z-lab DFlash, native MTP)"] --> CV["convert_model()"]
CV --> CK
TR --> CK["config.json + model.safetensors<br/>speculators_model_type, speculators_config, transformer_layer_config"]
CK --> VL["vLLM SpeculatorsConfig.from_pretrained"]
VL --> SPEC["speculative_config: method + num_speculative_tokens"]
VL --> ARCH["architectures rewritten to<br/>Eagle3LlamaForCausalLM / DFlashDraftModel"]
SPEC --> SRV["vllm serve: draft proposes, target verifies"]
ARCH --> SRV
T --> SRV
What the config actually contains¶
The table below is read from the two checkpoints fetched on 2026-08-26, with the field definitions from src/speculators/models/{eagle3,dflash}/config.py at 0faffeb.
| Field | Qwen3-8B-speculator.eagle3 |
gemma-4-31B-it-speculator.dflash |
Meaning |
|---|---|---|---|
speculators_model_type |
eagle3 |
dflash |
Registry discriminator; vLLM's method |
speculators_version |
0.2.0.dev11 |
0.5.0.dev53 |
Which library defaults applied |
draft_vocab_size |
32000 |
32000 |
Rows of lm_head; size of d2t |
transformer_layer_config.vocab_size |
151936 |
262144 |
Target vocabulary; size of t2d |
transformer_layer_config.hidden_size |
4096 |
5376 |
Must equal the target's hidden size |
transformer_layer_config.num_hidden_layers |
1 |
5 |
Draft depth (the draft's own choice) |
transformer_layer_config.model_type |
llama |
llama |
Decoder family used to build layers |
| aux layer ids | absent | [1, 17, 29, 47, 58] |
Which target layers feed the FC projection |
norm_before_residual |
true |
n/a | EAGLE-3 residual placement |
block_size |
n/a | 8 |
DFlash block width |
sample_from_anchor |
n/a | absent (defaults false) |
Anchor is a bonus token, so block_size - 1 drafts |
proposal_methods[0] |
greedy, speculative_tokens: 3 |
greedy, speculative_tokens: 8 |
Proposal length the engine will use |
speculators_config.verifier |
Qwen/Qwen3-8B, ["Qwen3ForCausalLM"] |
google/gemma-4-31B-it, [] |
Target binding |
Only one proposal method exists in the library: GreedyTokenProposalConfig, with speculative_tokens (default 5), verifier_accept_k (default 1), and accept_tolerance (default 0.0).4 Both non-default knobs are explicitly lossy. verifier_accept_k: 5 accepts a draft token that merely lands in the verifier's top 5, and accept_tolerance bounds how far in log likelihood it may sit from the top token; the field description claims "Values up to 5 have shown to minimally impact accuracy", with no citation in the source.4 Every published checkpoint inspected here ships verifier_accept_k: 1, accept_tolerance: 0.0, which is the distribution-preserving setting.
EAGLE-3 and DFlash as configurations¶
Both algorithms sit in the same format and differ in three concrete places: how many target layers they consume, whether drafting is a chain or a block, and how the first draft layer is wired.
- EAGLE-3 concatenates auxiliary hidden states from selected target layers, projects them through
fc, and drafts autoregressively.13 The first decoder layer is patched soq_proj,k_proj, andv_projtake2 * hidden_sizeinputs, because the layer receivescat([embeds, hidden]).10 Onlyllamaandqwen3decoder families are implemented;model_classes[tl_config.model_type]raisesKeyErrorfor anything else, and vLLM raisesValueError: Unsupported model_type ... for Eagle3 speculatorfor the same set.1021 - DFlash predicts a whole block in one forward pass using a non-causal mask over verifier hidden states and mask-token embeddings.14 It always builds Qwen3-style decoder layers regardless of what
transformer_layer_config.model_typesays. The gemma DFlash checkpoint proves it:model_typeisllama, yet the safetensors index containslayers.N.self_attn.{q,k}_norm.weightof shape[256], which is the Qwen3 per-head RMSNorm that Llama layers do not have.3 The training docs say the same: "DFlash always uses a Qwen3-style decoder regardless".17 - What must match the target.
scripts/train.py:create_transformer_layer_configcopiesvocab_size,hidden_size,num_attention_heads,num_key_value_heads,head_dim,max_position_embeddings,initializer_range,rms_norm_eps, and the rope parameters from the verifier config. Onlynum_hidden_layers,intermediate_size, the sliding-window layout, and the decoder family are the draft's own choices. The--draft-configdocumentation states the constraint bluntly: "The drafthidden_sizemust match the verifier (mismatch is not yet supported)."17 - Which target layers. When aux ids are unset, both sides independently default to
(2, num_layers // 2, num_layers - 3):resolve_target_layer_idsin speculators emits it with a warning, and vLLM'sget_eagle3_default_aux_hidden_state_layersreturns exactly the same tuple.1922 That agreement is why the Qwen3-8B EAGLE-3 checkpoint works despite recording no aux ids at all, and it is also the trap: a draft trained with custom--target-layer-idswhose checkpoint omits them will be served against the default layers with no error. - Layer-index skew. For
dflashanddspark, vLLM rewrites the ids:"target_layer_ids": [i - 1 for i in aux_layer_ids], with the comment "DFlash configs use different indexing for the target layers, see #40727". EAGLE-3 ids pass through unchanged.21 The same integer therefore means different layers depending onspeculators_model_type.
Validated: the format contract, vocabulary pruning, and the serve decision¶
The block below is executed and asserted. It encodes the shape rules read out of the two repositories, runs them against three real published checkpoints (one of which fails), then quantifies the one acceptance effect that is specific to this format: the pruned draft vocabulary carried by t2d/d2t. The general accept/reject proof is not repeated here; it lives in speculative decoding, and the walltime theorem in speculative decoding economics. Run: python3 speculators-draft-model-format.py.
# Runnable on system python3 (numpy). Validates the speculators checkpoint format itself:
# (1) the config.json <-> safetensors shape contract that decides whether a draft loads
# against its target at all, exercised on three REAL published checkpoints;
# (2) what a pruned draft vocabulary (the t2d/d2t buffers) costs in acceptance, and what it
# does NOT cost (correctness);
# (3) whether the checkpoint's declared speculative_tokens is worth serving.
# Checkpoint numbers are the values fetched on 2026-08-26 from each repo's config.json and
# the safetensors header (first 8 bytes = header length, then the JSON tensor index).
import numpy as np
# ---------------------------------------------------------------- 1. shape contract
CKPTS = {
"RedHatAI/Qwen3-8B-speculator.eagle3": dict(
model_type="eagle3", hidden=4096, verifier_vocab=151936, draft_vocab=32000,
draft_layers=1, tlc_model_type="llama", aux_ids=None, spec_tokens=3,
block_size=None, sample_from_anchor=None,
shapes={"fc.weight": (4096, 12288), "lm_head.weight": (32000, 4096),
"embed_tokens.weight": (151936, 4096), "t2d": (151936,), "d2t": (32000,),
"layers.0.self_attn.q_proj.weight": (4096, 8192)}),
"RedHatAI/Qwen3-8B-speculator.dflash": dict(
model_type="dflash", hidden=4096, verifier_vocab=151936, draft_vocab=32000,
draft_layers=5, tlc_model_type="qwen3", aux_ids=[2, 10, 18, 26, 34], spec_tokens=7,
block_size=8, sample_from_anchor=None,
shapes={"fc.weight": (4096, 20480), "lm_head.weight": (32000, 4096),
"embed_tokens.weight": (151936, 4096), "t2d": (151936,), "d2t": (32000,),
"layers.0.self_attn.q_proj.weight": (4096, 4096)}),
"RedHatAI/gemma-4-31B-it-speculator.dflash": dict(
model_type="dflash", hidden=5376, verifier_vocab=262144, draft_vocab=32000,
draft_layers=5, tlc_model_type="llama", aux_ids=[1, 17, 29, 47, 58], spec_tokens=8,
block_size=8, sample_from_anchor=None,
shapes={"fc.weight": (5376, 26880), "lm_head.weight": (32000, 5376),
"embed_tokens.weight": (262144, 5376), "t2d": (262144,), "d2t": (32000,),
"layers.0.self_attn.q_proj.weight": (8192, 5376)}),
}
def audit(name, c):
"""Every rule below is read off speculators @0faffeb or vLLM @61d4f56, not invented."""
bad = []
s = c["shapes"]
# models/eagle3/core.py: num_aux = len(eagle_aux_hidden_state_layer_ids) or 3;
# fc = Linear(num_aux * hidden_size, hidden_size). vLLM llama_eagle3.py mirrors it.
num_aux = len(c["aux_ids"]) if c["aux_ids"] else 3
if s["fc.weight"][1] != num_aux * c["hidden"]:
bad.append(f"fc in_features {s['fc.weight'][1]} != {num_aux} aux x {c['hidden']}")
# model.py DraftVocabMixin._init_vocab: lm_head is hidden -> draft_vocab_size,
# embed_tokens is verifier_vocab_size rows, t2d/d2t sized by the two vocabularies.
if s["lm_head.weight"] != (c["draft_vocab"], c["hidden"]):
bad.append(f"lm_head {s['lm_head.weight']} != {(c['draft_vocab'], c['hidden'])}")
if s["embed_tokens.weight"][0] != c["verifier_vocab"]:
bad.append("embed_tokens rows != verifier vocab_size")
if s["t2d"][0] != c["verifier_vocab"] or s["d2t"][0] != c["draft_vocab"]:
bad.append("t2d/d2t length does not match (verifier_vocab, draft_vocab)")
# eagle3 patches q/k/v to take cat([embeds, hidden]) => 2x hidden in_features
# (models/eagle3/model_definitions.py _patch_eagle3_projections). dflash does not.
want = 2 * c["hidden"] if c["model_type"] == "eagle3" else c["hidden"]
if s["layers.0.self_attn.q_proj.weight"][1] != want:
bad.append(f"q_proj in_features {s['layers.0.self_attn.q_proj.weight'][1]} != {want}")
# models/dflash/core.py:212 speculative_tokens = block_size - (0 if sample_from_anchor else 1)
if c["model_type"] == "dflash":
anchor = bool(c["sample_from_anchor"]) # config default is False
want_tok = c["block_size"] if anchor else c["block_size"] - 1
if c["spec_tokens"] != want_tok:
bad.append(f"speculative_tokens {c['spec_tokens']} != block_size-{0 if anchor else 1}"
f" = {want_tok} (sample_from_anchor={anchor})")
# vllm/.../speculators/algos.py update_eagle3 raises on any other draft model_type.
if c["model_type"] == "eagle3" and c["tlc_model_type"] not in ("llama", "qwen3"):
bad.append(f"vLLM cannot dispatch eagle3 draft model_type {c['tlc_model_type']}")
if bad:
raise ValueError(f"{name}: " + "; ".join(bad))
return num_aux
assert audit("eagle3-qwen3", CKPTS["RedHatAI/Qwen3-8B-speculator.eagle3"]) == 3
assert audit("dflash-qwen3", CKPTS["RedHatAI/Qwen3-8B-speculator.dflash"]) == 5
# FAILURE CASE, on a real published checkpoint: gemma dflash declares 8 speculative tokens
# with block_size 8 and no sample_from_anchor key, which the current rule makes 7.
try:
audit("dflash-gemma", CKPTS["RedHatAI/gemma-4-31B-it-speculator.dflash"])
raise AssertionError("gemma dflash should not pass the audit")
except ValueError as e:
assert "speculative_tokens 8 != block_size-1 = 7" in str(e), e
gemma_defect = str(e)
# Shape drift is caught, not silently absorbed: swap in a 3-aux fc for a 5-aux config.
broken = {**CKPTS["RedHatAI/Qwen3-8B-speculator.dflash"]}
broken["shapes"] = {**broken["shapes"], "fc.weight": (4096, 12288)}
try:
audit("aux-drift", broken)
raise AssertionError("aux-count drift must be rejected")
except ValueError as e:
assert "fc in_features 12288 != 5 aux x 4096" in str(e)
# ------------------------------------------------- 2. what draft-vocab pruning costs
# The format ships t2d (bool, verifier_vocab) and d2t (int64, draft_vocab): the draft can
# only ever propose the draft_vocab_size tokens t2d selects. Acceptance is bounded by the
# target mass inside that set; correctness is not.
rng = np.random.default_rng(7)
V, D = 2000, 1200
p = rng.random(V) + 0.02
p /= p.sum()
keep = np.zeros(V, bool)
keep[np.argsort(-p)[:D]] = True # t2d: keep the D most frequent tokens
oov = p[~keep].sum() # target mass the draft can never propose
q = np.where(keep, p, 0.0)
q = q / q.sum() # best possible pruned draft: p restricted to t2d
def spec_step(p, q, n, rng):
"""One speculative position: propose from q, accept w.p. min(1, p/q), else residual."""
x = rng.choice(len(q), size=n, p=q)
acc = rng.random(n) < np.minimum(1.0, p[x] / np.maximum(q[x], 1e-300))
out = x.copy()
r = np.maximum(0.0, p - q)
r = r / r.sum() if r.sum() > 0 else p
nrej = int((~acc).sum())
if nrej:
out[~acc] = rng.choice(len(p), size=nrej, p=r)
return out, acc
N = 400_000
commits, acc = spec_step(p, q, N, rng)
emp = np.bincount(commits, minlength=V) / N
ceiling = np.minimum(p, q).sum()
assert abs(ceiling - (1.0 - oov)) < 1e-12, (ceiling, 1 - oov) # exact ceiling identity
assert abs(acc.mean() - ceiling) < 0.01, (acc.mean(), ceiling) # attained by the ideal draft
assert np.max(np.abs(emp - p)) < 0.01 # still lossless
# The tokens outside the draft vocab are still emitted, at their exact target rate.
assert abs(emp[~keep].sum() - oov) < 0.01, (emp[~keep].sum(), oov)
assert emp[~keep].sum() > 0.0
# FAILURE CASE: the "accept if the draft token is the target's argmax" shortcut. This is
# what verifier_accept_k=1 reads like if the engine compares top-1 instead of running the
# rejection rule; it is lossless only under greedy targets, and skews sampled output.
pn = np.array([0.5, 0.3, 0.2])
qn = np.array([0.4, 0.35, 0.25])
top = int(np.argmax(pn))
xs = rng.choice(3, size=N, p=qn)
naive = np.where(xs == top, xs, rng.choice(3, size=N, p=pn))
emp_naive = np.bincount(naive, minlength=3) / N
assert emp_naive[top] > pn[top] + 0.15, (emp_naive[top], pn[top]) # argmax over-represented
assert np.max(np.abs(emp_naive - pn)) > 0.15 # not the target distribution
# The same p, q under the rejection rule are lossless, so the skew is the rule's, not the draft's.
cn, an = spec_step(pn, qn, N, rng)
assert np.max(np.abs(np.bincount(cn, minlength=3) / N - pn)) < 0.01
# --------------------------------------- 3. is the declared speculative_tokens worth it
# General theorem lives in the economics page; this instantiates it for one checkpoint.
# Qwen3-8B eagle3 draft: 399,523,840 compute-relevant params (all tensors except the frozen
# embed_tokens copy and t2d/d2t) against Qwen3-8B's 8,190,735,360; bandwidth-bound proxy only.
c_draft = 399_523_840 / 8_190_735_360
gamma = 3 # the checkpoint's speculative_tokens
def block_eff(a, g):
return g + 1.0 if a == 1.0 else (1.0 - a ** (g + 1)) / (1.0 - a)
def speedup(a, g, c):
return block_eff(a, g) / (1.0 + g * c)
def breakeven(g, c):
lo, hi = 0.0, 1.0
for _ in range(200):
mid = (lo + hi) / 2
lo, hi = (mid, hi) if speedup(mid, g, c) < 1.0 else (lo, mid)
return (lo + hi) / 2
a_star = breakeven(gamma, c_draft)
assert abs(speedup(a_star, gamma, c_draft) - 1.0) < 1e-9 # crossover is exact
assert speedup(a_star - 0.02, gamma, c_draft) < 1.0 # below it, a net loss
assert speedup(a_star + 0.02, gamma, c_draft) > 1.0
assert abs(block_eff(0.0, gamma) - 1.0) < 1e-12 # only the bonus token
assert abs(block_eff(1.0, gamma) - 4.0) < 1e-12
# A 22% out-of-draft-vocab workload caps per-position acceptance at 0.78 (evaluation page's
# multilingual figure). Cheap draft: still far above break-even. Heavy draft: not.
assert speedup(0.78, gamma, c_draft) > 2.4
a_star_heavy = breakeven(gamma, 0.30)
assert a_star_heavy > 0.5 and speedup(0.50, gamma, 0.30) < 1.0
print("1. shape audit: eagle3-qwen3 num_aux=3 OK, dflash-qwen3 num_aux=5 OK")
print(f" rejected -> {gemma_defect}")
print(f"2. draft vocab {D}/{V}: out-of-draft target mass={oov:.4f}, "
f"acceptance ceiling={ceiling:.4f}, measured={acc.mean():.4f}, "
f"commit maxdev={np.max(np.abs(emp - p)):.4f}")
print(f" naive argmax-match rule on p={pn.tolist()}: p[top]={pn[top]:.2f} served as "
f"{emp_naive[top]:.4f}; rejection rule on the same pair stays within "
f"{np.max(np.abs(np.bincount(cn, minlength=3) / N - pn)):.4f}")
print(f"3. gamma={gamma}, draft/target cost={c_draft:.4f} -> break-even a*={a_star:.4f}, "
f"speedup at a=0.78 is {speedup(0.78, gamma, c_draft):.2f}x; "
f"a heavier draft (c=0.30) needs a*={a_star_heavy:.4f}")
Executed output:
1. shape audit: eagle3-qwen3 num_aux=3 OK, dflash-qwen3 num_aux=5 OK
rejected -> dflash-gemma: speculative_tokens 8 != block_size-1 = 7 (sample_from_anchor=False)
2. draft vocab 1200/2000: out-of-draft target mass=0.1708, acceptance ceiling=0.8292, measured=0.8298, commit maxdev=0.0002
naive argmax-match rule on p=[0.5, 0.3, 0.2]: p[top]=0.50 served as 0.7005; rejection rule on the same pair stays within 0.0012
3. gamma=3, draft/target cost=0.0488 -> break-even a*=0.1279, speedup at a=0.78 is 2.50x; a heavier draft (c=0.30) needs a*=0.5090
Three operational readings. First, the config-to-weights contract is checkable offline, from config.json plus the safetensors header alone, without loading a single tensor: fc.weight alone reveals how many target layers a checkpoint expects. Second, draft-vocabulary pruning costs acceptance and nothing else: the out-of-draft tokens are still emitted at their exact target rate through the residual, so a 32k draft vocab against a 151936-token target is a throughput decision, not a quality one. The acceptance ceiling equals one minus the out-of-draft target mass exactly, which is why the workload matters more than the checkpoint: evaluating speculative decoding reports roughly 22% of multilingual target tokens landing outside a 32k pruned vocabulary while Math and Coding are barely affected. Third, break-even acceptance for this checkpoint is around 0.13, far below any plausible workload, so a cheap EAGLE-3 draft is nearly always worth serving on latency grounds; the same is not true of a draft one third the target's cost, which needs better than 0.51.
How to use it¶
Serving needs no speculative flags: vLLM reads the checkpoint. The short form loads the target named in speculators_config.verifier.name_or_path.23
# Reference template (needs vLLM installed + a GPU). Not executed here.
# Short form: the checkpoint names its own verifier.
vllm serve RedHatAI/Qwen3-8B-speculator.eagle3
# Long form: pass the target first, override num_speculative_tokens, or pair the
# draft with a quantized target. Keys as documented at speculators 0faffeb.
vllm serve Qwen/Qwen3-8B \
-tp 1 \
--speculative-config '{
"model": "RedHatAI/Qwen3-8B-speculator.eagle3",
"num_speculative_tokens": 3,
"method": "eagle3"
}'
Converting a third-party checkpoint uses the Python API, which supports eagle3, mtp, and dflash.5
# Reference template (needs speculators + transformers installed). Not executed here.
from speculators.convert import convert_model
convert_model(
model="z-lab/Qwen3-8B-DFlash-b16",
verifier="Qwen/Qwen3-8B",
algorithm="dflash",
output_path="./converted",
)
The speculators convert CLI is not equivalent to that API. Its --algorithm option is click.Choice(["eagle", "eagle3", "mtp"]), so dflash is rejected at the CLI even though convert_model implements it; and eagle, which the CLI accepts and documents with two worked examples, has no branch in convert_model and falls through to raise ValueError(f"Unsupported algorithm: {algorithm}").65 Prefer the Python API until that is reconciled.
Loading in Python resolves the subclass from speculators_model_type automatically, and SpeculatorModel.from_pretrained will auto-convert an external DFlash checkpoint if given a verifier=; any other unrecognised external format raises NotImplementedError.75
How to develop with it¶
Training runs from scripts/train.py, single-GPU or under torchrun, with --fsdp-shard for parameter sharding.17 The load-bearing choices:
--speculator-typeselects the algorithm (eagle3,dflash,dspark,peagle,mtp), and several defaults key off it insideTrainConfig._resolve_derived_defaults:draft_archbecomesllamafor eagle3 andqwen3otherwise,num_layersbecomes 5 for the DFlash family and 1 otherwise,loss_fnbecomescefor dflash andkl_divotherwise,block_sizebecomes 16 for dflash and 8 for dspark, andper_position_loss_weightbecomesdpacefor dflash.18- Hidden states come from the target, not the draft. Run
scripts/launch_vllm.py <target>first. If custom--target-layer-idsare passed there, the same ids minus the appended final layer must be passed totrain.py, becauselaunch_vllm.pyappendsnum_hidden_layerswhen--include-last-layeris on (the default) and training takes only the auxiliary layers.20 --on-missing generate --on-generate deleteis online training;--on-missing raisewith a populated--hidden-states-pathis offline;--on-generate cacheis the hybrid that generates in epoch one and reuses afterwards.17--dry-runbuilds the speculator, initialises weights, writes a checkpoint, and exits before training, specifically so the artifact can be validated in vLLM before a full run is launched; the same directory feeds back through--from-pretrained.17- Adding an algorithm means a directory under
src/speculators/models/, a config class decorated@SpeculatorModelConfig.register("myalgo"), a model class decorated with the matching@SpeculatorModel.register, training factory classmethods, and CLI arguments; the registry means the training script never needs to learn about the new type.25
How to maintain it¶
- Pin what wrote the checkpoint.
speculators_versionis stamped automatically from the installed package. Treat it as the key fact when a checkpoint behaves differently from a fresh one:0.2.0.dev11and0.5.0checkpoints were written under different algorithm defaults.32 - Re-derive defaults, do not remember them.
_resolve_derived_defaultschanged the DFlash out-of-the-box recipe to 5 layers, D-PACE, cross-entropy, andblock_size=16on the basis of issue #979, while DSpark keptblock_size=8because "that combination was never tested there".18 Any DFlash checkpoint older than that change was trained under a different recipe. - Record provenance around the run.
speculators/provenance.pycollects the command line, the git SHA andgit diff HEADof an editable install, and versions ofspeculators,vllm,transformers,torch, andcompressed-tensors. Keep that alongside the checkpoint; the checkpoint itself records only the speculators version.26 - Re-check the checkpoint after every library bump, using the offline audit in the executed block. A
config.jsonwritten by an older version can carry field values the current code would never produce. - Verify the verifier binding is populated.
VerifierConfig.architecturesexists "to validate architecture compatibility of different verifiers with the speculator", but both RedHatAI DFlash checkpoints inspected here ship"architectures": [], so the check has nothing to compare against.23
How to run it in production¶
- Measure acceptance on the real workload before committing.
scripts/evaluate/evaluate.py sweep --target http://localhost:8000/v1runs the nineRedHatAI/speculator_benchmarkssubsets and writesperf_results.csv;evaluate.py throughputreports acceptance only. SPEED-Bench categories are also supported via--dataset speedbench/<config> --speedbench-data-dir.24 Method selection and interpretation belong to evaluating speculative decoding. - Do not assume the number in
config.jsonis optimal.num_speculative_tokensin the long-form--speculative-configoverridesproposal_methods[0].speculative_tokens. The checkpoint's value is the training-time--ttt-stepsfor EAGLE-3 (default 3) orblock_size - 1for DFlash, neither of which is a serving measurement.1112 - Match
sample_from_anchorbetween checkpoint and engine. The DFlash page is explicit: "When deploying to inference engines, ensure the engine'ssample_from_anchorsetting matches your model's config", because the flag changes both which slots were trained and how predictions are harvested.14 - Do not serve
--sliding-window-non-causalcheckpoints yet. The training docs state "vLLM currently doesn't support these models".17 - Pair the draft with a quantized target deliberately. The serving tutorial presents combining a quantized target with an unquantized speculator as a supported configuration, and the decision guide states the draft architecture is independent of the verifier architecture including quantized variants.2315 Neither claim is accompanied by a measurement in the repository.
- DFlash maturity. The DFlash page carries its own caveat: "DFlash is under active development. Not all hardware configurations have been validated yet".14
Failure modes¶
- Aux-layer count drift.
fcisLinear(num_aux * hidden_size, hidden_size), withnum_auxderived from the recorded ids or defaulting to 3.11 A checkpoint trained on five aux layers cannot load against a config claiming three; the executed block rejects exactly this. - Aux-layer identity drift, which is silent. Neither the count nor the shapes change if the same number of different layers is tapped. A checkpoint that omits its aux ids is served against
(2, L//2, L-3)by vLLM regardless of what it was trained on, with no warning at serve time.22 norm_before_residualdefault disagreement. The speculators field defaults toFalse; vLLM'supdate_eagle3readsconfig_dict.get("norm_before_residual", True).821 A checkpoint that omits the key is built one way by speculators and another by vLLM, with no shape change and therefore no error. All three RedHatAI EAGLE-3 checkpoints inspected here set it explicitly totrue, so this bites hand-written or third-party configs, not the published ones.sliding_window_non_causaldefault disagreement. Same pattern, opposite direction: the speculators field defaults toFalse, while vLLM computes"causal": not config_dict.get("sliding_window_non_causal", True), so an omitted key yields non-causal masking in the engine.921- DFlash layer-index off-by-one. vLLM subtracts one from every id for
dflashanddsparkbut not foreagle3.21 Copying aux ids between algorithms shifts the tap by one layer. - A published checkpoint that fails its own rule.
RedHatAI/gemma-4-31B-it-speculator.dflashdeclaresspeculative_tokens: 8withblock_size: 8and nosample_from_anchor, where the current code emitsblock_size - 1 = 7; its Qwen3-8B sibling correctly declares 7.312 Which value the engine honours was not tested here. - Unloaded weights are NaN, not zero.
_init_vocabinitialiseslm_head,embed_tokens, andverifier_lm_headto NaN deliberately "so it's easy to detect if they're never loaded".7 A NaN-producing draft is a load-path bug, not a training bug. t2dmust be loaded before verifier weights.load_verifier_weightsraisesValueErrorift2dis unset whileuse_draft_vocabis true, because it slices the verifier LM head by that mask.7- Lossy proposal knobs shipped as config.
verifier_accept_k > 1oraccept_tolerance > 0stop preserving the target distribution; the executed block shows what a top-1-match rule does to a sampled output stream.4 - CLI and API disagree on conversion.
speculators convert --algorithm dflashis rejected by the CLI;--algorithm eagleis accepted and then raisesValueError: Unsupported algorithm: eagle.65
References¶
- speculators repository (read at
0faffeb3bd547b4451a978d7aaf26a2f01b83d62, 2026-08-25): https://github.com/vllm-project/speculators - speculators EAGLE-3 algorithm page (fetched 2026-08-26): https://docs.vllm.ai/projects/speculators/en/latest/user_guide/algorithms/eagle3
- speculators DFlash algorithm page (fetched 2026-08-26): https://docs.vllm.ai/projects/speculators/en/latest/user_guide/algorithms/dflash
- speculators algorithm decision guide: https://docs.vllm.ai/projects/speculators/en/latest/user_guide/algorithms/decision_guide
- speculators train.py CLI reference: https://docs.vllm.ai/projects/speculators/en/latest/cli/train
- speculators launch_vllm.py CLI reference: https://docs.vllm.ai/projects/speculators/en/latest/cli/launch_vllm
- speculators "Serve in vLLM" tutorial: https://docs.vllm.ai/projects/speculators/en/latest/user_guide/tutorials/serve_vllm
- vLLM speculators config loader (read at
61d4f56635e0f2faae4bd60e6da88a53c9d3affb, 2026-08-26): https://github.com/vllm-project/vllm/blob/main/vllm/transformers_utils/configs/speculators/algos.py - RedHatAI speculator model collection: https://huggingface.co/collections/RedHatAI/speculator-models
RedHatAI/Qwen3-8B-speculator.eagle3(config.json and safetensors index fetched 2026-08-26): https://huggingface.co/RedHatAI/Qwen3-8B-speculator.eagle3RedHatAI/gemma-4-31B-it-speculator.dflash: https://huggingface.co/RedHatAI/gemma-4-31B-it-speculator.dflash- EAGLE paper (cited by the speculators EAGLE-3 page): https://arxiv.org/abs/2401.15077
- DFlash paper (cited by the speculators DFlash page): https://arxiv.org/abs/2602.06036
Related: Speculative decoding · Speculative decoding economics · Evaluating speculative decoding · DSpark speculative decoding · Inference serving · Serving OSS models · Glossary
-
README.mdat0faffeb, Overview section: "Speculators is a library for training speculative decoding draft models that deploy directly to LLM inference engines like vLLM." ↩ -
src/speculators/config.pyat0faffeb:VerifierConfig(name_or_path,architectures, the latter described as used "to validate architecture compatibility of different verifiers with the speculator, if needed"),SpeculatorsConfig(algorithm,proposal_methods,default_proposal_method,verifier, with a validator requiringdefault_proposal_methodto match one configuredproposal_type), andSpeculatorModelConfig(speculators_model_type,speculators_versiondefaulting toversion("speculators"),speculators_config;auto_package = "speculators.models",schema_discriminator = "speculators_model_type";from_pretrainedraisesNotImplementedErrorandfrom_dictraisesValueErrorwhenspeculators_model_typeis missing). ↩↩↩↩↩ -
Fetched 2026-08-26.
config.jsonviahttps://huggingface.co/<id>/raw/main/config.jsonforRedHatAI/Qwen3-8B-speculator.eagle3,RedHatAI/Llama-3.1-8B-Instruct-speculator.eagle3,RedHatAI/gpt-oss-20b-speculator.eagle3,RedHatAI/Qwen3-8B-speculator.dflash,RedHatAI/gemma-4-31B-it-speculator.dflash; safetensors tensor index via HTTP range requests on the first bytes ofmodel.safetensorsfor the first, fourth, and fifth. File listings viahttps://huggingface.co/api/models/<id>. The gemma DFlash index containslayers.{0..4}.self_attn.q_norm.weightandk_norm.weightof shape[256]despitetransformer_layer_config.model_type == "llama". ↩↩↩↩↩↩ -
src/speculators/proposals/greedy.pyat0faffeb:GreedyTokenProposalConfigwithproposal_type: Literal["greedy"],speculative_tokens(default 5,ge=1),verifier_accept_k(default 1,ge=1),accept_tolerance(default 0.0,ge=0.0, description ends "Values up to 5 have shown to minimally impact accuracy."). It is the onlyTokenProposalConfigsubclass exported bysrc/speculators/proposals/__init__.py. ↩↩↩ -
src/speculators/convert/entrypoints.pyat0faffeb:convert_model(model, verifier, algorithm: Literal["eagle3", "mtp", "dflash"], output_path="converted", validate_device=None, **kwargs)with branches foreagle3,mtp,dflashand a finalraise ValueError(f"Unsupported algorithm: {algorithm}").maybe_convert_external_checkpointreturns the path unchanged whenspeculators_model_typeis present, and otherwise only recognises DFlash ("dflash_config" in config_dict or any("DFlash" in a for a in architectures)), raisingNotImplementedErrorfor anything else andValueErrorif no verifier was supplied. ↩↩↩↩↩ -
src/speculators/__main__.pyat0faffeb: theconvertcommand declaresclick_type=click.Choice(["eagle", "eagle3", "mtp"])and its docstring gives two--algorithm eagleexamples, whileconvert_model(see above) has noeaglebranch and does have adflashone. Reported here as a docs/code and CLI/API divergence found at this commit, not as a fixed issue. ↩↩ -
src/speculators/model.pyat0faffeb:DraftVocabMixin._init_vocabsetsuse_draft_vocab = draft_vocab_size != verifier_vocab_size, registerst2d(bool, verifier vocab) andd2t(long, draft vocab), and initialiseslm_head,embed_tokens,verifier_lm_headwithtorch.nn.init.constant_(..., torch.nan)under the comment "Initialize weights to nan so it's easy to detect if they're never loaded".load_vocab_mappingsvalidatest2d.shape[0] == verifier_vocab_size,t2d.sum() == draft_vocab_size, andd2t.shape[0] == draft_vocab_size.load_verifier_weightsraisesValueError("t2d tensor hasn't been set...") whenuse_draft_vocabis true andt2dis unset.SpeculatorModel.from_pretrainedresolves the subclass viaregistered_model_class_from_config. ↩↩↩ -
src/speculators/models/eagle3/config.pyat0faffeb:Eagle3SpeculatorConfigfieldsdraft_vocab_size(default 32000),norm_before_residual(defaultFalse),target_hidden_size,eagle_aux_hidden_state_layer_ids,norm_before_fc(defaultFalse),fc_norm(defaultFalse, mutually exclusive withnorm_before_fc),norm_output(defaultFalse),embed_requires_grad(defaultFalse);architecturesdefaults to["Eagle3Speculator"]andtransformer_layer_configdefaults toQwen3Config. ↩ -
src/speculators/models/dflash/config.pyat0faffeb:DFlashSpeculatorConfigfieldsdraft_vocab_size(default 32000),block_size(default 8),target_hidden_size,aux_hidden_state_layer_ids,mask_token_id,sliding_window_non_causal(defaultFalse),sample_from_anchor(defaultFalse);architecturesdefaults to["DFlashSpeculator"]. Two internal inconsistencies at this commit: the class docstring says "vocabulary mapping between draft (64K) and target (128K)" whiledraft_vocab_sizedefaults to 32000, and the registered model class isDFlashDraftModel(src/speculators/models/dflash/core.py:38), which is also what the published checkpoints declare, notDFlashSpeculator. ↩ -
src/speculators/models/eagle3/model_definitions.pyat0faffeb:Eagle3FirstLayerMixin._patch_eagle3_projectionsreplacesq_proj,k_proj,v_projwith2 * config.hidden_sizeinput features, andforwardsplits the incomingcat([embeds, hidden])at the midpoint.model_classeshas exactly two entries,"llama"and"qwen3";src/speculators/models/eagle3/core.py:65indexes it withtl_config.model_type. ↩↩ -
src/speculators/models/eagle3/core.pyat0faffeb:num_aux = len(config.eagle_aux_hidden_state_layer_ids) if config.eagle_aux_hidden_state_layer_ids else 3;self.fc = torch.nn.Linear(num_aux * self.hidden_size, self.hidden_size, bias=False).Eagle3DraftModel.from_training_argsbuildsGreedyTokenProposalConfig(speculative_tokens=kwargs["ttt_steps"])and setstransformer_layer_configfrom the verifier config. ↩↩ -
src/speculators/models/dflash/core.py:212at0faffeb:speculative_tokens = block_size if sample_from_anchor else block_size - 1, feedingGreedyTokenProposalConfig(speculative_tokens=speculative_tokens). The same method sets"transformer_layer_config": verifier_configand takesblock_sizefromkwargs.get("block_size", 8). ↩↩ -
docs/user_guide/algorithms/eagle3.mdat0faffeb, identical to the fetched page: "The target model produces hidden states at selected layers, which are concatenated and projected through an FC layer alongside token embeddings. These pass through Llama-style decoder layers (default: 1) and an LM head to produce draft logits." Note the divergence: the doc says "Llama-style", the training default--draft-archisllamafor eagle3, butEagle3SpeculatorConfig.transformer_layer_configdefaults toQwen3Configand the code supports both families. The page lists pretrained speculators forQwen/Qwen3-8B,meta-llama/Llama-4-Maverick-17B-128E-Instruct,openai/gpt-oss-20b, andgoogle/gemma-4-31B-it. ↩↩ -
docs/user_guide/algorithms/dflash.mdat0faffeb, identical to the fetched page: "This block-parallel approach can yield 2--3x larger speedups than Eagle-3 on synchronous requests" (a project claim, with no benchmark on the page and none reproduced here); "The draft model uses Qwen3-style transformer layers but can be paired with any supported verifier"; thesample_from_anchorsemantics ("Producesblock_size - 1speculative tokens" when false,block_sizewhen true); "When deploying to inference engines, ensure the engine'ssample_from_anchorsetting matches your model's config"; and "DFlash is under active development. Not all hardware configurations have been validated yet". Its pretrained table lists onlygoogle/gemma-4-31B-it, whiledocs/index.mdat the same commit also links aRedHatAI/Qwen3-8B-speculator.dflash. ↩↩↩ -
docs/user_guide/algorithms/decision_guide.mdat0faffeb: five algorithms, "All are lossless"; the support table gives draft layers as Llama-style for Eagle-3 and P-EAGLE, Qwen3-style for DFlash and DSpark, native MTP layers for MTP; "Eagle-3, P-EAGLE, DFlash, and DSpark can be paired with any supported verifier model (including quantized variants)"; MTP "does not train from scratch". Note thatdocs/user_guide/getting_started.mdat the same commit lists only Eagle-3 and DFlash under "Supported Algorithms". ↩↩ -
docs/user_guide/getting_started.mdat0faffeb: "Training requires internal hidden states from the target model, which are extracted by serving the target model with vLLM", with online, offline, and hybrid modes described. ↩ -
docs/cli/train.mdat0faffeb:--speculator-typeoptions;--draft-archdefaultllamawith "DFlash always uses a Qwen3-style decoder regardless";--draft-configwith "The drafthidden_sizemust match the verifier (mismatch is not yet supported)";--dry-run;--on-missing/--on-generate;--ttt-stepsdefault 3;--block-sizedefault 16 for dflash and 8 for dspark;--sliding-window-non-causalwith "Note: vLLM currently doesn't support these models". One doc/code contradiction found at this commit: the page gives--max-anchorsdefault 3072, whilesrc/speculators/train/config/schema.py:229setsdefault=512(the DSpark model code separately uses 3072 as its own fallback insrc/speculators/models/dspark/core.py). ↩↩↩↩↩↩ -
src/speculators/train/config/schema.pyat0faffeb,TrainConfig._resolve_derived_defaults:draft_arch->llamaif eagle3 elseqwen3;norm_before_fcandnorm_output->is_eagle3;muon_lr->10 * lr;num_layers-> 5 for{dflash, dspark, dflash2}else 1;per_position_loss_weight->dpaceif dflash elsefixed-exp-decay;loss_fn->ceif dflash elsekl_div;block_size-> 16 if dflash else 8. The docstring attributes the DFlash recipe to https://github.com/vllm-project/speculators/issues/979 and states DSpark "keepsblock_size=8, since that combination was never tested there".norm_before_residualdefaults toTruein this training schema, againstFalseinEagle3SpeculatorConfig. ↩↩ -
src/speculators/models/utils.pyat0faffeb,resolve_target_layer_ids: returns the caller's ids when given, else[2, num_layers // 2, num_layers - 3]with a warning. ↩ -
docs/cli/launch_vllm.mdat0faffeb:--target-layer-idsdefault[2, num_layers//2, num_layers-3];--include-last-layerdefaultTrueappendsnum_hidden_layers; "If set, you must also pass the same layer ids to the training script using--target-layer-ids, excluding the final layer". ↩ -
vllm/transformers_utils/configs/speculators/algos.pyinvllm-project/vllmmainat61d4f56(file last modified by2f55ef254c70e110d637beeedf48238977ebb683, 2026-08-22), fetched 2026-08-26.update_eagle3:pre_trained_config["norm_before_residual"] = config_dict.get("norm_before_residual", True);eagle3_arch_map = {"qwen3": "Eagle3Qwen3ForCausalLM", "llama": "Eagle3LlamaForCausalLM"}withraise ValueError(f"Unsupported model_type {model_type} for Eagle3 speculator")otherwise; aux ids copied onlyif config_dict.get("eagle_aux_hidden_state_layer_ids").update_dflash:pre_trained_config["architectures"] = ["DFlashDraftModel"];"target_layer_ids": [i - 1 for i in aux_layer_ids]under the comment "DFlash configs use different indexing for the target layers, see #40727";"causal": not config_dict.get("sliding_window_non_causal", True).base.pybuild_vllm_speculative_configreturns{"method": config_dict.get("speculators_model_type"), "num_speculative_tokens": proposal_methods[0]["speculative_tokens"]}, remappingpeagletoeagle3withparallel_drafting: True. ↩↩↩↩↩↩ -
vllm/model_executor/models/interfaces.pyinvllm-project/vllmmainat61d4f56, fetched 2026-08-26:get_eagle3_default_aux_hidden_state_layersreturns(2, num_layers // 2, num_layers - 3).vllm/model_executor/models/llama_eagle3.pyat the same commit:self.num_aux_hidden_states = len(layer_ids) if layer_ids else 3andself.fc_input_size = target_hidden_size * self.num_aux_hidden_states. ↩↩ -
docs/user_guide/tutorials/serve_vllm.mdat0faffeb: the shortvllm serve ./checkpoints/checkpoint_bestform ("vLLM automatically recognizes thespeculators_configin your model and enables speculative decoding"), and the long form with--speculative-config '{"model": ..., "num_speculative_tokens": 3, "method": "eagle3"}', described as the way "to use a different target model with the speculator ... combine a quantized target model, with a speculative decoding model". ↩↩ -
docs/user_guide/tutorials/evaluating_performance.mdat0faffeb:python evaluate.py sweep --target http://localhost:8000/v1over "all 9 subsets fromRedHatAI/speculator_benchmarks" producingperf_results.csv;evaluate.py throughputfor acceptance rates only; SPEED-Bench configsqualitative,throughput_1k,throughput_2k,throughput_8k,throughput_32kvia--speedbench-data-dir. ↩ -
docs/developer/add_algorithm.mdat0faffeb: create a module undersrc/speculators/models, a config class with@SpeculatorModelConfig.register("myalgo"), a model class with the matching register decorator, training factory classmethods, and CLI arguments; "the training script doesn't need to know about every algorithm and adding a new algorithm doesn't require modifying the training script". ↩↩ -
src/speculators/provenance.pyat0faffeb:TRACKED_PACKAGES = ("speculators", "vllm", "transformers", "torch", "compressed-tensors"), plusgit_sha,git_diff, andatomic_writehelpers, described as "Used by training, evaluation, and vLLM-launch scripts to record command lines, git state, and package versions." ↩