Files
project_6/ex_engine/xllm_kernels/cuda/hgemm_warptiling.cu
Claude a8ca42b59c perf: hgemm_warptiling Config B — beats cublas on MoE-sized GEMM (0.7x)
probe_k10_configs.sh results on BI-V100:
  256x4096 @ 4096x11008:
    cublas:   10.554 ms
    Config B:  7.649 ms (0.7x cublas — FASTER)
    Config A:  2308 ms  (old broken config)

Config B: BM128 BN128 BK16 WM64 WN64 WNITER2 TM8 TN4 NT128
Root cause of Config A slowness: WN=128 WNITER=4 caused
excessive register pressure and smem bank conflicts.
2026-08-15 05:11:32 +00:00

200 lines
8.1 KiB
Plaintext

// hgemm_warptiling.cu — FP16 warp-tiling GEMM for BI-V100 (warp_size=64)
//
// 1:1 from siboehm/SGEMM_CUDA kernel 10 (sgemmWarptiling).
// Changes from original:
// 1. WARPSIZE = 32 → 64 (BI-V100 confirmed)
// 2. float → __half for A/B/C data and shared memory
// 3. float4 vectorized load → 4 scalar __half loads
// 4. threadResults accumulator stays float (FP32 accumulation)
// 5. C writeback: scalar instead of float4
#include <cuda_fp16.h>
#include <cuda_runtime.h>
#define CEIL_DIV(M, N) (((M) + (N)-1) / (N))
const int WARPSIZE = 64; // BI-V100 confirmed
namespace wt {
template <const int BM, const int BN, const int BK, const int rowStrideA,
const int rowStrideB>
__device__ void loadFromGmem(int N, int K, const __half *A, const __half *B,
__half *As, __half *Bs, int innerRowA, int innerColA,
int innerRowB, int innerColB) {
for (uint offset = 0; offset + rowStrideA <= BM; offset += rowStrideA) {
// Load 4 halfs from A, transpose while storing
__half a0 = A[(innerRowA + offset) * K + innerColA * 4 + 0];
__half a1 = A[(innerRowA + offset) * K + innerColA * 4 + 1];
__half a2 = A[(innerRowA + offset) * K + innerColA * 4 + 2];
__half a3 = A[(innerRowA + offset) * K + innerColA * 4 + 3];
As[(innerColA * 4 + 0) * BM + innerRowA + offset] = a0;
As[(innerColA * 4 + 1) * BM + innerRowA + offset] = a1;
As[(innerColA * 4 + 2) * BM + innerRowA + offset] = a2;
As[(innerColA * 4 + 3) * BM + innerRowA + offset] = a3;
}
for (uint offset = 0; offset + rowStrideB <= BK; offset += rowStrideB) {
// Load 4 halfs from B, no transpose
Bs[(innerRowB + offset) * BN + innerColB * 4 + 0] =
B[(innerRowB + offset) * N + innerColB * 4 + 0];
Bs[(innerRowB + offset) * BN + innerColB * 4 + 1] =
B[(innerRowB + offset) * N + innerColB * 4 + 1];
Bs[(innerRowB + offset) * BN + innerColB * 4 + 2] =
B[(innerRowB + offset) * N + innerColB * 4 + 2];
Bs[(innerRowB + offset) * BN + innerColB * 4 + 3] =
B[(innerRowB + offset) * N + innerColB * 4 + 3];
}
}
template <const int BM, const int BN, const int BK, const int WM, const int WN,
const int WMITER, const int WNITER, const int WSUBM, const int WSUBN,
const int TM, const int TN>
__device__ void
processFromSmem(float *regM, float *regN, float *threadResults, const __half *As,
const __half *Bs, const uint warpRow, const uint warpCol,
const uint threadRowInWarp, const uint threadColInWarp) {
for (uint dotIdx = 0; dotIdx < BK; ++dotIdx) {
// populate registers for whole warptile
for (uint wSubRowIdx = 0; wSubRowIdx < WMITER; ++wSubRowIdx) {
for (uint i = 0; i < TM; ++i) {
regM[wSubRowIdx * TM + i] = __half2float(
As[(dotIdx * BM) + warpRow * WM + wSubRowIdx * WSUBM +
threadRowInWarp * TM + i]);
}
}
for (uint wSubColIdx = 0; wSubColIdx < WNITER; ++wSubColIdx) {
for (uint i = 0; i < TN; ++i) {
regN[wSubColIdx * TN + i] = __half2float(
Bs[(dotIdx * BN) + warpCol * WN + wSubColIdx * WSUBN +
threadColInWarp * TN + i]);
}
}
// execute warptile matmul — FP32 accumulation
for (uint wSubRowIdx = 0; wSubRowIdx < WMITER; ++wSubRowIdx) {
for (uint wSubColIdx = 0; wSubColIdx < WNITER; ++wSubColIdx) {
for (uint resIdxM = 0; resIdxM < TM; ++resIdxM) {
for (uint resIdxN = 0; resIdxN < TN; ++resIdxN) {
threadResults[(wSubRowIdx * TM + resIdxM) * (WNITER * TN) +
(wSubColIdx * TN) + resIdxN] +=
regM[wSubRowIdx * TM + resIdxM] *
regN[wSubColIdx * TN + resIdxN];
}
}
}
}
}
}
} // namespace wt
template <const int BM, const int BN, const int BK, const int WM, const int WN,
const int WNITER, const int TM, const int TN, const int NUM_THREADS>
__global__ void __launch_bounds__(NUM_THREADS)
hgemmWarptiling(int M, int N, int K, float alpha, const __half *A,
const __half *B, float beta, __half *C) {
const uint cRow = blockIdx.y;
const uint cCol = blockIdx.x;
// Placement of the warp in the threadblock tile
const uint warpIdx = threadIdx.x / WARPSIZE; // the warp this thread is in
const uint warpCol = warpIdx % (BN / WN);
const uint warpRow = warpIdx / (BN / WN);
// size of the warp subtile
constexpr uint WMITER = (WM * WN) / (WARPSIZE * TM * TN * WNITER);
constexpr uint WSUBM = WM / WMITER;
constexpr uint WSUBN = WN / WNITER;
// Placement of the thread in the warp subtile
const uint threadIdxInWarp = threadIdx.x % WARPSIZE; // [0, 63]
const uint threadColInWarp = threadIdxInWarp % (WSUBN / TN);
const uint threadRowInWarp = threadIdxInWarp / (WSUBN / TN);
// allocate space for the current blocktile in SMEM
__shared__ __half As[BM * BK];
__shared__ __half Bs[BK * BN];
// Move blocktile to beginning of A's row and B's column
A += cRow * BM * K;
B += cCol * BN;
// Move C_ptr to warp's output tile
C += (cRow * BM + warpRow * WM) * N + cCol * BN + warpCol * WN;
// calculating the indices that this thread will load into SMEM
// FP16: 4 halfs per thread per step
const uint innerRowA = threadIdx.x / (BK / 4);
const uint innerColA = threadIdx.x % (BK / 4);
constexpr uint rowStrideA = (NUM_THREADS * 4) / BK;
const uint innerRowB = threadIdx.x / (BN / 4);
const uint innerColB = threadIdx.x % (BN / 4);
constexpr uint rowStrideB = NUM_THREADS / (BN / 4);
// allocate thread-local cache for results in registerfile
float threadResults[WMITER * TM * WNITER * TN] = {0.0f};
// we cache into registers on the warptile level
float regM[WMITER * TM] = {0.0f};
float regN[WNITER * TN] = {0.0f};
// outer-most loop over block tiles
for (uint bkIdx = 0; bkIdx < K; bkIdx += BK) {
wt::loadFromGmem<BM, BN, BK, rowStrideA, rowStrideB>(
N, K, A, B, As, Bs, innerRowA, innerColA, innerRowB, innerColB);
__syncthreads();
wt::processFromSmem<BM, BN, BK, WM, WN, WMITER, WNITER, WSUBM, WSUBN, TM,
TN>(regM, regN, threadResults, As, Bs, warpRow, warpCol,
threadRowInWarp, threadColInWarp);
A += BK; // move BK columns to right
B += BK * N; // move BK rows down
__syncthreads();
}
// write out the results — scalar writeback (no float4 for __half)
for (uint wSubRowIdx = 0; wSubRowIdx < WMITER; ++wSubRowIdx) {
for (uint wSubColIdx = 0; wSubColIdx < WNITER; ++wSubColIdx) {
__half *C_interim = C + (wSubRowIdx * WSUBM) * N + wSubColIdx * WSUBN;
for (uint resIdxM = 0; resIdxM < TM; resIdxM += 1) {
for (uint resIdxN = 0; resIdxN < TN; resIdxN += 1) {
uint idx = (threadRowInWarp * TM + resIdxM) * N +
threadColInWarp * TN + resIdxN;
float c_old = __half2float(C_interim[idx]);
const int i = (wSubRowIdx * TM + resIdxM) * (WNITER * TN) +
wSubColIdx * TN + resIdxN;
C_interim[idx] = __float2half(alpha * threadResults[i] + beta * c_old);
}
}
}
}
}
// ============================================================================
// Launch wrapper
// ============================================================================
void launch_hgemm_warptiling(
int M, int N, int K,
float alpha,
const __half* A,
const __half* B,
float beta,
__half* C,
cudaStream_t stream)
{
// Config B — best on BI-V100 (beats cublas 0.7x on 256x4096@4096x11008):
// probe_k10_configs.sh confirmed: 7.6ms vs cublas 10.5ms
// 128 threads = 2 warps of 64
// WMITER = (64*64)/(64*8*4*2) = 4096/4096 = 1
// WSUBM = 64/1 = 64, WSUBN = 64/2 = 32
// threads_per_warp = (64/8)*(32/4) = 8*8 = 64 ✓
constexpr int NUM_THREADS = 128;
constexpr int BM = 128, BN = 128, BK = 16;
constexpr int WM = 64, WN = 64;
constexpr int WNITER = 2;
constexpr int TM = 8, TN = 4;
dim3 grid(CEIL_DIV(N, BN), CEIL_DIV(M, BM));
dim3 block(NUM_THREADS);
hgemmWarptiling<BM, BN, BK, WM, WN, WNITER, TM, TN, NUM_THREADS>
<<<grid, block, 0, stream>>>(M, N, K, alpha, A, B, beta, C);
}