Support CUDA-graph capture on AITER batch decode (opt-in) - #273
Conversation
Allow backend="aiter" batch decode under CUDA-graph capture. AITER PA v1's launch grid and .so variant are fixed at capture-time shapes, so the wrapper must capture at the maximum sequence length; the kernel early-exits per sequence on context_lens, making shorter replays correct. Previously backend="aiter" raised under use_cuda_graph=True. backend="auto" continues to use the in-tree fa2 kernel under capture: fa2's graph path is capacity-based and capture-order-independent, so it stays the safe default (no behavior change for auto). A one-time warning documents the AITER capture-at-max contract. Adds HIP graph-replay tests and decode benchmarks; updates README + CHANGELOG. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR enables opt-in CUDA-graph capture for ROCm AITER batch decode when callers explicitly set backend="aiter", removing the prior hard error under use_cuda_graph=True. It keeps backend="auto" behavior unchanged (still routes to fa2 under capture) and documents AITER’s stricter “capture at max sequence length, replay shorter only” contract via warnings and docs.
Changes:
- Allow
backend="aiter"to run under CUDA-graph capture, emitting a one-time-per-device warning describing the capture-at-max contract. - Update ROCm tests to validate AITER graph capture + replay correctness, while ensuring
backend="auto"still routes tofa2under capture. - Add ROCm benchmark scripts for eager aiter-vs-fa2 decode and for under-graph replay correctness + latency.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
flashinfer/decode_rocm.py |
Removes the hard failure for AITER+CUDA-graph and adds a one-time warning explaining the capture-at-max contract; preserves auto→fa2 under capture. |
tests/rocm_tests/test_batch_decode_aiter_hip.py |
Drops the obsolete “AITER+graph raises” expectation and adds coverage for AITER CUDA-graph capture/replay correctness and auto routing. |
benchmarks/rocm_benchmarks/bench_decode_graph_hip.py |
New acceptance/benchmark script for graph-captured AITER decode correctness and under-graph latency comparisons to fa2. |
benchmarks/rocm_benchmarks/bench_batch_decode_hip.py |
New eager decode benchmark sweeping batch/kv_len to quantify aiter vs fa2 baseline differences. |
README.md |
Documents that AITER decode under CUDA-graph is opt-in and requires capture at max seq len; clarifies auto routing behavior. |
CHANGELOG.md |
Notes the new opt-in AITER CUDA-graph decode support and new benchmarks/tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Address Copilot review: layout helpers built page indices as arange(batch*npages), which repacked the per-sequence page mapping when replaying with seq_len < capacity. Reserve a stable cap_pages block per sequence and take a prefix for shorter seq_lens, matching a real fixed-capacity paged-KV pool and exercising the capture-at-max contract faithfully. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
flashinfer/decode_rocm.py:1138
- The wrapper docstring earlier in this file still states that
use_cuda_graph=Trueis not supported withbackend="aiter"(seeBatchDecodeWithPagedKVCacheWrapper.__init__docs around decode_rocm.py:853-856). This PR changes the behavior to allow graph capture with explicitbackend="aiter", so the docstring is now misleading for API consumers.
# Resolve auto → concrete backend. AITER decode requires use_tensor_cores=False
# (the AITER PA v1 kernel handles its own dispatch internally). Under CUDA-graph
# capture, `auto` stays on fa2: fa2's graph path is capacity-based and correct
# regardless of capture-vs-replay sizes, whereas AITER's launch grid and .so
# variant are fixed at the shapes seen when the graph is captured and require
# capturing at the maximum sequence length. AITER decode under CUDA graph is
# therefore opt-in via an explicit backend="aiter" (see the capture-at-max
# warning below), not something `auto` selects silently.
Summary
Enable CUDA-graph capture for AITER batch decode via an explicit
backend="aiter". Previouslybackend="aiter"raised underuse_cuda_graph=True, so graph-captured decode always fell back to the in-treefa2kernel — forfeiting AITER's decode throughput on the serving hot path (vLLM/SGLang capture decode in graphs).backend="auto"is unchanged (still resolves tofa2under capture), so this change is purely additive and opt-in.What changed
flashinfer/decode_rocm.py— Remove the hard error that rejectedbackend="aiter"under CUDA-graph capture; add a one-time-per-device warning documenting AITER's capture-at-max-sequence-length contract.backend="auto"still routes tofa2under capture.tests/rocm_tests/test_batch_decode_aiter_hip.py— Drop the obsolete "aiter + graph raises" assertion; addtest_batch_decode_aiter_cuda_graph_replay[fp16/bf16](capture at a max seq len, replay shorter, assert parity vs eager AITER); keeptest_batch_decode_auto_routes_cuda_graph_to_fa2.benchmarks/rocm_benchmarks/bench_decode_graph_hip.py— New: graph capture+replay correctness check plus aiter-vs-fa2 under-graph latency.benchmarks/rocm_benchmarks/bench_batch_decode_hip.py— New: eager fa2-vs-aiter paged-decode sweep (the baseline that motivates the feature).README.md,CHANGELOG.md— Document the opt-in graph path and its capture-at-max contract.Architecture / design notes
Why opt-in rather than routed by
auto: AITER PA v1's launch grid and compiled.sovariant (npar_loops) are fixed at the shapes seen when the graph is captured. This is correct only when you capture at your maximum sequence length — the kernel early-exits per sequence oncontext_lens, so replays shorter than captured are correct, but longer ones are not. fa2's graph path, in contrast, is capacity-based and capture-order-independent. Because AITER's contract is stricter,automust not select it silently: doing so regressedtest_cuda_graph_batch_decode_with_paged_kv_cache, which captures at 1 page/seq and then replays at the fullkv_len.autoautoaiteraiterKnown limitation:
return_lse=Trueon the AITER decode path is served through the lazily-built fa2 shadow plan and is not covered under capture here; usereturn_lse=False(the decode norm) orbackend="fa2"if you need LSE under graph.Benchmark results
Paged decode, GQA 32/8, HD128, bf16, page_size 16, MI300X (gfx942). Under-graph replay latency at batch=128, seq=4096:
AITER is 1.31× faster under graph at this shape (eager sweep shows ~1.3–1.45× at batch ≥ 32; near parity / fa2-favored at batch ≤ 8, where routing should still prefer fa2). Correctness: graph replay matches eager AITER exactly (max|diff| = 0.0000) for seq ∈ {512, 1024, 2048, 4096} within captured capacity.
Test plan
tests/rocm_tests/test_batch_decode_aiter_hip.py(full file,--reruns 2, MI300X) — passtests/rocm_tests/test_batch_decode_kernels_hip.py -k cuda_graph(--reruns 2) — pass (confirmsautostill routes to fa2 under capture; no regression)benchmarks/rocm_benchmarks/bench_decode_graph_hip.py— replay parity vs eager + 1.31× under-graph speeduppre-commit run --fileson all changed files — pass