Skip to content

Pass uniform_q_len to the FlashInfer target-verify plan for EAGLE speculative decoding - #44115

Open
brandonfzhang wants to merge 5 commits into
sgl-project:mainfrom
brandonfzhang:brandonz/verify-plan-uniform-q-len
Open

brandonfzhang wants to merge 5 commits into
sgl-project:mainfrom
brandonfzhang:brandonz/verify-plan-uniform-q-len

Conversation

@brandonfzhang

@brandonfzhang brandonfzhang commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

EAGLE target verify gives every request exactly num_tokens_per_req (= draft_token_num) query rows: EagleVerifyInput builds qo_indptr as a constant-stride arange. FlashInfer's FA2 CUDA-graph planner cannot see that from the ragged CSR alone, so it sizes its query tile for the worst case (total_rows - bs + 1 rows per request). For a typical verify batch (32 q-heads, TP8, 4 draft tokens) that means running a 64-row tile on a 16-packed-row problem.

flashinfer-ai/flashinfer#6144 added a uniform_q_len keyword to BatchPrefillWithPagedKVCacheWrapper.plan(): a host-side promise that every request has exactly that many query rows, letting the planner size the tile for uniform_q_len * gqa_group_size packed rows. The same hint is the dispatch key for FlashInfer's specialized SM90 fp8-KV verify kernel from that PR.

Modifications

One wiring change in flashinfer_backend.py: call_begin_forward adds uniform_q_len=num_tokens_per_req to the paged plan kwargs for EAGLE verify batches (SpecInputType.EAGLE_VERIFY; DFLASH / NGRAM verify and draft extends keep the stock plan).

Capture fixes the verify kernel's query tile from the hint, so every plan through a captured verify wrapper must carry the same hint: the hint is passed on both plan paths, the real plan() and the SGLANG_OPT_FLASHINFER_VERIFY_HOST_PLAN sync-free replay (fast_prefill_plan, which previously pinned the argument to 0).

Requires the FlashInfer >= 0.7.2.post1 pin (#46926), the first release with the plan(uniform_q_len=...) keyword; this PR should land after it. An earlier revision carried an inspect.signature capability probe to tolerate older FlashInfer; per review it is dropped.

Adds a CPU unit test for the gating (only EAGLE verify batches are hinted, unset num_tokens_per_req is not a promise).

Benchmarks

Qwen3.5-397B-A17B-FP8, TP8/EP8 on 8x H200, EAGLE (steps 3 / topk 1 / draft 4), 256k-context agentic workload at 10 concurrent sessions, 1 h steady state per arm. Reported metric: normalized interactivity (output tokens/s per request over each request's own decode, so it isolates decode speed from TTFT and queueing). Each arm is compared against a production run of the same day on the same hardware; cross-day control drift was 1.8% at p50 and 3.7% at p90.

configuration p50 p90
baseline (no hint, same-day control per arm) 113.5 / 115.5 tok/s 63.6 / 65.9 tok/s
uniform_q_len hint (planner tile only) 130.5 tok/s (+15.0%) 71.0 tok/s (+11.6%)
hint + FlashInfer's SM90 fp8-KV verify kernel (dispatches behind the same hint) 136.1 tok/s (+17.8%) 73.6 tok/s (+11.7%)

Corresponding ITL p50: −15.0% and −19.4%. Output tok/s +6%, accept length unchanged (3.30 vs 3.28), output lengths unchanged, GSM8K accuracy at parity (197 vs 196 / 200 correct, paired).

Checklist

  • Format your code: pre-commit run --all-files
  • Add unit tests
  • Update documentation / benchmarks as needed

🤖 Generated with Claude Code


CI States

Latest PR Test (Base): ❌ Run #41449065916
Latest PR Test (Extra): ❌ Run #41449065830
Latest PR Test (AMD ROCm 10): ❌ Run #41449065807

Tip

Base CI only covers the core cases, and its workload may not exercise your change. Use /rerun-test to run the tests your change touches, or copy this prompt for an agent:

Find the registered tests this PR affects and run them with /rerun-test, following https://github.com/sgl-project/sglang/blob/main/.agents/skills/rerun-test/SKILL.md

…culative decoding

EAGLE target verify gives every request exactly num_tokens_per_req
(== draft_token_num) query rows: EagleVerifyInput builds qo_indptr as a
constant-stride arange. FlashInfer's FA2 CUDA-graph planner can use that
promise (BatchPrefillWithPagedKVCacheWrapper.plan(..., uniform_q_len=N),
new in flashinfer-ai/flashinfer#6144) to size its query tile for
N * gqa_group_size packed rows instead of the ragged worst case
total_rows - bs + 1; without it the verify attention of a 32-head / TP8 /
4-draft-token model runs on a 64-row tile for a 16-row problem.

call_begin_forward now adds uniform_q_len=num_tokens_per_req to the paged
begin_forward kwargs when the input is an EAGLE verify batch, the installed
FlashInfer's plan() takes the keyword (checked once per wrapper class with
inspect.signature; **kwargs-only fails closed) and the plan is the real one
(fast_prefill_plan never serves the custom-mask verify plan). On an older
FlashInfer the call is byte-for-byte the previous one. FlashInfer re-checks
the promise against qo_indptr on the host and plans without the hint if it
does not hold, so the keyword cannot break a plan. Other verify families
(DFLASH, NGRAM) and draft extends keep the stock plan.

No runtime effect until the FlashInfer pin includes sgl-project#6144. With it,
measured on Qwen3.5-397B-FP8 with EAGLE on 8x H200 at 10 concurrent
agentic sessions: the planner hint alone -15.0 % ITL p50; with
FlashInfer's specialized SM90 fp8-KV verify kernel, which dispatches
behind the same hint, -19.4 % ITL p50, +6 % output tok/s, accept length
unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@YAMY1234

Copy link
Copy Markdown
Collaborator

Overall the code logic LGTM. Maybe we can wait for the FlashInfer version bump, then retest the performance and run CI.

@nvpohanh nvpohanh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[by Claude Code] Severity: style | Confidence: High

Issue

The Benchmarks section gives results but no commands, so others cannot rerun them. This PR has no effect with the pinned FlashInfer 0.7.1, so the FlashInfer build also matters.

Fix

Please add:

  • The server launch command for Qwen3.5-397B-A17B-FP8 on 8x H200 (TP8/EP8, EAGLE 3/1/4), and which FlashInfer build you used (one that includes flashinfer-ai/flashinfer#6144). For the "hint only" row, say how the SM90 kernel was turned off, for example FLASHINFER_DISABLE_EAGLE_VERIFY_FP8KV_SM90=1.
  • The benchmark command for the 256k-context agentic workload, with the trace or dataset and its length and session settings. If the trace is private, a public long-context run such as python -m sglang.bench_serving against the same server is enough.
  • The GSM8K command behind "197 vs 196 / 200".

detain added a commit to detain/sglang that referenced this pull request Oct 15, 2026
STACK=qwennext-stack-204-2026-10-15, code tip c8792c2. Entries: 200
sgl-project#43527 QSA cache sharding (default OFF), 201 sgl-project#39716 tokenizer-clone
preprocessing (DEFAULT-ON), 202 sgl-project#38650 reconcile (upstream adopted,
entry-156 helper dropped), 203 sgl-project#42388 linear lossless-verify (default
OFF, replayssm-exclusive A/B arm), 204 no-GPU test maintenance trio.
Header/CAVEATS: FI 0.7.1 venv bump marked resolved (entries 162-164
PCIe-IPC ACTIVE next restart; 10-09/10-14 INERT notes annotated
HISTORY), sgl-project#44115 re-skipped at 0.7.1 (uniform_q_len still absent from
wrapper plan()), former DEFER/DEDICATED-WINDOW block replaced with
resolution pointers, new BEHAVIOR WATCH bullet for the restart.

Co-Authored-By: opencode <noreply@opencode.ai>
detain pushed a commit to detain/sglang that referenced this pull request Oct 19, 2026
…tune-gate test fix (b0fce3b), and correct the PCIe-IPC "ACTIVE" claim

Entry 228 records the FI bump in both halves: the clean
cherry-pick of upstream 44be30d/sgl-project#44591 (2 Dockerfile ARGs,
the sole pyproject pin at python/pyproject.toml:42, and the
engine.py:1925 assert_pkg_version FLOOR) and the operator's
prod-venv swap 0.7.1 -> 0.7.2 (one-line freeze diff, no
jit-cache/cubin companions). It carries the 53-module/198-
symbol API audit (1 benign missing symbol), the pin-vs-wheel
reasoning (assert_pkg_version is a FLOOR -- the real hazard
was `uv pip install -e 'python[dev]'` silently downgrading
the wheel back to 0.7.1), the deliberate sglang-kernel 0.4.9
floor with the sgl-project#44590 pairing constraint, the rollback order,
the first-boot cost (0.7.2/120f holds nothing but a 0-byte
log => re-JIT of sampling + XQA, autotune sweep cold; 147 s
baseline), sgl-project#44115 as PERMANENTLY not-applicable (plan
positional list byte-identical, no uniform_q_len), sgl-project#43471 as
never version-gated, the native-ReplaySSM convergence lever
and the new `cake` mm_fp4 backend.

CORRECTION: entries 162-164 (PCIe-IPC vocab gathers) were
never ACTIVE. The wheel satisfies a dependency; the transport
is behind SGLANG_ENABLE_PCIE_IPC_ALLREDUCE = EnvBool(False)
with no CLI flag, unset in the prod run script and absent from
the live process env. Prod TP4 vocab gathers and the bf16
decode all-reduce remain on NCCL; testing it is the top
pending A/B (harness arms pcie_ipc_on / pcie_ipc_on_ll /
pcie_ipc_off_recheck). Annotated in place at all eight stale
claims (header STACK block, 10-15 updated block, FOURTH-merge
synced line, entry 162 bracket, merge-4 entry line, the
merge-4 battery bullet, the entries-200-204 BEHAVIOR WATCH
bullet, the LIVE VENV bullet).

Entry 229 records the wheel-independent autotune-gate fix:
7P/1F -> 9P (stale auto-Mock met entry 225's b12x arm; the
fp8_utils per-tensor patch pins a real production probe, so
it is legitimate, not a workaround; WIP DBG scaffolding
stripped pre-commit; diff is +29/-2, not +31/-2).

Header/CAVEATS: STACK 227 -> 229, tip -> 2b46270, new
updated: block for 228-229, MERGE sgl-project#5 STILL OWED refreshed
(origin/main 716c915, +325), the FI-0.7.2 ADDENDUM to the
pinned-baseline bullet (autotune_gate 8P -> 9P chain, NEW
test_jit_cache 42P, kernels_namespace REMAINS 76P -- the
"78P" claim is false and appears nowhere in this file), a new
top-priority BEHAVIOR WATCH bullet for the first 0.7.2 boot,
and the supersede-in-place annotation on the "NOT TAKEN (a)
FLASHINFER IS NOT BUMPED" bullet (direction-correct,
mechanism-wrong). Push state recorded truthfully: detain
carries the branch up to 3c44685; these commits are
unpushed. stack.txt only; LIVE ONLY AFTER RESTART.
detain added a commit to detain/sglang that referenced this pull request Oct 29, 2026
…k-274, wanted_stack LANDED marks, CAVEATS docs-only ledger items

(A) Header: STACK line 260->274; three wave lines (+spec-mamba-2026-10-28
(pr45491+pr45435+pr45472+pr45670), +admission-2026-10-28(pr46272*+
pr43484*+pr46496*+pr46643*+pr45607*+pr45789*), +parsers-2026-10-28
(pr44124+pr45631+pr45855+pr46167)); tip line -> 2714457; updated
261-274 headline block (entry-119 SUPERSEDED by upstream sgl-project#45435/sgl-project#45472;
classic-pool mamba admission gate w/ upstream ImportError CAUGHT;
UNCONDITIONAL latch reset = TTFT watch; coder-parser union; logits
trilogy as CONTRACT UNIT; 14 picks, 0 integrator conflicts).

(B) wanted_stack.txt: FAM-A5 + FAM-B234 blocks deleted -> LANDED records
(chain shas, interlock audit, supersession + behavior-change pointers
into entries 261-270); SECTION-B sgl-project#44124/trilogy blocks -> LANDED;
sgl-project#37958/sgl-project#45559 block -> DONE (verdict now in CAVEATS).

(C) DOCS-ONLY LEDGER ITEMS: CAVEATS gains the sgl-project#37958/sgl-project#45559 VERIFIED-
INERT bullet (two independent hard gates, coarse-barrier fallback,
do-not-re-litigate-at-merge-sgl-project#5 line, revisit condition = dropping NEXTN
or HiCache) and the dated C0b/C0c corrections (sgl-project#44030 obsolete rationale,
sgl-project#44858 REVERSED by sgl-project#45289 [ported-pending wt-moe], sgl-project#44115 permanent N/A,
10-28 open-status re-verification list, no-pin-moves note: 0.7.2/0.4.9
floor unchanged) with the C0a tick (sgl-project#37625/sgl-project#37941/sgl-project#39727 still
outstanding; only sgl-project#43927/sgl-project#44849 partially satisfied). C0b/C0c/C8 marked
DONE in wanted_stack; C0a stays open.

@nvpohanh nvpohanh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[by Claude Code] This review is done by agents. Please feel free to disagree if they do not make sense.

brandonfzhang and others added 2 commits October 29, 2026 18:24
…robe

Capture fixes the verify kernel's query tile from uniform_q_len, so every
plan through a captured verify wrapper must carry the same hint. The
SGLANG_OPT_FLASHINFER_VERIFY_HOST_PLAN replay path (fast_prefill_plan)
passed a hard-coded 0; it now takes uniform_q_len and the call site sets
the hint for both plan paths.

With the pin moving past FlashInfer 0.7.2.post1 (sgl-project#46926), plan() always
has the keyword: the inspect-based capability probe, its imports and its
test cases are dropped, and the helper docstring now states the captured-
wrapper contract instead of describing a host-side fallback that does not
hold across replays.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

This comment has been minimized.

SGLang now always hints the EAGLE verify plan, and production captures
go through the hinted path, so the test's direct reference plan and its
step-0 capture stand-in must pass uniform_q_len too for the
plan_info-equality contract to compare like with like.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

This comment has been minimized.

- Rewrite the call_begin_forward and host-plan test comments in plain
  English.
- Explain when num_tokens_per_req <= 0 in _verify_plan_uniform_q_len.
- Remove test_verify_plan_uniform_q_len.py; it only mocked the helper on
  CPU. test_flashinfer_host_plan.py covers the hint on real wrappers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nvpohanh

nvpohanh commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator

[by Claude Code] Pushed b06b088 to address the latest review comments.

This PR still depends on the FlashInfer upgrade to 0.7.2.post1 in #46926. The currently pinned FlashInfer 0.7.2 has no uniform_q_len keyword in BatchPrefillWithPagedKVCacheWrapper.plan(), so this PR must merge after #46926.

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Required checks failed on b06b088; this PR cannot be merged until they pass.

PR title (run)

PR title must use "[Type] scope: title" or "[Type] [Hardware] scope: title". Type must be one of: Feat, Fix, Refactor, Perf, Doc, Test, CI, Chore. Scope is required and may be any module or model name, with unrestricted letter case (letters, numbers, dots, underscores, slashes, or hyphens). Hardware is optional; use a single tag without spaces. Use uppercase hardware tags (AMD, NVIDIA, NPU, MI300X), except for exactly these four spellings: Intel, Apple, Huawei, Ascend. Reverts must use GitHub's default format: Revert "original title". Use one space between fields and after the colon, with a non-empty title and no leading or trailing whitespace. Examples: "[Fix] attention: Fix KV cache indexing", "[Feat] qwen3: Add model support", "[Perf] [AMD] flash-attn: Optimize decode performance".
Edit the title; the check reruns on its own.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants