Skip to content

[Speculative Decoding] Add TLI (Token-Level Intersection) — lossless speculative decoding for heterogeneous vocabularies#22883

Open
jmamou wants to merge 33 commits into
sgl-project:mainfrom
jmamou:tli
Open

[Speculative Decoding] Add TLI (Token-Level Intersection) — lossless speculative decoding for heterogeneous vocabularies#22883
jmamou wants to merge 33 commits into
sgl-project:mainfrom
jmamou:tli

Conversation

@jmamou

@jmamou jmamou commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Motivation

SGLang's STANDALONE speculative decoding mode supports arbitrary external draft models, but
currently 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.py

    Builds a normalized token intersection between target and draft vocabularies at startup.
    The space-prefix character (Ġ, , or other) is detected dynamically by tokenizing
    a literal space, so unusual tokenizers are handled correctly. Models without unk_token_id
    (e.g. Llama 3, SmolLM2) are supported via an eos_token_id fallback. 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.py

    TLIWorker extends StandaloneWorker (which loads a fully independent draft model with no
    shared embeddings / lm_head). It overrides three hooks:

    Hook What it does
    forward_draft_extend Maps prompt token IDs target→draft before KV-cache prefill
    forward_draft_extend_after_decode Maps accepted tokens target→draft, runs update, restores to target vocab
    capture_for_decode Constrains draft logits to the intersection before top-k sampling

    The 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 weight
    matrix is replaced with a compact [intersection_size × hidden_dim] version
    (_PrunedReindexLMHead), reducing the dominant draft-decoding matmul from
    O(B × V_draft × H) to O(B × K × H) where K ≪ V_draft. CUDA graphs are
    captured after pruning so the graph encodes the smaller computation.

Modified files:

  • python/sglang/srt/speculative/spec_info.py

    • Add TLI = auto() to SpeculativeAlgorithm enum.
    • Add is_tli() predicate.
    • Dispatch to TLIWorker in get_worker_cls(), with a guard that rejects overlap
      scheduling (not yet supported for spec v2).
  • python/sglang/srt/server_args.py

    • Add "TLI" to --speculative-algorithm choices.
    • Add TLI validation block in check_server_args:
      • Requires --speculative-draft-model-path.
      • Disables overlap scheduling and mixed chunked prefill (unsupported).
      • Auto-selects speculative_num_steps / speculative_num_draft_tokens via the existing
        auto_choose_speculative_params helper when not explicitly set.
      • Enforces speculative_eagle_topk == 1 (TLI does not support multi-candidate trees).
      • Enforces speculative_num_draft_tokens == speculative_num_steps + 1.
    • Group "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 5

Parameters 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 with
greedy decoding (temperature=0) to both a baseline server and a TLI server sharing the same
target 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", or
a 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_mixed dataset (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 after
space-prefix normalization by VocabMapping.

Target model Draft model Device Vocab inter. (% of target) Vocab inter. (% of draft) Baseline (tok/s) TLI (tok/s) Accept len Speedup
meta-llama/Llama-3.1-8B-Instruct Qwen/Qwen2.5-0.5B-Instruct H100 NVL 96 GB 85.43% 72.24% 1226 2021 4.21 1.65×
meta-llama/Llama-3.1-8B-Instruct Qwen/Qwen2.5-0.5B-Instruct A100 80GB PCIe 85.43% 72.24% 666 1124 4.09 1.69×
meta-llama/Llama-3.1-8B-Instruct Qwen/Qwen2.5-0.5B-Instruct RTX A6000 48 GB 85.43% 72.24% 410 909 3.81 2.22×

Notes

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:

Feature HuggingFace Transformers (PR #35029, merged ✅) This PR (SGLang)
unk_token_id fallback Required — blocks Llama 3, SmolLM2 Falls back to eos_token_id; warns; only errors if neither exists ✅
Draft LM head pruning _PruneReindexingLMHead _PrunedReindexLMHead; CUDA graphs captured post-pruning ✅
Draft sampling Greedy (argmax) Top-k softmax over intersection tokens ✅
Clone on intersection miss Always Only when tokens actually fall outside intersection ✅

Checklist

  • Format your code according to the Format code with pre-commit.
    • All pre-commit hooks pass: trim-trailing-whitespace, isort, ruff, black, codespell.
  • Add unit tests according to the Run and add unit tests.
    • test/registered/unit/spec/test_vocab_mapping.py: 36 CPU-only unit tests covering _detect_space_sign, _normalize_token, intersection construction, unk_token_id fallback, bidirectional ID mapping, and logit constraining.
  • Update documentation according to Write documentations.
    • docs/advanced_features/speculative_decoding.md: added TLI section (How it works, constraints, usage example), updated quick-guidance bullets, method comparison table, and --speculative-algorithm parameter reference.
  • Provide accuracy and speed benchmark results according to Test the accuracy and Benchmark the speed.
  • Follow the SGLang code style guidance.

cc: @orenpereg @keyboardAnt @wan-danfeng


CI States

Latest PR Test (Base): ❌ Missing run-ci label -- add it to run CI tests.
Latest PR Test (Extra): ❌ Blocked -- run-ci is required first.

@github-actions github-actions Bot added documentation Improvements or additions to documentation speculative-decoding labels Apr 15, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions github-actions Bot added quant LLM Quantization amd dependencies Pull requests that update a dependency file lora Multi-modal multi-modal language model deepseek hicache Hierarchical Caching for SGLang sgl-kernel blackwell SM100/SM120 npu diffusion SGLang Diffusion model-gateway jit-kernel labels Apr 20, 2026
jmamou added 2 commits April 26, 2026 11:37
- 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
@jmamou

jmamou commented May 4, 2026

Copy link
Copy Markdown
Contributor Author

Note on Speed Benchmarks

The speed benchmarks in this PR were produced using the SPEED-Bench dataset infrastructure introduced in a companion PR: #24149 (feat(bench): add SPEED-Bench dataset support to bench_serving).

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 100

The two PRs are otherwise independent — #24149 adds a general-purpose benchmark dataset and has no dependency on TLI.

@jmamou
jmamou requested a review from zijiexia as a code owner May 26, 2026 09:37
jmamou added a commit to jmamou/sglang that referenced this pull request May 26, 2026
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]>
@jmamou
jmamou requested a review from JustinTong0323 as a code owner May 26, 2026 12:14
@jmamou

jmamou commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

/rerun-failed-ci

github-actions Bot and others added 2 commits June 15, 2026 17:03
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
@jmamou
jmamou requested a review from sogalin as a code owner June 17, 2026 10:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

amd blackwell SM100/SM120 deepseek dependencies Pull requests that update a dependency file diffusion SGLang Diffusion documentation Improvements or additions to documentation hicache Hierarchical Caching for SGLang jit-kernel lora model-gateway Multi-modal multi-modal language model npu quant LLM Quantization sgl-kernel speculative-decoding

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants