I have a naive cuda kernel to do half gemm like below
#include <cuda_runtime.h>
#include <cuda_fp16.h>
#define BM 128
#define BN 128
#define BK 32
#define TM 8
#define TN 8
__global__ void threadTilingKernel(const half* A, const half* B, half* C, int M, int N, int K) {
int tid = threadIdx.y * blockDim.x + threadIdx.x; // 0 ~ 255
int load_a_smem_m = tid / BK; // 0, 0, ..., 0, 1, 1, ..., 1, 2, 2, ..., 2, ... , 7, 7, ..., 7
int load_a_smem_k = tid % BK; // 0, 1, ..., 31, 0, 1, ..., 31, 0, 1, ..., 31, ... , 0, 1, ..., 31
int load_b_smem_k = tid / BN; // 0, 0, ..., 0, 1, 1, ..., 1
int load_b_smem_n = tid % BN; // 0, 1, ..., 127, 0, 1, ..., 127
constexpr int NUM_THREAD = BN / TN * BM / TM; // 256
constexpr int NUM_ROW_A_SMEM = NUM_THREAD / BK; // 8
constexpr int NUM_ROW_B_SMEM = NUM_THREAD / BN; // 2
constexpr int NUM_ITER_LOAD_A_SMEM = BM / NUM_ROW_A_SMEM; // 16
constexpr int NUM_ITER_LOAD_B_SMEM = BK / NUM_ROW_B_SMEM; // 16
__shared__ half a_smem[BM][BK];
__shared__ half b_smem[BK][BN];
// int c_reg_int[TM * TN /2] = {0};
// half* c_reg = reinterpret_cast<half*>(c_reg_int[0]);
half c_reg[TM * TN] = {0.0f};
for (int bk = 0; bk < K; bk += BK) {
for (int a_iter = 0; a_iter < NUM_ITER_LOAD_A_SMEM; ++a_iter) {
a_smem[load_a_smem_m + a_iter * NUM_ROW_A_SMEM][load_a_smem_k] = A[(blockIdx.y * BM + load_a_smem_m + a_iter * NUM_ROW_A_SMEM) * K + bk + load_a_smem_k];
}
for (int b_iter = 0; b_iter < NUM_ITER_LOAD_B_SMEM; ++b_iter) {
b_smem[load_b_smem_k + b_iter * NUM_ROW_B_SMEM][load_b_smem_n] = B[(bk + load_b_smem_k + b_iter * NUM_ROW_B_SMEM) * N + blockIdx.x * BN + load_b_smem_n];
}
__syncthreads();
for (int tm = 0; tm < TM; ++tm) {
for (int tn = 0; tn < TN; ++tn) {
for (int k = 0; k < BK; ++k) {
c_reg[tm * TN + tn] += a_smem[threadIdx.y * TM + tm][k] * b_smem[k][threadIdx.x * TN + tn];
}
}
}
__syncthreads();
}
for (int tm = 0; tm < TM; ++tm) {
for (int tn = 0; tn < TN; ++tn) {
C[(blockIdx.y * BM + threadIdx.y * TM + tm) * N + blockIdx.x * BN + threadIdx.x * TN + tn] = c_reg[tm * TN + tn];
}
}
}
int main() {
return 0;
}
I compiled the file on NVidia A10 GPU with nvcc test.cu -Xptxas -warn-spills -res-usage -arch=sm_86 -O3 -o tmp
The result is
ptxas info : 0 bytes gmem
ptxas info : Compiling entry function '_Z18threadTilingKernelPK6__halfS1_PS_iii' for 'sm_86'
ptxas info : Function properties for _Z18threadTilingKernelPK6__halfS1_PS_iii
128 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads
ptxas info : Used 48 registers, 16384 bytes smem, 388 bytes cmem[0]
AFAIK, each cuda kernel can use at most 255 registers, and this kernel only used 48 registers.
I Have two questions here:
-
Why did this kernel use 128 byte stack frame? Since it still has enough registers (255 - 48 = 207) to hold data, I think no extra stack frame (on local memory) is needed here.
-
When I replace
half c_reg[TM * TN] = {0.0f}with allocateint c_reg_int[TM * TN /2] = {0};and cast it to the half ptr, the stack frame usage becomes to 0, I wonder what is the different between the two case.