Skip to content

[AMD][Radeon] Add the Support of RDNA3/RDNA3.5(gfx11) WMMA - #2044

Merged
LeiWang1999 merged 12 commits into
tile-ai:mainfrom
jiawei-real:feat/rdna3-wmma-support
Apr 22, 2026
Merged

LeiWang1999 merged 12 commits into
tile-ai:mainfrom
jiawei-real:feat/rdna3-wmma-support

Conversation

@jiawei-real

@jiawei-real jiawei-real commented Apr 14, 2026 •

Copy link
Copy Markdown
Contributor

Hi @LeiWang1999 @zhangnju, this PR extends the AMD RDNA WMMA path to better support gfx11 while keeping gfx12 behavior intact. This implementation is based on PR #1951 and fixes Issue #1984 . Here are the main changes:

  • add RDNA-generation-aware WMMA codegen for HIP so gfx11 and gfx12 emit the correct builtin ABI
  • refactor WMMA layout selection to use per-architecture factories in the emitter
  • add gfx11-specific fragment modeling for duplicated half-wave ownership via replicated T.Fragment

Detailed Changes

  • src/target/codegen_hip.cc
    • select the correct WMMA builtin form for gfx11 vs gfx12
    • update operand vector typing for RDNA WMMA codegen
  • src/target/codegen_hip.h, src/target/rt_mod_hip.cc, src/target/utils.h, src/target/utils.cc
    • plumb Target into HIP codegen and add RDNA generation helpers
  • tilelang/intrinsics/wmma_layout.py
    • split layout definitions by architecture
    • add layout factory helpers
    • add gfx11 fragment-forward helpers for duplicated half-wave ownership
    • add A_colmajor support for gfx11&gfx12
  • tilelang/intrinsics/wmma_macro_generator.py
    • cache architecture-selected layout functions in WMMAIntrinEmitter
    • use architecture-specific formulas for local_size_a/local_size_b
    • model gfx11 A/B fragment ownership with forward_fn + replicate=2
    • add A_colmajor shape infer support for gfx11&gfx12

Why

gfx11 WMMA differs from gfx12 in two important ways:

  • input operands use a different ABI/layout shape
  • logical A/B fragment elements are duplicated across the two half-waves

Validation

  • Using this PR, I successfully run the amd flash attention fwd/bwd example on gfx11 (including AI Max+ 395 (RX8060S iGPU) and PRO W7900 (which is provided py @zhangnju)) and gfx12 (RX9070XT) GPUs:
  • RX8060S FWD:
image
  • RX8060S BWD:
image
  • W7900 FWD:
image
  • W7900 BWD:
image
  • RX 9070XT FWD:
6563e3cc-e0cb-4766-b336-dc0ac501d513
  • RX 9070XT BWD:
596b677b-d693-47c8-8920-7ed8ecc9ecc6
  • @petersktang also successfully ran the above tests on Radeon 890m (see the comments for more details).

Summary by CodeRabbit

  • New Features

    • RDNA generation detection (gfx11/gfx12) propagated to HIP codegen, emitters, and examples; example helper to select RDNA-specific kernels/configs.
  • Improvements

    • WMMA builtins, operand packing, fragment replication, and shared-memory load/store layouts parameterized per RDNA generation for improved correctness and performance.
    • Kernel search spaces and data-flow adjusted for RDNA constraints.
  • Bug Fixes

    • Unsupported RDNA generations now produce explicit errors.

@github-actions

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the TileLang project.

Please remember to run pre-commit run --all-files in the root directory of the project to ensure your changes are properly linted and formatted. This will help ensure your contribution passes the format check.

We appreciate you taking this step! Our team will review your contribution, and we look forward to your awesome work! 🚀

@coderabbitai

coderabbitai Bot commented Apr 14, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds RDNA-generation awareness across HIP WMMA codegen and tilelang: targets are propagated into HIP codegen, RDNA generation is detected/validated (gfx11/gfx12), WMMA builtin naming, operand packing/typedefs, and WMMA layout/emitter logic are selected per generation.

Changes

Cohort / File(s) Summary
Target utilities
src/target/utils.h, src/target/utils.cc
Add TargetGetRDNAGeneration(Target) declaration and implementation; register FFI "tl.TargetGetRDNAGeneration"; returns 11/12/0 based on mcpu.
HIP codegen
src/target/codegen_hip.h, src/target/codegen_hip.cc
Add CodeGenTileLangHIP::SetTarget(Target) and target_ member; WMMA emission queries RDNA gen, validates gfx11/gfx12, selects builtin name and AB packing (ab_half_elems, ab_vec_typedef) per generation.
Runtime module wiring
src/target/rt_mod_hip.cc
Call cg.SetTarget(target) after cg.Init(output_ssa) in HIP module builders to propagate Target into codegen.
WMMA layout library
tilelang/intrinsics/wmma_layout.py
Replace generic layouts with separate gfx11/gfx12 layout functions and store/index/replicate helpers; add factories that dispatch by rdna_gen, and introduce gfx11-specific fragment-replication logic and validation.
WMMA macro emitter
tilelang/intrinsics/wmma_macro_generator.py
Select RDNA generation from target, wire per-gen layout/forward/replicate/store funcs, adjust local sizes, legalize shared-buffer indexing, parameterize fragment replication and loads/stores, and validate target.
GEMM lowering callsite
tilelang/tileop/gemm/__init__.py
Stop passing mbar_phase_expr into chosen implementation’s lower(...) call (call site now omits that argument).
Example (AMD)
examples/amd/example_amd_flash_attn_bwd.py
Add IsRDNA() helper; introduce RDNA-specific config spaces and kernel code paths (reduced WMMA configs, conditional shared buffers, casts, and altered copy/cast flows).

Sequence Diagram

sequenceDiagram
    participant Compiler as Compiler/Lowering
    participant RtMod as rt_mod_hip.cc
    participant CodeGen as CodeGenTileLangHIP
    participant Utils as TargetGetRDNAGeneration
    participant MacroGen as WMMAIntrinEmitter
    participant LayoutLib as wmma_layout.py

    Compiler->>RtMod: BuildTileLangHIP(target)
    RtMod->>CodeGen: cg.Init(output_ssa)
    RtMod->>CodeGen: cg.SetTarget(target)
    RtMod->>CodeGen: cg.Finish()

    CodeGen->>Utils: TargetGetRDNAGeneration(target)
    Utils-->>CodeGen: rdna_gen (11/12/0)

    CodeGen->>CodeGen: validate rdna_gen in {11,12}
    CodeGen->>CodeGen: select builtin name, ab_half_elems, ab_vec_typedef

    MacroGen->>Utils: detect rdna generation from target
    Utils-->>MacroGen: rdna_gen
    MacroGen->>LayoutLib: request per-gen layout/forward/replicate/store funcs
    LayoutLib-->>MacroGen: gen-specific helpers
    MacroGen->>CodeGen: emit WMMA call using selected names, typedefs, casts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • Gongen-Ali
  • LeiWang1999

"🐰 I sniffed the Target and found gfx eleven and twelve,
Split fragments, packed halves, and hopped in my shell.
Typedefs swapped places, layouts learned to bend,
Builtins chose their names, and the tile maps now send.
WMMA sings in two voices, the rabbit gives a cheer,
Hops off with packed vectors — RDNA hopping near!"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[AMD][Radeon] Add the Support of RDNA3/RDNA3.5(gfx11) WMMA' directly and accurately describes the main objective of the PR—extending WMMA support to gfx11 RDNA generations while preserving gfx12 behavior.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jiawei-real jiawei-real changed the title [AMD][Radeon] Add the Support of RDNA3/RDNA3.5(gfx11) GPU [AMD][Radeon] Add the Support of RDNA3/RDNA3.5(gfx11) WMMA Apr 14, 2026
@jiawei-real jiawei-real mentioned this pull request Apr 14, 2026
1 task done

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tilelang/tileop/gemm/__init__.py (1)

131-143: ⚠️ Potential issue | 🔴 Critical

Pass mbar_phase_expr through to the implementation lowerer.

Line 143 drops an argument that tilelang/tileop/gemm/gemm_base.py still defines and tilelang/tileop/gemm/gemm_tcgen05.py still uses for barrier-phase selection. With the current call, TCGEN5 lowering will silently fall back to phase 0, which can break pipelined mbarrier parity.

Proposed fix
-        return impl_class(self).lower(layout_map, target, thread_bounds, thread_var)
+        return impl_class(self).lower(
+            layout_map, target, thread_bounds, thread_var, mbar_phase_expr
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tilelang/tileop/gemm/__init__.py` around lines 131 - 143, The lower method on
the GEMM op drops the mbar_phase_expr argument when calling the implementation
lowerer, causing implementations like gemm_tcgen05.py (and the base in
gemm_base.py) that expect mbar_phase_expr to receive a default/0 phase; update
the call in lower (and its callsite signature if needed) to pass mbar_phase_expr
through to impl_class(self).lower so the selected implementation receives the
barrier-phase expression; locate the lower method in
tilelang/tileop/gemm/__init__.py and ensure its call to
_select_gemm_instruction/_get_implementation_class then
impl_class(self).lower(layout_map, target, thread_bounds, thread_var,
mbar_phase_expr) matches the implementations in gemm_base.py and
gemm_tcgen05.py.
🧹 Nitpick comments (2)
tilelang/intrinsics/wmma_macro_generator.py (1)

490-500: Make target mandatory instead of defaulting it to None.

_detect_rdna_generation() now rejects None unconditionally, but WMMAIntrinEmitter.__init__ still advertises target: Target | None = None. That leaves a runtime trap for any caller that used the old default. Tightening the constructor signature would make the API match the new requirement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tilelang/intrinsics/wmma_macro_generator.py` around lines 490 - 500, The
constructor signature of WMMAIntrinEmitter still allows target: Target | None =
None while _detect_rdna_generation(Target | None) now rejects None; change
WMMAIntrinEmitter.__init__ to require a non-optional Target (e.g., target:
Target) and remove the default None, update any internal calls that passed None
to pass a valid Target, and adjust all call sites/tests to provide a Target;
ensure references to _detect_rdna_generation remain the same and that any
callers that relied on the old default are updated to construct or pass a real
Target instance.
src/target/utils.cc (1)

291-299: Export TargetGetRDNAGeneration through the existing FFI registry.

Right now this helper is only callable from C++, so the Python side had to duplicate the same mcpu parsing in tilelang/intrinsics/wmma_macro_generator.py:490-500. Exposing it in the TVM_FFI_STATIC_INIT_BLOCK() below would keep RDNA target detection in one place.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/target/utils.cc` around lines 291 - 299, Add an FFI binding that exposes
the existing C++ helper TargetGetRDNAGeneration to other languages: inside the
existing TVM_FFI_STATIC_INIT_BLOCK() register a global/PackedFunc (e.g.
"target.GetRDNAGeneration") whose body calls TargetGetRDNAGeneration(target) and
returns the int result; ensure the registration signature accepts a Target (or
ObjectRef) and returns an int so Python can call it instead of duplicating mcpu
parsing. Use the exact symbol TargetGetRDNAGeneration in the registered body to
locate the implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/target/codegen_hip.cc`:
- Around line 1114-1160: The code emits __fp16 vector types unconditionally; add
a dtype guard that rejects any non-fp16 A/B before constructing
wmma_builtin/ab_vec_typedef/call_wmma_code: check the operand dtypes (e.g.
A_dtype and B_dtype) and require is_float16() for both, and if not,
ICHECK(false) or return an error describing unsupported non-fp16 WMMA operands;
keep the existing rdna_gen/wmma_builtin/ab_vec_typedef logic but only run it
after the dtype check so we fail fast (or clamp upstream) until bf16 codegen is
implemented.

---

Outside diff comments:
In `@tilelang/tileop/gemm/__init__.py`:
- Around line 131-143: The lower method on the GEMM op drops the mbar_phase_expr
argument when calling the implementation lowerer, causing implementations like
gemm_tcgen05.py (and the base in gemm_base.py) that expect mbar_phase_expr to
receive a default/0 phase; update the call in lower (and its callsite signature
if needed) to pass mbar_phase_expr through to impl_class(self).lower so the
selected implementation receives the barrier-phase expression; locate the lower
method in tilelang/tileop/gemm/__init__.py and ensure its call to
_select_gemm_instruction/_get_implementation_class then
impl_class(self).lower(layout_map, target, thread_bounds, thread_var,
mbar_phase_expr) matches the implementations in gemm_base.py and
gemm_tcgen05.py.

---

Nitpick comments:
In `@src/target/utils.cc`:
- Around line 291-299: Add an FFI binding that exposes the existing C++ helper
TargetGetRDNAGeneration to other languages: inside the existing
TVM_FFI_STATIC_INIT_BLOCK() register a global/PackedFunc (e.g.
"target.GetRDNAGeneration") whose body calls TargetGetRDNAGeneration(target) and
returns the int result; ensure the registration signature accepts a Target (or
ObjectRef) and returns an int so Python can call it instead of duplicating mcpu
parsing. Use the exact symbol TargetGetRDNAGeneration in the registered body to
locate the implementation.

In `@tilelang/intrinsics/wmma_macro_generator.py`:
- Around line 490-500: The constructor signature of WMMAIntrinEmitter still
allows target: Target | None = None while _detect_rdna_generation(Target | None)
now rejects None; change WMMAIntrinEmitter.__init__ to require a non-optional
Target (e.g., target: Target) and remove the default None, update any internal
calls that passed None to pass a valid Target, and adjust all call sites/tests
to provide a Target; ensure references to _detect_rdna_generation remain the
same and that any callers that relied on the old default are updated to
construct or pass a real Target instance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 607d155f-6357-41dc-9e58-3a5f9677ad4b

📥 Commits

Reviewing files that changed from the base of the PR and between 74fc980 and 3142b34.

📒 Files selected for processing (8)
  • src/target/codegen_hip.cc
  • src/target/codegen_hip.h
  • src/target/rt_mod_hip.cc
  • src/target/utils.cc
  • src/target/utils.h
  • tilelang/intrinsics/wmma_layout.py
  • tilelang/intrinsics/wmma_macro_generator.py
  • tilelang/tileop/gemm/__init__.py

Comment thread src/target/codegen_hip.cc
Comment on lines +1114 to +1160
// Get RDNA Generation
ICHECK(target_.defined()) << "CodeGenTileLangHIP target is not set";
int rdna_gen = tvm::tl::TargetGetRDNAGeneration(target_);
ICHECK(rdna_gen == 11 || rdna_gen == 12)
<< "Unsupported RDNA target for WMMA: gfx" << target_->str();

// Determine wmma builtin name from shape
// shape = "f32_16x16x16_f16_w32" ->
// "__builtin_amdgcn_wmma_f32_16x16x16_f16_w32_gfx12" For gfx12 targets use
// the _gfx12 suffix variant.
std::string wmma_builtin = "__builtin_amdgcn_wmma_" + shape + "_gfx12";
// the _gfx12 suffix variant, which gfx11 targets don't have.
std::string wmma_builtin = "__builtin_amdgcn_wmma_" + shape;

int ab_half_elems = 16;
std::string ab_vec_typedef = "tl_v16f16";
if (rdna_gen == 12) {
wmma_builtin += "_gfx12";
ab_half_elems = 8;
ab_vec_typedef = "tl_v8f16";
}

// Emit the WMMA call.
// For gfx12:
// Signature: v8f32 = wmma_builtin(v8f16 a, v8f16 b, v8f32 c)
// where v8f16 = __fp16 x 8, v8f32 = float x 8.
// For gfx11:
// Signature: v8f32 = wmma_builtin(v16f16 a, v16f16 b, v8f32 c)
// where v16f16 = __fp16 x 16, v8f32 = float x 8.
//
// A/B buffers hold half_t (fp16), C/D buffers hold float.
// Each element index accesses a packed vector of 8 elements.
// Each element index accesses a packed vector of 8/16 elements.
//
// Using typedef'd vector types for the cast:
// typedef __attribute__((__vector_size__(8 * sizeof(__fp16)))) __fp16
// tl_v8f16; typedef __attribute__((__vector_size__(8 * sizeof(float))))
// typedef __attribute__((__vector_size__(8/16 * sizeof(__fp16)))) __fp16
// tl_v8/16f16; typedef __attribute__((__vector_size__(8 * sizeof(float))))
// float tl_v8f32;
std::string call_wmma_code = R"({
typedef __attribute__((__vector_size__(8 * sizeof(__fp16)))) __fp16 tl_v8f16;
typedef __attribute__((__vector_size__({ab_half_elems} * sizeof(__fp16)))) __fp16 {ab_vec_typedef};
typedef __attribute__((__vector_size__(8 * sizeof(float)))) float tl_v8f32;
*((tl_v8f32*){c_ref} + {c_bias}) = {wmma_builtin}(
*((tl_v8f16*){a_ref} + {a_bias}),
*((tl_v8f16*){b_ref} + {b_bias}),
*(({ab_vec_typedef}*){a_ref} + {a_bias}),
*(({ab_vec_typedef}*){b_ref} + {b_bias}),
*((tl_v8f32*){c_ref} + {c_bias}));
})";
Replacer wmma_replacer;
wmma_replacer.register_rule("{wmma_builtin}", wmma_builtin);
wmma_replacer.register_rule("{ab_half_elems}", std::to_string(ab_half_elems));
wmma_replacer.register_rule("{ab_vec_typedef}", ab_vec_typedef);

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.

⚠️ Potential issue | 🟠 Major

Reject non-float16 RDNA WMMA operands before emitting __fp16 vectors.

Lines 1126-1154 always lower A/B as tl_v8/16f16 backed by __fp16, regardless of the A_dtype / B_dtype passed into this intrinsic. If a non-fp16 configuration reaches this path, we'll generate the wrong builtin ABI instead of failing fast. Please add explicit dtype guards here, or clamp the upstream emitter to fp16-only until bf16 codegen exists.

Proposed guard
+    ICHECK(A_dtype == "float16x8" || A_dtype == "float16x16")
+        << "RDNA WMMA currently supports only float16 A operands";
+    ICHECK(B_dtype == "float16x8" || B_dtype == "float16x16")
+        << "RDNA WMMA currently supports only float16 B operands";
+
     int ab_half_elems = 16;
     std::string ab_vec_typedef = "tl_v16f16";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/target/codegen_hip.cc` around lines 1114 - 1160, The code emits __fp16
vector types unconditionally; add a dtype guard that rejects any non-fp16 A/B
before constructing wmma_builtin/ab_vec_typedef/call_wmma_code: check the
operand dtypes (e.g. A_dtype and B_dtype) and require is_float16() for both, and
if not, ICHECK(false) or return an error describing unsupported non-fp16 WMMA
operands; keep the existing rdna_gen/wmma_builtin/ab_vec_typedef logic but only
run it after the dtype check so we fail fast (or clamp upstream) until bf16
codegen is implemented.

@petersktang

petersktang commented Apr 14, 2026 •

Copy link
Copy Markdown

I am running this on AMD AI HX 370 with 890M on Ubuntu 24.04 LTS - linux 6.17.0-1017-oem (ROCM 7.2.1). Not successful.

[16:44:36] ....tilelang/src/transform/verify_parallel_loop.cc:73: Warning: Data race detected: `Output[bz, q_block_offset + i, by, j]`is written by multiple threads in loop [i], Example:
; Z3 Prover is disabled.If you believe this is a false positive, pass `PassKey.TL_DISABLE_DATA_RACE_CHECK` to pass key to disable this check.

[16:44:36] ....tilelang/src/transform/producer_consumer_ws.cc:2355: Warning: [WS] skipped: no TMA copies in pipeline loop
[16:44:36] ....tilelang/src/transform/verify_parallel_loop.cc:73: Warning: Data race detected: `Output[bz, q_block_offset + i, by, j]`is written by multiple threads in loop [i], Example:
; Z3 Prover is disabled.If you believe this is a false positive, pass `PassKey.TL_DISABLE_DATA_RACE_CHECK` to pass key to disable this check.

[16:44:36] ....tilelang/src/transform/producer_consumer_ws.cc:2355: Warning: [WS] skipped: no TMA copies in pipeline loop
[16:44:36] ....tilelang/src/transform/verify_parallel_loop.cc:73: Warning: Data race detected: `Output[bz, q_block_offset + i, by, j]`is written by multiple threads in loop [i], Example:
; Z3 Prover is disabled.If you believe this is a false positive, pass `PassKey.TL_DISABLE_DATA_RACE_CHECK` to pass key to disable this check.

[16:44:36] ....tilelang/src/transform/producer_consumer_ws.cc:2355: Warning: [WS] skipped: no TMA copies in pipeline loop
[16:44:36] ....ilelang/src/transform/verify_parallel_loop.cc:73: Warning: Data race detected: `Output[bz, q_block_offset + i, by, j]`is written by multiple threads in loop [i], Example:
; Z3 Prover is disabled.If you believe this is a false positive, pass `PassKey.TL_DISABLE_DATA_RACE_CHECK` to pass key to disable this check.

[16:44:36] ....tilelang/src/transform/verify_parallel_loop.cc:73: Warning: Data race detected: `Output[bz, q_block_offset + i, by, j]`is written by multiple threads in loop [i], Example:
; Z3 Prover is disabled.If you believe this is a false positive, pass `PassKey.TL_DISABLE_DATA_RACE_CHECK` to pass key to disable this check.

[16:44:36] ....tilelang/src/transform/verify_parallel_loop.cc:73: Warning: Data race detected: `Output[bz, q_block_offset + i, by, j]`is written by multiple threads in loop [i], Example:
; Z3 Prover is disabled.If you believe this is a false positive, pass `PassKey.TL_DISABLE_DATA_RACE_CHECK` to pass key to disable this check.

[16:44:36]....tilelang/src/transform/producer_consumer_ws.cc:2355: Warning: [WS] skipped: no TMA copies in pipeline loop

[16:44:36] ....tilelang/src/transform/producer_consumer_ws.cc:2355: Warning: [WS] skipped: no TMA copies in pipeline loop

[16:44:36] ....tilelang/src/transform/producer_consumer_ws.cc:2355: Warning: [WS] skipped: no TMA copies in pipeline loop


2026-04-14 16:44:36,476 ERROR:Auto-tuning failed: No configuration successfully compiled and passed benchmarking/validation.
Traceback (most recent call last):
  File "....tilelang/examples/amd/example_amd_flash_attn_fwd.py", line 294, in <module>
    main(args.batch, args.heads, args.seq_len, args.dim, args.is_causal, args.groups)
    ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "....tilelang/examples/amd/example_amd_flash_attn_fwd.py", line 267, in main
    kernel = fast_flashattn(batch, heads, seq_len, dim, is_causal, groups=groups)
File "....tilelang/tilelang/autotuner/tuner.py", line 719, in __call__
    artifact = autotuner.run()
File "....tilelang/tilelang/autotuner/tuner.py", line 603, in run
    raise RuntimeError(error_msg)

@jiawei-real

Copy link
Copy Markdown
Contributor Author

@petersktang Could you please give me more detailed information? For example, just run a SINGLE tuning config or the basic gemm kernel (examples/gemm/example_gemm.py)?

@jiawei-real

Copy link
Copy Markdown
Contributor Author

I am running this on AMD AI HX 370 with 890M on Ubuntu 24.04 LTS - linux 6.17.0-1017-oem (ROCM 7.2.1). Not successful.

[16:44:36] ....tilelang/src/transform/verify_parallel_loop.cc:73: Warning: Data race detected: `Output[bz, q_block_offset + i, by, j]`is written by multiple threads in loop [i], Example:
; Z3 Prover is disabled.If you believe this is a false positive, pass `PassKey.TL_DISABLE_DATA_RACE_CHECK` to pass key to disable this check.

[16:44:36] ....tilelang/src/transform/producer_consumer_ws.cc:2355: Warning: [WS] skipped: no TMA copies in pipeline loop
[16:44:36] ....tilelang/src/transform/verify_parallel_loop.cc:73: Warning: Data race detected: `Output[bz, q_block_offset + i, by, j]`is written by multiple threads in loop [i], Example:
; Z3 Prover is disabled.If you believe this is a false positive, pass `PassKey.TL_DISABLE_DATA_RACE_CHECK` to pass key to disable this check.

[16:44:36] ....tilelang/src/transform/producer_consumer_ws.cc:2355: Warning: [WS] skipped: no TMA copies in pipeline loop
[16:44:36] ....tilelang/src/transform/verify_parallel_loop.cc:73: Warning: Data race detected: `Output[bz, q_block_offset + i, by, j]`is written by multiple threads in loop [i], Example:
; Z3 Prover is disabled.If you believe this is a false positive, pass `PassKey.TL_DISABLE_DATA_RACE_CHECK` to pass key to disable this check.

[16:44:36] ....tilelang/src/transform/producer_consumer_ws.cc:2355: Warning: [WS] skipped: no TMA copies in pipeline loop
[16:44:36] ....ilelang/src/transform/verify_parallel_loop.cc:73: Warning: Data race detected: `Output[bz, q_block_offset + i, by, j]`is written by multiple threads in loop [i], Example:
; Z3 Prover is disabled.If you believe this is a false positive, pass `PassKey.TL_DISABLE_DATA_RACE_CHECK` to pass key to disable this check.

[16:44:36] ....tilelang/src/transform/verify_parallel_loop.cc:73: Warning: Data race detected: `Output[bz, q_block_offset + i, by, j]`is written by multiple threads in loop [i], Example:
; Z3 Prover is disabled.If you believe this is a false positive, pass `PassKey.TL_DISABLE_DATA_RACE_CHECK` to pass key to disable this check.

[16:44:36] ....tilelang/src/transform/verify_parallel_loop.cc:73: Warning: Data race detected: `Output[bz, q_block_offset + i, by, j]`is written by multiple threads in loop [i], Example:
; Z3 Prover is disabled.If you believe this is a false positive, pass `PassKey.TL_DISABLE_DATA_RACE_CHECK` to pass key to disable this check.

[16:44:36]....tilelang/src/transform/producer_consumer_ws.cc:2355: Warning: [WS] skipped: no TMA copies in pipeline loop

[16:44:36] ....tilelang/src/transform/producer_consumer_ws.cc:2355: Warning: [WS] skipped: no TMA copies in pipeline loop

[16:44:36] ....tilelang/src/transform/producer_consumer_ws.cc:2355: Warning: [WS] skipped: no TMA copies in pipeline loop


2026-04-14 16:44:36,476 ERROR:Auto-tuning failed: No configuration successfully compiled and passed benchmarking/validation.
Traceback (most recent call last):
  File "....tilelang/examples/amd/example_amd_flash_attn_fwd.py", line 294, in <module>
    main(args.batch, args.heads, args.seq_len, args.dim, args.is_causal, args.groups)
    ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "....tilelang/examples/amd/example_amd_flash_attn_fwd.py", line 267, in main
    kernel = fast_flashattn(batch, heads, seq_len, dim, is_causal, groups=groups)
File "....tilelang/tilelang/autotuner/tuner.py", line 719, in __call__
    artifact = autotuner.run()
File "....tilelang/tilelang/autotuner/tuner.py", line 603, in run
    raise RuntimeError(error_msg)

And maybe your Linux version is not suitable: https://rocm.docs.amd.com/projects/radeon-ryzen/en/latest/docs/install/installryz/native_linux/install-ryzen.html

"For ROCm on Ryzen, it is required to operate on the 6.14-1018 OEM kernel or newer."

@petersktang

Copy link
Copy Markdown

am already on linux 6.17.0

@petersktang

petersktang commented Apr 14, 2026 •

Copy link
Copy Markdown

I ran python examples/gemm/example_gemm.py and got the below (gfx1150):

2026-04-14 17:08:55 [TileLang:tilelang.jit.kernel:INFO]: TileLang begins to compile kernel gemmwithout_idx=[-1]`
2026-04-14 17:08:55 [TileLang:tilelang.language.eager.builder:CRITICAL]: Failed to build prim_func from _gemm_ssr
args={}
source=def make_closure(A_region, B_region, C_buf, block_K, clear_accum, in_dtype, k_pack, local_size_a, local_size_b, micro_size_k, warp_cols, warp_rows, wmma_emitter):

def _gemm_ssr(__tb):
    __tb_fl = 'gemm_wmma.py'
    __tb_fn = '_gemm_ssr'
    range = __tb.override('range')
    __tb.set_fileline(__tb_fl, 101, __tb_fn)

    def _gemm_ssr(**__kwargs) -> None:
        __tb.set_fileline(__tb_fl, 102, __tb_fn)
        A_local = __tb.bind('A_local', __tb.rval('T', T).alloc_local(__tb.rval('warp_rows', warp_rows) * __tb.rval('local_size_a', local_size_a) * __tb.rval('k_pack', k_pack), __tb.rval('in_dtype', in_dtype)))
        __tb.set_fileline(__tb_fl, 103, __tb_fn)
        B_local = __tb.bind('B_local', __tb.rval('T', T).alloc_local(__tb.rval('warp_cols', warp_cols) * __tb.rval('local_size_b', local_size_b) * __tb.rval('k_pack', k_pack), __tb.rval('in_dtype', in_dtype)))
        __tb.set_fileline(__tb_fl, 104, __tb_fn)
        for __0 in __tb.ctx_if(__tb.rval('clear_accum', clear_accum)):
            __tb.set_fileline(__tb_fl, 104, __tb_fn)
            for _ in __tb.ctx_then(__0):
                __tb.set_fileline(__tb_fl, 105, __tb_fn)
                __tb.eval(__tb.rval('T', T).clear(__tb.rval('C_buf', C_buf)))
        __tb.set_fileline(__tb_fl, 106, __tb_fn)
        for __1 in __tb.ctx_for(__tb.rval('T', T).serial(0, __tb.rval('block_K', block_K) // (__tb.rval('micro_size_k', micro_size_k) * __tb.rval('k_pack', k_pack)))):
            __tb.set_fileline(__tb_fl, 106, __tb_fn)
            ki = __tb.bind('ki', __1)
            __tb.set_fileline(__tb_fl, 107, __tb_fn)
            __tb.eval(__tb.rval('wmma_emitter', wmma_emitter).ldmatrix_a(__tb.rval('A_local', A_local), __tb.rval('A_region', A_region), __tb.rval('ki', ki)))
            __tb.set_fileline(__tb_fl, 108, __tb_fn)
            __tb.eval(__tb.rval('wmma_emitter', wmma_emitter).ldmatrix_b(__tb.rval('B_local', B_local), __tb.rval('B_region', B_region), __tb.rval('ki', ki)))
            __tb.set_fileline(__tb_fl, 109, __tb_fn)
            __tb.eval(__tb.rval('wmma_emitter', wmma_emitter).wmma(__tb.rval('A_local', A_local), __tb.rval('B_local', B_local), __tb.rval('C_buf', C_buf), __tb.rval('ki', ki)))
    return _gemm_ssr
return _gemm_ssr

Traceback (most recent call last):
File "...tilelang/examples/gemm/example_gemm.py", line 69, in
main()
~~~~^^
File "...tilelang/examples/gemm/example_gemm.py", line 32, in main
kernel = matmul(1024, 1024, 1024, 128, 128, 32)
File "...tilelang/tilelang/jit/init.py", line 465, in call
kernel = self.compile(*args, **kwargs)
File "...tilelang/tilelang/jit/init.py", line 395, in compile
kernel_result = compile(
prim_func,
...<6 lines>...
compile_flags=self.compile_flags,
)`

`

File "...tilelang/tilelang/tileop/gemm/gemm_wmma.py", line 100, in lower
@T.prim_func

File "...tilelang/tilelang/language/eager/builder.py", line 1286, in prim_func
return impl(func) if func is not None else impl

File "..i.tilelang/tilelang/language/eager/builder.py", line 1284, in impl
raise e

File "...tilelang/tilelang/language/eager/builder.py", line 1277, in impl
ir_gen.gen(builder)(**annot)

File "..i.tilelang/tilelang/tileop/gemm/gemm_wmma.py", line 107, in _gemm_ssr
wmma_emitter.ldmatrix_a(A_local, A_region, ki)

File "...tilelang/tilelang/intrinsics/wmma_macro_generator.py", line 212, in ldmatrix_a
return _warp_ldmatrix_a(A_local_buf, A_shared_buf, ki, thread_binding, rk)

File "...tilelang/tilelang/language/eager/builder.py", line 781, in call
res = self.ir_gen.gen(builder)(*args, **kwargs)

File "...tilelang/tilelang/intrinsics/wmma_macro_generator.py", line 210, in _warp_ldmatrix_a
A_local_buf[i * k_pack * local_size_a + local_id] = A_buf[A_base0 + l + row, A_base1 + r + col]

File "...tilelang/3rdparty/tvm/python/tvm/tir/buffer.py", line 232, in getitem
return BufferLoad(self, expr_indices)

File "...tilelang/3rdparty/tvm/python/tvm/tir/expr.py", line 1120, in init
self.init_handle_by_constructor(

File "python/tvm_ffi/cython/object.pxi", line 186, in tvm_ffi.core.Object.init_handle_by_constructor
File "python/tvm_ffi/cython/function.pxi", line 123, in tvm_ffi.core.ConstructorCall
File "python/tvm_ffi/cython/error.pxi", line 159, in tvm_ffi.core.CHECK_CALL
File "/home/petersktang/Workspace/tile-ai.tilelang/3rdparty/tvm/src/tir/ir/expr.cc", line 865, in tvm::tir::__TVMFFIStaticInitFunc33()::{lambda(tvm::tir::Buffer, tvm::ffi::Array<tvm::PrimExpr, void>, tvm::ffi::Optional<tvm::PrimExpr, void>, tvm::Span)#1}::operator()(tvm::tir::Buffer, tvm::ffi::Array<tvm::PrimExpr, void>, tvm::ffi::Optional<tvm::PrimExpr, void>, tvm::Span) const
return BufferLoad(buffer, indices, predicate, span);

File "...tilelang/3rdparty/tvm/src/tir/ir/expr.cc", line 825, in tvm::tir::BufferLoad::BufferLoad(tvm::tir::Buffer, tvm::ffi::Array<tvm::PrimExpr, void>, tvm::ffi::Optional<tvm::PrimExpr, void>, tvm::Span)
ICHECK_EQ(buffer->shape.size(), indices.size())

File "..tilelang/3rdparty/tvm/include/tvm/runtime/logging.h", line 321, in tvm::runtime::detail::LogFatal::~LogFatal()
GetEntry().Finalize();

File "...tilelang/3rdparty/tvm/include/tvm/runtime/logging.h", line 337, in tvm::runtime::detail::LogFatal::Entry::Finalize()
InternalError error(file_, lineno_, stream_.str());

tvm.error.InternalError: Check failed: buffer->shape.size() == indices.size() (3 vs. 2) : Buffer A_shared is 3-dimensional, cannot be indexed with the 2-dimensional indices provided.`

@jiawei-real

Copy link
Copy Markdown
Contributor Author

I ran python examples/gemm/example_gemm.py and got the below (gfx1150):

2026-04-14 17:08:55 [TileLang:tilelang.jit.kernel:INFO]: TileLang begins to compile kernel gemmwithout_idx=[-1]` 2026-04-14 17:08:55 [TileLang:tilelang.language.eager.builder:CRITICAL]: Failed to build prim_func from _gemm_ssr args={} source=def make_closure(A_region, B_region, C_buf, block_K, clear_accum, in_dtype, k_pack, local_size_a, local_size_b, micro_size_k, warp_cols, warp_rows, wmma_emitter):

def _gemm_ssr(__tb):
    __tb_fl = 'gemm_wmma.py'
    __tb_fn = '_gemm_ssr'
    range = __tb.override('range')
    __tb.set_fileline(__tb_fl, 101, __tb_fn)

    def _gemm_ssr(**__kwargs) -> None:
        __tb.set_fileline(__tb_fl, 102, __tb_fn)
        A_local = __tb.bind('A_local', __tb.rval('T', T).alloc_local(__tb.rval('warp_rows', warp_rows) * __tb.rval('local_size_a', local_size_a) * __tb.rval('k_pack', k_pack), __tb.rval('in_dtype', in_dtype)))
        __tb.set_fileline(__tb_fl, 103, __tb_fn)
        B_local = __tb.bind('B_local', __tb.rval('T', T).alloc_local(__tb.rval('warp_cols', warp_cols) * __tb.rval('local_size_b', local_size_b) * __tb.rval('k_pack', k_pack), __tb.rval('in_dtype', in_dtype)))
        __tb.set_fileline(__tb_fl, 104, __tb_fn)
        for __0 in __tb.ctx_if(__tb.rval('clear_accum', clear_accum)):
            __tb.set_fileline(__tb_fl, 104, __tb_fn)
            for _ in __tb.ctx_then(__0):
                __tb.set_fileline(__tb_fl, 105, __tb_fn)
                __tb.eval(__tb.rval('T', T).clear(__tb.rval('C_buf', C_buf)))
        __tb.set_fileline(__tb_fl, 106, __tb_fn)
        for __1 in __tb.ctx_for(__tb.rval('T', T).serial(0, __tb.rval('block_K', block_K) // (__tb.rval('micro_size_k', micro_size_k) * __tb.rval('k_pack', k_pack)))):
            __tb.set_fileline(__tb_fl, 106, __tb_fn)
            ki = __tb.bind('ki', __1)
            __tb.set_fileline(__tb_fl, 107, __tb_fn)
            __tb.eval(__tb.rval('wmma_emitter', wmma_emitter).ldmatrix_a(__tb.rval('A_local', A_local), __tb.rval('A_region', A_region), __tb.rval('ki', ki)))
            __tb.set_fileline(__tb_fl, 108, __tb_fn)
            __tb.eval(__tb.rval('wmma_emitter', wmma_emitter).ldmatrix_b(__tb.rval('B_local', B_local), __tb.rval('B_region', B_region), __tb.rval('ki', ki)))
            __tb.set_fileline(__tb_fl, 109, __tb_fn)
            __tb.eval(__tb.rval('wmma_emitter', wmma_emitter).wmma(__tb.rval('A_local', A_local), __tb.rval('B_local', B_local), __tb.rval('C_buf', C_buf), __tb.rval('ki', ki)))
    return _gemm_ssr
return _gemm_ssr

Traceback (most recent call last): File "...tilelang/examples/gemm/example_gemm.py", line 69, in main() ~~~~^^ File "...tilelang/examples/gemm/example_gemm.py", line 32, in main kernel = matmul(1024, 1024, 1024, 128, 128, 32) File "...tilelang/tilelang/jit/init.py", line 465, in call kernel = self.compile(*args, **kwargs) File "...tilelang/tilelang/jit/init.py", line 395, in compile kernel_result = compile( prim_func, ...<6 lines>... compile_flags=self.compile_flags, )`

`

File "...tilelang/tilelang/tileop/gemm/gemm_wmma.py", line 100, in lower @T.prim_func

File "...tilelang/tilelang/language/eager/builder.py", line 1286, in prim_func return impl(func) if func is not None else impl

File "..i.tilelang/tilelang/language/eager/builder.py", line 1284, in impl raise e

File "...tilelang/tilelang/language/eager/builder.py", line 1277, in impl ir_gen.gen(builder)(**annot)

File "..i.tilelang/tilelang/tileop/gemm/gemm_wmma.py", line 107, in _gemm_ssr wmma_emitter.ldmatrix_a(A_local, A_region, ki)

File "...tilelang/tilelang/intrinsics/wmma_macro_generator.py", line 212, in ldmatrix_a return _warp_ldmatrix_a(A_local_buf, A_shared_buf, ki, thread_binding, rk)

File "...tilelang/tilelang/language/eager/builder.py", line 781, in call res = self.ir_gen.gen(builder)(*args, **kwargs)

File "...tilelang/tilelang/intrinsics/wmma_macro_generator.py", line 210, in _warp_ldmatrix_a A_local_buf[i * k_pack * local_size_a + local_id] = A_buf[A_base0 + l + row, A_base1 + r + col]

File "...tilelang/3rdparty/tvm/python/tvm/tir/buffer.py", line 232, in getitem return BufferLoad(self, expr_indices)

File "...tilelang/3rdparty/tvm/python/tvm/tir/expr.py", line 1120, in init self.init_handle_by_constructor(

File "python/tvm_ffi/cython/object.pxi", line 186, in tvm_ffi.core.Object.init_handle_by_constructor File "python/tvm_ffi/cython/function.pxi", line 123, in tvm_ffi.core.ConstructorCall File "python/tvm_ffi/cython/error.pxi", line 159, in tvm_ffi.core.CHECK_CALL File "/home/petersktang/Workspace/tile-ai.tilelang/3rdparty/tvm/src/tir/ir/expr.cc", line 865, in tvm::tir::__TVMFFIStaticInitFunc33()::{lambda(tvm::tir::Buffer, tvm::ffi::Array<tvm::PrimExpr, void>, tvm::ffi::Optional<tvm::PrimExpr, void>, tvm::Span)#1}::operator()(tvm::tir::Buffer, tvm::ffi::Array<tvm::PrimExpr, void>, tvm::ffi::Optional<tvm::PrimExpr, void>, tvm::Span) const return BufferLoad(buffer, indices, predicate, span);

File "...tilelang/3rdparty/tvm/src/tir/ir/expr.cc", line 825, in tvm::tir::BufferLoad::BufferLoad(tvm::tir::Buffer, tvm::ffi::Array<tvm::PrimExpr, void>, tvm::ffi::Optional<tvm::PrimExpr, void>, tvm::Span) ICHECK_EQ(buffer->shape.size(), indices.size())

File "..tilelang/3rdparty/tvm/include/tvm/runtime/logging.h", line 321, in tvm::runtime::detail::LogFatal::~LogFatal() GetEntry().Finalize();

File "...tilelang/3rdparty/tvm/include/tvm/runtime/logging.h", line 337, in tvm::runtime::detail::LogFatal::Entry::Finalize() InternalError error(file_, lineno_, stream_.str());

tvm.error.InternalError: Check failed: buffer->shape.size() == indices.size() (3 vs. 2) : Buffer A_shared is 3-dimensional, cannot be indexed with the 2-dimensional indices provided.`

Thx for your reply! I've reproduced your problem, and it seems that something went wrong after merging updated main branch! Let me check if anything conflicts.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tilelang/intrinsics/wmma_macro_generator.py (1)

197-198: Potential staged-buffer misread if 3D buffers are passed unsliced: leading axes pinned to min=0.

Line 197/243 extracts A_other/B_other from region mins. For a 2D buffer, this is empty; for rank > 2, it captures leading dimension mins (e.g., [0] for stage axis). If a 3D staged buffer is passed directly without slicing, rk is effectively ignored and loads always read stage 0.

Currently, all 46 callsites pass 2D buffers so this is not active. However, staged 3D buffers do exist in the codebase (e.g., example_warp_specialize_gemm_barrierpipe_stage2.py), and the method signature (in MFMA as Buffer | BufferRegion) permits them. Adding a runtime guard would prevent silent correctness errors if the interface is misused in the future.

Proposed guard
@@ def ldmatrix_a(self, A_local_buf, A_shared_buf, ki, rk=0):
         A_region = self._legalize_to_buffer_region(A_shared_buf)
+        if isinstance(A_shared_buf, Buffer) and len(A_region.region) > 2:
+            raise ValueError(
+                "ldmatrix_a expects a 2D buffer or a sliced BufferRegion for rank > 2"
+            )
         A_buf = A_region.buffer
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tilelang/intrinsics/wmma_macro_generator.py` around lines 197 - 198, The code
extracts A_other/B_other from A_region.region[:-2] and B_region.region[:-2]
which silently captures leading axes for rank>2 and can mask staged-buffer
misreads; add a runtime guard in the same scope (near where A_other, B_other and
rk are used in wmma_macro_generator.py) that checks the region rank/leading dims
and errors if a 3+D BufferRegion is passed unsliced: require
len(A_region.region) <= 2 (and same for B_region) or verify all leading-region
mins are 0 and document and raise a clear exception (ValueError) if not, so
callers must explicitly slice staged 3D buffers before calling the function.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tilelang/intrinsics/wmma_macro_generator.py`:
- Around line 206-207: The code uses a single-letter variable `l` in both A and
B buffer indexing which trips Ruff E741; rename these ambiguous locals to
descriptive names (e.g., replace `l` with `row_base` in the A-branch and with
`col_base` in the B-branch, or choose consistent names that match intent) and
update all uses in the A_local_buf and B_local_buf assignment expressions and
any subsequent calculations that reference `l` (look for symbols like
A_local_buf, B_local_buf, k_pack, chunk, ki, micro_size_k, warp_m,
warp_row_tiles, micro_size_x, local_size_a, local_size_b, local_id and replace
`l` in those expressions accordingly) so the indices and arithmetic remain
identical but no single-letter `l` remains.

---

Nitpick comments:
In `@tilelang/intrinsics/wmma_macro_generator.py`:
- Around line 197-198: The code extracts A_other/B_other from
A_region.region[:-2] and B_region.region[:-2] which silently captures leading
axes for rank>2 and can mask staged-buffer misreads; add a runtime guard in the
same scope (near where A_other, B_other and rk are used in
wmma_macro_generator.py) that checks the region rank/leading dims and errors if
a 3+D BufferRegion is passed unsliced: require len(A_region.region) <= 2 (and
same for B_region) or verify all leading-region mins are 0 and document and
raise a clear exception (ValueError) if not, so callers must explicitly slice
staged 3D buffers before calling the function.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 23993cc1-c9f2-41ee-a64d-32f641bb9839

📥 Commits

Reviewing files that changed from the base of the PR and between 3142b34 and 293d605.

📒 Files selected for processing (1)
  • tilelang/intrinsics/wmma_macro_generator.py

Comment on lines +206 to +207
l, r = (rk * chunk + ki * (k_pack * micro_size_k), warp_m * warp_row_tiles + i * micro_size_x)
A_local_buf[i * k_pack * local_size_a + local_id] = A_buf[A_base0 + l + row, A_base1 + r + col]
A_local_buf[i * k_pack * local_size_a + local_id] = A_buf[

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.

⚠️ Potential issue | 🟡 Minor

Rename ambiguous l locals to satisfy Ruff E741.

Line 214 and Line 260 use l as a variable name; Ruff flags this as ambiguous (E741). Rename to explicit names (e.g., row_base, col_base) in both A/B branches.

Also applies to: 214-215, 252-253, 260-261

🧰 Tools
🪛 Ruff (0.15.9)

[error] 206-206: Ambiguous variable name: l

(E741)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tilelang/intrinsics/wmma_macro_generator.py` around lines 206 - 207, The code
uses a single-letter variable `l` in both A and B buffer indexing which trips
Ruff E741; rename these ambiguous locals to descriptive names (e.g., replace `l`
with `row_base` in the A-branch and with `col_base` in the B-branch, or choose
consistent names that match intent) and update all uses in the A_local_buf and
B_local_buf assignment expressions and any subsequent calculations that
reference `l` (look for symbols like A_local_buf, B_local_buf, k_pack, chunk,
ki, micro_size_k, warp_m, warp_row_tiles, micro_size_x, local_size_a,
local_size_b, local_id and replace `l` in those expressions accordingly) so the
indices and arithmetic remain identical but no single-letter `l` remains.

@jiawei-real

Copy link
Copy Markdown
Contributor Author

@petersktang I have found the conflict and already fixed it, could you plz 1. clear your local tilelang cache and 2. try again? I've tested locally and both of them passed.

Root Cause

WMMAIntrinEmitter.ldmatrix_a() and ldmatrix_b() in tilelang/intrinsics/wmma_macro_generator.py only indexed the last two dimensions of the legalized shared buffer region.

This issue was exposed by PR #2002 , which changed the pipeline flow so software-pipelined shared buffers could be multi-versioned earlier in lowering and gain an extra leading stage dimension, for example:

  • [M, K] -> [stage, M, K]
  • [K, N] -> [stage, K, N]

WMMA still assumed that those shared buffers were always 2-D, so it emitted 2-D indexing for a 3-D buffer. That mismatch caused errors such as:

Buffer A_shared is 3-dimensional, cannot be indexed with the 2-dimensional indices provided.

Fix

The fix preserves any leading dimensions from the legalized BufferRegion and only applies WMMA tile offsets to the final two spatial dimensions.

In practice, the load path now:

  • collects leading indices from region[:-2]
  • keeps the original base indices for the last two dimensions
  • builds the final access as leading_dims + (row_dim, col_dim)

This approach was implemented by following the corresponding MFMA shared-load logic in tilelang/intrinsics/mfma_macro_generator.py. The WMMA path now mirrors the MFMA behavior by:

  • preserving the leading dimensions from the buffer region
  • appending WMMA tile-local offsets only to the last two dimensions

This keeps WMMA shared loads compatible with both regular 2-D shared buffers and pipelined multi-versioned shared buffers.

@petersktang

Copy link
Copy Markdown

Results after running examples/gemm/example_gemm.py looks good:

`
c:
tensor([[ 5.1219e+01, 3.5469e+01, 4.6875e-01, ..., -1.1056e+02,
-1.2789e+01, -1.9312e+01],
[ 3.2938e+01, -3.5312e+01, 3.1938e+01, ..., 2.3047e+01,
-9.0234e+00, 5.9414e+00],
[-1.6922e+01, -1.2094e+01, -2.7285e+00, ..., 6.6758e+00,
-2.9781e+01, 4.5078e+00],
...,
[-1.5805e+01, -3.4969e+01, 2.0078e+01, ..., -1.6328e+01,
-6.1211e+00, -5.7938e+01],
[ 5.0859e+00, 2.1531e+01, -2.9434e+00, ..., 3.5625e+01,
3.5801e+00, 4.0188e+01],
[ 1.4565e-02, 8.3062e+01, -3.8000e+01, ..., -6.3656e+01,
-4.4180e+00, 4.2562e+01]], device='cuda:0', dtype=torch.float16)
ref_c:
tensor([[ 5.1219e+01, 3.5469e+01, 4.6875e-01, ..., -1.1056e+02,
-1.2789e+01, -1.9312e+01],
[ 3.2938e+01, -3.5312e+01, 3.1938e+01, ..., 2.3047e+01,
-9.0234e+00, 5.9414e+00],
[-1.6922e+01, -1.2094e+01, -2.7285e+00, ..., 6.6758e+00,
-2.9781e+01, 4.5078e+00],
...,
[-1.5805e+01, -3.4969e+01, 2.0078e+01, ..., -1.6328e+01,
-6.1211e+00, -5.7938e+01],
[ 5.0859e+00, 2.1531e+01, -2.9434e+00, ..., 3.5625e+01,
3.5801e+00, 4.0188e+01],
[ 1.4404e-02, 8.3062e+01, -3.8000e+01, ..., -6.3656e+01,
-4.4180e+00, 4.2562e+01]], device='cuda:0', dtype=torch.float16)
All check passed.
CUDA Source:
#include <hip/hip_runtime.h>
#include <tl_templates/hip/gemm.h>
#include <tl_templates/hip/copy.h>
#include <tl_templates/hip/reduce.h>
#include <tl_templates/hip/ldsm.h>
#include <tl_templates/hip/threadblock_swizzle.h>
#include <tl_templates/hip/debug.h>

extern "C" global void launch_bounds(128) gemm_kernel(half_t* restrict A, half_t* restrict B, half_t* restrict C) {
extern shared align(1024) uchar buf_dyn_shmem[];
float C_local[128];
#pragma unroll
for (int i = 0; i < 32; ++i) {
float broadcast_var = 0.000000e+00f;
(float4)(C_local + (i * 4)) = make_float4(broadcast_var, broadcast_var, broadcast_var, broadcast_var);
}
#pragma unroll
for (int i_1 = 0; i_1 < 4; ++i_1) {
(uint4)(((half_t*)buf_dyn_shmem) + ((((i_1 * 1024) + ((((int)threadIdx.x) >> 2) * 32)) + (((((((int)threadIdx.x) & 31) >> 4) + ((((int)threadIdx.x) & 3) >> 1)) & 1) * 16)) + (((((((int)threadIdx.x) & 15) >> 3) + (((int)threadIdx.x) & 1)) & 1) * 8))) = (uint4)(A + ((((((int)blockIdx.y) * 131072) + (i_1 * 32768)) + ((((int)threadIdx.x) >> 2) * 1024)) + ((((int)threadIdx.x) & 3) * 8)));
}
#pragma unroll
for (int i_2 = 0; i_2 < 4; ++i_2) {
(uint4)(((half_t*)buf_dyn_shmem) + (((((((((((int)threadIdx.x) & 15) >> 3) * 2048) + (i_2 * 512)) + ((((int)threadIdx.x) >> 4) * 64)) + ((((((int)threadIdx.x) >> 6) + ((((int)threadIdx.x) & 7) >> 2)) & 1) * 32)) + (((((((int)threadIdx.x) & 63) >> 5) + ((((int)threadIdx.x) & 3) >> 1)) & 1) * 16)) + (((((((int)threadIdx.x) & 31) >> 4) + (((int)threadIdx.x) & 1)) & 1) * 8)) + 12288)) = (uint4)(B + ((((i_2 * 8192) + ((((int)threadIdx.x) >> 4) * 1024)) + (((int)blockIdx.x) * 128)) + ((((int)threadIdx.x) & 15) * 8)));
}
#pragma unroll
for (int i_3 = 0; i_3 < 4; ++i_3) {
(uint4)(((half_t*)buf_dyn_shmem) + (((((i_3 * 1024) + ((((int)threadIdx.x) >> 2) * 32)) + (((((((int)threadIdx.x) & 31) >> 4) + ((((int)threadIdx.x) & 3) >> 1)) & 1) * 16)) + (((((((int)threadIdx.x) & 15) >> 3) + (((int)threadIdx.x) & 1)) & 1) * 8)) + 4096)) = (uint4)(A + (((((((int)blockIdx.y) * 131072) + (i_3 * 32768)) + ((((int)threadIdx.x) >> 2) * 1024)) + ((((int)threadIdx.x) & 3) * 8)) + 32));
}
#pragma unroll
for (int i_4 = 0; i_4 < 4; ++i_4) {
(uint4)(((half_t*)buf_dyn_shmem) + (((((((((((int)threadIdx.x) & 15) >> 3) * 2048) + (i_4 * 512)) + ((((int)threadIdx.x) >> 4) * 64)) + ((((((int)threadIdx.x) >> 6) + ((((int)threadIdx.x) & 7) >> 2)) & 1) * 32)) + (((((((int)threadIdx.x) & 63) >> 5) + ((((int)threadIdx.x) & 3) >> 1)) & 1) * 16)) + (((((((int)threadIdx.x) & 31) >> 4) + (((int)threadIdx.x) & 1)) & 1) * 8)) + 16384)) = (uint4)(B + (((((i_4 * 8192) + ((((int)threadIdx.x) >> 4) * 1024)) + (((int)blockIdx.x) * 128)) + ((((int)threadIdx.x) & 15) * 8)) + 32768));
}
for (int k = 0; k < 30; ++k) {
__syncthreads();
#pragma unroll
for (int i_5 = 0; i_5 < 4; ++i_5) {
(uint4)(((half_t*)buf_dyn_shmem) + (((((((k + 2) % 3) * 4096) + (i_5 * 1024)) + ((((int)threadIdx.x) >> 2) * 32)) + (((((((int)threadIdx.x) & 31) >> 4) + ((((int)threadIdx.x) & 3) >> 1)) & 1) * 16)) + (((((((int)threadIdx.x) & 15) >> 3) + (((int)threadIdx.x) & 1)) & 1) * 8))) = (uint4)(A + ((((((((int)blockIdx.y) * 131072) + (i_5 * 32768)) + ((((int)threadIdx.x) >> 2) * 1024)) + (k * 32)) + ((((int)threadIdx.x) & 3) * 8)) + 64));
}
#pragma unroll
for (int i_6 = 0; i_6 < 4; ++i_6) {
(uint4)(((half_t*)buf_dyn_shmem) + ((((((((((k + 2) % 3) * 4096) + (((((int)threadIdx.x) & 15) >> 3) * 2048)) + (i_6 * 512)) + ((((int)threadIdx.x) >> 4) * 64)) + ((((((int)threadIdx.x) >> 6) + ((((int)threadIdx.x) & 7) >> 2)) & 1) * 32)) + (((((((int)threadIdx.x) & 63) >> 5) + ((((int)threadIdx.x) & 3) >> 1)) & 1) * 16)) + (((((((int)threadIdx.x) & 31) >> 4) + (((int)threadIdx.x) & 1)) & 1) * 8)) + 12288)) = (uint4)(B + ((((((k * 32768) + (i_6 * 8192)) + ((((int)threadIdx.x) >> 4) * 1024)) + (((int)blockIdx.x) * 128)) + ((((int)threadIdx.x) & 15) * 8)) + 65536));
}
{
half_t A_local[64];
half_t B_local[64];
__syncthreads();
for (int ki = 0; ki < 2; ++ki) {
for (int i_7 = 0; i_7 < 4; ++i_7) {
for (int col = 0; col < 2; ++col) {
(uint4)(A_local + ((i_7 * 16) + (col * 8))) = (uint4)(((half_t*)buf_dyn_shmem) + (((((((k % 3) * 4096) + (((((int)threadIdx.x) & 63) >> 5) * 2048)) + (i_7 * 512)) + ((((int)threadIdx.x) & 15) * 32)) + (((((((int)threadIdx.x) & 7) >> 2) + ki) & 1) * 16)) + (((((((int)threadIdx.x) & 3) >> 1) + col) & 1) * 8)));
}
}
for (int j = 0; j < 4; ++j) {
for (int row = 0; row < 16; ++row) {
B_local[((j * 16) + row)] = ((half_t*)buf_dyn_shmem)[((((((((((k % 3) * 4096) + ((((int)threadIdx.x) >> 6) * 2048)) + (ki * 1024)) + (row * 64)) + (((((row & 7) >> 2) + (j >> 1)) & 1) * 32)) + (((((row & 3) >> 1) + (j & 1)) & 1) * 16)) + (((((((int)threadIdx.x) & 15) >> 3) + (row & 1)) & 1) * 8)) + (((int)threadIdx.x) & 7)) + 12288)];
}
}
for (int i_8 = 0; i_8 < 4; ++i_8) {
for (int j_1 = 0; j_1 < 4; ++j_1) {
{
typedef attribute((vector_size(16 * sizeof(__fp16)))) __fp16 tl_v16f16;
typedef attribute((vector_size(8 * sizeof(float)))) float tl_v8f32;
((tl_v8f32)C_local + ((i_8 * 4) + j_1)) = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(
((tl_v16f16)A_local + i_8),
((tl_v16f16)B_local + j_1),
((tl_v8f32)C_local + ((i_8 * 4) + j_1)));
};
}
}
}
}
}
{
half_t A_local_1[64];
half_t B_local_1[64];
for (int ki_1 = 0; ki_1 < 2; ++ki_1) {
for (int i_9 = 0; i_9 < 4; ++i_9) {
for (int col_1 = 0; col_1 < 2; ++col_1) {
(uint4)(A_local_1 + ((i_9 * 16) + (col_1 * 8))) = (uint4)(((half_t*)buf_dyn_shmem) + (((((((((int)threadIdx.x) & 63) >> 5) * 2048) + (i_9 * 512)) + ((((int)threadIdx.x) & 15) * 32)) + (((((((int)threadIdx.x) & 7) >> 2) + ki_1) & 1) * 16)) + (((((((int)threadIdx.x) & 3) >> 1) + col_1) & 1) * 8)));
}
}
for (int j_2 = 0; j_2 < 4; ++j_2) {
for (int row_1 = 0; row_1 < 16; ++row_1) {
B_local_1[((j_2 * 16) + row_1)] = ((half_t*)buf_dyn_shmem)[(((((((((((int)threadIdx.x) >> 6) * 2048) + (ki_1 * 1024)) + (row_1 * 64)) + (((((row_1 & 7) >> 2) + (j_2 >> 1)) & 1) * 32)) + (((((row_1 & 3) >> 1) + (j_2 & 1)) & 1) * 16)) + (((((((int)threadIdx.x) & 15) >> 3) + (row_1 & 1)) & 1) * 8)) + (((int)threadIdx.x) & 7)) + 12288)];
}
}
for (int i_10 = 0; i_10 < 4; ++i_10) {
for (int j_3 = 0; j_3 < 4; ++j_3) {
{
typedef attribute((vector_size(16 * sizeof(__fp16)))) __fp16 tl_v16f16;
typedef attribute((vector_size(8 * sizeof(float)))) float tl_v8f32;
((tl_v8f32)C_local + ((i_10 * 4) + j_3)) = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(
((tl_v16f16)A_local_1 + i_10),
((tl_v16f16)B_local_1 + j_3),
((tl_v8f32)C_local + ((i_10 * 4) + j_3)));
};
}
}
}
}
{
half_t A_local_2[64];
half_t B_local_2[64];
for (int ki_2 = 0; ki_2 < 2; ++ki_2) {
for (int i_11 = 0; i_11 < 4; ++i_11) {
for (int col_2 = 0; col_2 < 2; ++col_2) {
(uint4)(A_local_2 + ((i_11 * 16) + (col_2 * 8))) = (uint4)(((half_t*)buf_dyn_shmem) + ((((((((((int)threadIdx.x) & 63) >> 5) * 2048) + (i_11 * 512)) + ((((int)threadIdx.x) & 15) * 32)) + (((((((int)threadIdx.x) & 7) >> 2) + ki_2) & 1) * 16)) + (((((((int)threadIdx.x) & 3) >> 1) + col_2) & 1) * 8)) + 4096));
}
}
for (int j_4 = 0; j_4 < 4; ++j_4) {
for (int row_2 = 0; row_2 < 16; ++row_2) {
B_local_2[((j_4 * 16) + row_2)] = ((half_t*)buf_dyn_shmem)[(((((((((((int)threadIdx.x) >> 6) * 2048) + (ki_2 * 1024)) + (row_2 * 64)) + (((((row_2 & 7) >> 2) + (j_4 >> 1)) & 1) * 32)) + (((((row_2 & 3) >> 1) + (j_4 & 1)) & 1) * 16)) + (((((((int)threadIdx.x) & 15) >> 3) + (row_2 & 1)) & 1) * 8)) + (((int)threadIdx.x) & 7)) + 16384)];
}
}
for (int i_12 = 0; i_12 < 4; ++i_12) {
for (int j_5 = 0; j_5 < 4; ++j_5) {
{
typedef attribute((vector_size(16 * sizeof(__fp16)))) __fp16 tl_v16f16;
typedef attribute((vector_size(8 * sizeof(float)))) float tl_v8f32;
((tl_v8f32)C_local + ((i_12 * 4) + j_5)) = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(
((tl_v16f16)A_local_2 + i_12),
((tl_v16f16)B_local_2 + j_5),
((tl_v8f32)C_local + ((i_12 * 4) + j_5)));
};
}
}
}
}
#pragma unroll
for (int i_13 = 0; i_13 < 128; ++i_13) {
C[(((((((((((int)blockIdx.y) * 131072) + (((((int)threadIdx.x) & 63) >> 5) * 65536)) + ((i_13 >> 5) * 16384)) + ((i_13 & 7) * 2048)) + (((((int)threadIdx.x) & 31) >> 4) * 1024)) + (((int)blockIdx.x) * 128)) + ((((int)threadIdx.x) >> 6) * 64)) + (((i_13 & 31) >> 3) * 16)) + (((int)threadIdx.x) & 15))] = ((half_t)C_local[i_13]);
}
}

tilelang Latency: 0.543604384615385ms
`
It is still failing when running examples/amd/example_amd_flash_attn_bwd.py.

@petersktang

Copy link
Copy Markdown

example_amd_flash_attn_fwd.py is successful.

2026-04-14 19:05:00,442 WARNING: Incompatible input tensor properties detected between cached tensors and tensors regenerated for the current configuration trial. This can happen if different tuning configurations require different input shapes/dtypes and input tensor caching is enabled. To ensure fresh, compatible inputs are generated for every trial you can disable caching by setting: cache_input_tensors=Falsewithin your.set_compile_args(...)` call.

Tuned Latency 21.765731811523438 with config {'block_M': 32, 'block_N': 32, 'num_split_q': 64, 'threads': 64, 'num_stages': 0, 'enable_rasterization': True, 'k_pack': 1, 'panel_size': 7, 'qk_coalesced_width': 8, 'v_coalesced_width': 4} at index 40
Bench configurations: 98%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▍ | 41/42 [00:18<00:00, 2.31it/s, best_latency=17.7]2026-04-14 19:05:00,746 WARNING:
Incompatible input tensor properties detected between cached tensors and tensors regenerated for the current configuration trial. This can happen if different tuning configurations require different input shapes/dtypes and input tensor caching is enabled.
To ensure fresh, compatible inputs are generated for every trial you can disable caching by setting:
cache_input_tensors=False
within your .set_compile_args(...) call.

Tuned Latency 65.1008529663086 with config {'block_M': 32, 'block_N': 32, 'num_split_q': 32, 'threads': 32, 'num_stages': 0, 'enable_rasterization': True, 'k_pack': 1, 'panel_size': 8, 'qk_coalesced_width': 8, 'v_coalesced_width': 4} at index 41
Bench configurations: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 42/42 [00:18<00:00, 2.24it/s, best_latency=17.7]
Autotuning finished. Best Configuration: {'block_M': 32, 'block_N': 16, 'num_split_q': 64, 'threads': 64, 'num_stages': 0, 'enable_rasterization': True, 'k_pack': 1, 'panel_size': 8, 'qk_coalesced_width': 8, 'v_coalesced_width': 4}
Verifying correctness...
All checks pass.
Reference (PyTorch): 41.86 ms | 1.64 TFlops
Fast Flash Attention V2 (Tile-lang): 15.93 ms | 4.31 TFlops`

@jiawei-real

Copy link
Copy Markdown
Contributor Author

example_amd_flash_attn_fwd.py is successful.

2026-04-14 19:05:00,442 WARNING: Incompatible input tensor properties detected between cached tensors and tensors regenerated for the current configuration trial. This can happen if different tuning configurations require different input shapes/dtypes and input tensor caching is enabled. To ensure fresh, compatible inputs are generated for every trial you can disable caching by setting: cache_input_tensors=Falsewithin your.set_compile_args(...)` call.

Tuned Latency 21.765731811523438 with config {'block_M': 32, 'block_N': 32, 'num_split_q': 64, 'threads': 64, 'num_stages': 0, 'enable_rasterization': True, 'k_pack': 1, 'panel_size': 7, 'qk_coalesced_width': 8, 'v_coalesced_width': 4} at index 40 Bench configurations: 98%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▍ | 41/42 [00:18<00:00, 2.31it/s, best_latency=17.7]2026-04-14 19:05:00,746 WARNING: Incompatible input tensor properties detected between cached tensors and tensors regenerated for the current configuration trial. This can happen if different tuning configurations require different input shapes/dtypes and input tensor caching is enabled. To ensure fresh, compatible inputs are generated for every trial you can disable caching by setting: cache_input_tensors=False within your .set_compile_args(...) call.

Tuned Latency 65.1008529663086 with config {'block_M': 32, 'block_N': 32, 'num_split_q': 32, 'threads': 32, 'num_stages': 0, 'enable_rasterization': True, 'k_pack': 1, 'panel_size': 8, 'qk_coalesced_width': 8, 'v_coalesced_width': 4} at index 41 Bench configurations: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 42/42 [00:18<00:00, 2.24it/s, best_latency=17.7] Autotuning finished. Best Configuration: {'block_M': 32, 'block_N': 16, 'num_split_q': 64, 'threads': 64, 'num_stages': 0, 'enable_rasterization': True, 'k_pack': 1, 'panel_size': 8, 'qk_coalesced_width': 8, 'v_coalesced_width': 4} Verifying correctness... All checks pass. Reference (PyTorch): 41.86 ms | 1.64 TFlops Fast Flash Attention V2 (Tile-lang): 15.93 ms | 4.31 TFlops`

That's it, bwd implementation does not perform special adaptation for RDNA (fwd does), which might be why bwd isn't working.

@jiawei-real

jiawei-real commented Apr 14, 2026 •

Copy link
Copy Markdown
Contributor Author

@petersktang @zhangnju @LeiWang1999 BTW, I discovered a legacy issue from PR #1951 : layout inference in tilelang/intrinsics/wmma_macro_generator.py doesn't seem to adapt to the case where trans_A=true. I'm not sure if there is any specific reason for this.....This will cause an error when running T.gemm(xxx, transpose_A=True). Maybe I'll try to fix it later.

I made a quick-fixing version for this test, you can have a try:

import sys
import torch
import torch.nn.functional as F
import tilelang
import tilelang.language as T
from tilelang.tileop.base import GemmWarpPolicy
import itertools
import argparse
from functools import partial
import numpy as np
import time


def IsRDNA():
    if torch.cuda.is_available():
        gpu_name = torch.cuda.get_device_name().strip()
        return "Radeon" in gpu_name
    else:
        print("Error: GPU Device is not detected")
        sys.exit(1)


def ref_program(Q, K, V, is_causal, groups=1):
    assert Q.size(2) == K.size(2) * groups, f"Q heads {Q.size(2)} K heads {K.size(2)} groups {groups}"
    assert Q.size(2) == V.size(2) * groups, f"Q heads {Q.size(2)} V heads {V.size(2)} groups {groups}"
    dim = Q.size(-1)
    K_ref = K.repeat_interleave(groups, dim=2)
    V_ref = V.repeat_interleave(groups, dim=2)
    scores = torch.einsum("bqhd,bkhd->bhqk", Q, K_ref)
    scores = scores / torch.sqrt(torch.tensor(dim, dtype=scores.dtype))
    if is_causal:
        seq_len = Q.size(1)
        mask = torch.tril(torch.ones(seq_len, seq_len, device=scores.device))
        mask = mask.unsqueeze(0).unsqueeze(0)
        scores = scores.masked_fill(mask == 0, float("-inf"))
    attention_weights = F.softmax(scores, dim=-1)
    output = torch.einsum("bhqk,bkhd->bqhd", attention_weights, V_ref)
    lse = torch.logsumexp(scores, dim=-1).float()
    return output, lse


def get_fwd_configs():
    # Match the standalone forward example on RDNA. WMMA configs larger than
    # 32x32 can trigger layout issues when bridging the softmax fragment into
    # the second GEMM's A-layout.
    if IsRDNA():
        block_M = [16, 32, 64]
        block_N = [16, 32, 64]
        threads = [32, 64]
        num_split_q = [16, 32, 64]
        num_stages = [0]
        k_pack = [1]
    else:
        block_M = [32, 64, 128, 256]
        block_N = [32, 64, 128, 256]
        threads = [128, 256, 512]
        num_split_q = [64, 128, 256]
        num_stages = [0, 1]
        k_pack = [2]
    enable_rasterization = [True]
    panel_size = [7, 8, 9, 10]
    qk_coalesced_width = [8]
    v_coalesced_width = [4]

    valid_configs = []

    for m, n, s, t, stages, r, k, p, qkw, vw in itertools.product(
        block_M, block_N, num_split_q, threads, num_stages, enable_rasterization, k_pack, panel_size, qk_coalesced_width, v_coalesced_width
    ):
        if IsRDNA() and m == 16 and n == 16 and t == 64:
            continue
        valid_configs.append(
            {
                "block_M": m,
                "block_N": n,
                "num_split_q": s,
                "threads": t,
                "num_stages": stages,
                "enable_rasterization": r,
                "k_pack": k,
                "panel_size": p,
                "qk_coalesced_width": qkw,
                "v_coalesced_width": vw,
            }
        )
    return valid_configs


@tilelang.autotune(configs=get_fwd_configs(), cache_input_tensors=True)
@tilelang.jit(out_idx=[3, 4])
def fast_flashattn(
    batch,
    heads,
    seq_len,
    dim,
    is_causal,
    groups,
    block_M: int,
    block_N: int,
    num_split_q: int,
    threads: int,
    num_stages: int,
    enable_rasterization: bool,
    k_pack: int,
    panel_size: int,
    qk_coalesced_width: int,
    v_coalesced_width: int,
):
    scale = (1.0 / dim) ** 0.5
    head_kv = heads // groups
    q_shape = [batch, seq_len, heads, dim]
    kv_shape = [batch, seq_len, head_kv, dim]
    dtype = T.float16
    accum_dtype = T.float32

    vec_size = qk_coalesced_width
    v_vec_size = v_coalesced_width

    @T.prim_func
    def main(
        Q: T.Tensor(q_shape, dtype),
        K: T.Tensor(kv_shape, dtype),
        V: T.Tensor(kv_shape, dtype),
        Output: T.Tensor(q_shape, dtype),
        LSE: T.Tensor([batch, heads, seq_len], accum_dtype),
    ):
        with T.Kernel(num_split_q, batch * heads, threads=threads) as (b_split, byz_combined):
            T.use_swizzle(panel_size, enable=enable_rasterization)

            bz = byz_combined // heads
            by = byz_combined % heads

            num_q_blocks = T.ceildiv(seq_len, block_M)

            bx_loop_var = T.alloc_var(T.int32)
            bx_loop_var = b_split

            while bx_loop_var < num_q_blocks:
                acc_o = T.alloc_fragment([block_M, dim], accum_dtype)
                m_i = T.alloc_fragment([block_M], accum_dtype)
                l_i = T.alloc_fragment([block_M], accum_dtype)

                T.fill(acc_o, 0)
                T.fill(m_i, -T.infinity(accum_dtype))
                T.fill(l_i, 0)

                current_bx = bx_loop_var
                q_block_offset = current_bx * block_M

                Q_shared = T.alloc_shared([block_M, dim], dtype)
                K_shared = T.alloc_shared([block_N, dim], dtype)
                V_shared = T.alloc_shared([block_N, dim], dtype)
                # Bridge the WMMA D-layout softmax fragment into the A-layout
                # expected by GEMM 2 on RDNA GPUs.
                if IsRDNA():
                    P_shared = T.alloc_shared([block_M, block_N], dtype)
                acc_s_cast = T.alloc_fragment([block_M, block_N], dtype)

                acc_s = T.alloc_fragment([block_M, block_N], accum_dtype)
                m_prev = T.alloc_fragment([block_M], accum_dtype)
                scale_factor = T.alloc_fragment([block_M], accum_dtype)

                T.copy(Q[bz, q_block_offset : q_block_offset + block_M, by, :], Q_shared, coalesced_width=vec_size)

                loop_end_k = T.ceildiv(q_block_offset + block_M, block_N) if is_causal else T.ceildiv(seq_len, block_N)

                row_sum = T.alloc_fragment([block_M], accum_dtype)

                for k in T.Pipelined(loop_end_k, num_stages=num_stages):
                    kv_idx = k * block_N

                    T.copy(K[bz, kv_idx : kv_idx + block_N, by // groups, :], K_shared, coalesced_width=vec_size)
                    T.copy(V[bz, kv_idx : kv_idx + block_N, by // groups, :], V_shared, coalesced_width=v_vec_size)

                    if is_causal:
                        for i, j in T.Parallel(block_M, block_N):
                            acc_s[i, j] = T.if_then_else(q_block_offset + i >= kv_idx + j, 0, -T.infinity(acc_s.dtype))
                    else:
                        T.clear(acc_s)
                    T.gemm(
                        Q_shared,
                        K_shared,
                        acc_s,
                        transpose_B=True,
                        k_pack=k_pack,
                        policy=GemmWarpPolicy.FullRow,
                    )

                    for i, j in T.Parallel(block_M, block_N):
                        acc_s[i, j] = acc_s[i, j] * scale

                    T.copy(m_i, m_prev)
                    T.reduce_max(acc_s, m_i, dim=1, clear=False)
                    for i in T.Parallel(block_M):
                        m_i[i] = T.max(m_i[i], m_prev[i])

                    for i in T.Parallel(block_M):
                        if m_prev[i] == -T.infinity(accum_dtype):
                            scale_factor[i] = 0.0
                        else:
                            scale_factor[i] = T.exp(m_prev[i] - m_i[i])

                        l_i[i] *= scale_factor[i]

                    for i, j in T.Parallel(block_M, dim):
                        acc_o[i, j] *= scale_factor[i]

                    for i, j in T.Parallel(block_M, block_N):
                        if acc_s[i, j] == -T.infinity(acc_s.dtype):
                            acc_s[i, j] = 0.0
                        else:
                            acc_s[i, j] = T.exp(acc_s[i, j] - m_i[i])

                    T.reduce_sum(acc_s, row_sum, dim=1)
                    for i in T.Parallel(block_M):
                        l_i[i] += row_sum[i]

                    if IsRDNA():
                        for i, j in T.Parallel(block_M, block_N):
                            P_shared[i, j] = T.cast(acc_s[i, j], dtype)
                        T.copy(P_shared, acc_s_cast)
                    else:
                        T.copy(acc_s, acc_s_cast)

                    T.gemm(acc_s_cast, V_shared, acc_o, policy=GemmWarpPolicy.FullRow)

                l_inv = T.alloc_fragment([block_M], accum_dtype)
                for i in T.Parallel(block_M):
                    safe_l = T.if_then_else(l_i[i] > 1e-6, l_i[i], 1.0)
                    l_inv[i] = 1.0 / safe_l

                for i, j in T.Parallel(block_M, dim):
                    Output[bz, q_block_offset + i, by, j] = acc_o[i, j] * l_inv[i]

                for i in T.Parallel(block_M):
                    if q_block_offset + i < seq_len:
                        lse_val = T.if_then_else(l_i[i] > 0, T.log(l_i[i]) + m_i[i], -T.infinity(accum_dtype))
                        LSE[bz, by, q_block_offset + i] = lse_val

                bx_loop_var = current_bx + num_split_q

    return main


def get_bwd_configs():
    # Keep the RDNA search space aligned with the WMMA-friendly tile sizes
    # verified above. Larger tiles and some warp/block combinations are either
    # unsupported or known to trigger invalid lowering on RDNA.
    if IsRDNA():
        block_M = [16, 32]
        block_N = [16, 32]
        threads = [32, 64]
        num_stages = [0]
        panel_size = [7, 8]
    else:
        block_M = [16, 32, 64, 128, 256]
        block_N = [16, 32, 64, 128, 256]
        threads = [64, 128, 256, 512, 1024]
        num_stages = [0, 1, 2]
        panel_size = [7, 8, 9, 10]
    enable_rasterization = [True]

    configs = []
    for m, n, stages, t, r, p in itertools.product(block_M, block_N, num_stages, threads, enable_rasterization, panel_size):
        if IsRDNA() and m == 16 and n == 16 and t == 64:
            continue
        configs.append(
            {
                "block_M": m,
                "block_N": n,
                "num_stages": stages,
                "threads": t,
                "enable_rasterization": r,
                "panel_size": p,
            }
        )

    return configs


@tilelang.jit(out_idx=[2])
def flashattn_bwd_preprocess(batch, heads, seq_len, dim):
    dtype = T.float16
    accum_dtype = T.float32
    shape = [batch, seq_len, heads, dim]
    blk = 32

    @T.prim_func
    def flash_bwd_prep(O: T.Tensor(shape, dtype), dO: T.Tensor(shape, dtype), Delta: T.Tensor([batch, heads, seq_len], accum_dtype)):
        with T.Kernel(batch, heads, T.ceildiv(seq_len, blk)) as (bz, bx, by):
            o = T.alloc_fragment([blk, blk], dtype)
            do = T.alloc_fragment([blk, blk], dtype)
            acc = T.alloc_fragment([blk, blk], accum_dtype)
            delta = T.alloc_fragment([blk], accum_dtype)
            T.clear(acc)
            for k in range(T.ceildiv(dim, blk)):
                T.copy(O[bz, by * blk : (by + 1) * blk, bx, k * blk : (k + 1) * blk], o)
                T.copy(dO[bz, by * blk : (by + 1) * blk, bx, k * blk : (k + 1) * blk], do)
                for i, j in T.Parallel(blk, blk):
                    acc[i, j] += o[i, j] * do[i, j]
            T.reduce_sum(acc, delta, 1)
            T.copy(delta, Delta[bz, bx, by * blk : (by + 1) * blk])

    return flash_bwd_prep


@tilelang.autotune(configs=get_bwd_configs(), cache_input_tensors=True)
@tilelang.jit
def flashattn_bwd(
    batch,
    heads,
    seq_len,
    dim,
    is_causal,
    groups,
    block_M: int,
    block_N: int,
    num_stages: int,
    threads: int,
    enable_rasterization: bool,
    panel_size: int,
):
    sm_scale = (1.0 / dim) ** 0.5
    head_kv = heads // groups
    q_shape = [batch, seq_len, heads, dim]
    kv_shape = [batch, seq_len, head_kv, dim]
    dtype = T.float16
    accum_dtype = T.float32

    @T.prim_func
    def flash_bwd_kernel(
        Q: T.Tensor(q_shape, dtype),
        K: T.Tensor(kv_shape, dtype),
        V: T.Tensor(kv_shape, dtype),
        dO: T.Tensor(q_shape, dtype),
        lse: T.Tensor([batch, heads, seq_len], accum_dtype),
        Delta: T.Tensor([batch, heads, seq_len], accum_dtype),
        dQ: T.Tensor(q_shape, accum_dtype),
        dK: T.Tensor(kv_shape, accum_dtype),
        dV: T.Tensor(kv_shape, accum_dtype),
    ):
        with T.Kernel(heads, T.ceildiv(seq_len, block_M), batch, threads=threads) as (bx, by, bz):
            T.use_swizzle(panel_size, enable=enable_rasterization)
            T.annotate_layout({dQ: make_dq_layout(dQ)})

            K_shared = T.alloc_shared([block_M, dim], dtype)
            V_shared = T.alloc_shared([block_M, dim], dtype)
            q_shared = T.alloc_shared([block_N, dim], dtype)
            do_shared = T.alloc_shared([block_N, dim], dtype)
            lse_shared = T.alloc_shared([block_N], accum_dtype)
            delta_shared = T.alloc_shared([block_N], accum_dtype)
            ds_shared = T.alloc_shared([block_M, block_N], dtype)
            if IsRDNA():
                # Bridge the WMMA D-layout fragment produced by GEMM/elementwise
                # ops into the A-layout expected by the following GEMM.
                p_shared = T.alloc_shared([block_M, block_N], dtype)
                ds_trans_shared = T.alloc_shared([block_N, block_M], dtype)

            p_cast = T.alloc_fragment([block_M, block_N], dtype)
            qkT = T.alloc_fragment([block_M, block_N], accum_dtype)
            P_acc = T.alloc_fragment([block_M, block_N], accum_dtype)
            dP = T.alloc_fragment([block_M, block_N], accum_dtype)

            dv = T.alloc_fragment([block_M, dim], accum_dtype)
            dk = T.alloc_fragment([block_M, dim], accum_dtype)
            dq = T.alloc_fragment([block_N, dim], accum_dtype)

            T.copy(K[bz, by * block_M : (by + 1) * block_M, bx // groups, :], K_shared)
            T.copy(V[bz, by * block_M : (by + 1) * block_M, bx // groups, :], V_shared)
            T.clear(dv)
            T.clear(dk)

            loop_st = T.floordiv(by * block_M, block_N) if is_causal else 0
            loop_ed = T.ceildiv(seq_len, block_N)

            for k in T.Pipelined(loop_st, loop_ed, num_stages=num_stages):
                T.copy(Q[bz, k * block_N : (k + 1) * block_N, bx, :], q_shared)
                T.clear(qkT)

                T.gemm(K_shared, q_shared, qkT, transpose_B=True, policy=T.GemmWarpPolicy.FullRow)

                T.copy(lse[bz, bx, k * block_N : (k + 1) * block_N], lse_shared)

                for i, j in T.Parallel(block_M, block_N):
                    P_acc[i, j] = T.exp(qkT[i, j] * sm_scale - lse_shared[j])

                if is_causal:
                    for i, j in T.Parallel(block_M, block_N):
                        P_acc[i, j] = T.if_then_else(by * block_M + i <= k * block_N + j, P_acc[i, j], 0.0)

                T.copy(dO[bz, k * block_N : (k + 1) * block_N, bx, :], do_shared)
                T.clear(dP)

                T.gemm(V_shared, do_shared, dP, transpose_B=True, policy=T.GemmWarpPolicy.FullRow)

                if IsRDNA():
                    for i, j in T.Parallel(block_M, block_N):
                        p_shared[i, j] = T.cast(P_acc[i, j], dtype)
                    T.copy(p_shared, p_cast)
                else:
                    T.copy(P_acc, p_cast)
                T.gemm(p_cast, do_shared, dv, policy=T.GemmWarpPolicy.FullRow)

                T.copy(Delta[bz, bx, k * block_N : (k + 1) * block_N], delta_shared)

                if IsRDNA():
                    for i, j in T.Parallel(block_M, block_N):
                        dP[i, j] = P_acc[i, j] * (dP[i, j] - delta_shared[j]) * sm_scale
                    for i, j in T.Parallel(block_M, block_N):
                        p_shared[i, j] = T.cast(dP[i, j], dtype)
                    T.copy(p_shared, p_cast)
                    T.gemm(p_cast, q_shared, dk, policy=T.GemmWarpPolicy.FullRow)
                    for i, j in T.Parallel(block_M, block_N):
                        ds_trans_shared[j, i] = T.cast(dP[i, j], dtype)
                else:
                    for i, j in T.Parallel(block_M, block_N):
                        p_cast[i, j] = P_acc[i, j] * (dP[i, j] - delta_shared[j]) * sm_scale
                    T.gemm(p_cast, q_shared, dk, policy=T.GemmWarpPolicy.FullRow)
                    T.copy(p_cast, ds_shared)

                T.clear(dq)
                if IsRDNA():
                    T.gemm(ds_trans_shared, K_shared, dq, policy=T.GemmWarpPolicy.FullRow)
                else:
                    T.gemm(ds_shared, K_shared, dq, transpose_A=True)
                for i, j in T.Parallel(block_N, dim):
                    T.atomic_add(dQ[bz, k * block_N + i, bx, j], dq[i, j])

            for i, j in T.Parallel(block_M, dim):
                T.atomic_add(dV[bz, by * block_M + i, bx // groups, j], dv[i, j])
                T.atomic_add(dK[bz, by * block_M + i, bx // groups, j], dk[i, j])

    return flash_bwd_kernel


def make_dq_layout(dQ):
    # atomicAdd can not be vectorized, so we need to reorder dq to match the
    # 8x8 GEMM fragment layout before copying it back to a dense tensor.
    return T.Layout(dQ.shape, lambda b, l, h, d: [b, l // 8, h, d // 8, (d % 2), 4 * (l % 8) + (d % 8) // 2])


@tilelang.jit(out_idx=[1])
def flashattn_bwd_postprocess(batch, heads, seq_len, dim):
    dtype = T.float16
    accum_dtype = T.float32
    shape = [batch, seq_len, heads, dim]
    blk = 64

    @T.prim_func
    def flash_bwd_post(dQ_in: T.Tensor(shape, accum_dtype), dQ_out: T.Tensor(shape, dtype)):
        with T.Kernel(T.ceildiv(seq_len, blk), heads, batch, threads=128) as (bx, by, bz):
            T.annotate_layout({dQ_in: make_dq_layout(dQ_in)})
            T.copy(
                dQ_in[bz, bx * blk : (bx + 1) * blk, by, :],
                dQ_out[bz, bx * blk : (bx + 1) * blk, by, :],
            )

    return flash_bwd_post


def debug_tensor_comparison(tensor1, tensor2, name, rtol=1e-3, atol=1e-3):
    print(f"\n=== {name} Comparison ===")
    print(f"Shape: {tensor1.shape} vs {tensor2.shape}")
    print(f"Data type: {tensor1.dtype} vs {tensor2.dtype}")
    print(f"Device: {tensor1.device} vs {tensor2.device}")

    diff = torch.abs(tensor1 - tensor2)
    max_diff = diff.max().item()
    mean_diff = diff.mean().item()
    std_diff = diff.std().item()

    print(f"Max difference: {max_diff:.6f}")
    print(f"Mean difference: {mean_diff:.6f}")
    print(f"Difference std: {std_diff:.6f}")

    if max_diff > atol:
        max_idx = torch.argmax(diff)
        max_idx = np.unravel_index(max_idx.cpu().numpy(), tensor1.shape)
        print(f"Max difference position: {max_idx}")
        print(f"Value1: {tensor1[max_idx].item():.6f}, Value2: {tensor2[max_idx].item():.6f}")

    nan_count1 = torch.isnan(tensor1).sum().item()
    nan_count2 = torch.isnan(tensor2).sum().item()
    inf_count1 = torch.isinf(tensor1).sum().item()
    inf_count2 = torch.isinf(tensor2).sum().item()

    print(f"NaN count: {nan_count1} vs {nan_count2}")
    print(f"Inf count: {inf_count1} vs {inf_count2}")

    relative_diff = diff / (torch.abs(tensor2) + 1e-8)
    max_relative_diff = relative_diff.max().item()
    mean_relative_diff = relative_diff.mean().item()

    print(f"Max relative difference: {max_relative_diff:.6f}")
    print(f"Mean relative difference: {mean_relative_diff:.6f}")

    close = torch.allclose(tensor1, tensor2, rtol=rtol, atol=atol)
    print(f"Within tolerance (rtol={rtol}, atol={atol}): {close}")

    return close, max_diff, mean_diff


def benchmark_function(func, *args, warmup=10, repeat=100):
    for _ in range(warmup):
        func(*args)

    if torch.cuda.is_available():
        torch.cuda.synchronize()

    times = []
    for _ in range(repeat):
        start = time.time()
        func(*args)
        if torch.cuda.is_available():
            torch.cuda.synchronize()
        end = time.time()
        times.append((end - start) * 1000)

    return np.median(times)


def main(batch: int = 1, heads: int = 8, seq_len: int = 4096, dim: int = 128, is_causal: bool = False, groups: int = 1):
    device = "cuda"
    dtype = torch.float16

    torch.manual_seed(42)
    torch.cuda.manual_seed(42)

    print(f"Test configuration: batch={batch}, heads={heads}, seq_len={seq_len}, dim={dim}, is_causal={is_causal}, groups={groups}")

    flops_per_gemm = 2.0 * batch * heads * seq_len * seq_len * dim
    total_flops = 5 * flops_per_gemm

    print(f"Total FLOPs: {total_flops / 1e12:.2f} TFlops")

    q = torch.randn(batch, seq_len, heads, dim, device=device, dtype=dtype)
    k = torch.randn(batch, seq_len, heads // groups, dim, device=device, dtype=dtype)
    v = torch.randn(batch, seq_len, heads // groups, dim, device=device, dtype=dtype)
    dO = torch.randn_like(q)

    print("Starting autotuning for Fast FlashAttention-V2 Forward Pass...")
    fwd_kernel = fast_flashattn(batch, heads, seq_len, dim, is_causal, groups)
    if fwd_kernel is None or fwd_kernel.config is None:
        print("Forward pass auto-tuning failed.")
        return
    print(f"Autotuning finished. Best Forward Configuration: {fwd_kernel.config}")

    ref_program_processed = partial(ref_program, is_causal=is_causal, groups=groups)

    profiler = fwd_kernel.get_profiler(tensor_supply_type=tilelang.TensorSupplyType.Normal)

    print("Verifying correctness...")
    profiler.assert_allclose(ref_program_processed, rtol=0.01, atol=0.01)
    print("Forward pass is correct.")

    o_tl, lse_tl = fwd_kernel(q, k, v)

    bwd_prep = flashattn_bwd_preprocess(batch, heads, seq_len, dim)
    delta_tl = bwd_prep(o_tl, dO)

    print("\nStarting FlashAttention-V2 backward pass autotuning...")
    bwd_kernel = flashattn_bwd(batch, heads, seq_len, dim, is_causal, groups)
    if bwd_kernel is None or bwd_kernel.config is None:
        print("Backward pass autotuning failed.")
        return
    print(f"Autotuning completed. Best backward pass configuration: {bwd_kernel.config}")

    dQ_accum = torch.zeros_like(q, dtype=torch.float32)
    dK_tl = torch.zeros_like(k, dtype=torch.float32)
    dV_tl = torch.zeros_like(v, dtype=torch.float32)

    bwd_kernel(q, k, v, dO, lse_tl, delta_tl, dQ_accum, dK_tl, dV_tl)

    post_kernel = flashattn_bwd_postprocess(batch, heads, seq_len, dim)
    dQ_tl = post_kernel(dQ_accum)

    q_ref = q.clone().detach().requires_grad_()
    k_ref = k.clone().detach().requires_grad_()
    v_ref = v.clone().detach().requires_grad_()

    o_ref, _ = ref_program(q_ref, k_ref, v_ref, is_causal, groups)
    o_ref.backward(dO)

    print("Verifying backward pass correctness...")
    dq_close, dq_max_diff, dq_mean_diff = debug_tensor_comparison(dQ_tl, q_ref.grad, "dQ", rtol=0.05, atol=0.05)
    if dq_close:
        print("dQ is correct.")
    else:
        print("dQ mismatch detected.")

    dk_close, dk_max_diff, dk_mean_diff = debug_tensor_comparison(dK_tl.to(torch.float16), k_ref.grad, "dK", rtol=0.05, atol=0.05)
    if dk_close:
        print("dK is correct.")
    else:
        print("dK mismatch detected.")

    dv_close, dv_max_diff, dv_mean_diff = debug_tensor_comparison(dV_tl.to(torch.float16), v_ref.grad, "dV", rtol=0.05, atol=0.05)
    if dv_close:
        print("dV is correct.")
    else:
        print("dV mismatch detected.")

    print("\n=== Performance Benchmarking ===")

    def run_reference_fwd_bwd():
        q_ref_bench = q.clone().detach().requires_grad_()
        k_ref_bench = k.clone().detach().requires_grad_()
        v_ref_bench = v.clone().detach().requires_grad_()

        o_ref_bench, _ = ref_program(q_ref_bench, k_ref_bench, v_ref_bench, is_causal, groups)

        o_ref_bench.backward(dO)

        if torch.cuda.is_available():
            torch.cuda.synchronize()

    ref_latency = benchmark_function(run_reference_fwd_bwd, warmup=10, repeat=100)
    print(f"Reference PyTorch Forward+Backward: {ref_latency:.2f} ms | {total_flops / ref_latency * 1e-9:.2f} TFlops")

    def run_complete_fwd_bwd():
        o_tl_bench, lse_tl_bench = fwd_kernel(q, k, v)

        delta_tl_bench = bwd_prep(o_tl_bench, dO)

        dQ_bench = torch.zeros_like(q, dtype=torch.float32)
        dK_bench = torch.zeros_like(k, dtype=torch.float32)
        dV_bench = torch.zeros_like(v, dtype=torch.float32)
        bwd_kernel(q, k, v, dO, lse_tl_bench, delta_tl_bench, dQ_bench, dK_bench, dV_bench)

        post_kernel(dQ_bench)

        if torch.cuda.is_available():
            torch.cuda.synchronize()

    tile_latency = benchmark_function(run_complete_fwd_bwd, warmup=10, repeat=100)
    print(
        f"Complete Flash Attention V2 Forward+Backward (Tile-lang): {tile_latency:.2f} ms | {total_flops / tile_latency * 1e-9:.2f} TFlops"
    )

    speedup = ref_latency / tile_latency
    print(f"Speedup: {speedup:.2f}x")

    print("Forward output: Passed")
    print(f"dQ: {'Passed' if dq_close else 'Failed'} (Max diff: {dq_max_diff:.6f})")
    print(f"dK: {'Passed' if dk_close else 'Failed'} (Max diff: {dk_max_diff:.6f})")
    print(f"dV: {'Passed' if dv_close else 'Failed'} (Max diff: {dv_max_diff:.6f})")

    if all([dq_close, dk_close, dv_close]):
        print("All checks passed!")
    else:
        print("Some checks failed, may need further debugging.")


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--batch", type=int, default=1, help="batch size")
    parser.add_argument("--heads", type=int, default=8, help="heads")
    parser.add_argument("--seq_len", type=int, default=1024, help="sequence length")
    parser.add_argument("--dim", type=int, default=64, help="dim")
    parser.add_argument("--is_causal", action="store_true", help="causal")
    parser.add_argument("--groups", type=int, default=1, help="groups")
    args = parser.parse_args()

    main(args.batch, args.heads, args.seq_len, args.dim, args.is_causal, args.groups)

@petersktang

Copy link
Copy Markdown

Code result below (Target: hip -keys=hip,gpu -max_num_threads=256 -max_shared_memory_per_block=65536 -max_threads_per_block=256 -mcpu=gfx1150 -mtriple=amdgcn-amd-amdhsa-hcc -thread_warp_size=64):

`Test configuration: batch=1, heads=8, seq_len=1024, dim=64, is_causal=False, groups=1
Total FLOPs: 0.01 TFlops
Starting autotuning for Fast FlashAttention-V2 Forward Pass...
Autotuning finished. Best Forward Configuration: {'block_M': 32, 'block_N': 64, 'num_split_q': 32, 'threads': 64, 'num_stages': 0, 'enable_rasterization': True, 'k_pack': 1, 'panel_size': 7, 'qk_coalesced_width': 8, 'v_coalesced_width': 4}
Verifying correctness...
Forward pass is correct.

Starting FlashAttention-V2 backward pass autotuning...
Autotuning completed. Best backward pass configuration: {'block_M': 32, 'block_N': 16, 'num_stages': 0, 'threads': 64, 'enable_rasterization': True, 'panel_size': 8}
Verifying backward pass correctness...

=== dQ Comparison ===
Shape: torch.Size([1, 1024, 8, 64]) vs torch.Size([1, 1024, 8, 64])
Data type: torch.float16 vs torch.float16
Device: cuda:0 vs cuda:0
Max difference: 0.000977
Mean difference: 0.000026
Difference std: 0.000028
NaN count: 0 vs 0
Inf count: 0 vs 0
Max relative difference: 741.000000
Mean relative difference: 0.006668
Within tolerance (rtol=0.05, atol=0.05): True
dQ is correct.

=== dK Comparison ===
Shape: torch.Size([1, 1024, 8, 64]) vs torch.Size([1, 1024, 8, 64])
Data type: torch.float16 vs torch.float16
Device: cuda:0 vs cuda:0
Max difference: 0.000977
Mean difference: 0.000025
Difference std: 0.000028
NaN count: 0 vs 0
Inf count: 0 vs 0
Max relative difference: 238.000000
Mean relative difference: 0.005222
Within tolerance (rtol=0.05, atol=0.05): True
dK is correct.

=== dV Comparison ===
Shape: torch.Size([1, 1024, 8, 64]) vs torch.Size([1, 1024, 8, 64])
Data type: torch.float16 vs torch.float16
Device: cuda:0 vs cuda:0
Max difference: 0.000488
Mean difference: 0.000021
Difference std: 0.000024
NaN count: 0 vs 0
Inf count: 0 vs 0
Max relative difference: 982.000000
Mean relative difference: 0.006630
Within tolerance (rtol=0.05, atol=0.05): True
dV is correct.

=== Performance Benchmarking ===
Reference PyTorch Forward+Backward: 6.18 ms | 0.87 TFlops
Complete Flash Attention V2 Forward+Backward (Tile-lang): 2.48 ms | 2.16 TFlops
Speedup: 2.49x
Forward output: Passed
dQ: Passed (Max diff: 0.000977)
dK: Passed (Max diff: 0.000977)
dV: Passed (Max diff: 0.000488)
All checks passed!`

@jiawei-real

jiawei-real commented Apr 14, 2026 •

Copy link
Copy Markdown
Contributor Author

The issue of transpose_A=true is fixed:

image

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/target/codegen_hip.cc (1)

1117-1118: Consider showing the mcpu value in the error message for clarity.

The current error message uses target_->str() which may produce verbose output. Showing the mcpu value directly would be more helpful for debugging.

Suggested improvement
     ICHECK(rdna_gen == 11 || rdna_gen == 12)
-        << "Unsupported RDNA target for WMMA: gfx" << target_->str();
+        << "Unsupported RDNA target for WMMA: expected gfx11xx or gfx12xx, got mcpu="
+        << Downcast<tvm::ffi::String>(target_->attrs.at("mcpu"));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/target/codegen_hip.cc` around lines 1117 - 1118, The ICHECK that
validates rdna_gen should include the specific mcpu value to make failures
clearer: update the check message that currently prints target_->str() to also
extract and show the target's mcpu (e.g., via target_->GetAttr<String>("mcpu")
or the appropriate accessor) alongside the existing text so the logged message
reads something like "Unsupported RDNA target for WMMA: gfx... (mcpu=...)"
referencing the ICHECK, rdna_gen, and target_->str()/mcpu symbols.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/amd/example_amd_flash_attn_bwd.py`:
- Around line 14-21: The IsRDNA() helper should not call sys.exit at import
time—replace the sys.exit(1) path with raising a descriptive exception (e.g.,
RuntimeError) so callers/tests can handle it, and make the function more robust
by expanding the GPU detection beyond a fragile "Radeon" substring (for example,
also check torch.version.hip or other ROCm/HIP indicators and accept "AMD" or
"Radeon" variants from torch.cuda.get_device_name()); also ensure any decorators
that call IsRDNA() run the check lazily (i.e., at runtime/validation time) or
handle the raised exception so imports/tests aren’t aborted.

In `@tilelang/intrinsics/wmma_layout.py`:
- Around line 268-271: The comment above the layout definition is mislabeled as
"gfx12 store index map" while the implementation and function name
(wmma_store_index_map_gfx11) implement the gfx11 layout; update that comment to
read "gfx11 store index map" (and any accompanying description that references
gfx12) so it correctly matches the function wmma_store_index_map_gfx11 and its
affine mapping description.

---

Nitpick comments:
In `@src/target/codegen_hip.cc`:
- Around line 1117-1118: The ICHECK that validates rdna_gen should include the
specific mcpu value to make failures clearer: update the check message that
currently prints target_->str() to also extract and show the target's mcpu
(e.g., via target_->GetAttr<String>("mcpu") or the appropriate accessor)
alongside the existing text so the logged message reads something like
"Unsupported RDNA target for WMMA: gfx... (mcpu=...)" referencing the ICHECK,
rdna_gen, and target_->str()/mcpu symbols.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e2c3dfaa-8582-4583-89e2-f3b6abcccd07

📥 Commits

Reviewing files that changed from the base of the PR and between 293d605 and fd864b0.

📒 Files selected for processing (5)
  • examples/amd/example_amd_flash_attn_bwd.py
  • src/target/codegen_hip.cc
  • src/target/utils.cc
  • tilelang/intrinsics/wmma_layout.py
  • tilelang/intrinsics/wmma_macro_generator.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/target/utils.cc

Comment on lines +14 to +21
def IsRDNA():
if torch.cuda.is_available():
gpu_name = torch.cuda.get_device_name().strip()
return "Radeon" in gpu_name
else:
print("Error: GPU Device is not detected")
sys.exit(1)

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.

⚠️ Potential issue | 🟡 Minor

IsRDNA() may cause ungraceful exit at import time.

This function is called at module load time (via @tilelang.autotune(configs=get_fwd_configs(), ...) decorators). If CUDA is unavailable, the script exits with sys.exit(1) which could disrupt test harnesses or import-time checks. Additionally, the "Radeon" string match may be fragile for edge cases (e.g., future GPU naming).

Consider:

  1. Raising an exception instead of sys.exit(1) to allow callers to handle gracefully.
  2. Adding a fallback or more robust detection (e.g., checking for ROCm/HIP platform attributes).
Suggested improvement
 def IsRDNA():
     if torch.cuda.is_available():
         gpu_name = torch.cuda.get_device_name().strip()
         return "Radeon" in gpu_name
     else:
-        print("Error: GPU Device is not detected")
-        sys.exit(1)
+        raise RuntimeError("IsRDNA() requires a CUDA/HIP device but none is available")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/amd/example_amd_flash_attn_bwd.py` around lines 14 - 21, The
IsRDNA() helper should not call sys.exit at import time—replace the sys.exit(1)
path with raising a descriptive exception (e.g., RuntimeError) so callers/tests
can handle it, and make the function more robust by expanding the GPU detection
beyond a fragile "Radeon" substring (for example, also check torch.version.hip
or other ROCm/HIP indicators and accept "AMD" or "Radeon" variants from
torch.cuda.get_device_name()); also ensure any decorators that call IsRDNA() run
the check lazily (i.e., at runtime/validation time) or handle the raised
exception so imports/tests aren’t aborted.

Comment thread tilelang/intrinsics/wmma_layout.py
@zhangnju

Copy link
Copy Markdown
Collaborator

@jiawei-real, Thanks for your contributions on Radeon! Good job! have you tested your PR on other gfx11 GPUs (7900) or gfx12 GPU?

@jiawei-real

jiawei-real commented Apr 14, 2026 •

Copy link
Copy Markdown
Contributor Author

@jiawei-real, Thanks for your contributions on Radeon! Good job! have you tested your PR on other gfx11 GPUs (7900) or gfx12 GPU?

Thanks to @zhangnju for the w7900 hardware support! Tested flashattn fwd/bwd, and both results are correct. The results have already been synced to the PR message.

@petersktang

petersktang commented Apr 14, 2026 •

Copy link
Copy Markdown

Tested example_amd_flash_attn_bwd.py on AMD 890M gfx1150

Target: hip -keys=hip,gpu -max_num_threads=256 -max_shared_memory_per_block=65536 -max_threads_per_block=256 -mcpu=gfx1150 -mtriple=amdgcn-amd-amdhsa-hcc -thread_warp_size=64
Test configuration: batch=1, heads=8, seq_len=1024, dim=64, is_causal=False, groups=1
Total FLOPs: 0.01 TFlops
Starting autotuning for Fast FlashAttention-V2 Forward Pass...
Autotuning finished. Best Forward Configuration: {'block_M': 32, 'block_N': 64, 'num_split_q': 32, 'threads': 64, 'num_stages': 0, 'enable_rasterization': True, 'k_pack': 1, 'panel_size': 7, 'qk_coalesced_width': 8, 'v_coalesced_width': 4}
Verifying correctness...
Forward pass is correct.

Starting FlashAttention-V2 backward pass autotuning...
Autotuning completed. Best backward pass configuration: {'block_M': 32, 'block_N': 16, 'num_stages': 0, 'threads': 64, 'enable_rasterization': True, 'panel_size': 8}
Verifying backward pass correctness...

=== dQ Comparison ===
Shape: torch.Size([1, 1024, 8, 64]) vs torch.Size([1, 1024, 8, 64])
Data type: torch.float16 vs torch.float16
Device: cuda:0 vs cuda:0
Max difference: 0.000977
Mean difference: 0.000026
Difference std: 0.000028
NaN count: 0 vs 0
Inf count: 0 vs 0
Max relative difference: 741.000000
Mean relative difference: 0.006668
Within tolerance (rtol=0.05, atol=0.05): True
dQ is correct.

=== dK Comparison ===
Shape: torch.Size([1, 1024, 8, 64]) vs torch.Size([1, 1024, 8, 64])
Data type: torch.float16 vs torch.float16
Device: cuda:0 vs cuda:0
Max difference: 0.000977
Mean difference: 0.000025
Difference std: 0.000028
NaN count: 0 vs 0
Inf count: 0 vs 0
Max relative difference: 238.000000
Mean relative difference: 0.005222
Within tolerance (rtol=0.05, atol=0.05): True
dK is correct.

=== dV Comparison ===
Shape: torch.Size([1, 1024, 8, 64]) vs torch.Size([1, 1024, 8, 64])
Data type: torch.float16 vs torch.float16
Device: cuda:0 vs cuda:0
Max difference: 0.000488
Mean difference: 0.000021
Difference std: 0.000024
NaN count: 0 vs 0
Inf count: 0 vs 0
Max relative difference: 982.000000
Mean relative difference: 0.006630
Within tolerance (rtol=0.05, atol=0.05): True
dV is correct.

=== Performance Benchmarking ===
Reference PyTorch Forward+Backward: 6.10 ms | 0.88 TFlops
Complete Flash Attention V2 Forward+Backward (Tile-lang): 2.57 ms | 2.09 TFlops
Speedup: 2.37x
Forward output: Passed
dQ: Passed (Max diff: 0.000977)
dK: Passed (Max diff: 0.000977)
dV: Passed (Max diff: 0.000488)
All checks passed!

@zhangnju

Copy link
Copy Markdown
Collaborator

@petersktang Thanks for your validation, glad to know even 890M can run tilelang, which is good news for ROCm Tilelang ecosystem

@jiawei-real

Copy link
Copy Markdown
Contributor Author

@zhangnju @petersktang Thanks again for your contributions to this work!

@LeiWang1999
LeiWang1999 requested review from LeiWang1999 and zhangnju and removed request for LeiWang1999 April 16, 2026 09:26
@jiawei-real

Copy link
Copy Markdown
Contributor Author

@zhangnju @LeiWang1999 The correctness has already been verified on the 9070XT and W7900, and the verification results have been updated in the PR message.

@zhangnju zhangnju 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.

TargetGetRDNAGeneration and _detect_rdna_generation() in wmma_macro_generator.py duplicates the same mcpu-string-parsing logic in Python

Comment thread src/target/utils.cc

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tilelang/intrinsics/wmma_macro_generator.py (1)

380-391: Use # comments instead of a bare triple-quoted string.

This triple-quoted block is an expression statement (not a docstring — the function’s first statements are asserts above), so it allocates a string each call and isn't picked up by any tooling. Convert to regular # comments.

♻️ Proposed change
-        """
-        gfx11 and gfx12 differ in how logical A/B fragment elements map to lanes.
-
-        gfx11 duplicates each logical A/B element across the two half-waves
-        (lane t and lane t + 16). A single-owner forward_thread_fn cannot
-        faithfully represent this one-to-many ownership, so we model it with
-        T.Fragment(..., forward_fn=..., replicate=2), where `rep` selects the
-        lower/upper half-wave copy.
-
-        gfx12 has a single unique owner for each logical element, so the
-        existing forward_thread_fn/forward_index_fn form is sufficient.
-        """
+        # gfx11 and gfx12 differ in how logical A/B fragment elements map to lanes.
+        #
+        # gfx11 duplicates each logical A/B element across the two half-waves
+        # (lane t and lane t + 16). A single-owner forward_thread_fn cannot
+        # faithfully represent this one-to-many ownership, so we model it with
+        # T.Fragment(..., forward_fn=..., replicate=2), where `rep` selects the
+        # lower/upper half-wave copy.
+        #
+        # gfx12 has a single unique owner for each logical element, so the
+        # existing forward_thread_fn/forward_index_fn form is sufficient.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tilelang/intrinsics/wmma_macro_generator.py` around lines 380 - 391, Replace
the literal triple-quoted string block in wmma_macro_generator.py (the
explanatory block about gfx11/gfx12 mapping near the assert lines) with regular
line comments: delete the standalone """...""" expression and reformat its
contents into one or more `#` comment lines immediately above the related code,
preserving the exact text and indentation; ensure no runtime string is created
and the explanatory text remains as comments describing the A/B fragment
ownership behavior for gfx11/gfx12 in the same location (keep surrounding
asserts and any variables unchanged).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/target/codegen_hip.cc`:
- Around line 1196-1200: The error message currently prefixes a hardcoded "gfx"
to target_->str(), producing wrong output; update the ICHECK message in the code
around TargetGetRDNAGeneration(target_) to either remove the literal "gfx" (so
it prints the full target string via target_->str() only) or explicitly include
the mcpu value instead of "gfx" by querying the target's mcpu attribute (use the
Target/mcpu accessor used in your codebase) and print that; adjust the ICHECK
call that references target_, TargetGetRDNAGeneration, and target_->str()
accordingly.

---

Nitpick comments:
In `@tilelang/intrinsics/wmma_macro_generator.py`:
- Around line 380-391: Replace the literal triple-quoted string block in
wmma_macro_generator.py (the explanatory block about gfx11/gfx12 mapping near
the assert lines) with regular line comments: delete the standalone """..."""
expression and reformat its contents into one or more `#` comment lines
immediately above the related code, preserving the exact text and indentation;
ensure no runtime string is created and the explanatory text remains as comments
describing the A/B fragment ownership behavior for gfx11/gfx12 in the same
location (keep surrounding asserts and any variables unchanged).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 697326da-a1b3-44e3-bdaa-a328efaa75b3

📥 Commits

Reviewing files that changed from the base of the PR and between 8568cde and c152d2d.

📒 Files selected for processing (3)
  • src/target/codegen_hip.cc
  • src/target/utils.cc
  • tilelang/intrinsics/wmma_macro_generator.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/target/utils.cc

Comment thread src/target/codegen_hip.cc
Comment on lines +1196 to +1200
// Get RDNA Generation
ICHECK(target_.defined()) << "CodeGenTileLangHIP target is not set";
int rdna_gen = tvm::tl::TargetGetRDNAGeneration(target_);
ICHECK(rdna_gen == 11 || rdna_gen == 12)
<< "Unsupported RDNA target for WMMA: gfx" << target_->str();

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.

⚠️ Potential issue | 🟡 Minor

Error message concatenates a stray "gfx" in front of the full target string.

target_->str() returns the entire target description (e.g. "hip -keys=hip,gpu -mcpu=gfx1100 ..."), not just the arch suffix, so this will print Unsupported RDNA target for WMMA: gfxhip -keys=.... Drop the literal gfx, or look up mcpu explicitly.

🛠 Proposed fix
-    ICHECK(rdna_gen == 11 || rdna_gen == 12)
-        << "Unsupported RDNA target for WMMA: gfx" << target_->str();
+    ICHECK(rdna_gen == 11 || rdna_gen == 12)
+        << "Unsupported RDNA target for WMMA: " << target_->str();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Get RDNA Generation
ICHECK(target_.defined()) << "CodeGenTileLangHIP target is not set";
int rdna_gen = tvm::tl::TargetGetRDNAGeneration(target_);
ICHECK(rdna_gen == 11 || rdna_gen == 12)
<< "Unsupported RDNA target for WMMA: gfx" << target_->str();
// Get RDNA Generation
ICHECK(target_.defined()) << "CodeGenTileLangHIP target is not set";
int rdna_gen = tvm::tl::TargetGetRDNAGeneration(target_);
ICHECK(rdna_gen == 11 || rdna_gen == 12)
<< "Unsupported RDNA target for WMMA: " << target_->str();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/target/codegen_hip.cc` around lines 1196 - 1200, The error message
currently prefixes a hardcoded "gfx" to target_->str(), producing wrong output;
update the ICHECK message in the code around TargetGetRDNAGeneration(target_) to
either remove the literal "gfx" (so it prints the full target string via
target_->str() only) or explicitly include the mcpu value instead of "gfx" by
querying the target's mcpu attribute (use the Target/mcpu accessor used in your
codebase) and print that; adjust the ICHECK call that references target_,
TargetGetRDNAGeneration, and target_->str() accordingly.

@jiawei-real
jiawei-real requested a review from zhangnju April 20, 2026 09:04
zhangnju
zhangnju previously approved these changes Apr 20, 2026

@LeiWang1999 LeiWang1999 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

overall lgtm, but a minor question for the unexpected change.

Comment thread tilelang/tileop/gemm/__init__.py
@jiawei-real

Copy link
Copy Markdown
Contributor Author

@LeiWang1999 @zhangnju, please take a final review~

@LeiWang1999

Copy link
Copy Markdown
Member

LGTM, Thanks!

@LeiWang1999
LeiWang1999 merged commit a640a89 into tile-ai:main Apr 22, 2026
2 checks passed
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.

4 participants