/* Kernels for layernorm backward pass. Compile example: nvcc -O3 --use_fast_math layernorm_backward.cu -o layernorm_backward version 1 is naive port from CPU code to kernel: parallelizes over B,T, loops over C ./layernorm_backward 1 version 2 moves a lot of reduction to shared memory over global memory ./layernorm_backward 2 */ #include #include #include #include #include #include #include "common.h" // ---------------------------------------------------------------------------- // CPU code reference void layernorm_forward_cpu(float* out, float* mean, float* rstd, const float* inp, const float* weight, const float* bias, int B, int T, int C) { // reference: https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html // both inp and out are (B,T,C) of the activations // mean and rstd are (B,T) buffers, to be used later in backward pass // at each position (b,t) of the input, the C-dimensional vector // of activations gets normalized, then scaled and shifted 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 mean float m = 0.0f; for (int i = 0; i < C; i++) { m += x[i]; } m = m/C; // calculate the variance (without any bias correction) float v = 0.0f; for (int i = 0; i < C; i++) { float xshift = x[i] - m; v += xshift * xshift; } 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] - m)); // normalize float o = n * weight[i] + bias[i]; // scale and shift out_bt[i] = o; // write } // cache the mean and rstd for the backward pass later mean[b * T + t] = m; rstd[b * T + t] = s; } } } void layernorm_backward_cpu(float* dinp, float* dweight, float* dbias, const float* dout, const float* inp, const float* weight, const float* mean, 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 mean_bt = mean[b * T + t]; const float rstd_bt = rstd[b * T + t]; // first: two reduce operations float dnorm_mean = 0.0f; float dnorm_norm_mean = 0.0f; for (int i = 0; i < C; i++) { float norm_bti = (inp_bt[i] - mean_bt) * rstd_bt; float dnorm_i = weight[i] * dout_bt[i]; dnorm_mean += dnorm_i; dnorm_norm_mean += dnorm_i * norm_bti; } dnorm_mean = dnorm_mean / C; 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] - mean_bt) * rstd_bt; float dnorm_i = weight[i] * dout_bt[i]; // gradient contribution to bias dbias[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 -= dnorm_mean; // term 2 dval -= norm_bti * dnorm_norm_mean; // term 3 dval *= rstd_bt; // final scale dinp_bt[i] += dval; } } } } // ---------------------------------------------------------------------------- // GPU kernels // super naive kernel that just parallelizes over B,T and loops over C __global__ void layernorm_backward_kernel1(float* dinp, float* dweight, float* dbias, const float* dout, const float* inp, const float* weight, const float* mean, const float* rstd, int B, int T, int C) { int idx = blockIdx.x * blockDim.x + threadIdx.x; if (idx >= B*T) return; int b = idx / T; int t = idx % 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 const float mean_bt = mean[b * T + t]; const float rstd_bt = rstd[b * T + t]; // first: two reduce operations float dnorm_mean = 0.0f; float dnorm_norm_mean = 0.0f; for (int i = 0; i < C; i++) { float norm_bti = (inp_bt[i] - mean_bt) * rstd_bt; float dnorm_i = weight[i] * dout_bt[i]; dnorm_mean += dnorm_i; dnorm_norm_mean += dnorm_i * norm_bti; } dnorm_mean = dnorm_mean / C; 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] - mean_bt) * rstd_bt; float dnorm_i = weight[i] * dout_bt[i]; // gradient contribution to bias atomicAdd(&dbias[i], dout_bt[i]); // gradient contribution to weight atomicAdd(&dweight[i], norm_bti * dout_bt[i]); // gradient contribution to input float dval = 0.0f; dval += dnorm_i; // term 1 dval -= dnorm_mean; // term 2 dval -= norm_bti * dnorm_norm_mean; // term 3 dval *= rstd_bt; // final scale dinp_bt[i] += dval; } } // uses shared memory instead for the reduces __global__ void layernorm_backward_kernel2(float* dinp, float* dweight, float* dbias, const float* dout, const float* inp, const float* weight, const float* mean, const float* rstd, int B, int T, int C) { extern __shared__ float shared[]; // size = 2 * C namespace cg = cooperative_groups; cg::thread_block block = cg::this_thread_block(); cg::thread_block_tile<32> warp = cg::tiled_partition<32>(block); int idx = blockIdx.x * warp.meta_group_size() + warp.meta_group_rank(); int N = B * T; if(idx >= N) { return; } // thread guards int b = idx / T; int t = idx % 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 mean_bt = mean[b * T + t]; const float rstd_bt = rstd[b * T + t]; // the first half of shared memory is bias, second is weight float* dbias_shared = shared; float* dweight_shared = shared + C; // init shared memory to zero #pragma unroll for(int i = threadIdx.x; i < C; i+= blockDim.x){ dbias_shared[i] = 0.0f; dweight_shared[i] = 0.0f; } __syncthreads(); // first: two reduce operations float dnorm_mean = 0.0f; float dnorm_norm_mean = 0.0f; for (int i = warp.thread_rank(); i < C; i += warp.size()) { float norm_bti = (inp_bt[i] - mean_bt) * rstd_bt; float dnorm_i = weight[i] * dout_bt[i]; dnorm_mean += dnorm_i; dnorm_norm_mean += dnorm_i * norm_bti; } dnorm_mean = cg::reduce(warp, dnorm_mean, cg::plus{}); dnorm_norm_mean = cg::reduce(warp, dnorm_norm_mean, cg::plus{}); dnorm_mean = dnorm_mean / C; dnorm_norm_mean = dnorm_norm_mean / C; // now iterate again and accumulate all the gradients for (int i = warp.thread_rank(); i < C; i += warp.size()) { float norm_bti = (inp_bt[i] - mean_bt) * rstd_bt; float dnorm_i = weight[i] * dout_bt[i]; // gradient contribution to bias atomicAdd(&dbias_shared[i], dout_bt[i]); // gradient contribution to weight atomicAdd(&dweight_shared[i], norm_bti * dout_bt[i]); // gradient contribution to input float dval = 0.0f; dval += dnorm_i; // term 1 dval -= dnorm_mean; // term 2 dval -= norm_bti * dnorm_norm_mean; // term 3 dval *= rstd_bt; // final scale dinp_bt[i] += dval; } __syncthreads(); // write to global memory for(int i = threadIdx.x; i < C; i+= blockDim.x){ atomicAdd(&dbias[i], dbias_shared[i]); atomicAdd(&dweight[i], dweight_shared[i]); } } // ---------------------------------------------------------------------------- // kernel launchers void layernorm_backward1(float* dinp, float* dweight, float* dbias, const float* dout, const float* inp, const float* weight, const float* mean, const float* rstd, int B, int T, int C, const int block_size) { const int N = B * T; const int grid_size = ceil_div(N, block_size); layernorm_backward_kernel1<<>>(dinp, dweight, dbias, dout, inp, weight, mean, rstd, B, T, C); } void layernorm_backward2(float* dinp, float* dweight, float* dbias, const float* dout, const float* inp, const float* weight, const float* mean, const float* rstd, int B, int T, int C, const int block_size) { const int N = B * T; const int grid_size = ceil_div(32*N, block_size); size_t shared_mem_size = 2 * C * sizeof(float); layernorm_backward_kernel2<<>>(dinp, dweight, dbias, dout, inp, weight, mean, rstd, B, T, C); } // kernel version dispatch void layernorm_backward(int kernel_num, float* dinp, float* dweight, float* dbias, const float* dout, const float* inp, const float* weight, const float* mean, const float* rstd, int B, int T, int C, const int block_size) { switch (kernel_num) { case 1: layernorm_backward1(dinp, dweight, dbias, dout, inp, weight, mean, rstd, B, T, C, block_size); break; case 2: layernorm_backward2(dinp, dweight, dbias, dout, inp, weight, mean, rstd, B, T, C, block_size); break; default: printf("Invalid kernel number\n"); exit(1); } } // ---------------------------------------------------------------------------- int main(int argc, char **argv) { srand(0); int B = 8; int T = 1024; int C = 768; int deviceIdx = 0; cudaCheck(cudaSetDevice(deviceIdx)); // first do the forward pass in CPU float* out = (float*)malloc(B * T * C * sizeof(float)); float* mean = (float*)malloc(B * T * sizeof(float)); float* rstd = (float*)malloc(B * T * sizeof(float)); float* inp = make_random_float(B * T * C); float* weight = make_random_float(C); float* bias = make_random_float(C); layernorm_forward_cpu(out, mean, rstd, inp, weight, bias, 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); float *dbias = make_zeros_float(C); layernorm_backward_cpu(dinp, dweight, dbias, dout, inp, weight, mean, 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 = 2; 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 float* d_dinp; float* d_dweight; float* d_dbias; float* d_dout; float* d_inp; float* d_weight; float* d_mean; float* d_rstd; cudaCheck(cudaMalloc(&d_dinp, B * T * C * sizeof(float))); cudaCheck(cudaMalloc(&d_dweight, C * sizeof(float))); cudaCheck(cudaMalloc(&d_dbias, C * sizeof(float))); cudaCheck(cudaMalloc(&d_dout, B * T * C * sizeof(float))); cudaCheck(cudaMalloc(&d_inp, B * T * C * sizeof(float))); cudaCheck(cudaMalloc(&d_weight, C * sizeof(float))); cudaCheck(cudaMalloc(&d_mean, B * T * sizeof(float))); cudaCheck(cudaMalloc(&d_rstd, B * T * sizeof(float))); // copy over the "inputs" to the backward call cudaCheck(cudaMemcpy(d_dout, dout, B * T * C * sizeof(float), cudaMemcpyHostToDevice)); cudaCheck(cudaMemcpy(d_inp, inp, B * T * C * sizeof(float), cudaMemcpyHostToDevice)); cudaCheck(cudaMemcpy(d_weight, weight, C * sizeof(float), cudaMemcpyHostToDevice)); cudaCheck(cudaMemcpy(d_mean, mean, B * T * sizeof(float), cudaMemcpyHostToDevice)); cudaCheck(cudaMemcpy(d_rstd, rstd, B * T * sizeof(float), cudaMemcpyHostToDevice)); // init the "outputs" of the backward call to zeros cudaCheck(cudaMemset(d_dinp, 0, B * T * C * sizeof(float))); cudaCheck(cudaMemset(d_dweight, 0, C * sizeof(float))); cudaCheck(cudaMemset(d_dbias, 0, C * sizeof(float))); // launch the kernel const int block_size = 256; layernorm_backward(kernel_num, d_dinp, d_dweight, d_dbias, d_dout, d_inp, d_weight, d_mean, d_rstd, B, T, C, block_size); // check the correctness of the kernel printf("Checking correctness...\n"); printf("dinp:\n"); validate_result(d_dinp, dinp, "dinp", B * T * C, 1e-3f); printf("dweight:\n"); validate_result(d_dweight, dweight, "dweight", C, 1e-3f); printf("dbias:\n"); validate_result(d_dbias, dbias, "dbias", C, 1e-3f); // now time the kernel int block_sizes[] = {32, 64, 128, 256, 512, 1024}; for (int j = 0; j < sizeof(block_sizes) / sizeof(int); j++) { int block_size = block_sizes[j]; int repeat_times = 100; float elapsed_time = benchmark_kernel(repeat_times, layernorm_backward, kernel_num, d_dinp, d_dweight, d_dbias, d_dout, d_inp, d_weight, d_mean, d_rstd, B, T, C, block_size); printf("block_size %4d time %.4f ms\n", block_size, elapsed_time); } // cleanups free(out); free(mean); free(rstd); free(inp); free(weight); free(bias); free(dout); free(dinp); free(dweight); free(dbias); cudaCheck(cudaFree(d_dinp)); cudaCheck(cudaFree(d_dweight)); cudaCheck(cudaFree(d_dbias)); cudaCheck(cudaFree(d_dout)); cudaCheck(cudaFree(d_inp)); cudaCheck(cudaFree(d_weight)); cudaCheck(cudaFree(d_mean)); cudaCheck(cudaFree(d_rstd)); return 0; }