Skip to content

feat: add FlashInfer attention backend for SM120 decode - #112

Open
yanbao1217 wants to merge 4 commits into
ThinkFlowLab:mainfrom
yanbao1217:flashinfer-sm120
Open

yanbao1217 wants to merge 4 commits into
ThinkFlowLab:mainfrom
yanbao1217:flashinfer-sm120

Conversation

@yanbao1217

@yanbao1217 yanbao1217 commented Oct 10, 2026 •

Copy link
Copy Markdown

Adds flashinfer as a selectable paged-decode attention backend (FlashInfer XQA kernels, flashinfer-python >= 0.7.0), giving RTX 50-series (SM120) a second attention path alongside Triton. FlashAttention 2/3/4 reject SM120 for paged KV, so Triton is currently the only option there. Refs #69.

Rebased onto the merged attention contract layer (2534e24 + 3defab4): FlashInfer registers through vllm_rlt.attention with explicit selection and an auto slot between FlashAttention-2 and Triton, declaring BackendCapabilities(cuda_graphs, async_scheduling). The SM gate keeps auto away from FlashInfer on SM8/9/10, so auto only reaches it where every FlashAttention generation is rejected.

Scope

  • Decode and per-token prefill with Triton-equivalent semantics, including CUDA Graph capture and asynchronous scheduling.
  • No packed prefill: the FA4-only path stays closed because the adapter provides no prefill entry point. No prefill/decode disaggregation.
  • float16 rejected at startup: kernels compute correctly, but the 0.7.0.post1 f16 JIT module crashes the process (host SIGFPE) on first in-process use on SM120.
  • Explicit startup validation: page sizes {16, 32, 64, 128} (compiled XQA variants), head_dim divisible by 16 within [16, 512], SM major {9, 10, 12} — validated on 120 only.

Design notes

  • Thin translator over the existing five-argument attention contract: empty-batch early return (a zero-size batch host-crashes the kernel dispatch), block_tables clamped non-negative so -1/zero/stale graph-replay padding stays invisible, seq_lens cast to uint32, explicit kv_layout="NHD".
  • The 128 MiB kernel workspace allocates lazily on first execution, so backend selection itself performs no device allocations; engines profile before any graph capture, so the workspace exists by then.
  • Auto-selection tests cover the SM120 matrix (FlashInfer selected when installed, Triton when the package is missing) and explicit no-fallback behavior.

Validation (RTX 5090, Ouro-1.4B BF16, block_size=16, four-loop decode)

  • CPU suite: 501 passed, 125 skipped (excluding six pre-existing test_scheduler_selection.py failures on current main: OuroConfig.tiny is missing); ruff clean.
  • GPU suite: oracle parity on non-contiguous layer slices; padding tri-state bitwise-invisible (-1 / zero / stale replay ids); zero-length rows exact zeros; batch-composition determinism (rtol=0, atol=0); CUDA-graph capture with rewritten buffers; token/exit-depth equality against Triton across sync/async/multi-stream x last_exited/shared x static buffers.
  • Real-model greedy generation smoke: coherent output with fixed-depth exits.
  • GSM8K-87 (single greedy pass, frozen protocol, HF baseline 59): Triton 61, FlashInfer 58. 64/87 generations are token-identical between the two backends; the 23 divergent ones flip near-tied greedy argmax decisions at scattered positions with unchanged exit depths. Kernel-level deviation vs a float32 oracle is 1e-3..4e-3 — exactly one BF16 ulp (the Triton kernel is near-exact by comparison because it shares the oracle's serial order). Documented in docs/flashinfer.md; Triton remains the default backend.

Environment requirements (docs/flashinfer.md)

pip install -e ".[flashinfer]", plus a CUDA toolkit with nvcc >= 12.9 reachable via CUDA_HOME (the kernels JIT-compile for compute_120f) and that toolkit's lib directory on LD_LIBRARY_PATH.

Follow-ups

  • Packed prefill support: generalize the remaining generation == 4 gate sites now that capabilities are declared.
  • Greedy-parity numerics (investigated, negative so far): the XQA path rejects float32 outputs (xqa.py asserts output dtype matches query), and 0.7.0.post1 → 0.7.1rc2 contains zero XQA changes, so no reduction-precision knob exists today; the 1-ulp difference is inherent to the different accumulation order. Remaining option: logit-gap forensics on the three flipped GSM8K answers.
  • Matched low/high-concurrency throughput report against Triton as a separate results PR.

🤖 Generated with Claude Code

Register FlashInfer (flashinfer-python >= 0.7.0, XQA kernels) as a
paged decode backend through the attention contract layer:

- explicit "flashinfer" selection plus an auto slot between
  FlashAttention-2 and Triton; the SM gate keeps auto away from it on
  SM8/9/10, so it is only reached where every FlashAttention generation
  is rejected (SM 12.x)
- capabilities declare CUDA graphs and async scheduling; packed prefill
  stays FA4-only because the adapter leaves the kernel without a
  prefill entry point
- validation rejects unsupported setups explicitly (float16 unvalidated
  on SM120, page sizes outside the compiled XQA variants, head_dim not
  divisible by 16); the workspace allocates lazily on first execution so
  backend selection performs no device allocations
- empty batches return early because the kernel host-dispatch SIGFPEs on
  batch 0; padded block-table columns are clamped non-negative and the
  kernel bounds reads by seq_lens, so -1/zero/stale padding stays
  invisible
- requires CUDA_HOME with nvcc >= 12.9 and that toolkit's lib directory
  on LD_LIBRARY_PATH for the just-in-time compiled modules
The run subcommand accepts --attention-backend (torch/triton/flashinfer,
default triton), records it in the run metadata, and passes it through
Generator to the engine instead of the hardcoded Triton selection.
Document installation and the two runtime requirements (CUDA_HOME with
nvcc >= 12.9 for JIT kernels, the toolkit lib directory on
LD_LIBRARY_PATH), scope and explicit rejections, backend selection
guidance, and the GSM8K-87 greedy variation measured on RTX 5090.
@yanbao1217

Copy link
Copy Markdown
Author

Matched throughput results for the FlashInfer vs Triton decode path (RTX 5090, Ouro-1.4B BF16, block_size=16, fixed four-loop depth).

Protocol follows the PR #30 methodology scaled to a 32 GB device: 512-token random-token inputs, 128 output tokens, 2x concurrency requests with closed-loop replacement, three trials per condition (medians below); timing covers prefill and drain and excludes loading, warmup, and tokenization; both backends run identical feature stacks (eager, and graphs = async scheduling + multi-stream + static buffers + pad-to-power-of-two + CUDA graphs).

stack concurrency Triton out-tok/s FlashInfer out-tok/s delta
eager 1 22.1 21.7 -2%
eager 8 151.6 151.4 ~0%
eager 32 441.0 429.8 -2.5%
graphs 1 49.7 65.9 +33%
graphs 8 220.7 246.6 +12%
graphs 32 392.5 384.0 -2%

Notes:

  • The FlashInfer advantage concentrates exactly where the XQA split-KV parallelism matters: the performance-oriented graphs stack at low concurrency (+33% for a single request), the typical single-user RTX 5090 configuration. At concurrency 32 both kernels saturate the GPU and the difference is within noise (-2%).
  • Under the eager stack the two backends are equal within noise: per-step host work dominates there, not attention.
  • Greedy paired runs: per-request output token counts matched 540/540 across all conditions and trials.
  • Raw per-trial data (including KV plans and selection info) is recorded locally; the benchmark driver stays out of the tree in line with keeping benchmark artifacts in PRs rather than in main (as with [FEAT] Capture fixed-depth speculative decode with CUDA Graphs #48).

🤖 Generated with Claude Code

Document the --attention-backend flag on the native run command, the
RTX 5090 Triton/FlashInfer scores on the frozen 87-question case, and
the expected single-pass greedy variation between BF16 kernels.

@0z5a 0z5a 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.

Thanks for adding FlashInfer through the existing attention backend contract and for the C1/C8/C32 measurements on RTX 5090. Reviewed 1a24e52; I have one selection concern before merging and several follow-up suggestions.

1. [P2] Restrict automatic selection to validated architectures.

auto places FlashInfer before Triton, and the constructor accepts SM90/SM100 with only a warning. A CPU selection probe using the original constructors with hardware/import boundaries mocked reproduces this on SM90: BF16, head dimension 128, page size 16, FA4/FA3 unavailable and FA2 rejecting the page size selects Triton at the base revision and FlashInfer at this head. SM100 has an analogous fallback change.

Could we gate automatic FlashInfer selection to the validated SM120 target for now, retain explicit opt-in on H100, and add fallback regression tests? The PR description's automatic-selection restriction is currently stronger than the code implements.

2. Backend capability reuse.

The kernel's self.capabilities duplicates the wrapper's _FLASHINFER_CAPABILITIES. Please use the existing backend contract as the single source of truth and exercise that contract in selection tests.

3. Metadata and workspace validation.

Please profile the per-invocation block_tables.clamp_min(0) and context_lengths.to(torch.uint32) overhead at larger batches before considering preparation once per KV batch. Preserve padding and graph-buffer update semantics if these tensors are reused.

The instance-owned 128 MiB workspace is a useful ownership choice. The runner currently serializes attention on its core stream; I have not established a workspace race. Please reuse the existing CUDA Graph test matrix for FlashInfer and cover replay, eager fallback, cancellation and request reuse to verify the workspace lifetime and ordering contract.

4. High-concurrency E2E coverage.

For follow-up validation, C64/C128 with dynamic arrivals, mixed prompt lengths and recurrence depths, cancellation/reuse, and both supported KV layouts would be useful. Please report latency percentiles, graph replay/fallback counts, KV reclamation and output/exit-depth correctness alongside output tok/s. Account for the reported BF16/GSM8K divergence explicitly; matching token counts alone does not establish generation equivalence.

I can help run the H100 side of this validation. Please ping me @0z5a if useful.

}
_AUTO_PRIORITY = ("flash_attn_4", "flash_attn_3", "flash_attn_2", "triton", "torch")
_FLASHINFER_CAPABILITIES = BackendCapabilities(cuda_graphs=True, async_scheduling=True)
_AUTO_PRIORITY = ("flash_attn_4", "flash_attn_3", "flash_attn_2", "flashinfer", "triton", "torch")

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.

[P2] Gate the automatic FlashInfer candidate to validated architectures. Putting FlashInfer before Triton changes the fallback on SM90/SM100 because its constructor accepts those architectures with only a warning. Using the pinned original selection/constructor functions with mocked hardware/import boundaries, SM90 BF16, head dimension 128 and page size 16 selects Triton at the base revision when FA4/FA3 are unavailable and FA2 rejects the page size, but selects FlashInfer here. This exceeds the stated SM120 validation scope. Please keep explicit SM90/SM100 opt-in available while restricting the auto candidate to validated targets, and add fallback tests for missing FlashAttention candidates.

@yanbao1217

Copy link
Copy Markdown
Author

Thanks @0z5a — acknowledged. Plan: gate the automatic candidate to SM120 while keeping explicit opt-in elsewhere, make the contract layer the single source for the capability declaration, profile the per-call clamp/cast conversions, and extend the graph-matrix coverage (replay, eager fallback, cancellation, request reuse) for FlashInfer. The C64/C128 dynamic-arrival validation sounds right as a separate follow-up; I'd gladly take you up on the H100 half once these land.

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.

2 participants