项目文件夹

文件

356 行
15 KiB
Plaintext

/*
Kernels for RMSNorm backward pass.
Compile example:
nvcc -O3 --use_fast_math -lcublas -lcublasLt rmsnorm_backward.cu -o rmsnorm_backward
./rmsnorm_backward 1
*/
#include <stdio.h>
#include <stdlib.h>
#include <cuda_runtime.h>
#include <assert.h>
#define ENABLE_BF16
#include "common.h"
// ----------------------------------------------------------------------------
// CPU code reference
void rmsnorm_forward_cpu(float* out, float* rstd,
const float* inp, const float* weight,
int B, int T, int C) {
float eps = 1e-5f;
for (int b = 0; b < B; b++) {
for (int t = 0; t < T; t++) {
// seek to the input position inp[b,t,:]
const float* x = inp + b * T * C + t * C;
// calculate the variance (without any bias correction)
float v = 0.0f;
for (int i = 0; i < C; i++) {
float xi = x[i];
v += xi * xi;
}
v = v/C;
// calculate the rstd (reciprocal standard deviation)
float s = 1.0f / sqrtf(v + eps);
// seek to the output position in out[b,t,:]
float* out_bt = out + b * T * C + t * C;
for (int i = 0; i < C; i++) {
float n = (s * x[i]); // normalize
float o = n * weight[i]; // scale and shift
out_bt[i] = o; // write
}
// cache the rstd for the backward pass later
rstd[b * T + t] = s;
}
}
}
void rmsnorm_backward_cpu(float* dinp, float* dweight,
const float* dout, const float* inp, const float* weight, const float* rstd,
int B, int T, int C) {
for (int b = 0; b < B; b++) {
for (int t = 0; t < T; t++) {
const float* dout_bt = dout + b * T * C + t * C;
const float* inp_bt = inp + b * T * C + t * C;
float* dinp_bt = dinp + b * T * C + t * C;
const float rstd_bt = rstd[b * T + t];
// first: the reduce operation
float dnorm_norm_mean = 0.0f;
for (int i = 0; i < C; i++) {
float norm_bti = inp_bt[i] * rstd_bt;
float dnorm_i = weight[i] * dout_bt[i];
dnorm_norm_mean += dnorm_i * norm_bti;
}
dnorm_norm_mean = dnorm_norm_mean / C;
// now iterate again and accumulate all the gradients
for (int i = 0; i < C; i++) {
float norm_bti = inp_bt[i] * rstd_bt;
float dnorm_i = weight[i] * dout_bt[i];
// gradient contribution to weight
dweight[i] += norm_bti * dout_bt[i];
// gradient contribution to input
float dval = 0.0f;
dval += dnorm_i; // term 1
dval -= norm_bti * dnorm_norm_mean; // term 2
dval *= rstd_bt; // final scale
dinp_bt[i] += dval;
}
}
}
}
// ----------------------------------------------------------------------------
// GPU kernel
__global__ void __launch_bounds__(512, 2) // todo - any warnings on Turing with only 1024 threads?
rmsnorm_backward_kernel10(floatX* dinp, floatX* dweight, float* scratch,
const floatX* dout, const floatX* inp, const floatX* weight,
const float* rstd, int B, int T, int C) {
// TODO: this kernel uses too much shared memory due to historical reasons of it coming from layernorm_backward.cu
// this memory use can be reduced by half later
int BLOCK_SIZE = blockDim.x;
int warpsInBlock = BLOCK_SIZE / WARP_SIZE; //number of warps in block
extern __shared__ float shared[];
int warpId = threadIdx.x / WARP_SIZE; // warp index within a block
int baseIdx = blockIdx.x * warpsInBlock + warpId;
int warpThreadIdx = threadIdx.x % WARP_SIZE; // Thread index within the warp
int warpsInGrid = gridDim.x * warpsInBlock;
int C_per_iteration = WARP_SIZE * x128::size;
int iterations_C = ceil_div(C, C_per_iteration); // + 2;
// the first half of shared memory is bias, second is weight
size_t rounded_C = ceil_div(C, (32 * x128::size)) * (32 * x128::size);
float* dweight_shared = shared + rounded_C;
// warp zero doesn't actually write to the _tmp_shared memory locations, so we don't need to reserve memory
// the obvious solution is to change the addressing below to use (threadId.x-32) as offset, but that causes
// register spills, so instead we mess with the base pointer here, which doesn't increase register usage.
float* dweight_tmp_shared = shared + 2 * rounded_C + f128::size * BLOCK_SIZE - 2 * WARP_SIZE * f128::size;
// init shared memory to zero
for(int i = threadIdx.x * f128::size; i < rounded_C; i += BLOCK_SIZE * f128::size) {
store128(dweight_shared + i, f128::zeros());
}
__syncthreads();
for (int bt = baseIdx; bt < B * T; bt += warpsInGrid) {
const floatX* dout_bt = dout + bt * C;
const floatX* inp_bt = inp +bt * C;
floatX* dinp_bt = dinp + bt * C;
// first: two reduce operations
float dnorm_mean = 0.0f;
float dnorm_norm_mean = 0.0f;
for (int i = warpThreadIdx * x128::size; i < C; i += WARP_SIZE * x128::size) {
x128 dout128_i = load128(dout_bt + i);
x128 inp128_i = load128(inp_bt + i);
x128 weight128_i = load128(weight + i);
for (int k = 0; k < x128::size; k++) {
float dnorm_i = (float)weight128_i[k] * (float)dout128_i[k];
dnorm_mean += dnorm_i;
dnorm_norm_mean += dnorm_i * (float)inp128_i[k];
}
}
const float rstd_bt = rstd[bt];
dnorm_norm_mean = warpReduceSum(dnorm_norm_mean) / C * rstd_bt;
for (int c = 0; c < iterations_C; c++) {
int global_index = (warpThreadIdx * x128::size) + (c * C_per_iteration);
x128 dout128 = x128::zeros();
x128 inp128 = x128::zeros();
x128 dinp128 = x128::zeros();
x128 weight128 = x128::zeros();
if(global_index < C) {
dout128 = load128cs(dout_bt + global_index);
inp128 = load128cs(inp_bt + global_index);
dinp128 = load128(dinp_bt + global_index);
weight128 = load128(weight + global_index);
}
for(int o = 0; o < x128::size / f128::size; ++o) {
f128 dweight_f;
for(int i = 0; i < f128::size; ++i) {
int x = o * f128::size + i;
float dout_i = (float)dout128[x];
float norm_bti = ((float)inp128[x]) * rstd_bt;
dweight_f[i] = norm_bti * dout_i;
float dval = 0.0f;
dval += (float) weight128[x] * (float)dout128[x]; // term 1
dval -= norm_bti * dnorm_norm_mean; // term 2
dval *= rstd_bt; // final scale
dinp128[x] = (floatX) ((float) dinp128[x] + dval);
}
if (warpId != 0) {
// this seems to generate a 64-bit store, instead of 128-bit.
// however, forcing 128-bit (e.g., using inline ptx), results in register
// spilling and much worse performance, so we'll keep it like this for now
// but ideally, we could reduce the register pressure a little.
store128(dweight_tmp_shared + threadIdx.x * f128::size, dweight_f);
}
__syncthreads();
if (warpId == 0) {
for (int j = 1; j < warpsInBlock; j++) {
f128 dweight_tmp = load128(dweight_tmp_shared + f128::size * (threadIdx.x + j * WARP_SIZE));
for(int i = 0; i < f128::size; ++i) {
dweight_f[i] += dweight_tmp[i];
}
}
}
__syncthreads();
if (warpId == 0) {
f128 dw_old = load128(dweight_shared + global_index + f128::size * o);
for(int i = 0; i < f128::size; ++i) {
dweight_f[i] += dw_old[i];
}
store128(dweight_shared + global_index + f128::size * o, dweight_f);
}
}
if(global_index < C) {
// cache in L2 as this is read by the next kernel, but bypass L1 to minimise thrashing
store128cg(dinp_bt + global_index, dinp128);
}
}
}
__syncthreads();
// Each block writes its partial sum to global memory
// The last block to finish becomes responsible for summing up all the partial sums
// This is done by atomically incrementing a flag (cleared to 0 before launching the kernel)
unsigned int* scratchFlag = (unsigned int*)(scratch);
// Increment scratch pointer by a full cacheline so that everything remains cacheline aligned
scratch += 32;
float* scratch_dweight = scratch + C;
for(int i = threadIdx.x * f128::size; i < C; i += BLOCK_SIZE * f128::size) {
// Write to global memory in the same "shared memory banking friendly" order
store128(scratch_dweight + i + 2*C*blockIdx.x, load128(dweight_shared + i));
}
__syncthreads();
// that portion of shared memory is no longer used, so we can repurpose it for the scratch flag.
unsigned int *tmp_flag = (unsigned int*)(shared + 2*rounded_C);
if (threadIdx.x == 0) {
*tmp_flag = atomicInc(scratchFlag, gridDim.x);
}
__syncthreads();
if (*tmp_flag == gridDim.x-1) {
// Reduction of the partial sums by the final block
// todo - there isn't enough parallelism even inside that single SM...
// ==> so could maybe split into another kernel with YET ANOTHER level of reduction?!
for(int i = threadIdx.x * f128::size; i < C; i += BLOCK_SIZE * f128::size) {
f128 dweight_accum = f128::zeros();
for (int read_block_idx = 0; read_block_idx < gridDim.x; read_block_idx++) {
int offset = i + 2*C*read_block_idx;
f128 dweight128 = load128(scratch_dweight + offset);
for(int k = 0; k < f128::size; k++) {
dweight_accum[k] += dweight128[k];
}
}
store128(dweight_shared + i, dweight_accum);
}
__syncthreads();
// convert from float/FP32 to floatX/BF16 for the final write
// this is separate because it cannot use as many warps as the above (f128 vs x128)
// todo - if we split this code into another kernel, we could maybe do it at the same time?
for (int c = warpId; c < iterations_C; c += warpsInBlock) {
int global_index = (warpThreadIdx * x128::size) + (c * C_per_iteration);
if (global_index >= C) {
break;
}
x128 dweight128 = load128(dweight + global_index);
for(int o = 0; o < x128::size / f128::size; ++o) {
f128 s_dw = load128(dweight_shared + global_index + o * f128::size);
for(int i = 0; i < f128::size; ++i) {
int x = o * f128::size + i;
dweight128[x] = (floatX)(s_dw[i] + (float)dweight128[x]);
}
}
store128(dweight + global_index, dweight128);
}
}
}
// ----------------------------------------------------------------------------
// Kernel launcher
void rmsnorm_backward(floatX* dinp, floatX* dweight, float* scratch,
const floatX* dout, const floatX* inp, const floatX* weight, const float* rstd,
int B, int T, int C, cudaStream_t stream) {
const int block_size = 512;
const int blocks_per_sm = 2; // supported on every architecture and less cache thrashing than 3
// const int grid_size = blocks_per_sm * deviceProp.multiProcessorCount;
const int grid_size = blocks_per_sm * cuda_num_SMs;
size_t rounded_C = ceil_div(C, (32 * x128::size)) * (32 * x128::size);
size_t shared_mem_size = (2 * rounded_C + 2 * (block_size - 32) * f128::size) * sizeof(float);
cudaCheck(cudaMemsetAsync(scratch, 0, 1 * sizeof(float), stream)); // only need to reset the flag to 0
rmsnorm_backward_kernel10<<<grid_size, block_size, shared_mem_size, stream>>>(dinp, dweight, scratch, dout, inp, weight, rstd, B, T, C);
cudaCheck(cudaGetLastError());
}
// ----------------------------------------------------------------------------
int main(int argc, char **argv) {
setup_main();
int B = 8;
int T = 1024;
int C = 1024;
// first do the forward pass in CPU
float* out = (float*)malloc(B * T * C * sizeof(float));
float* rstd = (float*)malloc(B * T * sizeof(float));
float* inp = make_random_float(B * T * C);
float* weight = make_random_float(C);
rmsnorm_forward_cpu(out, rstd, inp, weight, B, T, C);
// now do the backward pass, again on CPU
float *dout = make_random_float(B * T * C);
float *dinp = make_zeros_float(B * T * C);
float *dweight = make_zeros_float(C);
rmsnorm_backward_cpu(dinp, dweight, dout, inp, weight, rstd, B, T, C);
// the above calculations act as the reference
// now let's do the same on the GPU
// read kernel_num from command line
int kernel_num = 1;
if (argc > 1) {
kernel_num = atoi(argv[1]);
}
printf("Using kernel %d\n", kernel_num);
// move all the variables we need for backward pass onto the GPU
floatX* d_dinp;
floatX* d_dweight;
floatX* d_dout;
floatX* d_inp;
floatX* d_weight;
float* d_rstd;
float* d_scratch;
cudaCheck(cudaMalloc(&d_dinp, B * T * C * sizeof(floatX)));
cudaCheck(cudaMalloc(&d_dweight, C * sizeof(floatX)));
cudaCheck(cudaMalloc(&d_dout, B * T * C * sizeof(floatX)));
cudaCheck(cudaMalloc(&d_inp, B * T * C * sizeof(floatX)));
cudaCheck(cudaMalloc(&d_weight, C * sizeof(floatX)));
cudaCheck(cudaMalloc(&d_rstd, B * T * sizeof(float)));
cudaCheck(cudaMalloc(&d_scratch, (1024/32) * cuda_num_SMs * (2 * C + 1) * sizeof(float)));
// copy over the "inputs" to the backward call
cudaCheck(memcpy_convert(d_dout, dout, B * T * C));
cudaCheck(memcpy_convert(d_inp, inp, B * T * C));
cudaCheck(memcpy_convert(d_weight, weight, C));
cudaCheck(memcpy_convert(d_rstd, rstd, B * T));
// launch the kernel
int block_sizes[] = {32, 64, 128, 256, 512, /*768,*/ 1024};
for (int j = 0; j < sizeof(block_sizes) / sizeof(int); j++) {
int block_size = block_sizes[j];
// init the "outputs" of the backward call to zeros
cudaCheck(cudaMemset(d_dinp, 0, B * T * C * sizeof(floatX)));
cudaCheck(cudaMemset(d_dweight, 0, C * sizeof(floatX)));
rmsnorm_backward(d_dinp, d_dweight, d_scratch, d_dout, d_inp, d_weight, d_rstd, B, T, C, 0);
// check the correctness of the kernel
float error_threshold_dinp = sizeof(floatX) == 4 ? 1e-3f : 1e-1f; // allow larger errors for BF16/FP16
float error_threshold_dparams = sizeof(floatX) == 4 ? 1e-3f : 5e-1f; // much, much larger...
printf("Checking correctness...\n");
printf("dinp:\n");
validate_result(d_dinp, dinp, "dinp", B * T * C, error_threshold_dinp);
printf("dweight:\n");
validate_result(d_dweight, dweight, "dweight", C, error_threshold_dparams);
printf("All results match for block_size=%d.\n\n", block_size);
}
}