Skip to content

Commit c56c675

Browse files
[rocm-libraries] ROCm/rocm-libraries#6498 (commit 5961a2e)
[CK_TILE] Fix conditional rescale numerical instability in FMHA forward (#6498) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [CK_TILE] Fix conditional rescale numerical instability in FMHA forward ## Motivation Fix numerical instability in the conditional O-accumulator rescaling optimization for CK-Tile FMHA forward (FlashAttention-4, Algorithm 6, Eq. 6). The conditional rescale optimization skips the expensive O-accumulator rescale when the running row-max shift is within a threshold (tau = log2(256) = 8.0). The original implementation had a bug: attention weights P were computed in the `m_new` reference frame before the skip/rescale decision. In the skip branch, `m` was reverted to `m_old`, but P remained in the `m_new` frame, causing incorrect softmax normalization. This fix introduces a `p_row_correction` factor: in the skip branch, P is multiplied by `exp2(m_new - m_old)` to bring it back to the `m_old` reference frame. - **Correctness:** Fixes broken inference on long sequences where running-max drift causes exp2 overflow (observed as degraded image quality on MI350X Flux2 generation) - **Performance:** Neutral to +4% depending on workload shape ## Technical Details 6 pipeline header files (same pattern in each): - `block_fmha_pipeline_qr_ks_vs.hpp` - `block_fmha_pipeline_qr_ks_vs_async.hpp` - `block_fmha_pipeline_qr_ks_vs_async_trload.hpp` - `block_fmha_pipeline_qr_ks_vs_fp8.hpp` - `block_fmha_pipeline_qr_ks_vs_whole_k_prefetch.hpp` - `block_fmha_pipeline_qs_ks_vs.hpp` In each file: - Lower threshold from 10.0 to 8.0 (tau = log2(256)) - Add `p_row_correction` distributed tensor initialized to 1.0 - Rescale branch: standard rescale of O_acc and l; correction = 1.0 - Skip branch: compute correction = exp2(-acc_scale_log2), update l, revert m, store correction - New `p_spans` sweep applies per-row correction to `p_compute` before P*V GEMM - Move P-to-PDataType cast to after correction sweep ## Dependencies None — this PR is standalone. ## Test Plan - GPU validation on MI300X (gfx942, ROCm 6.4.1): - Command: `./build/bin/tile_example_fmha_fwd -b=2 -h=8 -s=4096 -d=128 -prec=bf16 -v=1 -warmup=1 -repeat=3` - GPU validation on MI350X (gfx950, ROCm 7.0): - Command: `./build/bin/tile_example_fmha_fwd -b=2 -h=8 -s=4096 -d=128 -prec=bf16 -v=1 -warmup=1 -repeat=3` - Command: `./build/bin/tile_example_fmha_fwd -b=2 -h=8 -s=4096 -d=128 -prec=fp16 -v=1 -warmup=1 -repeat=3` ## Test Result Accuracy vs FP32 reference (MI350X, gfx950): | Shape | max_diff | mean_diff | |-------|----------|-----------| | B=1 H=24 M=4096 K=128 bf16 | 9.1e-4 | 4.6e-5 | | B=4 H=32 M=4096 K=128 bf16 | 9.9e-4 | 4.6e-5 | | B=1 H=24 M=4096 K=128 fp16 | 1.2e-4 | 9.0e-6 | Performance (MI350X, gfx950, ROCm 7.0): | Shape | FA4 (TFlops) | Always-rescale (TFlops) | Delta | |-------|-------------|------------------------|-------| | B=1 H=24 M=4096 K=128 bf16 | 425.9 | 428.5 | neutral | | B=2 H=8 M=2048 K=256 bf16 | 513.9 | 509.0 | +1.0% | | B=1 H=64 M=2048 K=64 bf16 | 481.7 | 464.3 | +3.7% | Benchmark results (MI300X, gfx942, ROCm 6.4.1): No regression on MI300X. This correctness fix is performance-neutral. | Config | TFlops / GB/s | Time (ms) | |--------|-------------|-----------| | MHA bf16 b=2 h=8 s=4096 d=128 | 342.49 TFlops | 0.401 | | MHA fp16 b=2 h=8 s=4096 d=128 | 391.70 TFlops | 0.351 | | Causal MHA bf16 b=2 h=8 s=4096 d=128 | 227.07 TFlops | 0.303 | | GQA 4:1 bf16 b=2 h=32 hk=8 s=2048 d=128 | 324.69 TFlops | 0.423 | | GQA 8:1 bf16 b=2 h=64 hk=8 s=2048 d=128 | 348.09 TFlops | 0.790 | | LLaMA-70B prefill b=1 h=64 hk=8 s=4096 d=128 bf16 | 376.71 TFlops | 1.459 | | Long-seq bf16 b=1 h=16 s=16384 d=128 | 383.42 TFlops | 5.735 | | Decode b=64 h=32 hk=8 s_k=4096 d=128 bf16 | 691.64 GB/s | 1.554 | All validation tests pass (`valid:y`) on both MI300X and MI350X. Additional validation: - Uniform scores: softmax output matches FP32 reference (max_diff < 1e-3) - Large seqlen (4096+): no overflow or NaN in O-accumulator - Spike pattern: correct handling of sudden row-max jumps - Multiple spikes: correction applied correctly across multiple skip/rescale transitions - Deterministic: identical outputs across repeated runs - No performance regression on standard workloads
1 parent 22a99f9 commit c56c675

5 files changed

Lines changed: 514 additions & 201 deletions

include/ck_tile/ops/fmha/pipeline/block_fmha_pipeline_qr_ks_vs.hpp

Lines changed: 101 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -851,14 +851,99 @@ struct BlockFmhaPipelineQRKSVS
851851
}
852852
};
853853

854+
// Conditional rescaling: skip o_acc rescale when correction factor
855+
// exp2(acc_scale_log2) is negligible (< exp2(-8) ≈ 0.004, below BF16
856+
// precision). Adapted from FlashAttention-4 (Tri Dao, 2025).
857+
// Eliminates 70-90% of rescale operations in practice.
858+
//
859+
// For skip rows we stabilize P with m_old (the previous max) instead of
860+
// the new max m_j, so P is computed directly in the m_{j-1} frame and no
861+
// post-correction sweep is needed. For rescale rows we use m_j as usual.
862+
// FP8 quant modes (PERTENSOR/BLOCKSCALE/etc.) cast P to FP8 after
863+
// softmax. In the skip branch P is computed with m_old, so P can
864+
// exceed the FP8 representable range and saturate, corrupting the
865+
// P*V GEMM. Disable skip for all FP8 paths (threshold 0).
866+
static constexpr SMPLComputeDataType kRescaleThreshold =
867+
type_convert<SMPLComputeDataType>(
868+
QScaleEnum == BlockAttentionQuantScaleEnum::NO_SCALE ? 8.0f : 0.0f);
869+
870+
// Per-row stabilizer: m_old for skip rows, m_j for rescale rows.
871+
auto m_stab =
872+
make_static_distributed_tensor<SMPLComputeDataType>(m.get_tile_distribution());
873+
// Per-row rescale factor (exp2 of acc_scale_log2); only valid when
874+
// needs_rescale[i] is true.
875+
auto rescale_factor =
876+
make_static_distributed_tensor<SMPLComputeDataType>(m.get_tile_distribution());
877+
auto needs_rescale = make_static_distributed_tensor<bool>(m.get_tile_distribution());
878+
set_tile(needs_rescale, false);
879+
880+
constexpr auto m_spans = decltype(m)::get_distributed_spans();
881+
sweep_tile_span(m_spans[number<0>{}], [&](auto idx0) {
882+
constexpr auto i_idx = make_tuple(idx0);
883+
#if CK_TILE_FMHA_FWD_FAST_EXP2
884+
const auto acc_scale_log2 = [&]() {
885+
if constexpr(BiasEnum == BlockAttentionBiasEnum::ELEMENTWISE_BIAS ||
886+
BiasEnum == BlockAttentionBiasEnum::ALIBI)
887+
{
888+
return m_old[i_idx] - get_validated_m(m[i_idx]);
889+
}
890+
else
891+
{
892+
if constexpr(kHasLogitsSoftCap)
893+
{
894+
return m_old[i_idx] - get_validated_m(m[i_idx]);
895+
}
896+
else
897+
{
898+
auto row_max = scale_s * get_validated_m(m[i_idx]);
899+
return scale_s * m_old[i_idx] - row_max;
900+
}
901+
}
902+
}();
903+
904+
const bool need_rescale =
905+
(acc_scale_log2 < type_convert<SMPLComputeDataType>(-kRescaleThreshold));
906+
907+
if(need_rescale)
908+
{
909+
rescale_factor(i_idx) = exp2(acc_scale_log2);
910+
m_stab(i_idx) = m[i_idx];
911+
needs_rescale(i_idx) = true;
912+
}
913+
else
914+
{
915+
// Skip branch: stabilize P with m_old so P is already in
916+
// m_{j-1} frame; restore m to m_old for downstream iterations.
917+
m_stab(i_idx) = m_old[i_idx];
918+
m(i_idx) = m_old[i_idx];
919+
}
920+
#else
921+
const auto diff = m_old[i_idx] - get_validated_m(m[i_idx]);
922+
const bool need_rescale =
923+
(diff < type_convert<SMPLComputeDataType>(-kRescaleThreshold));
924+
925+
if(need_rescale)
926+
{
927+
rescale_factor(i_idx) = exp(diff);
928+
m_stab(i_idx) = m[i_idx];
929+
needs_rescale(i_idx) = true;
930+
}
931+
else
932+
{
933+
m_stab(i_idx) = m_old[i_idx];
934+
m(i_idx) = m_old[i_idx];
935+
}
936+
#endif
937+
});
938+
854939
constexpr auto p_spans = decltype(p_compute)::get_distributed_spans();
855940
sweep_tile_span(p_spans[number<0>{}], [&](auto idx0) {
856941
constexpr auto i_idx = make_tuple(idx0);
857942
#if CK_TILE_FMHA_FWD_FAST_EXP2
858943
// For BLOCKSCALE: precompute (m - shift) once per row
859944
// Bias/Alibi/SoftCap: exp2(s - m + shift) = exp2(s - (m - shift))
860945
// else: exp2(scale_s*s - scale_s*m + shift) = exp2(scale_s*s - (scale_s*m - shift))
861-
auto validated_m = get_validated_m(m[i_idx]);
946+
auto validated_m = get_validated_m(m_stab[i_idx]);
862947
auto row_max = scale_s * validated_m;
863948
if constexpr(QScaleEnum == BlockAttentionQuantScaleEnum::BLOCKSCALE)
864949
{
@@ -891,7 +976,7 @@ struct BlockFmhaPipelineQRKSVS
891976
}
892977
}
893978
#else
894-
p_compute(i_j_idx) = exp(s[i_j_idx] - get_validated_m(m[i_idx]));
979+
p_compute(i_j_idx) = exp(s[i_j_idx] - get_validated_m(m_stab[i_idx]));
895980
#endif
896981
});
897982
});
@@ -904,38 +989,20 @@ struct BlockFmhaPipelineQRKSVS
904989
constexpr auto o_spans = decltype(o_acc)::get_distributed_spans();
905990
sweep_tile_span(o_spans[number<0>{}], [&](auto idx0) {
906991
constexpr auto i_idx = make_tuple(idx0);
907-
#if CK_TILE_FMHA_FWD_FAST_EXP2
908-
const auto tmp = [&]() {
909-
if constexpr(BiasEnum == BlockAttentionBiasEnum::ELEMENTWISE_BIAS ||
910-
BiasEnum == BlockAttentionBiasEnum::ALIBI)
911-
{
912-
return exp2(m_old[i_idx] - get_validated_m(m[i_idx]));
913-
}
914-
else
915-
{
916-
if constexpr(kHasLogitsSoftCap)
917-
{
918-
919-
return exp2(m_old[i_idx] - get_validated_m(m[i_idx]));
920-
}
921-
else
922-
{
923-
auto row_max = scale_s * get_validated_m(m[i_idx]);
924-
return exp2(scale_s * m_old[i_idx] - row_max);
925-
}
926-
}
927-
}();
928-
#else
929-
const auto tmp = exp(m_old[i_idx] - get_validated_m(m[i_idx]));
930-
#endif
931-
l(i_idx) = tmp * l[i_idx] + rowsum_p[i_idx];
932-
sweep_tile_span(o_spans[number<1>{}], [&](auto idx1) {
933-
constexpr auto i_j_idx = make_tuple(idx0, idx1);
934-
// FIXME: this use different equation from FA v2 paper,
935-
// but produce correc result.
936-
// Is the equation wrong?
937-
o_acc(i_j_idx) *= tmp;
938-
});
992+
if(needs_rescale[i_idx])
993+
{
994+
const auto tmp = rescale_factor[i_idx];
995+
l(i_idx) = tmp * l[i_idx] + rowsum_p[i_idx];
996+
sweep_tile_span(o_spans[number<1>{}], [&](auto idx1) {
997+
constexpr auto i_j_idx = make_tuple(idx0, idx1);
998+
o_acc(i_j_idx) *= tmp;
999+
});
1000+
}
1001+
else
1002+
{
1003+
// Skip: P already in m_{j-1} frame, no o_acc rescale needed.
1004+
l(i_idx) = l[i_idx] + rowsum_p[i_idx];
1005+
}
9391006
});
9401007

9411008
if constexpr(kHasDropout)

include/ck_tile/ops/fmha/pipeline/block_fmha_pipeline_qr_ks_vs_async.hpp

Lines changed: 90 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -664,14 +664,88 @@ struct BlockFmhaPipelineQRKSVSAsync
664664
}
665665
};
666666

667+
// Conditional rescaling (FA4): skip when correction is negligible.
668+
// For skip rows we stabilize P with m_old so P is computed directly in
669+
// the m_{j-1} frame, eliminating the post-correction sweep.
670+
// FP8 quant modes cast P to FP8 after softmax. In the skip
671+
// branch P can exceed the FP8 representable range and saturate,
672+
// corrupting the P*V GEMM. Disable skip for all FP8 paths.
673+
static constexpr SMPLComputeDataType kRescaleThreshold =
674+
type_convert<SMPLComputeDataType>(
675+
QScaleEnum == BlockAttentionQuantScaleEnum::NO_SCALE ? 8.0f : 0.0f);
676+
677+
auto m_stab =
678+
make_static_distributed_tensor<SMPLComputeDataType>(m.get_tile_distribution());
679+
auto rescale_factor =
680+
make_static_distributed_tensor<SMPLComputeDataType>(m.get_tile_distribution());
681+
auto needs_rescale = make_static_distributed_tensor<bool>(m.get_tile_distribution());
682+
set_tile(needs_rescale, false);
683+
684+
constexpr auto m_spans = decltype(m)::get_distributed_spans();
685+
sweep_tile_span(m_spans[number<0>{}], [&](auto idx0) {
686+
constexpr auto i_idx = make_tuple(idx0);
687+
#if CK_TILE_FMHA_FWD_FAST_EXP2
688+
const auto acc_scale_log2 = [&]() {
689+
if constexpr(BiasEnum == BlockAttentionBiasEnum::ELEMENTWISE_BIAS ||
690+
BiasEnum == BlockAttentionBiasEnum::ALIBI)
691+
{
692+
return m_old[i_idx] - get_validated_m(m[i_idx]);
693+
}
694+
else
695+
{
696+
if constexpr(kHasLogitsSoftCap)
697+
{
698+
return m_old[i_idx] - get_validated_m(m[i_idx]);
699+
}
700+
else
701+
{
702+
auto row_max = scale_s * get_validated_m(m[i_idx]);
703+
return scale_s * m_old[i_idx] - row_max;
704+
}
705+
}
706+
}();
707+
708+
const bool need_rescale =
709+
(acc_scale_log2 < type_convert<SMPLComputeDataType>(-kRescaleThreshold));
710+
711+
if(need_rescale)
712+
{
713+
rescale_factor(i_idx) = exp2(acc_scale_log2);
714+
m_stab(i_idx) = m[i_idx];
715+
needs_rescale(i_idx) = true;
716+
}
717+
else
718+
{
719+
m_stab(i_idx) = m_old[i_idx];
720+
m(i_idx) = m_old[i_idx];
721+
}
722+
#else
723+
const auto diff = m_old[i_idx] - get_validated_m(m[i_idx]);
724+
const bool need_rescale =
725+
(diff < type_convert<SMPLComputeDataType>(-kRescaleThreshold));
726+
727+
if(need_rescale)
728+
{
729+
rescale_factor(i_idx) = exp(diff);
730+
m_stab(i_idx) = m[i_idx];
731+
needs_rescale(i_idx) = true;
732+
}
733+
else
734+
{
735+
m_stab(i_idx) = m_old[i_idx];
736+
m(i_idx) = m_old[i_idx];
737+
}
738+
#endif
739+
});
740+
667741
constexpr auto p_spans = decltype(p_compute)::get_distributed_spans();
668742
sweep_tile_span(p_spans[number<0>{}], [&](auto idx0) {
669743
constexpr auto i_idx = make_tuple(idx0);
670744
#if CK_TILE_FMHA_FWD_FAST_EXP2
671745
// For BLOCKSCALE: precompute (m - shift) once per row
672746
// Bias/Alibi/SoftCap: exp2(s - m + shift) = exp2(s - (m - shift))
673747
// else: exp2(scale_s*s - scale_s*m + shift) = exp2(scale_s*s - (scale_s*m - shift))
674-
auto validated_m = get_validated_m(m[i_idx]);
748+
auto validated_m = get_validated_m(m_stab[i_idx]);
675749
auto row_max = scale_s * validated_m;
676750
if constexpr(QScaleEnum == BlockAttentionQuantScaleEnum::BLOCKSCALE)
677751
{
@@ -704,7 +778,7 @@ struct BlockFmhaPipelineQRKSVSAsync
704778
}
705779
}
706780
#else
707-
p_compute(i_j_idx) = exp(s[i_j_idx] - get_validated_m(m[i_idx]));
781+
p_compute(i_j_idx) = exp(s[i_j_idx] - get_validated_m(m_stab[i_idx]));
708782
#endif
709783
});
710784
});
@@ -717,37 +791,20 @@ struct BlockFmhaPipelineQRKSVSAsync
717791
constexpr auto o_spans = decltype(o_acc)::get_distributed_spans();
718792
sweep_tile_span(o_spans[number<0>{}], [&](auto idx0) {
719793
constexpr auto i_idx = make_tuple(idx0);
720-
#if CK_TILE_FMHA_FWD_FAST_EXP2
721-
const auto tmp = [&]() {
722-
if constexpr(BiasEnum == BlockAttentionBiasEnum::ELEMENTWISE_BIAS ||
723-
BiasEnum == BlockAttentionBiasEnum::ALIBI)
724-
{
725-
return exp2(m_old[i_idx] - get_validated_m(m[i_idx]));
726-
}
727-
else
728-
{
729-
if constexpr(kHasLogitsSoftCap)
730-
{
731-
return exp2(m_old[i_idx] - get_validated_m(m[i_idx]));
732-
}
733-
else
734-
{
735-
auto row_max = scale_s * get_validated_m(m[i_idx]);
736-
return exp2(scale_s * m_old[i_idx] - row_max);
737-
}
738-
}
739-
}();
740-
#else
741-
const auto tmp = exp(m_old[i_idx] - get_validated_m(m[i_idx]));
742-
#endif
743-
l(i_idx) = tmp * l[i_idx] + rowsum_p[i_idx];
744-
sweep_tile_span(o_spans[number<1>{}], [&](auto idx1) {
745-
constexpr auto i_j_idx = make_tuple(idx0, idx1);
746-
// FIXME: this use different equation from FA v2 paper,
747-
// but produce correc result.
748-
// Is the equation wrong?
749-
o_acc(i_j_idx) *= tmp;
750-
});
794+
if(needs_rescale[i_idx])
795+
{
796+
const auto tmp = rescale_factor[i_idx];
797+
l(i_idx) = tmp * l[i_idx] + rowsum_p[i_idx];
798+
sweep_tile_span(o_spans[number<1>{}], [&](auto idx1) {
799+
constexpr auto i_j_idx = make_tuple(idx0, idx1);
800+
o_acc(i_j_idx) *= tmp;
801+
});
802+
}
803+
else
804+
{
805+
// Skip: P already in m_{j-1} frame, no o_acc rescale needed.
806+
l(i_idx) = l[i_idx] + rowsum_p[i_idx];
807+
}
751808
});
752809

753810
if constexpr(kHasDropout)

0 commit comments

Comments
 (0)