Adding Speculative Decoding
SkillDev toolsGuides your agent through adding new speculative decoding proposers to rapid_llm while keeping outputs lossless.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Adding Speculative Decoding skill
About this capability
Extend rapid_llm's speculative decoding beyond the shipped O5 n-gram proposer, the proposer interface and registry, single-pass verification, sampled-mode rejection sampling, KV budget and rollback, and the four high-risk coupling points (KV rollback, batch-state divergence, CUDA Graph, sampler RNG
What this skill tells your AI
The instructions your AI receives, as published by harleyszhang/rapid_llm in .claude/skills/add-speculative-decoding/SKILL.md and read by ahel’s review.
rapid_llm already ships O5: an n-gram proposer with greedy single-pass
verification. This skill extends that base — new proposers (draft model,
EAGLE, MTP) and sampled-mode verification. vLLM's MRV2 module
(vllm/v1/worker/gpu/spec_decode/) is the reference design throughout.
Losslessness is the hard bar — a faster engine that changes the output
is a broken engine.
The existing base (extend, never parallel)
| Responsibility | Module | State |
|---|---|---|
| Propose | rapid_llm/engine/ngram_proposer.py | NgramProposer(max_ngram_size=5, max_draft=6), token-ids-only input |
| Verify | continuous_engine._speculate_verify + executor/worker.py full-logits path | one forward per step, greedy argmax per position, bonus = argmax at the first mismatch |
| KV budget | scheduler.reserve_speculative(request, draft_rows) | fit = min(draft_rows, limit - seq_len); an exhausted pool truncates drafts, never evicts |
| Switch | env LITE_LLAMA_SPECULATE | off by default; decode only, prefill chunks never participate |
| Tests | tests/engine/test_ngram_proposer.py, test_continuous_engine.py |
If a responsibility does not map to this table (a second engine family, a different sampler home), stop and ask — do not create a parallel module. Two consequences of the current design:
- Verification is greedy-only: accept while
draft[j] == argmax(logits[j]), bonus is the argmax at the first mismatch. Lossless for greedy decoding only. Turning speculation on undertemperature > 0requires the rejection sampler below — do not just flip the flag. NgramProposeris standalone, not behind an interface. A second proposer must not fork the engine flow — build the interface first.
Proposer interface and registry
MRV2's shape, mapped onto rapid_llm (sketch, not final signatures):
class Proposer(Protocol):
def propose(self, token_ids: list[int], context: DraftContext) -> list[int]: ...
graph_safe: bool # capture-safe inside decode graphs
needs_hidden_states: bool # eagle/mtp need them; ngram does not
- Registry by name, factory with hard errors. Config selects the
proposer by name; an unsupported name raises immediately. vLLM's
init_speculator()(worker/gpu/spec_decode/__init__.py) throwsNotImplementedErrorfor methods the MRV2 stack does not support (ngram, medusa stay on the MRV1 stack) — an explicit error beats a silent fallback. - CUDA-graph honesty lives in the interface. MRV2 puts
init_cudagraph_manager()/capture()on the speculator base class, so every method opts in explicitly. rapid_llm's precedent is the KernelSpecgraph_safefield: a proposer that assembles per-step state on the host isgraph_safe=Falseand the runner refuses to capture with it, the same wayadd-model'ssupports_cuda_graph=Falseworks. - Model-based proposers share a base. MRV2's
DraftModelSpeculatorholds what every draft-model method needs: weight loading, draft KV management, greedy draft sampling, the TP argmax-reduction path (comm cost O(vocab) -> O(2 x tp_size)), and the draft-logits cache for probabilistic drafting. Per-method code is only the architecture-specific forward. Copy that split — the second proposer must not re-implement sampling and KV plumbing.needs_hidden_states=Truemaps to MRV2'suse_aux_hidden_state_outputs: the runner must emit aux layers, which changes the target model's output contract.
Stage 0 — interface alignment, still no code: one interface document per new draft source: type (draft model / EAGLE / MTP / n-gram), inputs (token ids only, or hidden states / aux layers too), outputs (does it produce draft probabilities? sampled verification needs them), call cadence, batch shapes, error semantics. The n-gram proposer and a future EAGLE proposer already differ on inputs; stage 0 exists to expose that before the interface hardens.
Verification and sampling
- Single batched verification — keep. The k drafts enter one forward (length k+1 per request), never k serial passes. Candidate positions use a chain mask (tree candidates are out of scope). Requests in a batch may carry different k — state the padding or varlen policy.
- Rejection sampling for sampled mode. Greedy argmax compare is the
degenerate case (
p_draftone-hot). The sampled path compares a uniform againstp_target / p_draftposition by position; at the first rejection, sample from the corrected distribution(p_target - p_draft).clamp(min=0).normalize(). Port the algorithm fromvllm/v1/sample/rejection_sampler.py(PyTorch, full LogitsProcessor stack). Reach for a Triton port (MRV2's GPU-native version) only when a profile says so, perkernel-microbenchmark. - The bonus token comes from the regular sampler, not from inside the rejection sampler. vLLM passes it in as an argument because the bonus must honor top_p/top_k/penalties, which the verification path does not support. Dropping the bonus throws away one free token per fully-accepted step.
- Separate RNG streams for draft and target. MRV2 salts the draft
noise (
_DRAFT_NOISE_SALT = 1 << 30) so probabilistic drafting cannot perturb the target's sampling stream — that separation is what makes same-seed reproducibility achievable at all. rapid_llm's sampler keeps per-request generators; the draft stream gets its own derived generator, never a shared one.
The four high-risk coupling points
Each needs a design note and dedicated tests:
- KV rollback.
reserve_speculativebudgets k draft rows for the verify stretch; after verification acceptsa <= kdrafts, token accounting, KV rows and the position cursor roll backk - atogether, as one consistent operation. Cover accept-all / accept-some / accept-none — and the truncate-not-evict path, where a full pool silently shrinks the draft count. - Batch-state divergence. Requests accept different counts in the same step. MRV2's answer is persistent per-request state tables with per-step inputs built by gather, so assembling one step's inputs never mutates the canonical state; its zero-bubble async mode goes further — scheduling optimistically as if all drafts will be accepted and correcting on the next step. Copy the persistent-table pattern; treat optimistic scheduling as a later optimization with its own correctness tests.
- CUDA Graph. Variable speculation length vs fixed graph shapes:
enumerate (k+1)-shaped decode graphs at capture time, and capture
draft-side graphs after the target's so both share one batch-shape
enumeration — that ordering is what MRV2 uses in
capture_model(). A proposer that cannot be capture-safe setsgraph_safe=Falseand runs eager while the rest captures. Conclusion plus measured data, not vibes. - Sampler RNG. Rejection-sampling uniforms and draft noise are state-managed per request and separated by salt (above); replaying the same seed must reproduce the same token stream.
Acceptance bars
- Losslessness, per proposer: greedy — speculation on vs off is token-identical; sampled — same-seed token-identical (preferred), or a distribution check: >= 100k tokens at the same temperature, chi-square p > 0.05, method and raw data reported.
- Acceptance rate is a committed measurement, not a guess. vLLM keeps
tests/v1/e2e/spec_decode/acceptance_rates/— per-method tests that assert acceptance on fixed workloads. Add the rapid_llm equivalent: a high-acceptance load (code continuation) shows positive TPOT/throughput gain; a low-acceptance load (high-temperature open generation) degrades <= 5%, and the adaptive cutoff (disable speculation below an acceptance threshold) triggers correctly. Report acceptance rate and average accepted tokens per step, on/off and across regimes. - The four coupling points are test-covered, including 0% and 100% acceptance edges.
- Cross-matrix, cell by cell (each cell = greedy-identical + no perf regression): speculation x {CUDA Graph, TP, quantization, comm-compute overlap, chunked prefill, prefix caching}; negative interactions recorded with the cause.
- Soak: >= 30 minutes at high concurrency — no state corruption, memory curve flat.
- Library isolation: every proposer has a mock; unit tests never need the real draft model; an upstream interface change fails a test immediately.
- Extensibility demo: adding a proposer (a mock is fine) touches only its file plus one registration.
Deliverables
- One interface document per draft source.
- Design note: the three-way separation, the integration points, the four coupling points.
- Proposer support matrix: method x interface version x status, with the explicit unsupported list.
- Test and benchmark report per
model-benchmark-and-report, plus speculation metrics: acceptance rate and average accepted tokens per step, on/off and across acceptance regimes. - README: principle, switches and tuning knobs, with a recorded on/off speed comparison.
- Git: new branch — interface+core / each proposer / runner integration / cross-validation and docs as separate commits, each green alone.
Cross-references
model-benchmark-and-report— evidence rules for the speedup claimskernel-microbenchmark— when the rejection sampler or draft forward shows up in profiles and earns a Triton portwrite-test— test placement;locate-numeric-divergence— when the on/off token streams disagreeadd-model—supports_cuda_graph=Falseprecedent for capture opt-out; draft-model proposers reuse its weight-loading machinery- vLLM reference (local checkout
open_source/vllm):vllm/v1/worker/gpu/spec_decode/— MRV2 speculator hierarchy and factory;vllm/v1/sample/rejection_sampler.py— the PyTorch rejection-sampling algorithm;tests/v1/e2e/spec_decode/acceptance_rates/— the acceptance-rate test pattern
Signals
- GitHub stars
- 195
- Forks
- 33
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
add-speculative-decoding- Source
- github.com/harleyszhang/rapid_llm