feat(hip): cascade attention support on ROCm using HIP - #221
Merged
demandal25 merged 6 commits intoMay 12, 2026
Conversation
Binds the full cascade kernel family for HIP: - merge_state / merge_state_in_place / merge_states (previously partial) - variable_length_merge_states / attention_sum / variable_length_attention_sum (new) Splits the existing cascade.cu into cascade.cu (kernels) + flashinfer_cascade_binding.cu (TORCH_LIBRARY bindings), mirroring the CUDA layout so gen_cascade_module() resolves both files unchanged. Exports MultiLevelCascadeAttentionWrapper, BatchDecodeWithSharedPrefixPagedKVCacheWrapper, BatchPrefillWithSharedPrefixPagedKVCacheWrapper, and the merge_state* family from flashinfer.__init__ on HIP, placed after the sys.modules injection so cascade.py resolves flashinfer.prefill / flashinfer.decode to the ROCm implementations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MergeStates, VariableLengthMergeStates, and VariableLengthAttentionSum now use num_threads=(bdx<=16)?64:256 on HIP instead of the CUDA-fixed 128, fitting one wavefront per threadblock for head_dim<=128. num_smem_stages is reduced from 4 to 1 since pred_load is synchronous on CDNA3 (no async pipeline), cutting LDS usage by 4x and improving occupancy without any correctness risk. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds an optional partial_state=(out, lse) kwarg to BatchPrefillWithPagedKVCacheWrapper.run(). When set, the prefill kernel merges its output with the prior cascade level's state in-register (no-split-KV path), or calls MergeStateInPlace after VariableLengthMergeStates (split-KV path), eliminating a separate merge_state_in_place kernel launch per cascade level. Plumbing: partial_o/partial_lse added to BatchPrefillPagedParams, batch_prefill.cu, batch_prefill_customize_config.jinja, and the JIT pybind declaration. The fused path is gated by FLASHINFER_HIP_FUSED_CASCADE=1 in MultiLevelCascadeAttentionWrapper (_HIP_FUSED_CASCADE flag in cascade.py) and is off by default. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
test_cascade_hip.py covers merge_state, merge_state_in_place,
merge_states (fp16/bf16, multiple shapes), merge_state_in_place with
boolean mask (all-ones, all-zeros, random), and fused_cascade_epilogue
correctness vs float32 reference.
test_shared_prefix_kernels_hip.py ports
test_batch_attention_with_shared_prefix_paged_kv_cache from the CUDA
suite (MultiLevelCascadeAttentionWrapper vs legacy shared-prefix
wrappers) and adds test_multilevel_cascade_fused_vs_unfused comparing
the FLASHINFER_HIP_FUSED_CASCADE path against the standalone
merge_state_in_place path for shared_kv_len in {128, 512} -- covering
both the no-split-KV and split-KV code paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add warp_sync_state to generic/cascade.cuh with HIP wave-64 barrier (__builtin_amdgcn_wave_barrier) for future persistent merge-states kernel - Flesh out test_fused_cascade_epilogue with real fused vs unfused comparison - Extend test coverage to head_dim=256, shared_kv_len=2048 in shared-prefix tests - Update JIT warmup fixture to precompile head_dim=[128,256] - Hoist duplicate MultiLevelCascadeAttentionWrapper construction out of if/else - Increase workspace buffers 32MB->128MB to support head_dim=256 memory needs - Fix stream variable types in cascade.cu for hipStream_t consistency - Replace assert with raise ValueError for partial_state validation in prefill_rocm.py - Remove redundant partial_o/partial_lse constructor initializers in default_prefill_params.cuh Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR extends FlashInfer’s cascade attention feature set on ROCm/HIP by wiring up the cascade kernel family (and associated wrappers) in the HIP extension, aligning the ROCm cascade module layout with CUDA, and adding an optional fused “cascade epilogue” path for prefill that merges a prior cascade state directly in-kernel.
Changes:
- Adds/exports ROCm cascade bindings (merge/merge-in-place/merge-states + variable-length variants + attention-sum variants) and makes HIP
flashinfer.__init__expose cascade wrappers + merge_state* APIs. - Introduces an optional fused cascade epilogue for HIP FA2 prefill via
partial_state=(out, lse)(gated byFLASHINFER_HIP_FUSED_CASCADE=1) and corresponding kernel parameter plumbing. - Adds ROCm test coverage for cascade ops and shared-prefix multi-level cascade behavior; tunes merge kernel launch geometry for CDNA3.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/rocm_tests/test_shared_prefix_kernels_hip.py | Adds ROCm tests comparing multilevel cascade vs shared-prefix wrappers and fused vs unfused paths. |
| tests/rocm_tests/test_cascade_hip.py | Adds ROCm tests for merge_state APIs, masks, and fused epilogue correctness. |
| tests/rocm_tests/test_activation_hip.py | Replaces large header docstring with SPDX header. |
| include/flashinfer/attention/generic/prefill.cuh | Implements HIP-only fused cascade epilogue merge and post-merge handling in dispatched path. |
| include/flashinfer/attention/generic/default_prefill_params.cuh | Adds partial_o / partial_lse pointers to prefill params. |
| include/flashinfer/attention/generic/cascade.cuh | Adds helper + CDNA3 tuning for merge/attention-sum kernels. |
| flashinfer/prefill_rocm.py | Adds partial_state plumbing/validation and passes optional partial tensors to ROCm FA2 op. |
| flashinfer/csrc_rocm/flashinfer_cascade_binding.cu | Registers ROCm cascade ops in the extension library. |
| flashinfer/csrc_rocm/cascade.cu | Implements ROCm C++ entrypoints for variable-length merge/attention-sum ops. |
| flashinfer/csrc_rocm/batch_prefill.cu | Extends ROCm paged prefill entrypoint with optional partial-state tensors. |
| flashinfer/csrc_rocm/batch_prefill_jit_pybind.cu | Updates the pybind declaration to match the new optional args. |
| flashinfer/csrc_rocm/batch_prefill_customize_config.jinja | Extends generated paged params with optional partial-state pointers. |
| flashinfer/cascade.py | Adds HIP fused-cascade toggle and routes multi-level cascade to the fused prefill path when enabled. |
| flashinfer/init.py | Exports cascade wrappers and merge_state* from the HIP entrypoint after ROCm module injection. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- batch_prefill.cu: assert partial_o and partial_lse are always paired - prefill_rocm.py: validate partial_state[1] (lse) dtype and device - cascade.cu: check indptr is int32 before casting in variable_length_merge_states and variable_length_attention_sum (int64 indptr would silently corrupt memory) - generic/cascade.cuh: document warp_sync_state caller (PersistentMergeStatesKernel) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
demandal25
merged commit May 12, 2026
b9a92b5
into
AMD-Ecosystem:amd-integration
0 of 2 checks passed
3 tasks
demandal25
added a commit
that referenced
this pull request
May 20, 2026
) ## Summary Suppresses a mypy `call-overload` error at `flashinfer/cascade.py:547` that has been failing `pre-commit` on every PR since cascade attention landed (#221), with a localized `# type: ignore[call-overload]` and an explanatory comment. ### What changed - **`flashinfer/cascade.py:547`** — Added `# type: ignore[call-overload]` to the `_HIP_FUSED_CASCADE` branch's call to `wrapper.run(..., partial_state=(out, lse))`, plus a two-line comment explaining the runtime aliasing that mypy cannot see. No behavior change. mypy now passes. ### Root cause `cascade.py` imports `BatchPrefillWithPagedKVCacheWrapper` from `flashinfer.prefill` (the CUDA module). At runtime on HIP, `flashinfer/__init__.py:262` does: ```python sys.modules["flashinfer.prefill"] = sys.modules["flashinfer.prefill_rocm"] ``` so `wrapper` is actually the HIP variant, whose `run()` accepts `partial_state`. mypy can't follow that runtime `sys.modules` swap — it always sees the CUDA `prefill.py` overloads, which don't declare `partial_state` — so it errors with: > `No overload variant of "run" of "BatchPrefillWithPagedKVCacheWrapper" matches argument types` This was the only mypy error in the tree, so every PR's `pre-commit` check has shipped red since #221. ### Design choice: minimal `type: ignore` vs broader refactor | Option | Cost | What it buys | |---|---|---| | **This PR**: `# type: ignore[call-overload]` + comment | 4 lines (3 added) | mypy passes; runtime semantics unchanged | | Move HIP-fused branch into a HIP-side helper + `cast(HIPWrapper, wrapper)` + fix HIP `@overload`s | ~10 lines, conditional import dance | mypy passes; `cast` documents intent slightly better; HIP `@overload`s match impl | | Add `partial_state` to CUDA `@overload`s with a runtime `raise NotImplementedError` | ~6 lines, CUDA-side surface change | API parity at type level | Semantically, `cast(HIPWrapper, wrapper)` and `# type: ignore[call-overload]` say the same thing to mypy — *"trust me, this call is valid"* — and carry identical runtime risk. Reasons for the minimal fix in this PR: 1. **`_HIP_FUSED_CASCADE` is opt-in and default off** (`os.environ.get("FLASHINFER_HIP_FUSED_CASCADE", "0") == "1"`). It's an experimental kernel-fusion optimization. A heavier mypy ceremony for an opt-in experimental branch is poor ROI. 2. **The underlying architectural wart is `_HIP_FUSED_CASCADE` itself**, not its visible type symptom. Routing through a HIP helper or adding `cast` tidies the symptom but doesn't remove the leak — `cascade.py` would still need to know HIP exists. 3. **Scope.** This PR exists to unblock CI for every other PR. Rolling in a structural refactor grows the review surface and the chance of merge conflicts with in-flight work. A future PR that promotes `FLASHINFER_HIP_FUSED_CASCADE` from experimental to default-on is the right moment to revisit this. The correct fix at that point is probably exposing the fusion through a uniform `run_and_merge(...)` method on both backends (CUDA's variant being two kernel launches), not adding `partial_state` to CUDA's signature. ### Why not also fix `partial_state` in `prefill_rocm.py`'s `@overload`s? The HIP wrapper's `@overload` declarations also lack `partial_state`, which is a latent contract gap (impl signature and overloads don't match). But `cascade.py` imports from the CUDA `prefill.py` module, so mypy never checks against the HIP overloads — fixing them has zero effect on this CI failure. Out of scope here; can be cleaned up alongside the future refactor above. ## Test plan - [x] `pre-commit run --all-files` — all hooks pass (mypy included) - [x] `pytest -n auto --reruns 2 -m "not slow"` — full fast suite passes (no behavior change expected) - [x] CI: `pre-commit` check goes green Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
6 tasks
demandal25
added a commit
that referenced
this pull request
May 21, 2026
## Summary Refresh the FlashInfer+ROCm README aimed at library consumers, refresh the Feature Support Matrix to match what has actually landed on `amd-integration`, and align the ROCm MLA wrapper with the rest of the ROCm backends so `backend="auto"` is accepted everywhere. ### What changed #### `README.md` - **Intro and structure.** Tighten the intro to call out HIP-in-repo kernels vs AITER dispatch up front; link to the Feature Support Matrix and AITER sections from the first paragraph. Cross-link CDNA3 / CDNA4 to AMD's official architecture whitepapers on first mention. - **Feature Support Matrix.** Replaced with a five-column table (Kernel / HIP / AITER / `backend="auto"` resolves to / Notes). New ✅ rows: Cascade (#221), MLA via AITER (#232), RoPE (#223), paged KV-cache append, RMSNorm via AITER (#232), sliding-window decode on the AITER path (#234), activation, quantization, and opt-in `torch.compile` (#210). Every ✅ is backed by a `tests/rocm_tests/test_*_hip.py`. FP8 status is folded into per-row notes rather than a dedicated column. - **GPU / ROCm / PyTorch.** Consolidated into one section with arch codenames inline (gfx942 → MI300X/MI325X = CDNA3, gfx950 → MI355X = CDNA4). `pip install torch` uses `--index-url` instead of `-f` so pip cannot silently fall back to a CPU-only PyPI wheel (matches CLAUDE.md). - **Getting Started.** Collapsed the Docker image table to the latest validated tag and pointed at Docker Hub for older releases. Dropped the manual `micromamba activate base` step (the env is auto-activated). Used the concrete image tag plus a `--name=flashinfer-rocm` in the `docker run` snippet. - **Trying the Examples.** Simplified to point at `examples/` plus one run command — no wget-based downloads. - **Install from Source.** Renamed from "Build from Source"; rewrote the ambiguous "Environment name varies …" note (and later removed it once the build / run blocks made the matching tag self-evident). - **AITER Support.** Collapsed the section intro to avoid re-listing conditions already in the matrix; cross-link Known Limitations. Rewrote Known Limitations preamble to state the two-group split (hard errors vs silently-ignored kwargs). Dropped the redundant Single Prefill Example (Basic Usage already shows the call pattern). - **Environment Variables.** New section documenting runtime env vars — `FLASHINFER_USE_TORCH_CUSTOM_OPS`, `FLASHINFER_HIP_FUSED_CASCADE`, `FLASHINFER_LOGGING_LEVEL`, `FLASHINFER_DISABLE_JIT`, `ROCM_PATH` / `ROCM_HOME`. Build-time vars stay in `CLAUDE.md` and are linked from here. - **Runtime Helpers.** Short snippet showing `is_aiter_supported` and `check_torch_rocm_compatibility`; calls out `validate_flashinfer_rocm_arch` as a build-time validator, not a runtime helper. - **CPX-mode pytest notes.** Split the dense paragraph into labelled bullets (Worker count / Reruns / `slow` marker / HIPBLAS retry). - **Basic Usage.** Moved to the end of the README as a closing example. - **License and Acknowledgements.** Added; the contributing reminder lives on its own line. #### `flashinfer/mla_rocm.py` + `tests/rocm_tests/test_mla_aiter_hip.py` - Accept `backend="auto"` as an alias for `"aiter"` on the ROCm MLA wrapper (default is now `"auto"` to match every other ROCm wrapper). Previously the wrapper raised `ValueError` on anything other than `"aiter"`, leaving MLA as the odd one out in the public API even though there is exactly one implementation to pick from on ROCm. - New tests: `test_mla_backend_accepts_auto_and_aiter` (parametrized over both values) and `test_mla_backend_rejects_unsupported` (confirms `backend="fa2"` still raises; runs without a GPU since the check fires before the AITER probe). ## Test plan - [x] `pre-commit run -a` passes. - [x] `pre-commit run markdownlint --files README.md` passes after every change. - [x] Every TOC entry resolves to an `##` heading in the body. - [x] Every ✅ in the Feature Support Matrix has a backing `tests/rocm_tests/test_*_hip.py`. - [x] `pytest tests/rocm_tests/test_mla_aiter_hip.py` — 11 passed. - [x] Render the README on the PR page and visually confirm tables, code blocks, and `<details>` sections look right. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
merge_state,merge_state_in_place,merge_states,variable_length_merge_states,attention_sum,variable_length_attention_sumcascade.cuinto kernels + bindings files, mirroring the CUDA layout sogen_cascade_module()resolves both files unchangedMultiLevelCascadeAttentionWrapper,BatchDecodeWithSharedPrefixPagedKVCacheWrapper,BatchPrefillWithSharedPrefixPagedKVCacheWrapper, and themerge_state*family fromflashinfer.__init__on HIPpartial_state=(out, lse)kwarg toBatchPrefillWithPagedKVCacheWrapper.run()for fused cascade epilogue (gated byFLASHINFER_HIP_FUSED_CASCADE=1)head_dim≤128, reducesnum_smem_stagesfrom 4→1 (pred_load is synchronous on CDNA3)warp_sync_statetogeneric/cascade.cuhwith__builtin_amdgcn_wave_barrier()on HIP and__syncwarp()on CUDA, enabling future persistent merge-states kernel supportDesign choice: no
cascade_rocm.py(unlikeprefill_rocm.py/decode_rocm.py)Other backends-divergent modules (
prefill,decode,mla) are forked into separate*_rocm.pyfiles and aliased viasys.modulesinflashinfer/__init__.py. Cascade is shared in a singlecascade.pywith a small inline HIP branch. This is intentional. The heuristic used:prefill.pyvsprefill_rocm.pyare ~3000 lines each, and the method bodies are genuinely different on each backend (JIT compilation path, backend dispatch list —fa2/fa3/cudnn/trtllmon CUDA vsfa2/aiter/autoon HIP — kernel param packing layouts, ROCm-specific PDL handling,partial_stateplumbing, AITER bootstrap, etc.). Sharing the file would mean nearly every method becoming a bigif IS_HIP: ... else: ...block. Forking is correct.cascade.py, by contrast, is mostly orchestration.MultiLevelCascadeAttentionWrapper.runloops over per-level prefill wrappers and merges results. Both the wrappers (BatchPrefillWithPagedKVCacheWrapper) and the merge helper (merge_state_in_place) are already backend-swapped via thesys.modulesalias in__init__.py, so the cascade orchestration is correct on both backends automatically — the loop body needs noif IS_HIP:at all. The only HIP-specific divergence is one optional kernel-fusion optimization, gated behind an env var.Concretely, if cascade were forked into
cascade_rocm.py, this PR would duplicate ~500 lines of identical orchestration plumbing (theMultiLevelCascadeAttentionWrapper,BatchDecodeWithSharedPrefixPagedKVCacheWrapper,BatchPrefillWithSharedPrefixPagedKVCacheWrapper, and themerge_state*wrapper functions) for the sake of one 4-line behavioral difference. Every future cascade.py improvement would then need to be ported twice. The cost would dwarf the benefit.Known consequence: the
partial_statekwarg on the HIP-sideBatchPrefillWithPagedKVCacheWrapper.run()does not exist on the CUDA variant, so static type-checking at thecascade.pycall site sees only the CUDA overloads and flags acall-overloaderror. This will need a localized# type: ignore[call-overload](or equivalent) at that call site. That is a meaningfully smaller maintenance cost than the ~500 LOC duplication acascade_rocm.pyfork would impose.Architecture note: two cascade merge paths in
generic/prefill.cuhThe ROCm prefill kernel uses
generic/cascade.cuhdifferently from the CUDAprefill.cuh:CUDA — cascade merging is always a post-kernel API call:
ROCm — two separate mechanisms:
Non-split-KV path (fused epilogue, lines 2325–2370): When
params.partial_o != nullptr && !partition_kv, the cascade merge is inlined inside the prefill kernel. The kernel reads the prior cascade level'spartial_o/partial_lsefrom global memory and blends them with the current output registers before the output write — no separate kernel launch, and no call intogeneric/cascade.cuh. This is the path enabled byFLASHINFER_HIP_FUSED_CASCADE=1.Split-KV path (post-kernel, lines 2636–2644): Follows the same pattern as CUDA —
VariableLengthMergeStatesmerges the KV-split chunks, thenMergeStateInPlacefolds in the prior cascade level. Both are post-kernel calls intogeneric/cascade.cuh.Test plan
test_cascade_hip.py: coversmerge_state,merge_state_in_place,merge_states(fp16/bf16, multiple shapes),merge_state_in_placewith boolean mask (all-ones, all-zeros, random), andfused_cascade_epiloguecorrectness vs float32 referencetest_shared_prefix_kernels_hip.py: portstest_batch_attention_with_shared_prefix_paged_kv_cachefrom the CUDA suite (MultiLevelCascadeAttentionWrappervs legacy shared-prefix wrappers); addstest_multilevel_cascade_fused_vs_unfusedcomparing theFLASHINFER_HIP_FUSED_CASCADEpath against the standalonemerge_state_in_placepath forhead_dimin {128, 256} andshared_kv_lenin {128, 512, 2048}🤖 Generated with Claude Code