karpathy--llm.c
382 行
15 KiB
Plaintext
382 行
15 KiB
Plaintext
/*
|
|
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 <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <cuda_runtime.h>
|
|
#include <assert.h>
|
|
#include <cooperative_groups.h>
|
|
#include <cooperative_groups/reduce.h>
|
|
#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<float>{});
|
|
dnorm_norm_mean = cg::reduce(warp, dnorm_norm_mean, cg::plus<float>{});
|
|
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<<<grid_size, block_size>>>(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<<<grid_size, block_size, shared_mem_size>>>(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;
|
|
}
|