Speculative prefill for LLM inference: draft model fills KV cache, verifier accepts or rejects. Benchmarked on Llama-3-8B/70B.
Python
1
13 commits
updated May 11, 2026
Speculative prefill for LLM inference. A small draft model fills the KV cache speculatively; a larger verifier accepts or rejects chunks based on hidden-state similarity. When the draft model is good enough (and it usually is), you skip a lot of redundant verifier compute and get faster time-to-first-token.
Standard LLM prefill processes the entire prompt with the full-size model. For long prompts on large models, this dominates TTFT. The idea here is simple: a smaller model from the same family can produce KV cache entries that are close enough to what the large model would compute. We verify at periodic checkpoints and only recompute on rejection.
This is the prefill-phase analog of speculative decoding — but instead of speculating on output tokens, we speculate on internal representations.
┌─────────────┐
│ Prompt │
└──────┬──────┘
│
┌──────▼──────┐
│ Scheduler │ chunk the prompt, plan verification points
└──────┬──────┘
│
┌────────────┼────────────┐
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ Draft Model │ │ Verifier │
│ (Llama-8B) │ │ (Llama-70B) │
└──────┬──────┘ └──────┬──────┘
│ │
│ KV cache entries │ hidden state comparison
└────────────┬────────────┘
│
┌──────▼──────┐
│ KV Cache │ accept/reject/trim
│ Manager │
└──────┬──────┘
│
┌──────▼──────┐
│ Accepted │ → continue to autoregressive decoding
│ KV Cache │
└─────────────┘
scheduler.py — Splits the prompt into chunks and decides where to place verification points. Tracks acceptance/rejection rates and triggers fallback to full verifier prefill if the draft model is doing badly.
kv_cache.py — Manages KV cache entries from the draft model. Handles the head-count mismatch between draft and verifier (GQA), projection when head dimensions differ, and trimming on rejection.
draft.py — Runs the draft model on prompt chunks, extracts KV cache entries and hidden states for verification.
verify.py — Runs the verifier on selected positions, compares hidden states via cosine similarity, and decides accept/reject.
speculative_prefill.py — The main loop tying everything together. Also includes standard_prefill() as a baseline.
hf_wrapper.py — SpecPrefillForCausalLM: a drop-in wrapper that loads a draft/verifier pair and provides a familiar .generate() interface.
The fallback is important — if the draft model is a poor match for the prompt (e.g., code vs. natural language domain mismatch), we don't want to waste time on bad speculation.
from specprefill import SpecPrefillForCausalLM, PrefillConfig
model = SpecPrefillForCausalLM.from_pretrained(
draft_model_name="meta-llama/Llama-3-8B",
verifier_model_name="meta-llama/Llama-3-70B",
)
# just works like a normal HF model
output = model.generate(input_ids, max_new_tokens=256)
# or run prefill separately to inspect stats
result = model.prefill(input_ids)
print(f"TTFT: {result.ttft_ms:.1f}ms, acceptance: {result.acceptance_rate:.1%}")
config = PrefillConfig(
draft_chunk_size=128, # tokens per draft chunk
verify_stride=64, # verification frequency
acceptance_threshold=0.92, # cosine sim threshold
max_rejection_rate=0.3, # fallback trigger
fallback_to_full=True, # enable/disable fallback
)
TTFT comparison between standard prefill and speculative prefill using Llama-3-8B (draft) + Llama-3-70B (verifier) on 2xA100-80GB. Each configuration was run 20 times after 3 warmup iterations.
python -m bench.benchmark_ttft \
--draft meta-llama/Llama-3-8B \
--verifier meta-llama/Llama-3-70B \
--prompt-lengths 512 1024 2048 4096 \
--num-runs 20
| Prompt Length | Method | p50 (ms) | p90 (ms) | p95 (ms) | p99 (ms) | Mean (ms) |
|---|---|---|---|---|---|---|
| 512 | Standard | 142.3 | 148.7 | 151.2 | 155.8 | 143.1 |
| 512 | SpecPrefill | 98.4 | 105.1 | 108.3 | 112.6 | 99.2 |
| 512 | Speedup | 1.45x | ||||
| 1024 | Standard | 271.8 | 283.4 | 289.1 | 296.3 | 273.6 |
| 1024 | SpecPrefill | 164.2 | 175.8 | 181.3 | 189.7 | 166.5 |
| 1024 | Speedup | 1.66x | ||||
| 2048 | Standard | 528.4 | 551.2 | 563.8 | 579.4 | 532.1 |
| 2048 | SpecPrefill | 287.1 | 308.6 | 318.4 | 331.2 | 291.3 |
| 2048 | Speedup | 1.84x | ||||
| 4096 | Standard | 1041.7 | 1089.3 | 1112.6 | 1148.2 | 1052.4 |
| 4096 | SpecPrefill | 512.3 | 548.7 | 564.1 | 589.3 | 519.8 |
| 4096 | Speedup | 2.03x |
Acceptance rate averaged 94.2% across all prompt lengths. Speedup scales with prompt length — longer prompts benefit more because the draft model's advantage compounds across more chunks.
pip install -e .
# with benchmarking deps
pip install -e ".[bench]"
MIT
Speculative prefill for LLM inference: draft model fills KV cache, verifier accepts or rejects. Benchmarked on Llama-3-8B/70B.
Python
1
13 commits
updated May 11, 2026
Speculative prefill for LLM inference. A small draft model fills the KV cache speculatively; a larger verifier accepts or rejects chunks based on hidden-state similarity. When the draft model is good enough (and it usually is), you skip a lot of redundant verifier compute and get faster time-to-first-token.
Standard LLM prefill processes the entire prompt with the full-size model. For long prompts on large models, this dominates TTFT. The idea here is simple: a smaller model from the same family can produce KV cache entries that are close enough to what the large model would compute. We verify at periodic checkpoints and only recompute on rejection.
This is the prefill-phase analog of speculative decoding — but instead of speculating on output tokens, we speculate on internal representations.
┌─────────────┐
│ Prompt │
└──────┬──────┘
│
┌──────▼──────┐
│ Scheduler │ chunk the prompt, plan verification points
└──────┬──────┘
│
┌────────────┼────────────┐
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ Draft Model │ │ Verifier │
│ (Llama-8B) │ │ (Llama-70B) │
└──────┬──────┘ └──────┬──────┘
│ │
│ KV cache entries │ hidden state comparison
└────────────┬────────────┘
│
┌──────▼──────┐
│ KV Cache │ accept/reject/trim
│ Manager │
└──────┬──────┘
│
┌──────▼──────┐
│ Accepted │ → continue to autoregressive decoding
│ KV Cache │
└─────────────┘
scheduler.py — Splits the prompt into chunks and decides where to place verification points. Tracks acceptance/rejection rates and triggers fallback to full verifier prefill if the draft model is doing badly.
kv_cache.py — Manages KV cache entries from the draft model. Handles the head-count mismatch between draft and verifier (GQA), projection when head dimensions differ, and trimming on rejection.
draft.py — Runs the draft model on prompt chunks, extracts KV cache entries and hidden states for verification.
verify.py — Runs the verifier on selected positions, compares hidden states via cosine similarity, and decides accept/reject.
speculative_prefill.py — The main loop tying everything together. Also includes standard_prefill() as a baseline.
hf_wrapper.py — SpecPrefillForCausalLM: a drop-in wrapper that loads a draft/verifier pair and provides a familiar .generate() interface.
The fallback is important — if the draft model is a poor match for the prompt (e.g., code vs. natural language domain mismatch), we don't want to waste time on bad speculation.
from specprefill import SpecPrefillForCausalLM, PrefillConfig
model = SpecPrefillForCausalLM.from_pretrained(
draft_model_name="meta-llama/Llama-3-8B",
verifier_model_name="meta-llama/Llama-3-70B",
)
# just works like a normal HF model
output = model.generate(input_ids, max_new_tokens=256)
# or run prefill separately to inspect stats
result = model.prefill(input_ids)
print(f"TTFT: {result.ttft_ms:.1f}ms, acceptance: {result.acceptance_rate:.1%}")
config = PrefillConfig(
draft_chunk_size=128, # tokens per draft chunk
verify_stride=64, # verification frequency
acceptance_threshold=0.92, # cosine sim threshold
max_rejection_rate=0.3, # fallback trigger
fallback_to_full=True, # enable/disable fallback
)
TTFT comparison between standard prefill and speculative prefill using Llama-3-8B (draft) + Llama-3-70B (verifier) on 2xA100-80GB. Each configuration was run 20 times after 3 warmup iterations.
python -m bench.benchmark_ttft \
--draft meta-llama/Llama-3-8B \
--verifier meta-llama/Llama-3-70B \
--prompt-lengths 512 1024 2048 4096 \
--num-runs 20
| Prompt Length | Method | p50 (ms) | p90 (ms) | p95 (ms) | p99 (ms) | Mean (ms) |
|---|---|---|---|---|---|---|
| 512 | Standard | 142.3 | 148.7 | 151.2 | 155.8 | 143.1 |
| 512 | SpecPrefill | 98.4 | 105.1 | 108.3 | 112.6 | 99.2 |
| 512 | Speedup | 1.45x | ||||
| 1024 | Standard | 271.8 | 283.4 | 289.1 | 296.3 | 273.6 |
| 1024 | SpecPrefill | 164.2 | 175.8 | 181.3 | 189.7 | 166.5 |
| 1024 | Speedup | 1.66x | ||||
| 2048 | Standard | 528.4 | 551.2 | 563.8 | 579.4 | 532.1 |
| 2048 | SpecPrefill | 287.1 | 308.6 | 318.4 | 331.2 | 291.3 |
| 2048 | Speedup | 1.84x | ||||
| 4096 | Standard | 1041.7 | 1089.3 | 1112.6 | 1148.2 | 1052.4 |
| 4096 | SpecPrefill | 512.3 | 548.7 | 564.1 | 589.3 | 519.8 |
| 4096 | Speedup | 2.03x |
Acceptance rate averaged 94.2% across all prompt lengths. Speedup scales with prompt length — longer prompts benefit more because the draft model's advantage compounds across more chunks.
pip install -e .
# with benchmarking deps
pip install -e ".[bench]"
MIT