Repository navigation
Conversation
🏷️ CI GuideRuns automatically on every PR:
Extended tests (opt-in via labels):
One backend per PR: PR title tags & labels: |
WAITING ON AUTHORPR Self-ReviewRevision Waiting for the PR author. The readiness report will appear after the self-review answers are complete for this revision. Questions to answer in the PR description
Edit the Missing or invalid answers
Shadow mode reports what would block but keeps the status successful. Set |
|
@brunomazzottiamd, this PR follows up on the emulated fp8 performance you raised in your review of #6782. In my reply there (#6782 (comment)) I proposed converting to fp16 once and running the fp16 kernel on archs without fp8 hardware. This PR implements that for the |
There was a problem hiding this comment.
🟡 Changes recommended
Descale multiplication and int32 stride arithmetic can overflow, producing NaNs or invalid memory reads, while the software FP8 emulation path lacks targeted test coverage.
4 open issues
What changed in this PR
This PR speeds up Triton MHA fp8 prefill on GPUs that emulate fp8 in software. It uses fp16 dot products for the bshd path while preserving existing dispatch for hardware-fp8 GPUs and other layouts.
Changes:
- Convert K and V to fp16 once per call and Q once per program, with descales applied during attention.
- Add a bit-exact comparison of packed, strided fp8 inputs against contiguous copies.
- Document conversion costs, memory trade-offs, and output dtype.
| File | Description |
|---|---|
op_tests/triton_tests/attention/test_mha_fp8.py |
Adds strided fp8 Q/K/V coverage. |
docs/triton_mha_fp8_sw_emulated.md |
Explains software-emulated fp8 behavior and trade-offs. |
aiter/ops/triton/_triton_kernels/flash_attn_triton_amd/fwd_prefill.py |
Adds the conversion kernel and fp16 prefill path. |
Review effort: Balanced
|
Merged The change also holds on Triton 3.9. Measured on gfx1101 with the PR head, same torch and ROCm build, only the Triton version differs;
|
|
@Ragua1 Can you rebase your pr? |
…emulated fp8 On archs in SW_EMULATED_FP8_ARCHS, Triton converts fp8 operands to fp16 in software inside every K and V tile of the prefill kernel. Convert k and v to fp16 once, and q once per program in attn_fwd right after it is loaded. Every fp8 value is exact in fp16, so the conversion skips the descales and attn_fwd applies them in fp32: q_descale * k_descale with SM_SCALE on the scores, v_descale on the output. - Add _fp8_to_fp16_bshd and _fp8_to_fp16 in fwd_prefill.py: strided bshd input, contiguous fp16 output, int64 offsets. - Add the constexpr FP8_AS_FP16 to attn_fwd and _attn_fwd_inner. attn_fwd loads the descales as the fp8 branch does and converts the q block to fp16 outside the K/V loop. With the constexpr unset the generated code is unchanged. - attention_forward_prefill_triton_impl takes this path when q, k and v are fp8, the layout is bshd and get_arch().supports_sw_emulated_fp8. The descale shapes are checked before the conversion; a missing descale becomes ones. - Archs with fp8 hardware, the bhsd and thd layouts, backward and decode are unchanged. Signed-off-by: Martin Domanský <ragua@email.cz>
… fp8 docs/triton_mha_fp8_sw_emulated.md: on gfx1100, gfx1101, gfx1102, gfx1150 and gfx1151 fp8 inputs bring no compute advantage over fp16 or bf16, the bshd prefill keeps fp16 copies of k and v for the call, flash_attn_fp8_func quantizes on every call, and the forward output for fp8 inputs is fp32. Signed-off-by: Martin Domanský <ragua@email.cz>
- attn_fwd: with FP8_AS_FP16, multiply q_descale and k_descale in fp32. The product was taken in the descale dtype, so fp16 descales whose product exceeds 65504 overflowed to inf; the IS_FP8 branch multiplies the fp32 dot result and does not. - _fp8_to_fp16_bshd: compute the head-dim offset in int64 as well, so a large stride_xd does not overflow int32. - attention_forward_prefill_triton_impl: allocate the default descale only when a descale is missing. - test_mha_fp8.py: add test_mha_fp8_sw_emulated_path. It forces supports_sw_emulated_fp8 so that the path also runs on archs with fp8 hardware, checks that k and v went through _fp8_to_fp16, and compares with attention_ref on the dequantized inputs, with fp32 descales and with fp16 descales of 256 for q and k. Signed-off-by: Martin Domanský <ragua@email.cz>
480a1d2 to
a002007
Compare
done |


Summary
On archs where Triton emulates fp8 in software, the fp8 MHA prefill now converts k and v to fp16 once before the kernel and q once per program, then runs the fp16 dot products. On gfx1101 this is 3.8x to 24.0x faster than today on all 238 measured shapes, median 15.8x, and the error against an fp32 reference is lower on all 238.
Archs with fp8 hardware are not affected: they compile the same
attn_fwdas onmain.Motivation
On archs without fp8 WMMA or fp8 conversion instructions, Triton converts fp8 operands to fp16 in software. #6782 lets the Triton MHA fp8 path run on these archs: gfx1100, gfx1101, gfx1102, gfx1150 and gfx1151. In the prefill kernel this conversion runs on every K and V tile of every program, after the tile is read from LDS. The fp8 kernel is therefore many times slower than the fp16 or bf16 kernel on the same data. #6770 shows the same effect in
fav3_sageon gfx1100.Today fp16 or bf16 attention is many times faster on these archs, so the fp8 path matters where the caller cannot switch to it: a model available only in fp8, or a workload whose conversion to fp16 or bf16 is not practical on the target card. With this change the fp8 call runs close to the bf16 kernel on the same data, at a median of 0.95 to 1.22 of its time per head configuration.
Every fp8 value is exact in fp16, so the conversion can skip the descales, and the kernel applies them in fp32.
Changes
attention_forward_prefill_triton_impl(fwd_prefill.py) takes the new path when q, k and v are fp8, the layout isbshd, andget_arch().supports_sw_emulated_fp8._fp8_to_fp16_bshd, a new Triton kernel: strided bshd input, contiguous output, int64 offsets, becauses * nheads * head_dimcan exceed 2^31 elements. The conversion is exact for e4m3 and e5m2, so no value is rounded and the fp16 range is never exceeded. The descale shapes are checked before the conversion, as the fp8 branch checks them. A missing descale becomes ones.attn_fwdgets a new constexprFP8_AS_FP16. When it is set,attn_fwdconverts q to fp16 once, right after loading the q block and outside the K/V loop. It loads the three descales with the same indexing as the fp8 branch, passesq_descale * k_descale, taken in fp32, to_attn_fwd_inner, and multiplies the output byv_descaleonce before the store._attn_fwd_innergets the same constexpr. When it is set,q_descale * k_descalejoins the existingSM_SCALEmultiply of the scores, a scalar product per tile. With the constexpr unset, which is every other caller, the generated code is unchanged: a bf16 call throughmha_v3.flash_attn_funccompiles to the same instructions onmainand with this change, and gives the same output.IS_FP8off. The output dtype check of the non-fp8 branch accepts this case. The output keeps the dtype the caller allocated, fp32 forflash_attn_3.fwd.bhsdand varlen (thd) layouts, the backward pass, decode, and rotary embeddings, which still act on the fp8 q._fp8_to_fp16_bshdgets areprfrommake_kernel_repr.op_tests/triton_tests/attention/test_mha_fp8.py: newtest_mha_fp8_strided_qkv. q, k and v are views into one packed fp8 qkv tensor, and the output must equal the output for contiguous copies bit for bit.op_tests/triton_tests/attention/test_mha_fp8.py: newtest_mha_fp8_sw_emulated_path. It forcessupports_sw_emulated_fp8, so the new path also runs on archs with fp8 hardware, and compares the output withattention_refon the dequantized inputs.docs/triton_mha_fp8_sw_emulated.md: new note on fp8 MHA inputs on archs with software-emulated fp8, see Documentation.Alternative considered: bf16 with the descales applied in the conversion
A smaller change converts k and v to bf16 with their descales applied, and q to bf16 with its descale in
attn_fwd. It changes onlyattn_fwd, not_attn_fwd_inner, and needs bf16 because descaled values can exceed the fp16 range. Measured in the same processes as this PR:1 - cosvs fp8 today, median ratiofwd_prefill.pyFrom seqlen 512 up this PR is faster than the bf16 alternative on 154 of 154 shapes. The 19 shapes where the alternative is faster are small calls. 13 of them take under 0.06 ms, and there the alternative saves at most 2.7 µs per call. On the other 6 it saves at most 1.3 % of the call. Summed over all 238 shapes, this PR takes 2948 ms and the alternative 3149 ms.
1 - cosof this PR is lower than that of the bf16 alternative on 238 of 238 shapes, median ratio 0.995.The fp16 kernel is faster than the bf16 kernel on this arch: plain
mha.flash_attn_funcin fp16 is faster than in bf16 on 238 of 238 shapes and runs at 0.92 of its bf16 time in the median (0.76–0.96). RDNA3 has f32 to f16 conversion instructions (V_CVT_F16_F32,V_CVT_PK_RTZ_F16_F32) but none to bf16, so the compiler rounds to bf16 with integer instructions. P is converted to the dtype of V on every tile. On32, 256, 32/8, 128, causalthe bf16 alternative'sattn_fwdhas 4587 instructions and this PR's 4042, and most of the difference is that rounding.Performance (if applicable)
Before = fp8 today, after = this PR. Both ran in the same process with rotated arm order,
mha_v3.flash_attn_funcwith fp8 q, k, v and descales, default autotune config. Time is the median over 10 fresh processes, GPU time only with the GPU queue filled ahead of each timed window, hot cache.The timing ran on a measurement tree that adds a runtime switch for the new path to this change. Its
attn_fwdcompiles to the same instructions as this PR in all 143 compiled variants. Its conversion kernel compiles to 1066 and 1973 instructions in its two variants, against 1066 and 1970 in this PR, and 250 of 250 compared outputs are equal.The full set is 238 shapes: 7 head configurations, seqlen 64 to 16384, causal and no mask, and cross-attention. The criterion was set before the run: drop the lowest and highest of the 10 per-process ratios, then the minimum is above 1 and the median minus 1 is larger than half the range. It holds on 238 of 238 shapes.
The converted k and v cost memory. With 8 GiB of the 16 GiB card already allocated, the largest seqlen that fits is 0.64 to 0.95 of today's, depending on the head configuration.
Measurement conditions, per-configuration results, memory limits
Shapes, bshd: 7 head configurations (q/kv heads and head dim
32/8, 128,64/8, 128,128/8, 128,48/8, 128,16/16, 64,16/16, 128,64/64, 112). Per configuration seqlen 64 to 16384 at batch 1 and at batch·seqlen = 8192 up to seqlen 4096, causal and not, and cross-attention 1024 x 4096 at batch 1 and 8. k and v are quantized once asflash_attn_fp8_funcdoes.Arms in one process: fp8 today with the new path switched off at runtime, this PR, the bf16 alternative, and
mha.flash_attn_funcon the bf16 inputs the fp8 tensors came from and on the same inputs in fp16. Median of 10 windows, 3 for fp8 today at seqlen 8192 and above. 5 rounds per process with rotated arm order, 10 fresh processes. No row ended its GPU spin before the timed window. The output of every arm was checked against fp32 SDPA in every round.Stack: TheRock ROCm 10.2 (
torch 2.15.0a0+rocm10.2.0a20260926), Triton3.8.0.post29,AITER_TRITON_ONLY=1, aitermain(457b1151) with this change, display driver 32.0.32015.7029.Speedup is fp8 today / this PR, median over the 8 processes left per shape. Peak memory is the memory allocated by one call above the inputs. Ranges are over the medians of the shapes.
32/8, 12864/8, 128128/8, 12848/8, 12816/16, 6416/16, 12864/64, 112By seqlen, self-attention shapes of all 7 head configurations:
The 14 cross-attention shapes 1024 x 4096 give 8.2x to 24.0x.
Accuracy: the dot products see the exact fp8 values, the descales are applied in fp32 to the scores and the output, and P is cast to fp16 instead of fp8. Against an fp32 reference,
1 - cosine similarityis lower than today on 238 of 238 shapes, median ratio 0.83. The maximum absolute error is higher than today on 86 shapes, median ratio 0.99.Memory limit: the largest seqlen at batch 1 that fits under a torch allocator cap of 90 % of the card, with 8 GiB already allocated. The attention kernel launch was stubbed; allocations and the conversion ran. The bf16 alternative has the same limits as this PR.
32/8, 12864/8, 128128/8, 12848/8, 12816/16, 6416/16, 12864/64, 112The largest batch at seqlen 4096 gives the same ratios, 0.64 to 0.95. On an empty card the GQA configurations reach the search's stop at 2^31 elements per tensor before running out of memory, with and without this PR. The extra memory of this PR is 2 bytes × 2 · batch · seqlen_k · kv_heads · head_dim, for the converted k and v. The measured limits match that model within one search step on 252 of 252 searches.
Testing
Not tested on MI250X or MI300X. The new path is taken only on the archs in
SW_EMULATED_FP8_ARCHS. On other archs the host code takes the existing branches, andattn_fwdcompiles to the same TTIR as onmainfor bf16, fp16 and theIS_FP8path, checked by compiling on gfx1101.test_mha_fp8_sw_emulated_pathforces the new path on these archs as well. Test results are under "Validation and unverified scope".Documentation
New
docs/triton_mha_fp8_sw_emulated.md: why fp16 or bf16 inputs are the better choice on archs with software-emulated fp8, what memory an fp8 call takes, and that the forward output for fp8 inputs is fp32. The public API and the output dtype do not change.Dependencies
Breaking Changes
None.
Possible follow-up: output dtype for fp8 inputs
Not part of this PR. For fp8 q, k and v,
flash_attn_3.fwdallocates the output in fp32 (interface_v3.py:226). On gfx1101 with this change, an fp16 output is faster than fp32 on 6 of 6 shapes and fits a 1.22x to 1.59x longer seqlen. #7232 adds anout_dtypeargument to_flash_attn_forwardinmha.py;interface_v3.pyis a separate entry point.Measurements and the case for fp16 or bf16
The comment above
interface_v3.py:226says that bfloat16 caused mismatches on a small set of tests. The publicmha_v3.flash_attn_functakes noout, so a caller cannot choose the output dtype. The choice does not depend on the arch, so it applies to archs with fp8 hardware as well. For comparison, FlashAttention-3 returns bf16 for fp8 inputs (hopper/flash_api.cpp).Measured on gfx1101 with this change, with the output preallocated through
flash_attn_3.fwd(out=...):1 - cosvs fp32 outputThe bf16 output is slower than the fp16 output on 6 of 6 shapes, by 0.6 % to 13.7 %. Storing a bf16 value takes 5 instructions on RDNA3, an fp16 value one
v_fma_mixlo_f16, which also absorbs the last multiply. Withinterface_v3.py:226changed to fp16 onmain(457b1151), the forward tests oftest_mha_fp8.py(-k "not backward") give 44 passed and 4 skipped, the same as without the change. With bf16 the result is the same.The risk of fp16 is its range. The output is a weighted average of V after the descale, so it overflows only where the unquantized V exceeds 65504. How often that happens in real models was not measured. bf16 has the fp32 range and the same memory saving, at a smaller speedup and a slightly higher error.
PR Self-Review
Reviewed revision
a002007
Behavior and intent
On gfx1100, gfx1101, gfx1102, gfx1150 and gfx1151, an fp8
bshdprefill throughfwd_prefill.pyconverts k and v to fp16 in a separate kernel and q inattn_fwd, and runs the fp16 dot products with the descales applied in fp32. Today the same call converts every K and V tile in software inside the loop, which makes it 3.8x to 24.0x slower on gfx1101. Archs with fp8 hardware, other layouts, backward and decode keep the current code; with the new constexpr unset,_attn_fwd_innercompiles to the same instructions as onmain.The change adds no tuning values. The arch condition uses the existing
get_arch().supports_sw_emulated_fp8from #6782.Validation and unverified scope
gfx1101 (60 CU), Windows 11, TheRock ROCm 10.2 (
torch 2.15.0a0+rocm10.2.0a20260926), Triton3.8.0.post29,AITER_TRITON_ONLY=1, aitermain@457b1151. Tests ran aspytest op_tests/triton_tests/attention/test_mha_fp8.py -qfrom the aiter root, through a local wrapper that bindsaiter.dtypesunderAITER_TRITON_ONLYand caps the PyTorch allocator at 90 % of memory. The wrapper does not change kernels, configs or tests.main(457b1151)8805a4f8)torch.OutOfMemoryErrorintest_mha_backwardortest_mha_backward_varlen, which this PR does not change. Which backward tests fail depends on the memory left by earlier tests in the same process.64-2048-2048-4fail on both trees, also in a fresh process, and are not counted in the last column.test_mha_fp8_strided_qkvpasses 8 of 8 with this change and 8 of 8 onmain. The timing run checked the output of every arm against fp32 SDPA on 238 shapes, 0 failures.test_mha_fp8_sw_emulated_pathpasses 4 of 4 with this change. On the previous headc2067660, before the review fixes, its 2fp16_largecases fail with NaN in the output.attn_fwdfrom this PR equalsmainfor bf16, fp16 and theIS_FP8path that archs with fp8 hardware take, compiled on gfx1101 with location info removed. TheIS_FP8arm used fp8 e4m3fn inputs withsupports_sw_emulated_fp8forced to False.test_mha_fp8.pyon this change before therepr,other=0.0, the new tests and the review fixes gives 124 passed, 52 skipped, 0 failed. Timed with Triton3.8.0.post28and the gfx1151attn_fwdconfig entry instead of the default one, this change is faster than today on 97 of 102 shapes by the same criterion and slower on none, median speedup 1.46x to 60.41x.3.8.0,HSA_OVERRIDE_GFX_VERSION=11.0.1because the WSL driver reports the card as gfx1102: this PR (8805a4f8) gives 79 passed, 57 failed, 52 skipped. All forward tests pass, including the two new tests. All 57 failures aretorch.OutOfMemoryErrorin the backward tests; 52 pass in a fresh process, the other 5 are the64-2048-2048-4cases above.test_mha_fp8_sw_emulated_pathruns the new path, and it was run only on gfx1101, where the forced flag is already true. The varlen andbhsdlayouts keep the in-kernel conversion and are not covered.PR-specific question
test_mha_fp8_strided_qkvpasses onmainas well, 8 of 8 on gfx1101. It does not catch a defect that exists before this PR. It guards the new conversion kernel: on the archs inSW_EMULATED_FP8_ARCHSit is the only test that feeds non-contiguous fp8 k and v to_fp8_to_fp16_bshd, whose strides differ from the contiguous copy it writes. The existing fp8 tests intest_mha_fp8.pyandtest_mha_v3.pybuild q, k and v withtorch.randn, so their bshd inputs are contiguous. A stride or mask error in the conversion would change the output for packed inputs only, and the bit-exact comparison catches it.test_mha_fp8_sw_emulated_pathis the test that runs the new path on archs with fp8 hardware, which the CI runners have. Itsfp16_largecases pass fp16 descales of 256 for q and k, whose product exceeds the fp16 range. They fail on the previous headc2067660, where the product was taken in the descale dtype, and pass with the fp32 product.The main boundary of the change is the condition in
attention_forward_prefill_triton_impl: fp8 q, k and v,bshdlayout andsupports_sw_emulated_fp8. Every other call keeps the current kernel and the same generated code. The converted k and v add 2 bytes per element for the call, which lowers the largest seqlen to 0.64–0.95 of today's with 8 GiB allocated. The conversion kernel computes offsets in int64, because one tensor can exceed 2^31 elements.Changed op tests
op_tests/triton_tests/attention/test_mha_fp8.py: addstest_mha_fp8_strided_qkv, 8 cases over seqlen 64 and 300, head dim 64 and 128, causal and not. It covers fp8 q, k and v that are views into one packed qkv tensor, with descales, through the publicmha_v3.flash_attn_func. It is not redundant: no existing fp8 test passes non-contiguous bshd inputs, and seqlen 300 exercises the partial last block of the conversion. The test has no arch condition of its own, so it runs on every arch inANY_FP8_ARCHS. On archs with hardware fp8 the conversion does not run, and there the test covers the existingattn_fwdwith strided fp8 inputs. It was run only on gfx1101.op_tests/triton_tests/attention/test_mha_fp8.py: addstest_mha_fp8_sw_emulated_path, 4 cases over causal and not and two descale sets: fp32 descales between 0.5 and 1.5, and fp16 descales of 256 for q and k with q and k scaled down by 2^-6. It setsGpuArch.supports_sw_emulated_fp8to true withmonkeypatch, so the new path runs on every arch inANY_FP8_ARCHSwhile the arch name and its configs stay real. It checks that_fp8_to_fp16ran twice, for k and v, and compares the output withattention_refon the dequantized fp32 inputs. It is not redundant: no other test runs the new path on archs with fp8 hardware. It was run only on gfx1101.Related
#6782, #6770, #7232