Skip to content

Commit ed0db4c

Browse files
kmbandyclaude
andcommitted
mt_pagedattn: fix RDNA4 WMMA operand order in decode + tile prefill
Per RDNA4 ISA §7.12.2, the wmma_f32_16x16x16_f16_w32_gfx12 instruction places the A operand with K-dim in lane (lane%16) and M-dim in slot, but the C/D output with M-dim in lane and N-dim in slot — those orientations are different. The kernel's I-major load + (Q,K) call order computes the correct product at the HW level, but reads the result as if D were also I-major, producing scores and acc that are effectively transposed. This manifested as garbage decode output after 1-2 tokens (the wrong-axis mask and softmax become dominant as K-token count grows) and as subtly-wrong prefill attention that PPL did not detect — the LM head still sampled plausible tokens from systematically-perturbed activations. Fix: swap the operand order at each mma site. With the swap, HW computes the transposed product, which the kernel's existing I-major read decodes as correctly-oriented scores_true and acc_true. No changes required to the load helpers, mask logic, softmax, or shfl_xor reductions. mt_pagedattn_decode.cu: mma(scores, Q_tile, K_tile) → mma(scores, K_tile, Q_tile) mma(acc, scores_h, V_tile) → mma(acc, V_tile, scores_h) mt_pagedattn_tile.cu (basic + prefetch variants, 4 sites total). Validation: - WMMA decode bit-identical to non-WMMA flash-decode (4 prompts, 4B). - WMMA tile prefill bit-identical to --no-kv-tier-paged-blocks mainline attention (4 prompts, 4B). - 35B coherent on simple prompts. - NIAH @ 6854 tokens on Qwen3.6-35B retrieves the secret passphrase. Also adds tests/wmma_rdna4_probe_v3.cu — a standalone HIP probe that encodes A/B/D positions per ISA §7.12.2 and confirms 256/256 element agreement on gfx1201. Useful as a regression check if future kernel work revisits the per-element layout. Debug lane-0 dumps in mt_pagedattn_decode.cu are gated by a constexpr MT_WMMA_DEBUG_DUMP=false so they cost nothing at runtime, but stay available for future investigation. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 8b3fd55 commit ed0db4c

3 files changed

Lines changed: 176 additions & 6 deletions

File tree

ggml/src/ggml-cuda/mt_pagedattn_decode.cu

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,21 @@ __global__ void mt_paged_attention_decode_kernel_wmma(
773773
const int valid_ctx_max = ctx_len_after_q;
774774
const int chunk_start = chunk_idx * CHUNK_KV;
775775

776+
// DEBUG: lane-0 trace for WMMA decode bug ([[wmma-decode-kernel-bug-2026-05-19]]).
777+
// Gate fires once per kernel call (head=0, seq=0, chunk=0, warp=0, lane=0,
778+
// first sub-tile only — see inside the sub-tile loop for the actual dumps).
779+
// Set to true to re-enable while debugging.
780+
constexpr bool MT_WMMA_DEBUG_DUMP = false;
781+
const bool debug_origin =
782+
MT_WMMA_DEBUG_DUMP &&
783+
(kv_head_idx == 0) && (seq_idx == 0) && (chunk_idx == 0) &&
784+
(wid == 0) && (lane == 0);
785+
if (debug_origin) {
786+
printf("[WMMA] >>> step ctx=%d q_len=%d total_q=%d chunk_start=%d chunk_end=%d\n",
787+
ctx_len_after_q, q_len, total_q, chunk_start,
788+
min(chunk_start + CHUNK_KV, valid_ctx_max));
789+
}
790+
776791
auto partial_chunk_base_for_head = [&](int head_idx) -> size_t {
777792
return ((((size_t) head_idx * n_seqs + seq_idx) * num_chunks) + (size_t) chunk_idx)
778793
* (size_t) max_q_len * (size_t) (HEAD_SIZE + 2);
@@ -882,10 +897,32 @@ __global__ void mt_paged_attention_decode_kernel_wmma(
882897
tile<16, 8, half2, DATA_LAYOUT_I_MAJOR> K_tile;
883898
const half2 * src = (const half2 *)(smem_k + n * K_INNER);
884899
load_ldmatrix(K_tile, src, HEAD_SIZE / 2);
885-
mma(scores, Q_tiles[n], K_tile);
900+
// RDNA4 WMMA: A operand has K-in-lane / M-in-slot layout; the
901+
// I-major load + (Q,K) call order computes (Q·K^T)^T, so the
902+
// kernel reads scores transposed. Swapping operands to (K,Q)
903+
// computes K·Q^T, which the kernel's I-major read decodes as
904+
// scores_true[Q-row][K-token]. See probe_v3 for ISA layout.
905+
mma(scores, K_tile, Q_tiles[n]);
886906
}
887907
}
888908

909+
// DEBUG: trace post-Q·K scores. Fires once per kernel call.
910+
// Also dump for lane 16 (owns row=0 cols 8..15) to see the half-warp pair.
911+
const bool debug_here = debug_origin && (sub_start == chunk_start);
912+
const bool debug_lane16 = MT_WMMA_DEBUG_DUMP &&
913+
(kv_head_idx == 0) && (seq_idx == 0) && (chunk_idx == 0) &&
914+
(wid == 0) && (lane == 16) && (sub_start == chunk_start);
915+
if (debug_here) {
916+
printf("[WMMA] scores lane0 [%g %g %g %g %g %g %g %g]\n",
917+
scores.x[0], scores.x[1], scores.x[2], scores.x[3],
918+
scores.x[4], scores.x[5], scores.x[6], scores.x[7]);
919+
}
920+
if (debug_lane16) {
921+
printf("[WMMA] scores lane16 [%g %g %g %g %g %g %g %g]\n",
922+
scores.x[0], scores.x[1], scores.x[2], scores.x[3],
923+
scores.x[4], scores.x[5], scores.x[6], scores.x[7]);
924+
}
925+
889926
tile<16, 8, half2, DATA_LAYOUT_I_MAJOR> scores_h;
890927
if (is_compute_warp) {
891928
// Scale + causal + row/sub_len mask
@@ -946,7 +983,33 @@ __global__ void mt_paged_attention_decode_kernel_wmma(
946983
tile<16, 8, half2, DATA_LAYOUT_I_MAJOR> V_tile;
947984
const half2 * src = (const half2 *)(smem_v + n * K_INNER);
948985
load_ldmatrix_trans(V_tile, src, HEAD_SIZE / 2);
949-
mma(acc[n], scores_h, V_tile);
986+
987+
// DEBUG: dump V_tile + scores_h + acc inputs for n=0 only.
988+
// For lane 0 (owns row=0, cols 0..7) AND lane 16 (row=0, cols 8..15).
989+
if (n == 0 && (debug_here || debug_lane16)) {
990+
const char * tag = debug_here ? "lane0 " : "lane16";
991+
printf("[WMMA] V_tile[0] %s half2[0]=(%g,%g) [1]=(%g,%g) [2]=(%g,%g) [3]=(%g,%g)\n",
992+
tag,
993+
__low2float(V_tile.x[0]), __high2float(V_tile.x[0]),
994+
__low2float(V_tile.x[1]), __high2float(V_tile.x[1]),
995+
__low2float(V_tile.x[2]), __high2float(V_tile.x[2]),
996+
__low2float(V_tile.x[3]), __high2float(V_tile.x[3]));
997+
printf("[WMMA] scores_h %s half2[0]=(%g,%g) [1]=(%g,%g) [2]=(%g,%g) [3]=(%g,%g)\n",
998+
tag,
999+
__low2float(scores_h.x[0]), __high2float(scores_h.x[0]),
1000+
__low2float(scores_h.x[1]), __high2float(scores_h.x[1]),
1001+
__low2float(scores_h.x[2]), __high2float(scores_h.x[2]),
1002+
__low2float(scores_h.x[3]), __high2float(scores_h.x[3]));
1003+
}
1004+
// Same RDNA4 WMMA operand-swap as the Q·K mma above.
1005+
mma(acc[n], V_tile, scores_h);
1006+
if (n == 0 && (debug_here || debug_lane16)) {
1007+
const char * tag = debug_here ? "lane0 " : "lane16";
1008+
printf("[WMMA] acc[0] post %s [%g %g %g %g %g %g %g %g]\n",
1009+
tag,
1010+
acc[0].x[0], acc[0].x[1], acc[0].x[2], acc[0].x[3],
1011+
acc[0].x[4], acc[0].x[5], acc[0].x[6], acc[0].x[7]);
1012+
}
9501013
}
9511014
}
9521015
__syncthreads();

ggml/src/ggml-cuda/mt_pagedattn_tile.cu

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,8 @@ __global__ void mt_paged_attention_tile_kernel(
450450
tile<16, 8, half2, DATA_LAYOUT_I_MAJOR> K_tile;
451451
const half2 * src = (const half2 *)(smem_k + n * K_INNER);
452452
load_ldmatrix(K_tile, src, HEAD_SIZE / 2);
453-
mma(scores, Q_tiles[n], K_tile);
453+
// RDNA4 WMMA operand order — see mt_pagedattn_decode.cu mma swap.
454+
mma(scores, K_tile, Q_tiles[n]);
454455
}
455456

456457
// Apply scale + causal mask. Each thread inspects its 8 elements:
@@ -544,7 +545,8 @@ __global__ void mt_paged_attention_tile_kernel(
544545
// exactly what we need for B in the matmul.
545546
const half2 * src = (const half2 *)(smem_v + n * K_INNER);
546547
load_ldmatrix_trans(V_tile, src, HEAD_SIZE / 2);
547-
mma(acc[n], scores_h, V_tile);
548+
// RDNA4 WMMA operand order — see mt_pagedattn_decode.cu mma swap.
549+
mma(acc[n], V_tile, scores_h);
548550
}
549551

550552
__syncthreads(); // before next iter overwrites smem_k / smem_v
@@ -823,7 +825,8 @@ __global__ void mt_paged_attention_tile_mw_kernel(
823825
(const half2 *)(smem_k + (n + 1) * K_INNER),
824826
HEAD_SIZE / 2);
825827
}
826-
mma(scores_acc[n % N_ACC], Q_tiles[n], K_pp[n & 1]);
828+
// RDNA4 WMMA operand order — see mt_pagedattn_decode.cu mma swap.
829+
mma(scores_acc[n % N_ACC], K_pp[n & 1], Q_tiles[n]);
827830
}
828831

829832
// Merge accumulators for downstream softmax.
@@ -935,7 +938,8 @@ __global__ void mt_paged_attention_tile_mw_kernel(
935938
(const half2 *)(smem_v + (n + 1) * K_INNER),
936939
HEAD_SIZE / 2);
937940
}
938-
mma(acc[n], scores_h, V_pp[n & 1]);
941+
// RDNA4 WMMA operand order — see mt_pagedattn_decode.cu mma swap.
942+
mma(acc[n], V_pp[n & 1], scores_h);
939943
}
940944
}
941945

tests/wmma_rdna4_probe_v3.cu

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// RDNA4 WMMA probe v3 — ground-truth per-element layout via ISA spec.
2+
//
3+
// Per RDNA4 ISA §7.12.2 (Matrix Element Storage in VGPRs), wave32, fp16/fp32:
4+
//
5+
// A (16x16 fp16): lane = {col[2], row[3:0]}, vgpr = {col[3], col[1]}, startPosn = col[0]
6+
// B (16x16 fp16): lane = {row[2], col[3:0]}, vgpr = {row[3], row[1]}, startPosn = row[0]
7+
// D (16x16 fp32): lane = {row[3], col[3:0]}, vgpr = row[2:0], startPosn = 0
8+
//
9+
// Each lane stores 8 fp16 across 4 dwords (halfx8). Slot s in halfx8 ↔ (vgpr=s/2, startPosn=s%2).
10+
//
11+
// Probe: write A[i][j] = i*100+j into halfx8 per-lane using the inverse ISA map.
12+
// Write B as true HW identity (1 where i==j else 0).
13+
// Compute C = A·B = A. Read C via the ISA's D-matrix map and verify.
14+
//
15+
// If output matches per-(i,j) expected, ISA understanding is correct → use as fix foundation.
16+
//
17+
// Build: hipcc --offload-arch=gfx1201 -O2 -o wmma_rdna4_probe_v3 tests/wmma_rdna4_probe_v3.cu
18+
19+
#include <hip/hip_runtime.h>
20+
#include <hip/hip_fp16.h>
21+
#include <cstdio>
22+
23+
using halfx8_t = __attribute__((ext_vector_type(8))) _Float16;
24+
using floatx8_t = __attribute__((ext_vector_type(8))) float;
25+
26+
// For lane L and slot s, return (row, col) of A's element at that position.
27+
__device__ inline void a_pos(int L, int s, int & row, int & col) {
28+
row = L & 0xF;
29+
const int col2 = (L >> 4) & 1; // bit 2 of col
30+
const int col3 = (s >> 2) & 1; // bit 3 of col (slot's high bit pair)
31+
const int col1 = (s >> 1) & 1; // bit 1 of col
32+
const int col0 = s & 1; // bit 0 of col = startPosn
33+
col = (col3 << 3) | (col2 << 2) | (col1 << 1) | col0;
34+
}
35+
36+
// For lane L and slot s, return (row, col) of B's element at that position.
37+
__device__ inline void b_pos(int L, int s, int & row, int & col) {
38+
col = L & 0xF;
39+
const int row2 = (L >> 4) & 1;
40+
const int row3 = (s >> 2) & 1;
41+
const int row1 = (s >> 1) & 1;
42+
const int row0 = s & 1;
43+
row = (row3 << 3) | (row2 << 2) | (row1 << 1) | row0;
44+
}
45+
46+
// For lane L and slot s, return (row, col) of D's element at that position.
47+
__device__ inline void d_pos(int L, int s, int & row, int & col) {
48+
col = L & 0xF;
49+
const int row3 = (L >> 4) & 1;
50+
row = (row3 << 3) | s; // s ∈ [0,8) is row[2:0]
51+
}
52+
53+
__global__ void probe_v3_kernel() {
54+
const int L = threadIdx.x;
55+
56+
halfx8_t a_frag;
57+
halfx8_t b_frag;
58+
floatx8_t c_frag = {0,0,0,0,0,0,0,0};
59+
60+
#pragma unroll
61+
for (int s = 0; s < 8; ++s) {
62+
int ai, aj, bi, bj;
63+
a_pos(L, s, ai, aj);
64+
b_pos(L, s, bi, bj);
65+
a_frag[s] = (_Float16)((float)(ai * 100 + aj)); // A[i][j] = i*100+j
66+
b_frag[s] = (_Float16)((bi == bj) ? 1.0f : 0.0f); // B = HW identity
67+
}
68+
69+
c_frag = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32_gfx12(a_frag, b_frag, c_frag);
70+
71+
// Read D and verify D[i][j] == A[i][j] == i*100+j.
72+
int n_ok = 0, n_bad = 0;
73+
#pragma unroll
74+
for (int s = 0; s < 8; ++s) {
75+
int di, dj;
76+
d_pos(L, s, di, dj);
77+
const int v = (int)c_frag[s];
78+
const int expected = di * 100 + dj;
79+
const bool ok = (v == expected);
80+
if (ok) ++n_ok; else ++n_bad;
81+
printf("LANE %02d SLOT %d D(r=%2d,c=%2d) val=%5d exp=%5d %s\n",
82+
L, s, di, dj, v, expected, ok ? "OK" : "MISMATCH");
83+
}
84+
if (L == 0) {
85+
printf("# (per-lane counts not aggregated; grep \"MISMATCH\" to check.)\n");
86+
}
87+
}
88+
89+
int main() {
90+
hipError_t err = hipSetDevice(0);
91+
if (err != hipSuccess) {
92+
fprintf(stderr, "hipSetDevice failed: %s\n", hipGetErrorString(err));
93+
return 1;
94+
}
95+
hipDeviceProp_t prop;
96+
hipGetDeviceProperties(&prop, 0);
97+
fprintf(stderr, "# device: %s (gcnArch=%s)\n", prop.name, prop.gcnArchName);
98+
99+
hipLaunchKernelGGL(probe_v3_kernel, dim3(1), dim3(32), 0, 0);
100+
err = hipDeviceSynchronize();
101+
if (err != hipSuccess) { fprintf(stderr, "kernel failed: %s\n", hipGetErrorString(err)); return 1; }
102+
return 0;
103+
}

0 commit comments

Comments
 (0)