Skip to content

Commit 2fbb48d

Browse files
committed
condense comments
1 parent 6cf0004 commit 2fbb48d

4 files changed

Lines changed: 53 additions & 120 deletions

File tree

aiter/ops/mha.py

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -314,11 +314,8 @@ def fmha_v3_fwd(
314314
) -> Tuple[Tensor, Tensor, Tensor, Tensor]: ...
315315

316316

317-
# OPUS gfx950 dense D=128 bf16 forward. In-file @compile_ops stub so mha.py is
318-
# self-contained like the sibling backends (no inline cross-module import). Named
319-
# with a leading underscore, with fc_name pointing at the pybind symbol
320-
# `fmha_fwd_hd128_bf16_opus_fwd` in the `module_fmha_fwd_hd128_bf16_opus` module.
321-
# The kernel writes `out` in place and returns None.
317+
# OPUS gfx950 dense D=128 bf16 forward: low-level @compile_ops stub bound to the
318+
# pybind symbol via fc_name. Writes `out` in place, returns None.
322319
@compile_ops(
323320
"module_fmha_fwd_hd128_bf16_opus",
324321
fc_name="fmha_fwd_hd128_bf16_opus_fwd",
@@ -342,15 +339,9 @@ def fmha_fwd_hd128_bf16_opus_fwd(
342339
causal: bool,
343340
out: Optional[Tensor] = None,
344341
) -> Tensor:
345-
"""Public wrapper: allocates `out` if needed and forwards to the OPUS gfx950
346-
D=128 bf16 kernel entry point (two-layer pattern, mirroring
347-
`fmha_fwd_with_sink_asm`).
348-
349-
The kernel applies `softmax_scale` to Q·K^T internally, handles the GQA head
350-
fan-out, and writes the attention output into `out` in place (no LSE). q/k/v
351-
are dense bshd ([batch, seq, head, dim]); `out` matches [B, S, H_q, D_v].
352-
This is the mha.py-local wrapper (plain function, no torch-op registration)
353-
and the sole owner of the OPUS D=128 binding/public API.
342+
"""Public wrapper for the OPUS gfx950 D=128 bf16 kernel: allocates `out`
343+
([B, S, H_q, D_v]) if needed and forwards. The kernel applies `softmax_scale`
344+
to Q·K^T internally, handles GQA fan-out, and produces no LSE. Dense bshd q/k/v.
354345
"""
355346
batch, q_seq_len, q_head_num, qk_head_dim = q.shape
356347
v_head_dim = v.size(3)
@@ -1845,18 +1836,16 @@ def can_impl_fmha_native():
18451836
return ret
18461837

18471838
def can_impl_fmha_fwd_hd128_bf16_opus():
1848-
# OPUS gfx950 dense D=128 bf16 forward (hand-written HIP kernel). Env-gated
1849-
# (OFF by default) so it only supersedes the v3/CK path when explicitly
1850-
# enabled via AITER_ENABLE_FMHA_OPUS. Inference-only: the kernel
1851-
# produces the output only (no softmax LSE, no dropout mask), so it must
1852-
# never capture a case that needs LSE / the autograd backward path.
1839+
# OPUS gfx950 dense D=128 bf16 forward. Env-gated (OFF by default) so it only
1840+
# supersedes v3/CK when enabled. Inference-only (no LSE/dropout mask), so it
1841+
# must never capture return_lse / the autograd backward path.
18531842
if int(os.environ.get("AITER_ENABLE_FMHA_OPUS", "0")) == 0:
18541843
return False
18551844
ret = get_gfx() == "gfx950"
18561845
ret = ret and (q.dtype == dtypes.bf16)
18571846
ret = ret and (hdim_q == 128 and hdim_v == 128)
18581847
ret = ret and (nhead_q % nhead_k == 0)
1859-
# dense only (no varlen), and the kernel requires seqlen_q == seqlen_k.
1848+
# dense only (no varlen); kernel requires seqlen_q == seqlen_k.
18601849
ret = ret and (cu_seqlens_q is None and cu_seqlens_kv is None)
18611850
ret = ret and (seqlen_q == seqlen_k)
18621851
# no bias / alibi / dropout / sliding-window / sink / quant-descale.
@@ -1865,8 +1854,7 @@ def can_impl_fmha_fwd_hd128_bf16_opus():
18651854
ret = ret and (window_size_left == -1 and window_size_right == -1)
18661855
ret = ret and (sink_size == 0 and sink_ptr is None)
18671856
ret = ret and (q_descale is None and k_descale is None and v_descale is None)
1868-
# inference-only: no LSE => cannot serve return_lse (grad implies return_lse)
1869-
# or the attention-probs path.
1857+
# inference-only: no LSE (grad implies return_lse) and no attn-probs.
18701858
ret = ret and (not return_lse) and (not return_softmax)
18711859
return ret
18721860

@@ -1950,12 +1938,8 @@ def _validate_cu(name: str, x: Optional[torch.Tensor]):
19501938
S_dmask = torch.empty((0,), dtype=torch.float32, device=q.device)
19511939
rng_state = torch.empty((2,), dtype=torch.int64, device=q.device)
19521940
elif can_impl_fmha_fwd_hd128_bf16_opus():
1953-
# OPUS gfx950 dense D=128 bf16 forward via the public wrapper (two-layer
1954-
# pattern like fmha_fwd_with_sink_asm). q/k/v are dense bshd [B,S,H,D]; the
1955-
# kernel applies softmax_scale to Q·K^T internally and handles the GQA head
1956-
# fan-out. Inference-only, so the LSE / dropout-mask / rng slots are
1957-
# placeholders (the gate guarantees not return_lse / not return_softmax, so
1958-
# the caller never consumes them) -- mirrors the sink-asm branch shape.
1941+
# OPUS gfx950 dense D=128 forward. Inference-only: the lse/S_dmask/rng slots
1942+
# are unused placeholders (gate guarantees not return_lse/return_softmax).
19591943
out_ = fmha_fwd_hd128_bf16_opus_fwd(
19601944
q,
19611945
k,

csrc/include/fmha_fwd_hd128_bf16_opus.h

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,7 @@ void fmha_fwd_hd128_bf16_opus_fwd(aiter_tensor_t& q,
2727
float softmax_scale);
2828

2929
#ifdef FMHA_FWD_HD128_BF16_OPUS_IMPL
30-
// ============================================================================
31-
// Implementation section - only compiled in the .cu translation unit
32-
// ============================================================================
30+
// Implementation section - only compiled in the .cu translation unit.
3331

3432
// opus_gqa_traits / opus_gqa_kargs / ceil_div / bf16_t.
3533
#include "fmha_fwd_hd128_bf16_opus_defs.h"

csrc/include/fmha_fwd_hd128_bf16_opus_kernel.hpp

Lines changed: 32 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
#include <bit>
88
#include <cstdint>
99

10-
namespace gqa_d128 {
11-
1210
using opus::operator""_I;
1311

1412
constexpr int MFMA_MASK = 0x08;
@@ -248,16 +246,10 @@ __device__ inline void attn_mask_vec2_imm(opus::u32_t rel_vgpr, opus::u32_t neg_
248246
);
249247
}
250248

251-
// Mask score columns of a KV tile to -inf where the column's global key position
252-
// exceeds `ref_pos` (attn_mask_vec2_imm masks a column when rel = ref_pos - k_pos
253-
// < THR). This single helper unifies the two masking uses, which have identical
254-
// bodies and differ only in `ref_pos`:
255-
// * causal masking: ref_pos = q_pos (= q_start_pos + lane's query row) -> masks
256-
// future keys (k_pos > q_pos).
257-
// * OOB masking: ref_pos = N - 1 -> masks padding keys past the valid sequence
258-
// (k_pos >= N), for arbitrary (non KV_TILE aligned) sequence lengths.
259-
// `rel` is int->u32 compared via signed `v_cmp_lt_i32`, so negative
260-
// (ref_pos - k_pos) stays correct. `lane_id` is passed in (used for lane_group).
249+
// Mask a KV tile's score columns to -inf where global key pos > ref_pos
250+
// (attn_mask_vec2_imm masks when rel = ref_pos - k_pos < THR). Unifies causal
251+
// (ref_pos = q_pos) and OOB (ref_pos = N-1) masking. `rel` is int->u32 compared
252+
// signed (v_cmp_lt_i32), so a negative ref_pos - k_pos stays correct.
261253
template<typename T, typename V>
262254
__device__ inline void attn_mask_tile(V& v_s, int ref_pos, int kv_tile_idx, opus::u32_t neg_inf_v, int lane_id) {
263255
using D_ACC = typename T::D_ACC;
@@ -298,8 +290,6 @@ __device__ inline void attn_mask_tile(V& v_s, int ref_pos, int kv_tile_idx, opus
298290
});
299291
}
300292

301-
} // namespace gqa_d128
302-
303293
// Sub-variant toggle: build with -DGQA_D128_DRAIN_NOINLINE=1 to force the drain helper
304294
// to NOT inline (one drain frame live at runtime); default (0) lets AMDGPU inline it.
305295
#ifndef GQA_D128_DRAIN_NOINLINE
@@ -311,15 +301,12 @@ __device__ inline void attn_mask_tile(V& v_s, int ref_pos, int kv_tile_idx, opus
311301
#define GQA_D128_DRAIN_ATTR
312302
#endif
313303

314-
// ─── Simple path for the tiny case (num_kv_tiles <= 2). Faithfully mirrors the
315-
// pipelined path's prologue + per-tile cluster discipline (same async_load ->
316-
// s_waitcnt -> s_barrier ordering, same half-split exp, v_o pins, sched_barrier
317-
// fences and full mma1), just without the steady-state loop. Avoids padding 1-2
318-
// tiles up to the >=3/4-tile pipeline minimum. Used for causal & non-causal.
304+
// Simple path for the tiny case (num_kv_tiles <= 2): same prologue + cluster
305+
// discipline as the pipelined path but no steady-state loop, avoiding padding 1-2
306+
// tiles up to the pipeline minimum. Causal & non-causal.
319307
template<class Traits>
320308
GQA_D128_DRAIN_ATTR __device__ void gqa_d128_le2_tiles(opus_gqa_kargs kargs, char* smem_buf) {
321309
using namespace opus;
322-
using namespace gqa_d128;
323310
using T = opus::remove_cvref_t<Traits>;
324311
using D_ATTN = typename T::D_ATTN;
325312
using D_ACC = typename T::D_ACC;
@@ -518,14 +505,12 @@ GQA_D128_DRAIN_ATTR __device__ void gqa_d128_le2_tiles(opus_gqa_kargs kargs, cha
518505
store<T::VEC_O>(g_o, v_o_bf16, u_o);
519506
}
520507

521-
// ─── GQA accumulation helper (prefill-style runtime dispatch): one instantiation per
522-
// tile-count parity, selected at runtime by gqa_d128_kernel below. Each instantiation
523-
// compiles exactly ONE drain via `if constexpr (OddTail)`. Unlike the host two-kernel
524-
// scheme this also covers CAUSAL (per-block parity varies within a launch).
508+
// Pipelined accumulation: one instantiation per tile-count parity (OddTail),
509+
// each compiling exactly one drain via `if constexpr`. Runtime-dispatched by
510+
// gqa_d128_kernel; covers causal (per-block parity varies within a launch).
525511
template<class Traits, bool OddTail>
526512
GQA_D128_DRAIN_ATTR __device__ void gqa_d128_pipelined(opus_gqa_kargs kargs, char* smem_buf) {
527513
using namespace opus;
528-
using namespace gqa_d128;
529514
using T = opus::remove_cvref_t<Traits>;
530515
using D_ATTN = typename T::D_ATTN;
531516
using D_ACC = typename T::D_ACC;
@@ -629,21 +614,12 @@ GQA_D128_DRAIN_ATTR __device__ void gqa_d128_pipelined(opus_gqa_kargs kargs, cha
629614
const int causal_num_tiles = ceil_div(q_block_end, T::KV_TILE_SIZE);
630615
max_num_tiles = causal_num_tiles < max_num_tiles ? causal_num_tiles : max_num_tiles;
631616
}
632-
// The fixed prologue/epilogue schedule traverses every tile exactly once only
633-
// when the tile count is even and >= 4. For short / non-aligned N we pad up;
634-
// the extra tiles read OOB (-> 0 via buffer bounds) and are -inf masked below,
635-
// so they contribute nothing. Causal tile counts are already multiples of 4,
636-
// so this padding never affects the aligned causal fast path.
637-
//
638-
// NOTE: an OddTail-specialized drain (mirroring pa_sparse_prefill) was
639-
// prototyped to avoid padding odd tile counts, but it materially regressed
640-
// this register-bound kernel (VGPR spills 0 -> 26-147, and odd-shape wall
641-
// time 0.319 -> 0.369 ms) because duplicating the drain inflates register
642-
// pressure past the occupancy-2 budget. The padded approach is faster and
643-
// simpler, so it is retained. See the task report for the A/B data.
644-
// True tile count (no even-padding); the parity matches the OddTail instantiation
645-
// chosen by the dispatcher. Pipeline minimum is 3 tiles (odd) or 4 (even); the
646-
// min-pad preserves parity so it stays consistent with the dispatcher's choice.
617+
// Fixed schedule needs a >=3 (odd) / >=4 (even) tile count; short/non-aligned N
618+
// is padded up (extra tiles read 0 via buffer bounds and are -inf masked ->
619+
// contribute nothing; causal counts are already multiples of 4). An OddTail
620+
// drain avoiding odd-count padding was tried but regressed this register-bound
621+
// kernel (VGPR spills past the occ-2 budget), so the parity-preserving min-pad
622+
// is kept.
647623
if constexpr (OddTail) {
648624
if (max_num_tiles < 3) max_num_tiles = 3;
649625
} else {
@@ -655,23 +631,14 @@ GQA_D128_DRAIN_ATTR __device__ void gqa_d128_pipelined(opus_gqa_kargs kargs, cha
655631
[[maybe_unused]] const int q_start_pos = q_block_start + warp_id * T::Q_TILE_SIZE;
656632
[[maybe_unused]] const opus::u32_t neg_inf_v = std::bit_cast<opus::u32_t>(-opus::numeric_limits<D_ACC>::infinity());
657633

658-
// Out-of-bound KV masking: mask any score column whose global kv index is
659-
// >= N to -inf before the softmax (the final KV tile is partial when N is not
660-
// a multiple of KV_TILE; its OOB columns read 0 via the buffer-resource bounds
661-
// and must not enter the softmax). The internal (kv+1)*KV_TILE > N gate makes
662-
// this a no-op unless the tile is actually partial.
663-
//
664-
// *** INVARIANT (correctness) — OOB mask on the FINAL tile only ***
665-
// This lambda is now called on the FINAL KV tile ONLY (even-drain max-1,
666-
// odd-drain p+2, and the two le2 last-tile sites). That is valid ONLY because
667-
// max_num_tiles == num_kv_tiles (NO padding):
668-
// the `eff <= 2` dispatch routes tiny tile counts to gqa_d128_le2_tiles, so
669-
// the `< 3 / < 4` min-pad above is DEAD CODE in this pipelined path and only
670-
// the last real tile can ever be OOB. Interior/prologue tiles are always full,
671-
// so their OOB masks were removed (they were runtime-gated no-ops anyway).
672-
// If even-padding is EVER reintroduced, the removed sites (prologue tile 0,
673-
// even-drain max-3 / max-2, odd-drain p+1) MUST be restored — padded trailing
674-
// tiles would be OOB and, unmasked, corrupt the softmax (wrong results).
634+
// OOB KV masking: mask columns with global kv index >= N to -inf (final tile is
635+
// partial when N % KV_TILE != 0; OOB reads are 0 via buffer bounds). The
636+
// (kv+1)*KV_TILE > N gate makes it a no-op unless the tile is partial.
637+
// INVARIANT: called on the FINAL tile only (even max-1, odd p+2, le2 last).
638+
// Valid ONLY because max_num_tiles == num_kv_tiles (no padding: eff<=2 -> le2,
639+
// so the min-pad above is dead code here). If even-padding is reintroduced, the
640+
// removed interior/prologue OOB sites MUST be restored or padded tiles corrupt
641+
// the softmax.
675642
auto mask_oob_kv = [&](auto& v_s_tile, int kv_tile_idx) {
676643
if ((kv_tile_idx + 1) * T::KV_TILE_SIZE > kargs.N) {
677644
attn_mask_tile<T>(v_s_tile, kargs.N - 1, kv_tile_idx, neg_inf_v, lane_id);
@@ -709,8 +676,7 @@ GQA_D128_DRAIN_ATTR __device__ void gqa_d128_pipelined(opus_gqa_kargs kargs, cha
709676
attn_mask_tile<T>(v_s[0], q_start_pos + (lane_id % T::W_M), 0, neg_inf_v, lane_id);
710677
}
711678
}
712-
// [OOB last-tile-only] prologue tile 0 is always full here (pipelined path has
713-
// num_kv_tiles >= 3); no OOB mask. See mask_oob_kv invariant above.
679+
// [last-tile-only] tile 0 always full here; no OOB mask (see invariant).
714680
m_row = attn_row_max<T>(v_s[0]);
715681
attn_sub_row<T>(v_s[0], m_row);
716682
asm volatile("" : "+v"(v_s[0]) ::);
@@ -722,9 +688,8 @@ GQA_D128_DRAIN_ATTR __device__ void gqa_d128_pipelined(opus_gqa_kargs kargs, cha
722688

723689
async_load<T::VEC_KV>(g_k, s_k[0].ptr, u_gk, u_sk, kv_tile(2));
724690

725-
// Main loop (steady state): 2 KV tiles per iteration. Shared by both tile-count
726-
// parities (kept as a single copy so the hot path's register pressure is
727-
// unchanged — duplicating it would spill VGPRs and slow every tile).
691+
// Steady-state loop: 2 KV tiles/iteration, shared by both parities (single copy
692+
// to avoid the VGPR spill that duplicating the hot path would cause).
728693
for (int j = 3; j < max_num_tiles - 1; j += 2) {
729694
// Cluster 0:
730695
async_load<T::VEC_KV>(g_v, s_v[1].ptr, u_gv, u_sv, kv_tile(j - 2));
@@ -882,8 +847,7 @@ GQA_D128_DRAIN_ATTR __device__ void gqa_d128_pipelined(opus_gqa_kargs kargs, cha
882847
attn_mask_tile<T>(v_s[1], q_start_pos + (lane_id % T::W_M), max_num_tiles - 3, neg_inf_v, lane_id);
883848
}
884849
}
885-
// [OOB last-tile-only] interior tile (max-3) is always full; OOB mask runs
886-
// only on the final tile (max-1). See mask_oob_kv invariant above.
850+
// [last-tile-only] interior tile (max-3) always full; no OOB mask (see invariant).
887851
s_waitcnt_lgkmcnt(0_I);
888852
s_waitcnt_vmcnt(number<T::k_buffer_load_insts + T::v_buffer_load_insts>{});
889853
__builtin_amdgcn_sched_barrier(0);
@@ -941,8 +905,7 @@ GQA_D128_DRAIN_ATTR __device__ void gqa_d128_pipelined(opus_gqa_kargs kargs, cha
941905
attn_mask_tile<T>(v_s[0], q_start_pos + (lane_id % T::W_M), max_num_tiles - 2, neg_inf_v, lane_id);
942906
}
943907
}
944-
// [OOB last-tile-only] interior tile (max-2) is always full; OOB mask runs
945-
// only on the final tile (max-1). See mask_oob_kv invariant above.
908+
// [last-tile-only] interior tile (max-2) always full; no OOB mask (see invariant).
946909
s_waitcnt_lgkmcnt(0_I);
947910
s_waitcnt_vmcnt(number<T::v_buffer_load_insts>{});
948911
__builtin_amdgcn_sched_barrier(0);
@@ -999,8 +962,7 @@ GQA_D128_DRAIN_ATTR __device__ void gqa_d128_pipelined(opus_gqa_kargs kargs, cha
999962
attn_mask_tile<T>(v_s[1], q_start_pos + (lane_id % T::W_M), max_num_tiles - 1, neg_inf_v, lane_id);
1000963
}
1001964
}
1002-
// Final KV tile (max-1): the only tile that can be partial/OOB (see the
1003-
// mask_oob_kv invariant above) — keep the OOB mask here.
965+
// Final tile (max-1): the only OOB-capable tile; keep the mask (see invariant).
1004966
mask_oob_kv(v_s[1], max_num_tiles - 1);
1005967
s_waitcnt_lgkmcnt(0_I);
1006968
s_waitcnt_vmcnt(0_I);
@@ -1073,8 +1035,7 @@ GQA_D128_DRAIN_ATTR __device__ void gqa_d128_pipelined(opus_gqa_kargs kargs, cha
10731035
attn_mask_tile<T>(v_s[1], q_start_pos + (lane_id % T::W_M), p + 1, neg_inf_v, lane_id);
10741036
}
10751037
}
1076-
// [OOB last-tile-only] non-final tile (p+1) is always full; OOB mask runs only
1077-
// on the final tile (p+2). See mask_oob_kv invariant above.
1038+
// [last-tile-only] non-final tile (p+1) always full; no OOB mask (see invariant).
10781039
s_waitcnt_lgkmcnt(0_I);
10791040
s_waitcnt_vmcnt(0_I);
10801041
__builtin_amdgcn_sched_barrier(0);
@@ -1128,8 +1089,7 @@ GQA_D128_DRAIN_ATTR __device__ void gqa_d128_pipelined(opus_gqa_kargs kargs, cha
11281089
attn_mask_tile<T>(v_s[0], q_start_pos + (lane_id % T::W_M), p + 2, neg_inf_v, lane_id);
11291090
}
11301091
}
1131-
// Final KV tile (p+2): the only tile that can be partial/OOB (see the
1132-
// mask_oob_kv invariant above) — keep the OOB mask here.
1092+
// Final tile (p+2): the only OOB-capable tile; keep the mask (see invariant).
11331093
mask_oob_kv(v_s[0], p + 2);
11341094
s_waitcnt_lgkmcnt(0_I);
11351095
s_waitcnt_vmcnt(0_I);

0 commit comments

Comments
 (0)