Issue 204254
Summary [AMDGPU] Register allocator places all three v_wmma_f32_16x16x16_f16 operands on the same VGPR bank (gfx11/gfx12)
Labels new issue
Assignees
Reporter mgehre-amd
    ## Summary

On RDNA3 / RDNA3.5 (gfx11, e.g. `gfx1151`) the back-to-back issue latency of
`v_wmma_f32_16x16x16_f16` depends on the VGPR **bank** of its three operands
(bank = first VGPR number `% 4`). When the A (`src0`), B (`src1`) and
C (`src2`/`dst`) operands each start in a *distinct* bank, a dependent WMMA issues
every **32 cycles**. When any two operands share a bank, the instruction pays a
**+2 cycle** penalty (**34 cycles**).

The AMDGPU register allocator currently lays the operands out consecutively, which
on this instruction *always* produces a bank collision, so every WMMA emitted from
the intrinsic runs at 34 cyc instead of the achievable 32 cyc — a ~6% throughput
loss on WMMA-bound kernels that is invisible at the source level and cannot be
fixed without dropping to inline assembly.

## Environment

- Target: `amdgcn-amd-amdhsa`, `--offload-arch=gfx1151` (RDNA3.5; also applies to
  other gfx11 and the gfx12 WMMA encoding).
- Compiler: `clang version 23.0.0git` (ROCm/llvm-project @ 43215c73116c).
- Device measured on: Radeon 8060S (gfx1151).

## Root cause

Each f16 `16x16x16` WMMA operand occupies **8 VGPRs**, and `8 % 4 == 0`. When the
allocator assigns operands to consecutive register ranges (A at `vN`, B at `vN+8`,
C at `vN+16`, …) every operand's starting register is congruent mod 4, so all three
land on the **same** bank. The allocator never inserts the 1-register gap that would
move the operands onto separate banks.

## Reproducer

No rocWMMA required — the builtin alone reproduces it. Compile **device-only to
assembly** and look at the operand registers:

```c++
// wmma_bank.hip
#include <hip/hip_runtime.h>
typedef __fp16 half16 __attribute__((ext_vector_type(16)));   // A / B : 8 VGPRs each
typedef float  float8 __attribute__((ext_vector_type(8)));    // C     : 8 VGPRs

__global__ void k(float* out, const half16* ga, const half16* gb, int n) {
    half16 a = ga[threadIdx.x];   // loaded from distinct buffers so CSE cannot
    half16 b = gb[threadIdx.x];   // merge A and B into one register
    float8 c = {0,0,0,0,0,0,0,0};
    for (int i = 0; i < n; ++i)
        c = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(a, b, c);  // dependent chain
    if (threadIdx.x == 0) out[blockIdx.x] = c[0];
}
```

```sh
clang++ -x hip -O3 --offload-arch=gfx1151 --cuda-device-only -S wmma_bank.hip -o wmma_bank.s
grep v_wmma wmma_bank.s
```

### Actual output

```asm
v_wmma_f32_16x16x16_f16 v[1:8], v[9:16], v[17:24], v[1:8]
```

Operand banks:

| operand        | start reg | bank (`reg % 4`) |
| -------------- | --------- | ---------------- |
| A (`src0`)     | v9        | **1**            |
| B (`src1`)     | v17       | **1**            |
| C (`src2`/dst) | v1        | **1**            |

All three operands are on bank 1 → +2 cycle penalty.

### Expected output

Operands on three distinct banks, e.g. A on bank 0, B on bank 1, C on bank 2
(inserting small gaps such as `v8` / `v17`):

```asm
v_wmma_f32_16x16x16_f16 v[18:25], v[0:7], v[9:16], v[18:25]
;                         C bank2   A bank0  B bank1   C bank2
```

## Measured impact (gfx1151)

A fully dependent chain of 256 back-to-back WMMAs, timed with the
`HW_REG_SHADER_CYCLES` counter, single wave of 32 lanes:

| kernel                                 | cyc/WMMA   |
| -------------------------------------- | ---------- |
| `__builtin_amdgcn_wmma_*` (this issue) | **34.02**  |
| hand-written asm, operands on banks 0/1/2 | **32.00** |

Delta: **+2.0 cyc/WMMA (~6%)**, exactly the bank-collision penalty. The two kernels
are otherwise identical (same chain, same inputs, same accumulator).

## Suggested fix

Teach the AMDGPU register allocator / operand-assignment for the matrix
(`v_wmma_*` / `v_swmma_*`) instructions to prefer assigning `src0`, `src1` and
`src2`/`dst` to distinct VGPR banks (start register `% 4` pairwise distinct),
inserting alignment gaps where free registers allow. Because each operand is a
multiple of 4 VGPRs wide, consecutive allocation is the worst case; even a hint
that biases the three operands onto different `% 4` residues recovers the full
throughput.

## Notes

- A self-contained timing reproducer (intrinsic vs. distinct-bank inline asm,
  measured with `HW_REG_SHADER_CYCLES`) is included at the end of this report.
- The same 8-VGPR-operand / `% 4` reasoning applies to the other gfx11/gfx12 WMMA
  variants whose operands are multiples of 4 registers wide; only the f16→f32
  variant is shown here.

## Full timing reproducer

<details>
<summary><code>wmma_bank_repro.hip</code> — times the intrinsic kernel vs. a distinct-bank inline-asm kernel (click to expand)</summary>

```c++
/**
 * Minimal reproducer: hipcc does not place WMMA operands on distinct VGPR banks.
 *
 * On RDNA3 / RDNA3.5 (gfx11), v_wmma_f32_16x16x16_f16 reads three operands
 * (A = src0, B = src1, C = src2 = dst). The hardware can issue one back-to-back
 * (dependent) WMMA every 32 cycles ONLY when the three operands start in three
 * different VGPR banks (bank = first_vgpr_number % 4). If any two operands share
 * a bank, the instruction pays a +2 cycle penalty (34 cycles per WMMA).
 *
 * Each f16 16x16x16 operand occupies 8 VGPRs, and 8 % 4 == 0, so when the
 * register allocator lays operands out consecutively (A at vN, B at vN+8,
 * C at vN+16, ...) ALL THREE land on the SAME bank. The compiler never inserts
 * the small gap needed to separate them. This reproducer demonstrates that the
 * intrinsic-generated kernel runs at ~34 cyc/WMMA while a hand-written inline-asm
 * kernel with operands on distinct banks runs at the ideal 32 cyc/WMMA.
 *
 * What this program does:
 *   1. Disassembles its own embedded gfx code object and prints the v_wmma ops of
 *      both kernels, annotating the VGPR bank of each operand (manual inspection).
 *   2. Times a dependent chain of 256 WMMAs (back to back) for each kernel using
 *      the HW_REG_SHADER_CYCLES counter and reports cycles-per-WMMA.
 *
 * Build & run:  ./wmma_bank_repro.sh
 * NOT linked against rocWMMA — uses the __builtin_amdgcn_wmma_* intrinsic directly.
 */

#include <hip/hip_runtime.h>
#include <cstdio>
#include <cstdlib>
#include <vector>
#include <algorithm>


typedef __fp16 half16 __attribute__((ext_vector_type(16)));   // WMMA A / B operand: 8 VGPRs
typedef float  float8 __attribute__((ext_vector_type(8)));    // WMMA C operand:     8 VGPRs

#define HIP_CHECK(cmd) do {                                             \
    hipError_t e = (cmd);                                               \
    if (e != hipSuccess) {                                             \
        fprintf(stderr, "HIP error '%s' at %s:%d\n",                   \
                hipGetErrorString(e), __FILE__, __LINE__);            \
        exit(1);                                                       \
    }                                                                  \
} while (0)

#define WMMA_CHAIN 256 // Number of WMMAs back to back


// =============================================================================
// Kernel A: WMMA via the compiler intrinsic (register allocation up to hipcc).
//
// A and B are loaded from distinct global buffers so the compiler cannot merge
// them (CSE would otherwise collapse two identical fills into one register and
// hide the layout). C is a single accumulator reused by every WMMA, so the chain
// is fully dependent and cycles/WMMA == back-to-back issue latency.
// =============================================================================
__global__ __launch_bounds__(32, 8)
void wmma_intrinsic(float* __restrict__ result,
                    const half16* __restrict__ ga,
                    const half16* __restrict__ gb,
                    int iterations,
                    unsigned* __restrict__ cycles) {
    half16 a = ga[threadIdx.x];
    half16 b = gb[threadIdx.x];
    float8 c;
    #pragma unroll
    for (int i = 0; i < 8; i++) c[i] = 0.0f;

    unsigned start, stop;
    // Snapshot the shader cycle counter, then pin the WMMA chain *after* the read:
    // the empty asm makes `a` artificially depend on `start`, so the compiler
    // cannot sink the counter read into the middle of the chain.
    asm volatile("s_getreg_b32 %0, hwreg(HW_REG_SHADER_CYCLES, 0, 32)"
                 : "=s"(start) :: "memory");
    asm volatile("" : "+v"(a) : "s"(start));

    #pragma unroll 1
    for (int it = 0; it < iterations; it++) {
        #pragma unroll
        for (int i = 0; i < WMMA_CHAIN; i++)
            c = __builtin_amdgcn_wmma_f32_16x16x16_f16_w32(a, b, c);
    }

    // Second snapshot. Feeding c[0] in as an input forces the last WMMA to retire
    // before the counter is read.
    asm volatile("s_getreg_b32 %0, hwreg(HW_REG_SHADER_CYCLES, 0, 32)"
                 : "=s"(stop) : "v"(c[0]) : "memory");

    unsigned elapsed = (stop - start) & 0xfffff;   // counter is <32 bits; mask the low 20
    if (threadIdx.x == 0) {
        result[blockIdx.x] = c[0];
        cycles[blockIdx.x] = elapsed;
    }
}

// =============================================================================
// Kernel B: identical dependent WMMA chain, hand-written in inline asm with the
// operands placed on three DISTINCT VGPR banks:
//   A : v[0:7]    bank 0   (0 % 4)
//   B : v[9:16]   bank 1   (9 % 4)   gap at v8
//   C : v[18:25]  bank 2   (18 % 4)  gap at v17 (holds threadIdx.x)
// This is the layout the compiler *should* produce; it runs at 32 cyc/WMMA.
// =============================================================================
__global__ __launch_bounds__(32, 8)
void wmma_asm_bank_distinct(float* __restrict__ result, int iterations,
                            unsigned* __restrict__ cycles) {
    (void)result; (void)iterations; (void)cycles;
    asm volatile(
        // kernarg: result @0x0 (8B), iterations @0x8 (4B), cycles @0x10 (8B)
        "s_load_b32 s3, s[0:1], 0x8\n\t"
        "s_waitcnt lgkmcnt(0)\n\t"
        "s_cmp_lt_i32 s3, 1\n\t"
        "s_cbranch_scc1 2f\n\t"

        "v_mov_b32 v17, v0\n\t"   // save threadIdx.x out of the way (gap between B and C)

        // A = v[0:7] = packed f16 1.0
        "v_mov_b32 v0, 0x3c003c00\n\t" "v_mov_b32 v1, 0x3c003c00\n\t"
        "v_mov_b32 v2, 0x3c003c00\n\t" "v_mov_b32 v3, 0x3c003c00\n\t"
        "v_mov_b32 v4, 0x3c003c00\n\t" "v_mov_b32 v5, 0x3c003c00\n\t"
        "v_mov_b32 v6, 0x3c003c00\n\t" "v_mov_b32 v7, 0x3c003c00\n\t"
        "v_mov_b32 v8, 0\n\t"               // gap (bank 0) / store offset
        // B = v[9:16] = packed f16 1.0
        "v_mov_b32 v9, 0x3c003c00\n\t"  "v_mov_b32 v10, 0x3c003c00\n\t"
        "v_mov_b32 v11, 0x3c003c00\n\t" "v_mov_b32 v12, 0x3c003c00\n\t"
        "v_mov_b32 v13, 0x3c003c00\n\t" "v_mov_b32 v14, 0x3c003c00\n\t"
        "v_mov_b32 v15, 0x3c003c00\n\t" "v_mov_b32 v16, 0x3c003c00\n\t"
        // C = v[18:25] = 0
        "v_mov_b32 v18, 0\n\t" "v_mov_b32 v19, 0\n\t" "v_mov_b32 v20, 0\n\t" "v_mov_b32 v21, 0\n\t"
        "v_mov_b32 v22, 0\n\t" "v_mov_b32 v23, 0\n\t" "v_mov_b32 v24, 0\n\t" "v_mov_b32 v25, 0\n\t"

        "s_getreg_b32 s7, hwreg(HW_REG_SHADER_CYCLES, 0, 32)\n\t"

        "1:\n\t"
        "s_add_i32 s3, s3, -1\n\t"
        "s_cmp_lg_u32 s3, 0\n\t"
        ".rept 256\n\t"
        "v_wmma_f32_16x16x16_f16 v[18:25], v[0:7], v[9:16], v[18:25]\n\t"
        ".endr\n\t"
        "s_cbranch_scc1 1b\n\t"

        "s_getreg_b32 s8, hwreg(HW_REG_SHADER_CYCLES, 0, 32)\n\t"
        "s_sub_u32 s7, s8, s7\n\t"
        "s_and_b32 s7, s7, 0xfffff\n\t"

        "s_load_b64 s[4:5], s[0:1], 0x0\n\t"
        "s_load_b64 s[10:11], s[0:1], 0x10\n\t"
        "s_lshl_b32 s6, s2, 2\n\t"
        "s_waitcnt lgkmcnt(0)\n\t"
        "s_add_u32 s4, s4, s6\n\t"
        "s_addc_u32 s5, s5, 0\n\t"
        "s_add_u32 s10, s10, s6\n\t"
        "s_addc_u32 s11, s11, 0\n\t"
        "s_mov_b32 s6, exec_lo\n\t"
        "v_cmpx_eq_u32_e32 0, v17\n\t"
        "global_store_b32 v8, v18, s[4:5]\n\t"
        "v_mov_b32 v26, s7\n\t"
        "global_store_b32 v8, v26, s[10:11]\n\t"
        "s_mov_b32 exec_lo, s6\n\t"
        "2:\n\t"
        "s_endpgm\n\t"
        :
        :
        : "v0","v1","v2","v3","v4","v5","v6","v7","v8",
          "v9","v10","v11","v12","v13","v14","v15","v16","v17",
          "v18","v19","v20","v21","v22","v23","v24","v25","v26",
          "s3","s4","s5","s6","s7","s8","s10","s11","vcc_lo","exec_lo","memory"
    );
    __builtin_unreachable();
}

// -----------------------------------------------------------------------------
// Host side
// -----------------------------------------------------------------------------

int main() {
    int dev = 0;
    hipDeviceProp_t prop;
    HIP_CHECK(hipGetDeviceProperties(&prop, dev));
    printf("GPU: %s (%s)\n\n", prop.name, prop.gcnArchName);

    // ---- Time the dependent WMMA chains ------------------------------------
    printf("=================================================================\n");
    printf(" Back-to-back dependent WMMA latency (1 wave, 32 threads)\n");
    printf(" chain = %d WMMAs/inner iter; cyc/WMMA = elapsed / (iters * %d)\n",
           WMMA_CHAIN, WMMA_CHAIN);
    printf("=================================================================\n");

    const int iterations = 64;   // 64 * 256 = 16384 WMMAs; counter stays < 2^20
    float*    d_result;
    unsigned* d_cyc;
    half16*   d_a;
    half16*   d_b;
    HIP_CHECK(hipMalloc(&d_result, sizeof(float)));
    HIP_CHECK(hipMalloc(&d_cyc,    sizeof(unsigned)));
    HIP_CHECK(hipMalloc(&d_a, 32 * sizeof(half16)));
    HIP_CHECK(hipMalloc(&d_b, 32 * sizeof(half16)));

    // Fill A/B with f16 1.0 on the host (values are irrelevant to timing).
    std::vector<half16> ha(32), hb(32);
    for (int t = 0; t < 32; t++)
        for (int i = 0; i < 16; i++) { ha[t][i] = (__fp16)1.0f; hb[t][i] = (__fp16)1.0f; }
    HIP_CHECK(hipMemcpy(d_a, ha.data(), 32 * sizeof(half16), hipMemcpyHostToDevice));
    HIP_CHECK(hipMemcpy(d_b, hb.data(), 32 * sizeof(half16), hipMemcpyHostToDevice));

    const double wmmas = (double)iterations * WMMA_CHAIN;

    // Take the minimum over several launches to denoise the ~2-clock counter
    // granularity.
    auto run_min = [&](const char* label, int reps, auto launch) {
        unsigned best = ~0u;
        launch();  // warmup
        HIP_CHECK(hipDeviceSynchronize());
        for (int r = 0; r < reps; r++) {
            launch();
            HIP_CHECK(hipDeviceSynchronize());
            unsigned h = 0;
            HIP_CHECK(hipMemcpy(&h, d_cyc, sizeof(unsigned), hipMemcpyDeviceToHost));
            if (h < best) best = h;
        }
        float h_res = 0.0f;
        HIP_CHECK(hipMemcpy(&h_res, d_result, sizeof(float), hipMemcpyDeviceToHost));
        printf("  %-28s %9u cycles   %6.2f cyc/WMMA   (result=%.2e)\n",
               label, best, best / wmmas, h_res);
        return best / wmmas;
    };

    double cyc_intrin = run_min("intrinsic (compiler regalloc)", 20, [&]{
        wmma_intrinsic<<<1, 32>>>(d_result, d_a, d_b, iterations, d_cyc);
    });
    double cyc_asm = run_min("i<truncated>Please see the issue for the entire body.
_______________________________________________
llvm-bugs mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-bugs

Reply via email to