Skip to content

Commit f8ae6f6

Browse files
wlejonclaude
andcommitted
cuda: register-stage the plain-matmul WMMA K-loop prefetch (1.1-2.1x)
matmul_rm_wmma_kernel (the C = A(M,K) @ B(K,N) row-major FP16/BF16 GEMM behind the plain matmul() op) had the same single-buffered K-loop as the ABT kernel: each slab's global load stalled between the two __syncthreads with nothing to hide its HBM latency. Apply the identical register-staged prefetch (rm_prefetch_A/B -> rm_deposit_A/B): hoist the next slab's global reads into per-thread int4 registers before the WMMA compute so HBM latency overlaps the tensor cores, then do the fast register->shared deposit at the top of the next iteration. Shared memory stays single-buffered (occupancy unchanged -- doubling the tiles was measured slower on the ABT twin), and the mma_sync order is untouched, so FP32 accumulation is identical. 4090 FP16 matmul() (100 iters): 512^3 0.0166 -> 0.0108 ms (1.53x) 1024^3 0.0368 -> 0.0182 ms (2.02x) 2048^3 0.1353 -> 0.1205 ms (1.12x) 4096^3 1.2062 -> 0.9691 ms (1.24x) 64x4096x4096 0.1059 -> 0.0512 ms (2.07x) <- short-M linear-like 128x4096x4096 0.1060 -> 0.0507 ms (2.09x) 256x14336x4096 0.4001 -> 0.2733 ms (1.46x) Parity green on every consumer: matmul (fp32+fp16), matmul_backward, matmul_parity (CPU<->GPU), cpu_gpu_parity. Adds tests/bench_matmul_rm.cpp (brotensor_bench_matmul_rm, a hand-run microbench, not a ctest) as the measurement harness. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fdb1a0a commit f8ae6f6

3 files changed

Lines changed: 186 additions & 62 deletions

File tree

src/cuda/matmul.cu

Lines changed: 115 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,103 @@ constexpr int RM_FRAGS_N = RM_WN / RM_WMMA_N; // 2
195195
constexpr int RM_LDA_SMEM = RM_BK + 8; // 40, for A tile (BM rows, BK cols)
196196
constexpr int RM_LDB_SMEM = RM_BN + 8; // 72, for B tile (BK rows, BN cols)
197197

198+
// One thread's share of a K-slab, as int4s (8 x 16-bit each).
199+
constexpr int RM_A_SLAB_REGS = (RM_BM * RM_BK / 8) / RM_THREADS_PER_CTA; // 2
200+
constexpr int RM_B_SLAB_REGS = (RM_BK * RM_BN / 8) / RM_THREADS_PER_CTA; // 2
201+
202+
// Register-staged software prefetch — same latency-hiding transform as
203+
// fp16_matmul.cu's ABT kernel. The single-buffered loop stalled on each slab's
204+
// global load between the two __syncthreads; here the next slab's GLOBAL reads
205+
// are issued (into registers) *before* the WMMA compute so HBM latency overlaps
206+
// the tensor-core math, and the fast register->shared deposit happens at the top
207+
// of the next iteration. Shared memory stays single-buffered (occupancy is
208+
// unchanged) and the mma_sync order is identical, so results are bit-for-bit the
209+
// same. A tile is (BM,BK) accessed along K (identical to ABT's A); B tile is
210+
// (BK,BN) accessed along N — the natural (K,N) row-major layout, no transpose.
211+
template <typename T>
212+
__device__ __forceinline__ void rm_prefetch_A(
213+
int4 (&a_reg)[RM_A_SLAB_REGS], const T* __restrict__ A,
214+
int M, int K, int k0, int block_m, int tid) {
215+
using TR = rm_traits<T>;
216+
constexpr int kEPL = 8;
217+
#pragma unroll
218+
for (int li = 0; li < RM_A_SLAB_REGS; ++li) {
219+
const int lin = tid + li * RM_THREADS_PER_CTA;
220+
const int row = lin / (RM_BK / kEPL);
221+
const int gcol = (lin % (RM_BK / kEPL)) * kEPL;
222+
const int grow = block_m + row;
223+
const int gk = k0 + gcol;
224+
T tmp[kEPL];
225+
if (grow < M && gk + kEPL <= K) {
226+
*reinterpret_cast<int4*>(tmp) =
227+
*reinterpret_cast<const int4*>(&A[grow * K + gk]);
228+
} else {
229+
#pragma unroll
230+
for (int q = 0; q < kEPL; ++q) {
231+
const int gk_q = gk + q;
232+
tmp[q] = (grow < M && gk_q < K) ? A[grow * K + gk_q]
233+
: TR::from_f32(0.0f);
234+
}
235+
}
236+
a_reg[li] = *reinterpret_cast<int4*>(tmp);
237+
}
238+
}
239+
240+
template <typename T>
241+
__device__ __forceinline__ void rm_prefetch_B(
242+
int4 (&b_reg)[RM_B_SLAB_REGS], const T* __restrict__ B,
243+
int N, int K, int k0, int block_n, int tid) {
244+
using TR = rm_traits<T>;
245+
constexpr int kEPL = 8;
246+
#pragma unroll
247+
for (int li = 0; li < RM_B_SLAB_REGS; ++li) {
248+
const int lin = tid + li * RM_THREADS_PER_CTA;
249+
const int row = lin / (RM_BN / kEPL); // K-tile row
250+
const int gcol = (lin % (RM_BN / kEPL)) * kEPL; // N-tile col
251+
const int gk = k0 + row;
252+
const int gn = block_n + gcol;
253+
T tmp[kEPL];
254+
if (gk < K && gn + kEPL <= N) {
255+
*reinterpret_cast<int4*>(tmp) =
256+
*reinterpret_cast<const int4*>(&B[size_t(gk) * N + gn]);
257+
} else {
258+
#pragma unroll
259+
for (int q = 0; q < kEPL; ++q) {
260+
const int gn_q = gn + q;
261+
tmp[q] = (gk < K && gn_q < N) ? B[size_t(gk) * N + gn_q]
262+
: TR::from_f32(0.0f);
263+
}
264+
}
265+
b_reg[li] = *reinterpret_cast<int4*>(tmp);
266+
}
267+
}
268+
269+
template <typename T>
270+
__device__ __forceinline__ void rm_deposit_A(
271+
const int4 (&a_reg)[RM_A_SLAB_REGS], T As[RM_BM][RM_LDA_SMEM], int tid) {
272+
constexpr int kEPL = 8;
273+
#pragma unroll
274+
for (int li = 0; li < RM_A_SLAB_REGS; ++li) {
275+
const int lin = tid + li * RM_THREADS_PER_CTA;
276+
const int row = lin / (RM_BK / kEPL);
277+
const int gcol = (lin % (RM_BK / kEPL)) * kEPL;
278+
*reinterpret_cast<int4*>(&As[row][gcol]) = a_reg[li];
279+
}
280+
}
281+
282+
template <typename T>
283+
__device__ __forceinline__ void rm_deposit_B(
284+
const int4 (&b_reg)[RM_B_SLAB_REGS], T Bs[RM_BK][RM_LDB_SMEM], int tid) {
285+
constexpr int kEPL = 8;
286+
#pragma unroll
287+
for (int li = 0; li < RM_B_SLAB_REGS; ++li) {
288+
const int lin = tid + li * RM_THREADS_PER_CTA;
289+
const int row = lin / (RM_BN / kEPL);
290+
const int gcol = (lin % (RM_BN / kEPL)) * kEPL;
291+
*reinterpret_cast<int4*>(&Bs[row][gcol]) = b_reg[li];
292+
}
293+
}
294+
198295
template <typename T>
199296
__launch_bounds__(RM_THREADS_PER_CTA)
200297
__global__ void matmul_rm_wmma_kernel(const T* __restrict__ A,
@@ -223,72 +320,28 @@ __global__ void matmul_rm_wmma_kernel(const T* __restrict__ A,
223320
}
224321
}
225322

226-
for (int k0 = 0; k0 < K; k0 += RM_BK) {
227-
// ---- Load A tile (RM_BM x RM_BK): natural row-major (M,K) access ----
228-
{
229-
constexpr int kElemsPerLoad = 8; // int4 = 8 x 16-bit
230-
constexpr int kTotalElems = RM_BM * RM_BK;
231-
constexpr int kLoadsTotal = kTotalElems / kElemsPerLoad;
232-
constexpr int kLoadsPerThr = kLoadsTotal / RM_THREADS_PER_CTA;
233-
234-
#pragma unroll
235-
for (int li = 0; li < kLoadsPerThr; ++li) {
236-
const int lin = tid + li * RM_THREADS_PER_CTA;
237-
const int row = lin / (RM_BK / kElemsPerLoad);
238-
const int col_grp = lin % (RM_BK / kElemsPerLoad);
239-
const int gcol = col_grp * kElemsPerLoad;
240-
const int grow = block_m + row;
241-
const int gk = k0 + gcol;
242-
243-
T tmp[kElemsPerLoad];
244-
if (grow < M && gk + kElemsPerLoad <= K) {
245-
const int4* src = reinterpret_cast<const int4*>(&A[grow * K + gk]);
246-
*reinterpret_cast<int4*>(tmp) = *src;
247-
} else {
248-
#pragma unroll
249-
for (int q = 0; q < kElemsPerLoad; ++q) {
250-
const int gk_q = gk + q;
251-
tmp[q] = (grow < M && gk_q < K) ? A[grow * K + gk_q] : TR::from_f32(0.0f);
252-
}
253-
}
254-
*reinterpret_cast<int4*>(&As[row][gcol]) = *reinterpret_cast<int4*>(tmp);
255-
}
256-
}
323+
// K loop with register-staged prefetch (see rm_prefetch_* above): the next
324+
// slab's global reads are hoisted ahead of the WMMA math so HBM latency
325+
// overlaps the tensor cores. Shared memory is single-buffered; the mma_sync
326+
// order is unchanged, so FP32 accumulation is bit-for-bit identical.
327+
int4 a_reg[RM_A_SLAB_REGS];
328+
int4 b_reg[RM_B_SLAB_REGS];
329+
rm_prefetch_A<T>(a_reg, A, M, K, 0, block_m, tid);
330+
rm_prefetch_B<T>(b_reg, B, N, K, 0, block_n, tid);
257331

258-
// ---- Load B tile (RM_BK x RM_BN): natural row-major (K,N) access,
259-
// no transpose — this is the key difference from the ABT kernel. ----
260-
{
261-
constexpr int kElemsPerLoad = 8;
262-
constexpr int kTotalElems = RM_BK * RM_BN;
263-
constexpr int kLoadsTotal = kTotalElems / kElemsPerLoad;
264-
constexpr int kLoadsPerThr = kLoadsTotal / RM_THREADS_PER_CTA;
332+
for (int k0 = 0; k0 < K; k0 += RM_BK) {
333+
// Deposit the slab prefetched last iteration, then publish it.
334+
rm_deposit_A<T>(a_reg, As, tid);
335+
rm_deposit_B<T>(b_reg, Bs, tid);
336+
__syncthreads();
265337

266-
#pragma unroll
267-
for (int li = 0; li < kLoadsPerThr; ++li) {
268-
const int lin = tid + li * RM_THREADS_PER_CTA;
269-
const int row = lin / (RM_BN / kElemsPerLoad); // K-tile row
270-
const int col_grp = lin % (RM_BN / kElemsPerLoad);
271-
const int gcol = col_grp * kElemsPerLoad; // N-tile col
272-
const int gk = k0 + row;
273-
const int gn = block_n + gcol;
274-
275-
T tmp[kElemsPerLoad];
276-
if (gk < K && gn + kElemsPerLoad <= N) {
277-
const int4* src = reinterpret_cast<const int4*>(&B[size_t(gk) * N + gn]);
278-
*reinterpret_cast<int4*>(tmp) = *src;
279-
} else {
280-
#pragma unroll
281-
for (int q = 0; q < kElemsPerLoad; ++q) {
282-
const int gn_q = gn + q;
283-
tmp[q] = (gk < K && gn_q < N) ? B[size_t(gk) * N + gn_q] : TR::from_f32(0.0f);
284-
}
285-
}
286-
*reinterpret_cast<int4*>(&Bs[row][gcol]) = *reinterpret_cast<int4*>(tmp);
287-
}
338+
// Issue the next slab's global loads BEFORE compute so they overlap.
339+
const int k_next = k0 + RM_BK;
340+
if (k_next < K) {
341+
rm_prefetch_A<T>(a_reg, A, M, K, k_next, block_m, tid);
342+
rm_prefetch_B<T>(b_reg, B, N, K, k_next, block_n, tid);
288343
}
289344

290-
__syncthreads();
291-
292345
// ---- Compute on shared mem tiles ----
293346
// A frag: row_major from As, leading dim RM_LDA_SMEM, sub-tile at
294347
// (warp_m*WM+i*WMMA_M, kk).

tests/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ if(BROTENSOR_HAS_GPU)
126126
brotensor_add_test(brotensor_test_int8_attention test_int8_attention.cpp)
127127
if(BROTENSOR_WITH_CUDA)
128128
brotensor_add_test(brotensor_test_streams test_streams.cpp)
129+
# Hand-run microbench for the plain FP16 matmul WMMA path (not a ctest).
130+
brotensor_add_executable(brotensor_bench_matmul_rm bench_matmul_rm.cpp)
129131
endif()
130132

131133
# CPU↔GPU parity (GPU-only).

tests/bench_matmul_rm.cpp

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Microbench for the plain FP16 matmul WMMA path (matmul_rm_wmma_kernel):
2+
// C(M,N) = A(M,K) @ B(K,N), row-major, FP16 storage / FP32 accumulate.
3+
// Times a few representative shapes on CUDA. Not a ctest — built as a target
4+
// and run by hand to compare the register-prefetch kernel vs baseline.
5+
6+
#include <brotensor/ops.h>
7+
#include <brotensor/runtime.h>
8+
#include <brotensor/tensor.h>
9+
10+
#include <chrono>
11+
#include <cstdint>
12+
#include <cstdio>
13+
#include <random>
14+
#include <vector>
15+
16+
using brotensor::Device;
17+
using brotensor::Tensor;
18+
19+
static std::vector<uint16_t> to_fp16(const std::vector<float>& v) {
20+
std::vector<uint16_t> o(v.size());
21+
for (size_t i = 0; i < v.size(); ++i)
22+
o[i] = brotensor::fp32_to_fp16_bits(v[i]);
23+
return o;
24+
}
25+
26+
static void bench(int M, int N, int K, int iters) {
27+
std::mt19937 rng(0xC0DEu);
28+
std::uniform_real_distribution<float> dist(-0.5f, 0.5f);
29+
std::vector<float> A(size_t(M) * K), B(size_t(K) * N);
30+
for (auto& v : A) v = dist(rng);
31+
for (auto& v : B) v = dist(rng);
32+
auto Ah = to_fp16(A), Bh = to_fp16(B);
33+
Tensor Ag = Tensor::from_host_fp16_on(Device::CUDA, Ah.data(), M, K);
34+
Tensor Bg = Tensor::from_host_fp16_on(Device::CUDA, Bh.data(), K, N);
35+
36+
Tensor Cg;
37+
brotensor::matmul(Ag, Bg, Cg); // warm-up
38+
brotensor::sync_all();
39+
40+
using clk = std::chrono::steady_clock;
41+
const auto t0 = clk::now();
42+
for (int i = 0; i < iters; ++i) {
43+
Tensor tmp;
44+
brotensor::matmul(Ag, Bg, tmp);
45+
}
46+
brotensor::sync_all();
47+
const double ms =
48+
std::chrono::duration<double, std::milli>(clk::now() - t0).count();
49+
const double gflop = 2.0 * M * N * K * 1e-9;
50+
std::printf(" M=%-6d N=%-6d K=%-6d : %8.4f ms/iter %8.1f GFLOP/s\n",
51+
M, N, K, ms / iters, gflop / (ms / iters * 1e-3));
52+
}
53+
54+
int main() {
55+
brotensor::init();
56+
std::setvbuf(stdout, nullptr, _IONBF, 0);
57+
std::printf("[bench] matmul_rm FP16 WMMA path\n");
58+
const int it = 100;
59+
// Square GEMMs across scales.
60+
bench(512, 512, 512, it);
61+
bench(1024, 1024, 1024, it);
62+
bench(2048, 2048, 2048, it);
63+
bench(4096, 4096, 4096, it);
64+
// Skewed shapes (short M, wide K/N — transformer-linear-like).
65+
bench(64, 4096, 4096, it);
66+
bench(128, 4096, 4096, it);
67+
bench(256, 14336, 4096, it);
68+
return 0;
69+
}

0 commit comments

Comments
 (0)