Skip to content

[HIP] [JIT] [Build] gfx1201: SageAttention forward (INT8 QK, FP8 PV) - #6390

Open
Shan2L wants to merge 2 commits into
ROCm:mainfrom
Shan2L:gfx1201/sage-attention
Open

Shan2L wants to merge 2 commits into
ROCm:mainfrom
Shan2L:gfx1201/sage-attention

Conversation

@Shan2L

@Shan2L Shan2L commented Oct 10, 2026 •

Copy link
Copy Markdown

Part 1 of 4 of the restructured #6292 (see the series list there);

Motivation

aiter has no fast attention path for gfx1201 (RDNA4, Radeon AI PRO R9700). BF16 SDPA reaches ~68 TFLOPS there and the
Triton fav3_sage path (#1818) is untuned for RDNA4 (~15-18 TFLOPS). This PR adds a SageAttention-style forward for
gfx1201 written in HIP: INT8 Q K^T and FP8 P V on the RDNA4 WMMA units with FP32 accumulation.

This is the first PR of a series; later PRs add a hand-scheduled code object for the same kernel and a sparse variant.

Technical Details

Two commits:

  1. gfx1201_sage_prepare - BF16 [B, S, H, 128] Q/K -> INT8 with one FP32 scale per (batch, head, 32-row block),
    Q carrying softmax_scale * log2(e); V -> FP8 e4m3, transposed to [B, H, 128, S_pad], one FP32 scale per channel.
    Optional fused QK RMSNorm + rotate-half RoPE (rope_dim 32/64/96/128), rounded like the eager PyTorch BF16 sequence.
  2. gfx1201_sage_attention (torch.ops.aiter.gfx1201_sage_attention) - prepare + HIP kernel: online softmax, INT8 WMMA
    for Q K^T, FP8 WMMA for P V, FP32 accumulation, BF16 output. gfx1201_sage_attention_fwd runs the kernel alone.

Scope: gfx1201 only, head dim 128, non-causal, no varlen/mask, BF16 in/out, forward only. Both JIT modules are excluded
from prebuild unless gfx1201 is in GPU_ARCHS; the tests/benchmark exit early on other architectures.

Test Plan

  • python op_tests/test_gfx1201_sage_prepare.py - bitwise against a PyTorch reference of the quantization (+ norm/RoPE).
  • python op_tests/test_gfx1201_sage_attention.py - against attention_ref with the tolerance band of the Triton
    SageAttention test (atol 0.3, rtol 0.25, <= 0.5 % failing elements) plus cosine >= 0.998 and median row cosine >= 0.99;
    torch.library.opcheck.
  • python op_tests/op_benchmarks/hip/bench_gfx1201_sage_attention.py --compare_to_ref - speed + accuracy vs BF16 SDPA
    and Triton fav3_sage.
  • End-to-end MiniMax-H3 video generation (LightX2V) with a self-contained script, see "How to reproduce".

Test Result

Radeon AI PRO R9700 (gfx1201, 32 GB), public image above, this branch.

Op tests: test_gfx1201_sage_prepare.py and test_gfx1201_sage_attention.py ALL PASS (64 checks, incl. opcheck,
non-default stream, fused == unfused bitwise, input validation).

Kernel benchmark (randn inputs; sage includes prepare; accuracy vs the FP32 reference):

B S H this PR ms (TFLOPS) BF16 SDPA ms (TFLOPS) Triton fav3_sage ms (TFLOPS) vs SDPA
1 4096 24 1.38 (150) 3.10 (67) 11.38 (18) 2.25x
1 16452 24 19.6 (170) 47.7 (70) 232.3 (14) 2.43x
2 29760 2 10.4 (174) 26.2 (69) 99.5 (18) 2.52x
1 32760 24 79.6 (166) 188.4 (70) 744.3 (18) 2.37x
1 75600 5 83.2 (176) 209.6 (70) 1006.5 (15) 2.52x
1 113102 28 1051.5 (174) 2719.3 (67) 12564.0 (15) 2.59x
1 118808 3 121.0 (179) 312.5 (69) 1162.4 (19) 2.58x

Cosine vs FP32 reference 0.9992-0.9993 (Triton fav3_sage 0.9991-0.9993, BF16 SDPA 1.0000); relative L2 error
0.038-0.039 (Triton 0.039-0.042, BF16 SDPA 0.0023); 0 % elements outside the test band for all providers.

Real MiniMax-H3 tensors (DUMP_INPUTS, 4 inputs x steps 0/2 x layers below, heads 0/9/18/27, S = 117k-128k),
relative L2 error vs FP32 reference, mean / max over the 8 dumps per layer:

layer this PR Triton fav3_sage BF16 SDPA (max)
0 0.0037 / 0.0042 0.032 / 0.038 0.0017
10 0.0112 / 0.0127 0.035 / 0.041 0.0017
20 0.0109 / 0.0115 0.050 / 0.054 0.0017
30 0.0118 / 0.0141 0.044 / 0.051 0.0017
40 0.0121 / 0.0140 0.106 / 0.129 0.0017
45 0.072 / 0.114 0.089 / 0.138 0.0016
49 0.124 / 0.192 0.212 / 0.352 0.0016

All outputs finite. Layers 45/49 have the largest error for both quantized kernels; this PR is below Triton fav3_sage
on every layer.

End-to-end (repro.sh, TP2, street_dance, 3 steps, block offload):

DiT s/step (steps 1/2/3) speedup
BF16 SDPA 211.5 / 216.1 / 210.1
this PR 101.1 / 100.9 / 100.9 2.09-2.14x

The DiT self-attention went through the new op for all 150 calls (50 layers x 3 steps), 0 SDPA fallbacks. A second
run of each backend gives bitwise identical frames and audio.

decoded output vs BF16 SDPA run video PSNR (min frame) SSIM (min frame) audio SNR / cos
this PR 33.48 dB (27.46) 0.9645 (0.9115) 14.04 dB / 0.980
BF16 SDPA, K/V token order reversed (NOISE_FLOOR=1) 35.94 dB (28.62) 0.9788 (0.9355) 18.91 dB / 0.994

Reversing the K/V token order leaves attention mathematically unchanged and only perturbs the FP32 summation order,
so the second row shows how far rounding-level attention differences already move the 3-step output.

Frames 0/120/241/361 (left BF16 SDPA, middle this PR, right |difference| x 8):

frames_street_dance

End-to-end at the stock step count (STEPS=29 NOISE_FLOOR=1 repro.sh; 29 is the infer_steps of
minimax_h3_tp.json; same input, seed and config as above):

DiT s/step (mean of steps 2-29) pipeline total vs SDPA (s/step / total)
BF16 SDPA 211.6 6450 s
this PR 100.8 3220 s 2.10x / 2.00x
thu-ml SageAttention (reference) 347.5 10392 s 0.61x / 0.62x

Step 1 of this PR (125.1 s) includes the first-call JIT build. All 1450 DiT self-attention calls went through the
op, 0 SDPA fallbacks.

The reference row is the upstream SageAttention implementation as LightX2V uses it: sage_attn2 (the attn_type of
minimax_h3_tp.json) with thu-ml/SageAttention d1a57a5, through sageattn_qk_int8_pv_fp16_triton (INT8 QK^T,
FP16 PV), the kernel sage_attn2 selects on SM89; the CUDA kernels of the package do not build on ROCm. It is there
to separate the effect of SageAttention itself from this kernel, not as a performance baseline (the Triton kernel is
not tuned for gfx1201). 1450 calls per rank, 0 fallbacks.

decoded output vs BF16 SDPA run video PSNR (min frame) SSIM (min frame) audio SNR / cos
this PR 13.04 dB (9.58) 0.551 (0.357) 7.02 dB / 0.899
thu-ml SageAttention (reference) 14.09 dB (10.20) 0.597 (0.379) 7.13 dB / 0.903
BF16 SDPA, K/V token order reversed (noise floor) 14.64 dB (10.30) 0.629 (0.391) 9.16 dB / 0.938

Over 29 steps the sampling trajectory is chaotic: reordering the FP32 sums of exact BF16 attention alone already
produces a visibly different video (14.6 dB, SSIM 0.63 against the SDPA run), so pixel metrics against a single
reference run no longer measure kernel accuracy. LightX2V's own MiniMax-H3 scheduler
notes the same sensitivity: an FP32 result that "differs by an ulp on CUDA" is "amplified by the 48-layer
denoiser"
(scheduler.py). The reference SageAttention implementation also ends far
from the SDPA run (14.09 dB, SSIM 0.597, close to the exact-reorder floor), so a low PSNR against a single BF16 run at
29 steps is expected for SageAttention in general and is not specific to this kernel. This PR ends 1.0 dB / 0.05
SSIM below the reference implementation and 1.6 dB / 0.08 SSIM below the exact-reorder floor. Global statistics of
all outputs stay within the spread of the two exact runs:

run luma mean / std sharpness (Laplacian var.) mean frame-to-frame abs diff audio RMS / peak
BF16 SDPA 68.6 / 56.2 40.4 23.24 0.240 / 0.96
BF16 SDPA, K/V reversed 69.0 / 56.7 42.3 23.12 0.233 / 0.91
this PR 69.3 / 57.3 42.6 23.26 0.234 / 0.92
thu-ml SageAttention (reference) 69.4 / 57.2 41.9 22.92 0.238 / 0.93

29-step frames 0/120/241/361 (columns: BF16 SDPA, BF16 SDPA K/V reversed, this PR):

frames_street_dance_29steps

How to reproduce

Everything below uses public artifacts only: the rocm/pytorch:rocm10.0_ubuntu24.04_py3.13_pytorch_release_2.11.0
image, this branch, upstream LightX2V 0.5.0 (95e9b86b) and the public MiniMaxAI/MiniMax-H3 weights
(revision 42ed227e). Inputs are LightX2V's own example images (assets/inputs/imgs) with our prompts.

Kernel test / benchmark (1 GPU):

python op_tests/test_gfx1201_sage_prepare.py
python op_tests/test_gfx1201_sage_attention.py
python op_tests/op_benchmarks/hip/bench_gfx1201_sage_attention.py --compare_to_ref --csv bench.csv

The benchmark times each provider with CUDA events (2 warm-up + 5 timed calls), repeats this for 3 rounds with the
providers interleaved, and reports the median. Inputs are randn BF16 Q/K/V. sage includes the prepare stage,
sage_core is the attention kernel alone on prepared inputs; TFLOPS = 4 * B * H * S^2 * 128 / time.

End-to-end (2 GPUs, TP2) with the attached gfx1201_sage_repro.zip (repro.sh, h3_e2e.py, real_dumps.py, prompts),
run from the unzipped directory:

unzip gfx1201_sage_repro.zip -d repro && cd repro
huggingface-cli download MiniMaxAI/MiniMax-H3 --revision 42ed227ee7df40d41602854ae760620d6eb651fe --local-dir $MODEL
docker run --rm --device /dev/kfd --device /dev/dri --group-add video --ipc host --shm-size 64g \
  -e HIP_VISIBLE_DEVICES=0,1 -v $MODEL:/model -v $PWD:/repro \
  rocm/pytorch:rocm10.0_ubuntu24.04_py3.13_pytorch_release_2.11.0 bash /repro/repro.sh

h3_e2e.py runs stock LightX2V MiniMax-H3 ref2av (configs/minimax_h3/minimax_h3_tp.json, 768x1344, 362 frames,
seed 42) and only swaps the attention backend: it registers gfx1201_sage in LightX2V's attention registry, which calls
torch.ops.aiter.gfx1201_sage_attention for the DiT self-attention and falls back to SDPA for anything else (the
number of calls of each kind is printed at exit). Kernels that differ between the two runs only through attention:
RMSNorm/RoPE use LightX2V's torch_native/torch_real_rope, and FP32 GEMMs/convolutions go through rocBLAS so that
repeated runs are bitwise identical (hipBLASLt picks a per-process kernel for some FP32 GEMMs,
ModelTC/LightX2V#1594, opt-in fix proposed in ModelTC/LightX2V#1596). The pre-encode frames and audio are saved as *_raw.pt and compared directly (no codec in
the loop).

Notes for running on a 32 GB card with the public image (all handled by repro.sh):

  • LightX2V's requirements would replace the ROCm torch/triton with CUDA wheels; repro.sh pins the installed
    versions through PIP_CONSTRAINT, and installs aiter with AITER_USE_SYSTEM_TRITON=1.
  • The image ships the HIP runtime as a pip package (_rocm_sdk_core) with only libamdhip64.so.7; aiter's JIT links
    with -lamdhip64, so repro.sh adds an unversioned symlink to LIBRARY_PATH.
  • repro.sh uses LightX2V's AMD platform (PLATFORM=amd_rocm) and exports
    PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True: with amd_rocm, LightX2V 0.5.0 initializes the allocator before it
    sets this itself and the run OOMs at DiT step 1 ([Bug] PLATFORM=amd_rocm ignores expandable_segments:True, MiniMax-H3 OOMs on 32 GB GPUs ModelTC/LightX2V#1593, fixed in Set PYTORCH_CUDA_ALLOC_CONF before platform init ModelTC/LightX2V#1595).
  • The DiT (and the text encoder) do not fit next to the activations, so the config uses LightX2V's block offload,
    which for MiniMax-H3 needs the persistent AdaLN cache; h3_e2e.py adaln builds it once with LightX2V's
    tools/cache_minimax_h3_adaln. Offload makes the absolute s/step slower than a resident setup; both backends use
    the same config.
  • DUMP_INPUTS="street_dance calm_interview mountain_flyover snake_jungle" additionally dumps the real DiT Q/K/V
    (rank 0, steps 0 and last, selected layers; ~5 GB per input) and runs real_dumps.py, which compares this kernel,
    Triton fav3_sage and BF16 SDPA against an FP32 reference on them.
  • NOISE_FLOOR=1 additionally reruns BF16 SDPA with the K/V token order reversed (h3_e2e.py run --sdpa_kv_flip)
    and writes its comparison against the SDPA run to compare_floor.json.
  • STEPS=29 runs the stock step count instead of the default 3 (the AdaLN cache is built per step count; about
    1.8 h for the SDPA run and 0.8-0.9 h for the op run on R9700 with block offload).
  • OFFICIAL_SAGE=1 additionally runs LightX2V's own sage_attn2 with thu-ml/SageAttention d1a57a5 through
    sageattn_qk_int8_pv_fp16_triton (the reference row of the 29-step tables) and writes its comparison against the
    SDPA run to compare_official_sage.json (about 2.9 h at 29 steps).

Add gfx1201_sage_prepare, the input stage of a SageAttention-style
attention on gfx1201 (RDNA4): BF16 [B, S, H, 128] Q/K are quantized to
INT8 with one FP32 scale per (batch, head, 32-row block), Q carrying
softmax_scale * log2(e); V is quantized to FP8 e4m3, transposed to
[B, H, 128, S_pad], with one FP32 scale per channel. Optionally Q/K go
through RMSNorm and rotate-half RoPE (rope_dim 32/64/96/128) first,
rounded like the eager PyTorch BF16 sequence.

The module_gfx1201_sage_prepare JIT module is excluded from prebuild
unless gfx1201 is in GPU_ARCHS, and the op test exits early on other
architectures.

Signed-off-by: Sylvan Liu <Sylvan.Liu@amd.com>
Add gfx1201_sage_attention, a non-causal SageAttention forward for gfx1201
(RDNA4), registered as torch.ops.aiter.gfx1201_sage_attention:
BF16 [B, S, H, 128] Q/K/V go through gfx1201_sage_prepare, then a HIP
kernel computes Q K^T with INT8 WMMA and P V with FP8 WMMA, FP32
accumulation and online softmax, writing BF16. The optional QK
RMSNorm + RoPE arguments are forwarded to the prepare stage.
gfx1201_sage_attention_fwd runs the kernel alone on prepared tensors.

The op is lossy by design. op_tests/test_gfx1201_sage_attention.py checks
it against attention_ref with the tolerance band used by the Triton
SageAttention test; op_tests/op_benchmarks/hip/bench_gfx1201_sage_attention.py
times it against BF16 SDPA and Triton fav3_sage and can report accuracy
against an FP32 reference.

Signed-off-by: Sylvan Liu <Sylvan.Liu@amd.com>
@Shan2L
Shan2L requested a review from a team October 10, 2026 16:29
@github-actions

Copy link
Copy Markdown
Contributor

🏷️ CI Guide

Runs automatically on every PR:

  • ✅ Pre-checks (submodule verification, code formatting)
  • ✅ Aiter op tests (gfx942 + gfx950)
  • ✅ Triton tests on MI35X (only when aiter/ops/triton/** or related paths are changed)

Extended tests (opt-in via labels):

Label Tests
ci:gfx1250-ffm-triton Run the five-shard gfx1250 FFM Triton test suite
ci:triton-300x Run an additional Triton test job on MI300X in PRs (added automatically when gfx942 configs change); main branch always runs both MI35X and MI300X
ci:triton-355 Run the full Triton test suite on MI35X, not only the tests the change affects
ci:multi-gpu Aiter multi-GPU tests on the 8-GPU runner
ci:sglang SGLang integration tests: DeepSeek-R1-MXFP4 accuracy, Qwen 3.5 accuracy
ci:atom ATOM benchmark: DeepSeek-R1-0528, GPT-OSS-120B
ci:atom_full ATOM accuracy suite for PR and main models from ATOM models_accuracy.json
ci:vllm vLLM benchmark: DeepSeek-V4-Pro, MiniMax-M3-MXFP4
ci:all All standard extended tests (excludes ci:atom_full)

Only add ci:atom_full for FlyDSL or Triton upgrades.
Add labels via the sidebar or gh pr edit 6390 --add-label <label>

One backend per PR:
A PR changes one kernel backend: [Triton/Gluon] (Triton and Gluon count as one), [HIP], [ASM], [CK], [OPUS] or [FlyDSL]. If the title ends up with two backend tags, split the PR -- as stacked pull requests when one part cannot merge without the other.

PR title tags & labels:
Component tags ([Triton/Gluon], [HIP], [CK], [ASM], ...) are added to the PR title and as PR labels automatically from the changed files and re-synced on every push — change-type tags like [fix]/[Perf], op tags like [MLA], and human labels (ci:*) are left untouched. Add the no-auto-title label to stop the title rewrites; labels stay in sync either way.

@github-actions github-actions Bot changed the title [HIP] gfx1201: SageAttention forward (INT8 QK, FP8 PV) [HIP] [JIT] [Build] gfx1201: SageAttention forward (INT8 QK, FP8 PV) Oct 10, 2026
@zufayu
zufayu requested a review from kaiyang-1 October 12, 2026 01:25

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants