⚡ FlashSpec
Lossless speculative decoding for HuggingFace causal language models
Algorithm 1 rejection sampling and exact greedy · static KV cache + CUDA graphs on CUDA · experimental bandit and kernels
Project status: alpha, version 0.2.0.
What was measured, and only this: one model pair
(Qwen/Qwen2.5-1.5B-Instruct verified by Qwen/Qwen2.5-0.5B-Instruct),
one Tesla T4, fp16, batch 1, 10 MT-Bench and 10 HumanEval prompts, run
from main at git 8d036d7 with the default FlashSpecConfig(compile="auto"),
which turned the compiled path (static KV cache and CUDA graphs) on. On
that setup FlashSpec was faster than HuggingFace generate compiled the
same way in every cell of the table below: by at least 1.3x on both
HumanEval cells, and by less on MT-Bench. With the eager path
(compile=False) it was slower than plain HuggingFace generate in 3 of
the 4 cells of the same run (generated table further down). No other GPU,
model pair or batch size has been measured. Every H100 number in the docs
is a design target.
-
The first call compiles. On CUDA with Triton, the default
compile="auto"compiles the drafter and target forwards and captures CUDA graphs on the firstgenerate, which can take minutes. Later calls reuse them. The tables above and below exclude that first call (warmup prompts absorb it). To skip compilation, passFlashSpecConfig(compile=False)(the eager path, slower here) or--compile offto the server. The engine logs one line saying whether the compiled path is on and why. -
Correctness is checked in CI on CPU: a chi-squared test that emitted tokens follow the target when draft and target differ, and a token-for-token comparison of greedy output with
generate(do_sample=False). On the GPU in fp16, kernels for different shapes can round differently, so each greedy GPU run reports how many prompts matched (last columns of the table). -
The bandit is experimental. Every measured run used one drafter, so the selector has never been measured choosing between drafters.
-
The Triton and assembly kernels are experimental and not on the generation critical path; see Experimental kernels.
-
sampling.variant="typical"is lossy;"rejection"and"greedy"are the lossless variants.
This is a single-device library plus a small HTTP server, not a production serving stack.
Measured at git 8d036d7: Qwen/Qwen2.5-1.5B-Instruct verified by Qwen/Qwen2.5-0.5B-Instruct, Tesla T4, fp16, batch 1, gamma 4, 10 prompts per set (chat template), 128 new tokens. One model pair, one GPU. Speedups are medians of per-prompt ratios within the same run. The headline baseline is HuggingFace generate with a static cache compiled the same way (CUDA graphs); eager HuggingFace and assisted generation are context. Cells are the stored JSON values.
| dataset | mode | FlashSpec tok/s | compiled HF tok/s | vs compiled HF | eager HF tok/s | vs eager HF | vs HF assisted | acceptance | c (compiled) | greedy match vs compiled HF | greedy match vs eager HF |
|---|---|---|---|---|---|---|---|---|---|---|---|
| humaneval | greedy | 73.49117484423093 | 49.27609336228632 | 1.4929247183661758 | 25.272922944874917 | 2.868400153448608 | 2.968039793678412 | 0.7674472295086122 | 0.47651099333876806 | 10/10 | 9/10 |
| humaneval | T=0.7 | 65.40179447333742 | 49.24821559481592 | 1.326870202978973 | 25.444714453071143 | 2.562178271321323 | 3.06365302315802 | 0.7018560004266362 | 0.4770852877691722 | n/a | n/a |
| mt_bench | greedy | 55.69633073174627 | 49.99045416467851 | 1.1161741322307486 | 25.20267917967235 | 2.2281081944682457 | 3.0887126152344058 | 0.5310738087770345 | 0.47418886402988414 | 10/10 | 10/10 |
| mt_bench | T=0.7 | 53.447483345537975 | 49.189297069159096 | 1.0837177249808096 | 25.344508384460696 | 2.119677479254399 | 3.3014455192389267 | 0.5374968568894605 | 0.47562151975963995 | n/a | n/a |
Greedy: compiled FlashSpec produced the same tokens as hf_greedy_compiled, token for token (per-prompt output hashes are in the JSON), on 10/10 humaneval prompts and 10/10 mt_bench prompts.
Cold start: not recorded in this bundle. The first compiled call compiles the model and captures CUDA graphs, which can take minutes; the timed numbers above exclude it (warmup prompts absorb it).
What is FlashSpec?
Speculative decoding lets a small "draft" model propose γ tokens that a larger "target" model checks in one forward pass. FlashSpec's batch-1 step is:
- the drafter proposes γ tokens (γ incremental forwards on its KV cache);
- the target runs one forward over the pending token(s) plus the γ drafts;
- the accept/reject test runs on the device in PyTorch: Leviathan et al. Algorithm 1 rejection sampling at temperature > 0, or the longest prefix that matches the target's argmax at temperature 0 (greedy);
- one device-to-host read per step returns the accept count and the next token, and both caches are cropped by integer bookkeeping.
On CUDA with Triton available, FlashSpecConfig.compile runs that loop on
static KV caches and replays the fixed-shape drafter decode and target
verify calls as CUDA graphs (torch.compile(mode="reduce-overhead")).
Two parts are experimental. The UCB1/Thompson selector can choose between registered drafters, but it has only been measured with one. The Triton verification kernels and the optional assembly residual are not called on the generation path (see Experimental kernels).
On finite probabilities the output distribution follows Leviathan et al.,
2023, Theorem 1. The CI check is a chi-squared test on discrete token ids.
tests/integration/test_output_distribution.py uses fixed pairs with
draft distribution q different from target distribution p, at gamma
1 and gamma 4. It compares the first emitted token to p, and, when
gamma is 4, the second emitted token as well. N is 30,000 and the
vocabulary is 6, so each expected count stays above 5. The critical
value is the chi-square 1% point for 5 degrees of freedom, 15.086. The
same check runs on the PyTorch residual and on a kernel that implements
the same formula. An engine test uses a toy target whose next token
depends on the previous token and compares the first two generated
tokens with sampling from that target. tests/integration/test_e2e_sampling.py
is the gamma 1 case where the draft equals the target, so every draft
token is accepted. It does not exercise the residual. A failing
chi-squared test fails the suite.
Quickstart
git clone https://github.com/Mattral/FlashSpec && cd FlashSpec
# Linux + CUDA (accelerated Triton kernels):
pip install -e ".[dev,gpu]"
# Windows / macOS (pure-PyTorch reference kernels, numerically identical):
pip install -e ".[dev]"
# Optional CPU residual kernel (Linux x86-64, flashspec-asm-kernel>=0.1.2;
# other platforms install nothing):
pip install -e ".[asm]"
# Optional single-device HTTP server:
pip install -e ".[serve]"
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from flashspec import FlashSpecConfig, BanditConfig, SamplingConfig, TRITON_AVAILABLE
from flashspec.bandit import UCB1Selector
from flashspec.engine import SpeculativeEngine, TargetModel, load_drafters
print(f"Triton kernels available: {TRITON_AVAILABLE}")
# Public checkpoints. Both report vocab_size 32000.
# Llama-3 reports vocab_size 128256. Do not pair it with this drafter.
drafter_id = "Felladrin/Llama-160M-Chat-v1"
target_id = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
config = FlashSpecConfig(
device="cpu",
dtype="float32",
drafter_name=drafter_id,
target_name=target_id,
bandit=BanditConfig(n_arms=1, strategy="ucb1"),
sampling=SamplingConfig(gamma=4, temperature=1.0),
max_new_tokens=16,
)
# TargetModel(hub_id, device) passes device_map and needs accelerate,
# which this package does not depend on. Load the weights here and inject
# them, which is what benchmarks/harness.py does.
drafter_module = AutoModelForCausalLM.from_pretrained(
drafter_id, torch_dtype=torch.float32
).to(config.device)
target_module = AutoModelForCausalLM.from_pretrained(
target_id, torch_dtype=torch.float32
).to(config.device)
drafter_module.eval()
target_module.eval()
drafters = load_drafters(
[drafter_id],
device=config.device,
dtype=torch.float32,
temperature=config.sampling.temperature,
models={drafter_id: drafter_module},
)
target = TargetModel(
target_id,
torch.device(config.device),
torch.float32,
model=target_module,
)
engine = SpeculativeEngine(
config,
drafters,
target,
UCB1Selector(n_arms=1, window_size=config.bandit.window_size),
)
tokenizer = AutoTokenizer.from_pretrained(target_id)
prompt = tokenizer("The capital of France is", return_tensors="pt").input_ids
result = engine.generate(prompt, max_new_tokens=16)
new_ids = result.output_ids[0]
new_ids = new_ids[new_ids != -1]
print(tokenizer.decode(new_ids, skip_special_tokens=True))
Both checkpoints are public. benchmarks/harness.py refuses a drafter and a target whose vocabulary sizes differ and writes no result file. SpeculativeEngine itself does not apply that check. notebooks/01_quickstart.py is a separate CPU toy: the names are "toy", the log-probs are random, and it does not download these checkpoints. With one drafter the bandit cannot adapt, and the loader says so.
Results
The generated GPU blocks show the stored JSON values. The hand-written
tables below them use three significant figures; the JSON and CSV files
keep full precision. Each figure is classified in
benchmarks/results/PROVENANCE.md.
Commands and file names are in
benchmarks/results/README.md.
Measured — GPU, real prompts (MT-Bench and HumanEval, chat template)
This block is generated by scripts/ingest_gpu_bundle.py --readme-commit 8d036d7
from the result JSONs of the 0.2.0 bundle only (benchmarks/results/gpu/8d036d7/,
run from main). Do not edit it by hand. The earlier feature-branch
bundles (36134c0, the Phase 4 eager run that also covered TinyLlama, and
52993e4, the first compiled run) stay under benchmarks/results/gpu/ as
history and are not shown here.
Qwen/Qwen2.5-1.5B-Instruct verified, Qwen/Qwen2.5-0.5B-Instruct drafting
GPU Tesla T4 · torch 2.11.0+cu130 · gamma 4 · 20 result files · git 8d036d7. Prompts: chat_template; n per row: 10. Cells are the stored JSON values, not rounded.
| dataset | mode | method | tok/s median | tok/s p25 | tok/s p75 | acceptance | tokens / target fwd | target fwd / token | speedup vs plain HF (median) | speedup vs compiled plain HF (median) | speedup vs hf_assisted (median) | c (step profile) | greedy match | NaN/inf logits | peak GB |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| humaneval | greedy | flashspec_ucb | 26.02536363267037 | 24.113137938821488 | 26.416962512055154 | 0.7748324561339393 | 4.19672131147541 | 0.24609375 | 1.0193827466008485 | 0.5282968591639221 | 1.0318343857730912 | 0.845041661430388 | 10/10 | 0 | 3.9210314750671387 |
| humaneval | greedy | flashspec_compiled | 73.49117484423093 | 67.66749962983283 | 75.22223233061746 | 0.7674472295086122 | 4.169381107491857 | 0.24765625 | 2.868400153448608 | 1.4929247183661758 | 2.968039793678412 | 0.4937086544815969 | 9/10 | n/a | 3.9358386993408203 |
| humaneval | greedy | hf_greedy | 25.272922944874917 | 25.064466964565117 | 25.500231713400083 | n/a | 1.0078740157480315 | 1.0 | n/a | n/a | n/a | n/a | n/a | 0 | 3.9280996322631836 |
| humaneval | greedy | hf_greedy_compiled | 49.27609336228632 | 49.1847500987219 | 49.34777025069719 | n/a | 1.0078740157480315 | 1.0 | n/a | n/a | n/a | n/a | 9/10 | n/a | 3.922210216522217 |
| humaneval | greedy | hf_assisted | 24.19664417567833 | 22.245649238351376 | 25.200750728941774 | n/a | 7.231638418079096 | 0.14609375 | n/a | n/a | n/a | n/a | 10/10 | 0 | 4.010560035705566 |
| humaneval | T=0.7 | flashspec_ucb | 23.054835447878503 | 21.110460201553547 | 24.779995491896514 | 0.6882722339966006 | 3.710144927536232 | 0.27734375 | 0.9109461711070447 | 0.46804709580075965 | 1.0736463665588767 | 0.8629833155074336 | n/a | 0 | 3.9422059059143066 |
| humaneval | T=0.7 | flashspec_compiled | 65.40179447333742 | 59.35656427020519 | 68.5928206976523 | 0.7018560004266362 | 3.764705882352941 | 0.2734375 | 2.562178271321323 | 1.326870202978973 | 3.06365302315802 | 0.49779909081468227 | n/a | n/a | 3.9422059059143066 |
| humaneval | T=0.7 | hf_sample | 25.444714453071143 | 25.347879588597422 | 25.543325368357564 | n/a | 1.0078740157480315 | 1.0 | n/a | n/a | n/a | n/a | n/a | 0 | 3.9432334899902344 |
| humaneval | T=0.7 | hf_sample_compiled | 49.24821559481592 | 49.19260891088411 | 49.33891386116446 | n/a | 1.0078740157480315 | 1.0 | n/a | n/a | n/a | n/a | n/a | n/a | 3.936194896697998 |
| humaneval | T=0.7 | hf_assisted | 20.25776424898342 | 19.350477197434962 | 22.45474429590343 | n/a | 6.037735849056604 | 0.1734375 | n/a | n/a | n/a | n/a | n/a | 0 | 4.024886608123779 |
| mt_bench | greedy | flashspec_ucb | 19.572806929034726 | 18.512325739597685 | 20.655499375866352 | 0.5868378012374362 | 3.2653061224489797 | 0.3140625 | 0.7776848037196118 | 0.3925474950495035 | 1.1001251488328485 | 0.8672852493814422 | 9/10 | 0 | 3.902125835418701 |
| mt_bench | greedy | flashspec_compiled | 55.69633073174627 | 52.71416447738182 | 57.00448545307138 | 0.5310738087770345 | 3.18407960199005 | 0.321875 | 2.2281081944682457 | 1.1161741322307486 | 3.0887126152344058 | 0.48626613558395965 | 10/10 | n/a | 3.9118690490722656 |
| mt_bench | greedy | hf_greedy | 25.20267917967235 | 25.112631097015935 | 25.30654132302575 | n/a | 1.0078740157480315 | 1.0 | n/a | n/a | n/a | n/a | n/a | 0 | 3.9123334884643555 |
| mt_bench | greedy | hf_greedy_compiled | 49.99045416467851 | 49.59376567557413 | 50.24789576812648 | n/a | 1.0078740157480315 | 1.0 | n/a | n/a | n/a | n/a | 10/10 | n/a | 3.9069666862487793 |
| mt_bench | greedy | hf_assisted | 19.849014490485814 | 15.136578031686579 | 22.22945913877053 | n/a | 3.8905775075987843 | 0.26484375 | n/a | n/a | n/a | n/a | 10/10 | 0 | 3.9825186729431152 |
| mt_bench | T=0.7 | flashspec_ucb | 20.18121040233048 | 18.115274461600237 | 21.17643975210253 | 0.5437414602239845 | 3.3333333333333335 | 0.3078125 | 0.7952682871520586 | 0.41020068096677864 | 1.234533396317398 | 0.8612503546546161 | n/a | 0 | 3.9191641807556152 |
| mt_bench | T=0.7 | flashspec_compiled | 53.447483345537975 | 50.30655289847409 | 59.43461086804307 | 0.5374968568894605 | 3.2989690721649483 | 0.3109375 | 2.119677479254399 | 1.0837177249808096 | 3.3014455192389267 | 0.5416823432691761 | n/a | n/a | 3.923170566558838 |
| mt_bench | T=0.7 | hf_sample | 25.344508384460696 | 25.114667710180996 | 25.389395380475488 | n/a | 1.0078740157480315 | 1.0 | n/a | n/a | n/a | n/a | n/a | 0 | 3.9262752532958984 |
| mt_bench | T=0.7 | hf_sample_compiled | 49.189297069159096 | 49.090311507338804 | 49.27322962156904 | n/a | 1.0078740157480315 | 1.0 | n/a | n/a | n/a | n/a | n/a | n/a | 3.9209084510803223 |
| mt_bench | T=0.7 | hf_assisted | 15.343815220295882 | 9.802755592886559 | 19.533044949645106 | n/a | 3.9751552795031055 | 0.259375 | n/a | n/a | n/a | n/a | n/a | 0 | 4.005769729614258 |
- forward_cost (humaneval): c eager 0.8840264407247382, c compiled 0.47651099333876806
- forward_cost (humaneval): c eager 0.8922046962713643, c compiled 0.4770852877691722
- forward_cost (mt_bench): c eager 0.8978235042101232, c compiled 0.47562151975963995
- forward_cost (mt_bench): c eager 0.9040499147358744, c compiled 0.47418886402988414
Speedups are medians of per-prompt ratios against the method in the same run. NaN/inf counts come from one untimed probe generation per method on the first prompt.
Measured, 0.1.5 engine — TinyLlama-1.1B-Chat float32 · CPU · γ=4 · git f59b5b4
One HumanEval prompt, 16 new tokens, one drafter. Speedup is below 1.
| FlashSpec UCB | Vanilla AR | |
|---|---|---|
| tok/s | 1.75 | 4.30 |
| latency (s) | 9.14 | 3.72 |
| acceptance | 0.114 | unmeasured |
| speedup vs AR | 0.407 | unmeasured |
Measured, 0.1.5 engine — TinyLlama-1.1B-Chat float16 · Tesla T4 · γ=4 · git cb02ca1
Eight synthetic prompts, one drafter. Speedup is below 1.
| FlashSpec UCB | Vanilla AR | |
|---|---|---|
| tok/s | 17.6 ± 1.43 | 29.0 ± 0.757 |
| p50 latency (ms) | 1860 | 1090 |
| p95 latency (ms) | 2140 | 1170 |
| acceptance | 0.208 ± 0.00825 | unmeasured |
| speedup vs AR | 0.606 ± 0.0501 | unmeasured |
| gpu memory (GB) | 2.38 | 2.38 |
Microbenchmarks and historical
These rows are a random network, a sampling kernel, or the historical NF4 prompt. They sit below the paired end-to-end runs. The kernel and bandit measurements are under Experimental components.
CPU toy and residual — git da8182b
Random in-process network, and a PyTorch residual microbench. The toy speedup is below 1. The bandit row is a synthetic sampler, not that network.
| FlashSpec UCB | Vanilla AR | |
|---|---|---|
| tok/s | 808 ± 86.4 | 2320 ± 2.51 |
| p50 latency (ms) | 10.6 | 3.44 |
| acceptance | 0.312 ± 0.0600 | unmeasured |
| speedup vs AR | 0.348 ± 0.0376 | unmeasured |
| Vocab | PyTorch residual (ms) | PyTorch verification (ms) |
|---|---|---|
| 1024 | 0.0182 ± 0.000935 | 0.418 ± 0.0221 |
| 4096 | 0.0197 ± 0.000205 | 0.523 ± 0.0435 |
| Arm | Acceptance | tok/s |
|---|---|---|
| UCB on the switch | 0.556 | 5790 |
| Fixed A | 0.200 | 2570 |
| Fixed B | 0.200 | 3220 |
| Single-drafter control | 0.200 | 2880 |
An older notebook cell assigns 44.2 tok/s, acceptance 0.75, p50 22.1 ms; no timing log exists, so it is not used as a result.
Gamma sweep — measured (toy logprobs, synthetic)
Acceptance rate and throughput across speculation lengths γ ∈ {1, 2, 4, 8, 16}.
Data in benchmarks/results/gamma_sweep.csv.
| γ | α (mean) | Throughput (tok/s) | Step (ms) |
|---|---|---|---|
| 1 | 0.720 | 1,992 | 0.50 |
| 2 | 0.680 | 4,623 | 0.43 |
| 4 | 0.690 | 9,746 | 0.41 |
| 8 | 0.748 | 17,748 | 0.45 |
| 16 | 0.714 | 35,659 | 0.45 |
Throughput here is the sampling kernel on random log-probs. The CSV column
measurement_kindissampling_kernel_only_random_logprobs. It is not end-to-end inference and it is not an H100 result.
No H100 comparison exists, and Medusa and EAGLE are not implemented here, so no comparison with them exists.
Experimental components
Bandit (experimental, single-arm measured only)
Every language-model run in this README used one drafter, so the selector has not been measured choosing between drafters. The regret numbers below come from a synthetic K=3 simulation.
Bandit regret — measured, synthetic (T=10,000, K=3 arms)
In this simulation UCB1 and Thompson sampling stay inside the O(√(KT log T)) bound over 10,000 rounds. UCB1 cumulative regret at T=10,000: 100.2 (theory bound: ~526). Thompson cumulative regret at T=10,000: 18.9.
The figure is paper/figures/bandit_regret.jpg. The saved output is
notebooks/02_bandit_analysis.ipynb cell 3 (0-based index, execution
count 4): UCB1 100.2, Thompson 18.9, Oracle 0.0. This is a synthetic
K=3 arm experiment, not a throughput gain on a language model.
Experimental kernels (not on the critical path)
The engine does not call these kernels. A speculative step's time is the drafter and target forwards (milliseconds); the accept/reject test and the residual are microseconds and run in PyTorch on the device.
- Triton
verify_tokens/gather_accepted(flashspec.kernels).verify_tokenstiles over(batch × γ)positions; each thread reads two scalars (log p,log qat the draft token index) and writes one bool and one int32, so SRAM use does not grow with the vocabulary. It matches the pure-PyTorch reference numerically. There is no GPU CI runner:notebooks/colab_triton_tests.pyruns the@pytest.mark.gpuparity tests on Colab and writescolab_triton_tests.log. - Assembly residual (
flashspec-asm-kernel, the[asm]extra, Linux x86-64). A residual evaluation is microseconds against millisecond model forwards, so it does not change end-to-end throughput.
Triton verification kernel vs the PyTorch reference — measured (Tesla T4)
Saved output of notebooks/03_kernel_profiling.ipynb cell 3 (0-based
index, execution count 4), printed there as a Tesla T4 run. At batch 1
the Triton kernel is slower than the PyTorch reference.
| Shape | Reference (ms) | Triton (ms) | Speedup |
|---|---|---|---|
| B=1 γ=4 V=32k | 0.151 | 0.785 | 0.2× |
| B=8 γ=4 V=32k | 0.717 | 0.459 | 1.6× |
| B=1 γ=8 V=32k | 0.231 | 1.248 | 0.2× |
| B=32 γ=4 V=32k | 0.091 | 0.222 | 0.4× |
The Triton kernel is slower than the pure-PyTorch reference at batch 1 on this T4 measurement. A faster H100 result has not been measured. Do not treat the H100 roofline in
docs/kernels.mdas a profile.
Assembly residual ([asm] extra) — Linux, git 4e48c55, then git 1b0cda3
Milliseconds, backend asm. Compare backends inside one run. The two runs do not repeat the same ordering.
| Vocab | PyTorch residual | C baseline | ASM residual | PyTorch verify | ASM verify |
|---|---|---|---|---|---|
| 1024 | 0.0118 ± 0.00599 | 0.0109 ± 0.00177 | 0.0151 ± 0.00971 | 0.366 ± 0.0209 | 0.422 ± 0.0438 |
| 4096 | 0.0116 ± 0.000285 | 0.0140 ± 0.00214 | 0.0140 ± 0.00430 | 0.495 ± 0.0218 | 0.537 ± 0.0159 |
| 32000 | 0.0302 ± 0.00818 | 0.0430 ± 0.00268 | 0.0248 ± 0.0230 | 1.28 ± 0.638 | 1.32 ± 0.658 |
| Vocab | PyTorch residual | C baseline | ASM residual | PyTorch verify | ASM verify |
|---|---|---|---|---|---|
| 1024 | 0.00686 ± 0.00312 | 0.00609 ± 0.000588 | 0.00806 ± 0.00108 | 0.158 ± 0.0421 | 0.164 ± 0.0305 |
| 4096 | 0.00685 ± 0.000109 | 0.00785 ± 0.000559 | 0.00872 ± 0.000941 | 0.231 ± 0.0339 | 0.250 ± 0.0390 |
| 32000 | 0.0124 ± 0.00209 | 0.0235 ± 0.00266 | 0.0151 ± 0.0187 | 0.747 ± 0.393 | 0.730 ± 0.376 |
How it works
Batch-1 fast path (variant "rejection" or "greedy"):
sequenceDiagram
participant E as SpeculativeEngine
participant B as Bandit (experimental)
participant D as Drafter
participant T as Target
loop every decoding step
E->>B: select() → arm index
E->>D: draft γ tokens (γ forwards on its KV cache)
E->>T: verify pending + γ tokens [one forward]
E->>E: rejection_step or greedy_step on the device (PyTorch)
E->>E: one host read: n_accepted, next token
E->>D: commit: crop the cache, set the pending token
E->>T: commit: crop the cache, set the pending token
E->>B: update(arm, emitted tokens / step seconds)
end
Batching. A batch of more than one row uses the generic loop. Rows that accept different numbers of tokens finish the step at different context lengths. Each loop forwards only the largest group of rows that still share a length. Other lengths wait for a later loop. The KV cache is rolled only when that group has a single row. A larger group full-forwards. The returned tensor fills a shorter row with -1 so the tensor is rectangular. That fill is not an attention mask. The engine does not pad unequal prompts into one masked batch.
Correctness. The accept/reject criterion is u_i < min(1, p(xᵢ)/q(xᵢ))
computed in log-space to avoid underflow. Rejected positions sample a residual
token from max(0, p−q) / ‖max(0, p−q)‖₁ (Algorithm 1, Leviathan et al. 2023).
Together these preserve the target distribution exactly.
Compiled path. With FlashSpecConfig.compile on a CUDA device, the
drafter and target run on preallocated static KV caches; a crop moves the
write position instead of slicing tensors, and the fixed-shape decode and
verify calls replay as CUDA graphs. The prompt prefill stays eager.
Bandit (experimental). UCB1 score for arm k at round t:
μ̂ₖ + c · √(2 log t / nₖ). Unpulled arms are selected first. The reward
is emitted tokens per step second. In a synthetic K=3 simulation UCB1
regret at T=10,000 is 100.2 (theory bound ≈ 526) and Thompson's is 18.9.
No language-model run has used more than one drafter.
See docs/architecture.md for the full component
diagram and proof sketch.
Installation
# PyPI — cross-platform (Windows/macOS/Linux):
pip install flashspec
# PyPI — adds Triton (Linux only): the experimental Triton kernels.
# Linux CUDA builds of PyTorch already ship Triton, which torch.compile
# needs for the compiled path.
pip install flashspec[gpu]
# Optional AVX2 residual kernel. The wheel is a Linux x86-64 ELF library.
# On Windows and macOS the extra installs nothing and the PyTorch residual stays selected.
pip install flashspec[asm]
# Optional single-device HTTP API (FastAPI). One GPU or CPU, no streaming.
pip install flashspec[serve]
# From source:
git clone https://github.com/Mattral/FlashSpec && cd FlashSpec
pip install -e ".[dev,gpu]" # omit ",gpu" on Windows/macOS
# Docker (Linux + CUDA). No image is published; build it locally:
docker build -f deploy/Dockerfile -t flashspec .
docker run --gpus all flashspec make test
Windows / macOS:
pip install flashspecworks out of the box. Triton has no official PyPI wheels outside Linux, so the compiled path and the experimental Triton kernels are Linux + CUDA only. The engine's accept/reject test is PyTorch and runs everywhere PyTorch does. The kernels' pure-PyTorch reference isflashspec.kernels._reference. Check availability at runtime:from flashspec import TRITON_AVAILABLE.
Requirements
| Dependency | Version | Notes |
|---|---|---|
| Python | ≥ 3.11 | |
| PyTorch | ≥ 2.2 | CPU or CUDA |
| Triton | ≥ 3.0 | Optional · Linux + CUDA · needed by the compiled path (Linux CUDA PyTorch ships it) and the experimental kernels |
| flashspec-asm-kernel | ≥ 0.1.2, < 2 | Optional · Linux x86-64 · pip install flashspec[asm]. 0.1.2 ships a manylinux wheel with libflashspec_asm.so. |
| FastAPI, uvicorn, httpx | see serve extra |
Optional · pip install flashspec[serve] |
| CUDA | ≥ 12.0 | GPU path only |
| transformers | ≥ 4.40 | For HuggingFace model loading |
The asm extra installs flashspec-asm-kernel >= 0.1.2 and < 2 on Linux
x86-64. Release 0.1.2 is a manylinux wheel that contains
libflashspec_asm.so. Release 0.1.0 was an sdist with no shared library.
An import error, or an OSError from abi_version(), keeps the
pure-PyTorch residual. A residual evaluation is microseconds, and a model
forward is milliseconds, so this kernel does not change end-to-end
throughput. benchmarks/results/residual_linux_gha.json and
benchmarks/results/residual_linux_gha_1b0cda3.json are source builds
from the flashspec-asm repo, not a timing of the 0.1.2 wheel.
The PyPI project flashspec at version 0.1.2 was yanked by the maintainer.
Do not install flashspec==0.1.2. That yank does not apply to
flashspec-asm-kernel.
Testing
make test # unit + integration on CPU (no GPU required, ~2 min)
make test-chaos # adversarial bandit tests
make test-gpu # pytest -m gpu; needs CUDA. This repository has no GPU runner.
make bench-quick # sampling-kernel smoke test, no model weights, writes no result file
make bench-cpu # residual microbench, synthetic bandit demo, tiny_cpu.yaml
make bench # runs every config in benchmarks/configs; skips any checkpoint this machine cannot load
The distribution check in the suite is a chi-squared test
(tests/integration/test_output_distribution.py, N = 30,000, vocabulary 6,
critical value 15.086 at 5 degrees of freedom and α = 0.01). It is not a
Kolmogorov–Smirnov test. The CPU test job runs it. Pytest fails under 83%
line coverage, the measured unit-and-integration coverage rounded down.
The CI lint job also runs mypy --strict flashspec.
Repository layout
flashspec/ Installable package (engine, kernels, bandit, sampling, metrics)
benchmarks/ Runners, configs, and results/ for all paper numbers
tests/ Unit · integration · chaos suites (83% coverage floor)
notebooks/ Quickstart · bandit regret analysis · kernel profiling
docs/ MkDocs site (architecture, kernels, bandit, benchmarks)
paper/ Draft Zenodo preprint (article class) and bibliography
paper/joss/ JOSS-format draft, not under review
paper/figures/ Vector and raster figures (bandit_regret.jpg generated)
scripts/ CLI tools (export draft, download models, profile kernels)
deploy/ Dockerfile · docker-compose.yml · k8s manifests
Links
| Resource | URL |
|---|---|
| Documentation | flashspec.readthedocs.io |
| Preprint (Zenodo) | 10.5281/zenodo.20766119 |
| Draft preprint | paper/flashspec.tex |
| JOSS draft | paper/joss/paper.md |
| Benchmark guide | benchmarks/README.md |
| Changelog | CHANGELOG.md |
| Contributing | CONTRIBUTING.md |
| Publishing roadmap | PUBLISHING.md |
| Postponed work | docs/postponed.md |
Citation
@misc{myet2026flashspec,
title = {{FlashSpec}: Adaptive Speculative Decoding with Online Bandit
Draft Selection and {Triton}-Optimised Verification},
author = {Myet, Min Htet},
year = {2026},
month = jun,
howpublished = {Zenodo preprint},
doi = {10.5281/zenodo.20766119},
url = {https://doi.org/10.5281/zenodo.20766119},
}
Preprint DOI: 10.5281/zenodo.20766119 (June 2026). That record is the citable archive. paper/flashspec.tex is a later draft (October 2026, software 0.2.0) and has not been deposited.
Machine-readable metadata in CITATION.cff.
License
Apache 2.0 — see LICENSE.
Metadata
Release files for flashspec 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| flashspec-0.2.0.tar.gz | 638.6 kB | With provenance | Details for flashspec-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| flashspec-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for flashspec-0.2.0-py3-none-any.whl |
Total release size: 759.3 kB
Release files / flashspec-0.2.0.tar.gz
| Download URL | flashspec-0.2.0.tar.gz |
|---|---|
| Size | 638.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d6d322ba68461a1525cbab311a976a00c9004b0f8553c268ff9164f989b5d69d
|
|
BLAKE2b-256 checksum How to use checksums |
2e0a01ca7208b388070013d7f168f4392b632e5f09366d7d0821d19ab40a7083
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logRelease files / flashspec-0.2.0-py3-none-any.whl
| Download URL | flashspec-0.2.0-py3-none-any.whl |
|---|---|
| Size | 120.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fa9e337892b062cf7b8c05a67e34ed431fc387fb15812b07e8a77bca5f9bd0fa
|
|
BLAKE2b-256 checksum How to use checksums |
fb8398237a2f1108a4da0acdb48ff739822b4f16bc348f0015e2512b893131b3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logSecurity for flashspec 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for flashspec-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.