77#include < bit>
88#include < cstdint>
99
10- namespace gqa_d128 {
11-
1210using opus::operator " " _I;
1311
1412constexpr 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.
261253template <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.
319307template <class Traits >
320308GQA_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).
525511template <class Traits , bool OddTail>
526512GQA_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