[Speculative Decoding] Add TLI (Token-Level Intersection) — lossless speculative decoding for heterogeneous vocabularies#22883
[Speculative Decoding] Add TLI (Token-Level Intersection) — lossless speculative decoding for heterogeneous vocabularies#22883jmamou wants to merge 33 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces TLI (Token-Level Intersection), a lossless speculative decoding algorithm for models with heterogeneous vocabularies. The implementation includes a TLIWorker for managing the decoding process, a VocabMapping utility for token ID translation, and logic for pruning the draft model's LM head to improve efficiency. Documentation, CLI arguments, and unit tests are also provided, and optional dependency imports in the compressed tensors scheme are now wrapped in a try-except block. Feedback was given suggesting the use of logging instead of a silent pass when these imports fail.
- Add DEFAULT_TARGET_MODEL_TLI, DEFAULT_DRAFT_MODEL_TLI, and DEFAULT_CROSS_FAMILY_DRAFT_MODEL_TLI constants to test_utils.py - Add test/registered/spec/test_tli_speculative_decoding.py with same-tokenizer (Llama-3.1-8B + Llama-3.2-1B) and cross-family (Llama-3.1-8B + Qwen2.5-0.5B) test classes, each covering fa3, triton, and flashinfer attention backends - Tests verify GSM8K accuracy (>=0.69) and avg_spec_accept_length thresholds (>=3.0 same-tokenizer, >=2.5 cross-family) - Registered in stage-b-test-1-gpu-large CI suite
|
Note on Speed Benchmarks The speed benchmarks in this PR were produced using the SPEED-Bench dataset infrastructure introduced in a companion PR: #24149 ( To reproduce the results, first ensure #24149 is merged (or cherry-pick it locally), then run: python -m sglang.bench_serving \
--dataset-name speed-bench \
--dataset-path /path/to/throughput_1k.jsonl \
--speed-bench-category mixed \
--speed-bench-output-len 1024 \
--num-prompts 100The two PRs are otherwise independent — #24149 adds a general-purpose benchmark dataset and has no dependency on TLI. |
Per review feedback on sgl-project#23838, move the documentation note describing the vocab requirement (and the pointer to TLI) out of this PR. It will land in sgl-project#22883 alongside TLI itself. This PR keeps only the startup validation and error message.
Adds a startup-validation note above the STANDALONE example so users who hit the vocab-mismatch ValueError have an in-doc explanation and a pointer to TLI for cross-family pairs. Originally proposed in PR sgl-project#23838 and moved here per review feedback. Co-Authored-By: Claude Opus 4.7 <[email protected]>
|
/rerun-failed-ci |
Resolve merge conflicts and adapt TLI worker to V2 speculative decoding architecture: - Rewrite TLI monkey-patched methods to use V2 API: _draft_extend_for_prefill, _draft_extend_for_decode, draft_forward - Add is_tli() checks alongside is_standalone() for hidden state capture mode (CaptureHiddenMode.NULL), pool configuration, and cuda graph runners - Add TLI to spec_need_hidden_states exclusion list - Use per_step_draft_out_cache_loc and forward_context pattern from V2
Motivation
SGLang's
STANDALONEspeculative decoding mode supports arbitrary external draft models, butcurrently requires the draft model to share the same vocabulary as the target model. This rules
out the large universe of small, publicly available models as draft models for target models with
different tokenizers.
This PR implements TLI (Token-Level Intersection), an algorithm from our ICML 2025 spotlight paper "Accelerating LLM Inference with Lossless Speculative Decoding Algorithms for Heterogeneous Vocabularies" — Timor et al., arXiv 2502.05202. The same idea is described from a practical angle in our HuggingFace blog post Universal Assisted Generation. A reference implementation for HuggingFace Transformers was merged at huggingface/transformers#35029. A parallel implementation for vLLM is in progress at vllm-project/vllm#38174.
TLI is provably lossless: it always produces the same output distribution as greedy or
sampled decoding with the target model alone. It unlocks speculative decoding for any pair of
models whose vocabularies have a meaningful token overlap, regardless of tokenizer family.
Modifications
New files:
python/sglang/srt/speculative/vocab_mapping.pyBuilds a normalized token intersection between target and draft vocabularies at startup.
The space-prefix character (
Ġ,▁, or other) is detected dynamically by tokenizinga literal space, so unusual tokenizers are handled correctly. Models without
unk_token_id(e.g. Llama 3, SmolLM2) are supported via an
eos_token_idfallback. Exposes:constrain_draft_logits(logits)— sets non-intersection logits to−∞.map_target_to_draft_ids(ids)— remaps a tensor of target token IDs to draft vocab.map_draft_to_target_ids(ids)— inverse mapping.intersection_draft_ids— sorted draft-vocab indices in the intersection (used for LM head pruning).python/sglang/srt/speculative/tli_worker.pyTLIWorkerextendsStandaloneWorker(which loads a fully independent draft model with noshared embeddings / lm_head). It overrides three hooks:
forward_draft_extendforward_draft_extend_after_decodecapture_for_decodeThe rejection sampling step in the target model is not modified — losslessness follows
directly from the standard speculative decoding acceptance criterion.
Additionally implements
_try_prune_draft_lm_head(): at startup the draft LM head weightmatrix is replaced with a compact
[intersection_size × hidden_dim]version(
_PrunedReindexLMHead), reducing the dominant draft-decoding matmul fromO(B × V_draft × H)toO(B × K × H)whereK ≪ V_draft. CUDA graphs arecaptured after pruning so the graph encodes the smaller computation.
Modified files:
python/sglang/srt/speculative/spec_info.pyTLI = auto()toSpeculativeAlgorithmenum.is_tli()predicate.TLIWorkeringet_worker_cls(), with a guard that rejects overlapscheduling (not yet supported for spec v2).
python/sglang/srt/server_args.py"TLI"to--speculative-algorithmchoices.check_server_args:--speculative-draft-model-path.speculative_num_steps/speculative_num_draft_tokensvia the existingauto_choose_speculative_paramshelper when not explicitly set.speculative_eagle_topk == 1(TLI does not support multi-candidate trees).speculative_num_draft_tokens == speculative_num_steps + 1."TLI"with"STANDALONE"in the cuda-graph-max-bs default path.Usage example:
python -m sglang.launch_server \ --model-path meta-llama/Llama-3.1-8B-Instruct \ --speculative-algorithm TLI \ --speculative-draft-model-path Qwen/Qwen2.5-0.5B-Instruct \ --speculative-num-draft-tokens 5Parameters are auto-chosen if omitted — no manual tuning needed for basic use.
Accuracy Tests
TLI is provably lossless. A formal proof is given in the ICML 2025 paper
Timor et al., arXiv 2502.05202.
We also ran an empirical losslessness check (
check_accuracy.py): 50 prompts are sent withgreedy decoding (
temperature=0) to both a baseline server and a TLI server sharing the sametarget model, and outputs are compared exactly. In our runs with
Qwen2.5-7B-Instruct+Qwen2.5-0.5B-Instruct, 38/50 outputs matched character-for-character.The 12 mismatches were all minor whitespace/punctuation differences (e.g.
" "vs"\n", ora dropped filler word like
"and") that appear identically whether or not TLI is enabled —they are caused by non-deterministic floating-point accumulation in Flash Attention (non-associative
parallel reductions whose thread scheduling varies between runs), not by TLI. The same mismatches
occur when comparing two independent baseline runs against each other.
Speed Tests and Profiling
All benchmarks use the
speed_bench_mixeddataset (long-context summarization, output length ≈ 1024 tokens),batch_size=8,num_prompts=100,num_draft_tokens=5,speculative_eagle_topk=1. Vocab intersection is computed afterspace-prefix normalization by
VocabMapping.meta-llama/Llama-3.1-8B-InstructQwen/Qwen2.5-0.5B-Instructmeta-llama/Llama-3.1-8B-InstructQwen/Qwen2.5-0.5B-Instructmeta-llama/Llama-3.1-8B-InstructQwen/Qwen2.5-0.5B-InstructNotes
Llama-3.1-8B-Instruct + Qwen2.5-0.5B-Instruct is a genuine cross-family pair (different
tokenizer families, 85.43% vocab overlap). TLI delivers a strong 1.65–2.22× speedup across
devices, with the memory-bandwidth-bound A6000 showing the highest gain (2.22×).
Comparison with Related Implementations
This PR extends the reference implementation in HuggingFace Transformers
(PR #35029, merged ✅) with
several improvements suited to a high-throughput serving engine:
unk_token_idfallbackeos_token_id; warns; only errors if neither exists ✅_PruneReindexingLMHead✅_PrunedReindexLMHead; CUDA graphs captured post-pruning ✅Checklist
trim-trailing-whitespace,isort,ruff,black,codespell.test/registered/unit/spec/test_vocab_mapping.py: 36 CPU-only unit tests covering_detect_space_sign,_normalize_token, intersection construction,unk_token_idfallback, bidirectional ID mapping, and logit constraining.docs/advanced_features/speculative_decoding.md: added TLI section (How it works, constraints, usage example), updated quick-guidance bullets, method comparison table, and--speculative-algorithmparameter reference.cc: @orenpereg @keyboardAnt @wan-danfeng
CI States
Latest PR Test (Base): ❌ Missing
run-cilabel -- add it to run CI tests.Latest PR Test (Extra): ❌ Blocked --
run-ciis required first.