karpathy--llm.c
1274 行
49 KiB
Plaintext
1274 行
49 KiB
Plaintext
/*
|
|
GPT-2 Transformer Neural Net trained in raw CUDA
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <math.h>
|
|
#include <time.h>
|
|
#include <assert.h>
|
|
#include <float.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <assert.h>
|
|
#include <cublas_v2.h>
|
|
#include <cuda_runtime.h>
|
|
#include <cublasLt.h>
|
|
#include <cooperative_groups.h>
|
|
#include <cooperative_groups/reduce.h>
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// CUDA utils
|
|
|
|
// convenience macro for calculating grid/block dimensions for kernels
|
|
#define CEIL_DIV(M, N) (((M) + (N)-1) / (N))
|
|
|
|
// CUDA error checking
|
|
void cudaCheck(cudaError_t error, const char *file, int line) {
|
|
if (error != cudaSuccess) {
|
|
printf("[CUDA ERROR] at file %s:%d:\n%s\n", file, line,
|
|
cudaGetErrorString(error));
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
};
|
|
#define cudaCheck(err) (cudaCheck(err, __FILE__, __LINE__))
|
|
|
|
// cuBLAS error checking
|
|
void cublasCheck(cublasStatus_t status, const char *file, int line)
|
|
{
|
|
if (status != CUBLAS_STATUS_SUCCESS) {
|
|
printf("[cuBLAS ERROR]: %d %s %d\n", status, file, line);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
#define cublasCheck(status) { cublasCheck((status), __FILE__, __LINE__); }
|
|
|
|
// cuBLAS workspace. Hardcoding to 32MiB but only Hopper needs 32, for others 4 is OK
|
|
static size_t cublaslt_workspace_size = 32 * 1024 * 1024;
|
|
static void* cublaslt_workspace = NULL;
|
|
static cublasComputeType_t cublas_compute_type;
|
|
cublasHandle_t cublas_handle;
|
|
cublasLtHandle_t cublaslt_handle;
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// all the kernels
|
|
|
|
// warp-level reduction for finding the maximum value
|
|
__device__ float warpReduceMax(float val) {
|
|
for (int offset = 16; offset > 0; offset /= 2) {
|
|
val = fmaxf(val, __shfl_down_sync(0xFFFFFFFF, val, offset));
|
|
}
|
|
return val;
|
|
}
|
|
|
|
// warp-level reduction for summing values
|
|
__device__ float warpReduceSum(float val) {
|
|
for (int offset = 16; offset > 0; offset /= 2) {
|
|
val += __shfl_down_sync(0xFFFFFFFF, val, offset);
|
|
}
|
|
return val;
|
|
}
|
|
|
|
__global__ void encoder_forward_kernel2(float* out,
|
|
int* inp, float* wte, float* wpe,
|
|
int B, int T, int C) {
|
|
int idx = blockIdx.x * blockDim.x + threadIdx.x;
|
|
int N = B * T * C;
|
|
|
|
if (idx < N) {
|
|
int bt = idx / C;
|
|
int b = bt / T;
|
|
int t = bt % T;
|
|
int c = idx % C;
|
|
|
|
int ix = inp[b * T + t];
|
|
|
|
float* out_btc = out + b * T * C + t * C + c;
|
|
float* wte_ix = wte + ix * C + c;
|
|
float* wpe_tc = wpe + t * C + c;
|
|
*out_btc = *wte_ix + *wpe_tc;
|
|
}
|
|
}
|
|
|
|
|
|
__global__ void layernorm_forward_kernel3(float* __restrict__ out, float* __restrict__ mean, float* __restrict__ rstd,
|
|
const float* __restrict__ inp, const float* __restrict__ weight,
|
|
const float* __restrict__ bias, int N, int 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();
|
|
if(idx >= N) {
|
|
return;
|
|
}
|
|
|
|
// the row of input that this group of threads is responsible for
|
|
const float* x = inp + idx * C;
|
|
|
|
// mean
|
|
float sum = 0.0f;
|
|
for (int i = warp.thread_rank(); i < C; i += warp.size()) {
|
|
sum += x[i];
|
|
}
|
|
sum = cg::reduce(warp, sum, cg::plus<float>{});
|
|
float m = sum / C;
|
|
if(warp.thread_rank() == 0 && mean != nullptr) {
|
|
__stcs(mean + idx, m);
|
|
}
|
|
|
|
// rstd
|
|
sum = 0.0f;
|
|
for (int i = warp.thread_rank(); i < C; i += warp.size()) {
|
|
float diff = x[i] - m;
|
|
sum += diff * diff;
|
|
}
|
|
sum = cg::reduce(warp, sum, cg::plus<float>{});
|
|
float s = rsqrtf(sum / C + 1e-5f);
|
|
if(warp.thread_rank() == 0 && rstd != nullptr) {
|
|
__stcs(rstd + idx, s);
|
|
}
|
|
|
|
// final normalization and scaling by weight/bias
|
|
float* o = out + idx * C;
|
|
for (int c = warp.thread_rank(); c < C; c += warp.size()) {
|
|
// load and store using the .cs "streaming" hint to the compiler,
|
|
// indicating that this data will not be reused soon, and can be streamed through the caches
|
|
// this allows the threads to get more cache-hits for the (shared) weight and bias parameters
|
|
float n = s * (__ldcs(x+c) - m);
|
|
__stcs(o+c, n * weight[c] + bias[c]);
|
|
}
|
|
}
|
|
|
|
__global__ void add_bias(float* out, float* bias, int B, int T, int OC) {
|
|
int idx = blockIdx.x * blockDim.x + threadIdx.x;
|
|
int stride = blockDim.x * gridDim.x;
|
|
for (int i = idx; i < B*T*OC; i += stride) {
|
|
int col = i % OC;
|
|
out[i] += bias[col];
|
|
}
|
|
}
|
|
|
|
|
|
__global__ void permute_kernel(float* q, float* k, float* v,
|
|
const float* inp,
|
|
int B, int N, int NH, int d) {
|
|
// okay so now, this kernel wants Q,K,V to all be of shape (B, NH, N, d)
|
|
// but instead, we have a single tensor QKV (inp) of shape (B, N, 3, NH, d)
|
|
int idx = blockIdx.x * blockDim.x + threadIdx.x;
|
|
|
|
// Q[b][nh_][n][d_] = inp[b][n][0][nh_][d_]
|
|
|
|
if (idx < B * NH * N * d) {
|
|
int b = idx / (NH * N * d);
|
|
int rest = idx % (NH * N * d);
|
|
int nh_ = rest / (N * d);
|
|
rest = rest % (N * d);
|
|
int n = rest / d;
|
|
int d_ = rest % d;
|
|
|
|
int inp_idx = \
|
|
(b * N * 3 * NH * d)
|
|
+ (n * 3 * NH * d)
|
|
+ (0 * NH * d)
|
|
+ (nh_ * d)
|
|
+ d_;
|
|
|
|
q[idx] = __ldcs(&inp[inp_idx]);
|
|
k[idx] = __ldcs(&inp[inp_idx + NH * d]);
|
|
v[idx] = __ldcs(&inp[inp_idx + 2 * (NH * d)]);
|
|
}
|
|
}
|
|
|
|
__global__ void unpermute_kernel(float* inp, float *out, int B, int N, int NH, int d) {
|
|
// out has shape (B, nh, N, d) but we need to unpermute it to (B, N, nh, d)
|
|
int idx = blockIdx.x * blockDim.x + threadIdx.x;
|
|
|
|
// out[b][n][nh_][d_] <- inp[b][nh_][n][d_]
|
|
if (idx < B * NH * N * d) {
|
|
int b = idx / (NH * N * d);
|
|
int rest = idx % (NH * N * d);
|
|
int nh_ = rest / (N * d);
|
|
rest = rest % (N * d);
|
|
int n = rest / d;
|
|
int d_ = rest % d;
|
|
|
|
int other_idx = (b * NH * N * d) + (n * NH * d) + (nh_ * d) + d_;
|
|
out[other_idx] = __ldcs(&inp[idx]);
|
|
}
|
|
}
|
|
|
|
__device__ float& vec_at(float4& vec, int index) {
|
|
return reinterpret_cast<float*>(&vec)[index];
|
|
}
|
|
|
|
__device__ float vec_at(const float4& vec, int index) {
|
|
return reinterpret_cast<const float*>(&vec)[index];
|
|
}
|
|
|
|
__global__ void softmax_forward_kernel5(float* out, float inv_temperature, const float* inp, int N, int T) {
|
|
// inp, out shape: (N, T, T), where N = B * NH
|
|
// fuses the multiplication by scale inside attention
|
|
// directly autoregressive, so we only compute the lower triangular part
|
|
// uses the online softmax algorithm
|
|
assert(T % 4 == 0);
|
|
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();
|
|
if(idx >= N * T) {
|
|
return;
|
|
}
|
|
int own_pos = idx % T;
|
|
int pos_by_4 = own_pos / 4;
|
|
|
|
// one row of inp, i.e. inp[idx, :] of shape (T,)
|
|
const float* x = inp + idx * T;
|
|
|
|
// not INF, so we don't get NaNs accidentally when subtracting two values.
|
|
float maxval = -FLT_MAX;
|
|
float sumval = 0.0f;
|
|
|
|
const float4* x_vec = reinterpret_cast<const float4*>(x);
|
|
for (int i = warp.thread_rank(); i < pos_by_4; i += warp.size()) {
|
|
float4 v = x_vec[i];
|
|
float old_maxval = maxval;
|
|
for(int k = 0; k < 4; ++k) {
|
|
maxval = fmaxf(maxval, vec_at(v, k));
|
|
}
|
|
sumval *= expf(inv_temperature * (old_maxval - maxval));
|
|
for(int k = 0; k < 4; ++k) {
|
|
sumval += expf(inv_temperature * (vec_at(v, k) - maxval));
|
|
}
|
|
}
|
|
|
|
if(4*pos_by_4 + warp.thread_rank() <= own_pos) {
|
|
float old_maxval = maxval;
|
|
maxval = fmaxf(maxval, x[4*pos_by_4 + warp.thread_rank()]);
|
|
sumval *= expf(inv_temperature * (old_maxval - maxval));
|
|
sumval += expf(inv_temperature * (x[4*pos_by_4 + warp.thread_rank()] - maxval));
|
|
}
|
|
|
|
float global_maxval = cg::reduce(warp, maxval, cg::greater<float>{});
|
|
sumval *= expf(inv_temperature * (maxval - global_maxval));
|
|
|
|
float sum = cg::reduce(warp, sumval, cg::plus<float>{});
|
|
float norm = 1.f / sum;
|
|
|
|
// divide the whole row by the sum
|
|
for (int i = warp.thread_rank(); i <= own_pos; i += warp.size()) {
|
|
// recalculation is faster than doing the round-trip through memory.
|
|
float ev = expf(inv_temperature * (__ldcs(x + i) - global_maxval));
|
|
__stcs(out + idx * T + i, ev * norm);
|
|
}
|
|
}
|
|
|
|
__global__ void residual_forward_kernel(float* out, float* inp1, float* inp2, int N) {
|
|
int idx = blockIdx.x * blockDim.x + threadIdx.x;
|
|
if (idx < N) {
|
|
out[idx] = __ldcs(&inp1[idx]) + __ldcs(&inp2[idx]);
|
|
}
|
|
}
|
|
|
|
#define GELU_SCALING_FACTOR sqrtf(2.0f / M_PI)
|
|
__global__ void gelu_kernel(float* out, const float* inp, int N) {
|
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
|
if (i < N) {
|
|
float xi = inp[i];
|
|
float cube = 0.044715f * xi * xi * xi;
|
|
out[i] = 0.5f * xi * (1.0f + tanhf(GELU_SCALING_FACTOR * (xi + cube)));
|
|
}
|
|
}
|
|
|
|
__global__ void crossentropy_forward_kernel1(float* losses,
|
|
float* probs, int* targets,
|
|
int B, int T, int V) {
|
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
|
if (i < B * T) {
|
|
int b = i / T;
|
|
int t = i % T;
|
|
float* probs_bt = probs + b * T * V + t * V;
|
|
int ix = targets[b * T + t];
|
|
losses[b * T + t] = -logf(probs_bt[ix]);
|
|
}
|
|
}
|
|
|
|
__global__ void softmax_forward_kernel7(float* out, const float* inp, int N, int C) {
|
|
// out is (N, C) just like inp. Each row of inp will get softmaxed.
|
|
// same as kernel4, but optimised for very large Cs with advanced unrolling
|
|
|
|
// The trick is to read into a register array (all indices known at compile time)
|
|
// and always read UNROLL_FACTOR values to maximise memory level parallelism
|
|
// even if we would be out of bounds, we set the index to min(C-1, idx)
|
|
// so we just do some unnecessary reads (obviously bad for small C)
|
|
// the writes are in a separate loop with a conditional check for out of bounds
|
|
// making it separate is necessary to convince the compiler to do the right thing
|
|
const int UNROLL_FACTOR = 8;
|
|
const int warpsPerBlock = blockDim.x / 32;
|
|
|
|
extern __shared__ float shared[];
|
|
int idx = blockIdx.x;
|
|
int tid = threadIdx.x;
|
|
int warpId = threadIdx.x / 32; // warp index within a block
|
|
int laneId = threadIdx.x % 32; // thread index within a warp
|
|
|
|
// shared[] must be allocated to have 2 * warpsPerBlock elements
|
|
// first half for max values, the second half for sum values
|
|
float* maxvals = shared;
|
|
float* sumvals = &shared[warpsPerBlock];
|
|
|
|
if (tid >= C) {
|
|
maxvals[warpId] = -INFINITY;
|
|
sumvals[warpId] = 0.0f;
|
|
return;
|
|
}
|
|
|
|
const float* x = inp + idx * C; // input
|
|
float* y = out + idx * C; // output
|
|
|
|
// first, thread coarsening by directly accessing global memory in series
|
|
float maxval = -INFINITY;
|
|
for (int i = tid; i < C; i += blockDim.x * UNROLL_FACTOR) {
|
|
#pragma unroll
|
|
for (int u = 0; u < UNROLL_FACTOR; u++) {
|
|
maxval = fmaxf(maxval, x[min(C - 1, i + u*blockDim.x)]);
|
|
}
|
|
}
|
|
|
|
// now within-warp reductions for maxval
|
|
maxval = warpReduceMax(maxval);
|
|
// the 0th thread of each warp writes the maxval of that warp to shared memory
|
|
if (laneId == 0) maxvals[warpId] = maxval;
|
|
__syncthreads();
|
|
// now the 0th thread reduces the maxvals in shared memory, i.e. across warps
|
|
if (tid == 0) {
|
|
float val = maxvals[tid];
|
|
#pragma unroll
|
|
for (int i = 1; i < warpsPerBlock; i++) {
|
|
val = fmaxf(val, maxvals[i]);
|
|
}
|
|
// store the final max in the first position
|
|
maxvals[0] = val;
|
|
}
|
|
__syncthreads();
|
|
// broadcast the max to all threads
|
|
float offset = maxvals[0];
|
|
|
|
// compute expf and write the result to global memory
|
|
// + thread coarsening for sum
|
|
float sumval = 0.0f;
|
|
for (int i = tid; i < C; i += blockDim.x * UNROLL_FACTOR) {
|
|
float reg_array[UNROLL_FACTOR];
|
|
#pragma unroll
|
|
for (int u = 0; u < UNROLL_FACTOR; u++) {
|
|
reg_array[u] = __ldcs(&x[min(C - 1, i + u*blockDim.x)]);
|
|
}
|
|
#pragma unroll
|
|
for (int u = 0; u < UNROLL_FACTOR; u++) {
|
|
if (i + u*blockDim.x < C) {
|
|
float output = expf(reg_array[u] - offset);
|
|
y[min(C - 1, i + u*blockDim.x)] = output; // compiler likes redundant min()?!
|
|
sumval += output; // combined into the same loop unlike kernel3
|
|
}
|
|
}
|
|
}
|
|
|
|
// okay now we calculated exp(x - max(x))
|
|
// step 2: sum all the values and divide by the sum
|
|
|
|
// within-warp reduction for sumval
|
|
sumval = warpReduceSum(sumval);
|
|
// write sumval to shared memory
|
|
if (laneId == 0) sumvals[warpId] = sumval;
|
|
__syncthreads();
|
|
// inter-thread reduction of sum
|
|
if (tid == 0) {
|
|
float val = sumvals[tid];
|
|
#pragma unroll
|
|
for (int i = 1; i < warpsPerBlock; ++i) {
|
|
val += sumvals[i];
|
|
}
|
|
sumvals[0] = val;
|
|
}
|
|
__syncthreads();
|
|
// broadcast the sum to all threads
|
|
float sum = sumvals[0];
|
|
|
|
// divide the whole row by the sum
|
|
for (int i = tid; i < C; i += blockDim.x * UNROLL_FACTOR) {
|
|
float reg_array[UNROLL_FACTOR];
|
|
#pragma unroll
|
|
for (int u = 0; u < UNROLL_FACTOR; u++) {
|
|
reg_array[u] = y[min(C - 1, i + u*blockDim.x)];
|
|
}
|
|
#pragma unroll
|
|
for (int u = 0; u < UNROLL_FACTOR; u++) {
|
|
if (i + u*blockDim.x < C) {
|
|
y[i + u*blockDim.x] = reg_array[u] / sum;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// kernel launchers
|
|
|
|
void encoder_forward(float* out,
|
|
int* inp, float* wte, float* wpe,
|
|
int B, int T, int C) {
|
|
const int N = B * T * C;
|
|
const int block_size = 256;
|
|
const int grid_size = CEIL_DIV(N, block_size);
|
|
encoder_forward_kernel2<<<grid_size, block_size>>>(out, inp, wte, wpe, B, T, C);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
void layernorm_forward(float* out, float* mean, float* rstd,
|
|
float* inp, float* weight, float* bias,
|
|
int B, int T, int C) {
|
|
const int block_size = 1024;
|
|
const int N = B * T;
|
|
const int grid_size = CEIL_DIV(N * 32, block_size);
|
|
layernorm_forward_kernel3<<<grid_size, block_size>>>(out, mean, rstd, inp, weight, bias, N, C);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
// uses cuBLAS
|
|
void matmul_forward_cublas(float* out,
|
|
float* inp, float* weight, float* bias,
|
|
int B, int T, int C, int OC) {
|
|
const int sqrt_block_size = 32;
|
|
const float alpha = 1.0f;
|
|
const float beta = 0.0f;
|
|
cublasCheck(cublasSgemm(cublas_handle, CUBLAS_OP_T, CUBLAS_OP_N, OC, B*T, C, &alpha, weight, C, inp, C, &beta, out, OC));
|
|
|
|
// and now we still have to add the bias... (ew)
|
|
if (bias != NULL) {
|
|
int block_size = sqrt_block_size * sqrt_block_size;
|
|
int grid_size = CEIL_DIV(OC * B * T, block_size);
|
|
add_bias<<<grid_size, block_size>>>(out, bias, B, T, OC);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
}
|
|
|
|
// uses cuBLASLt to fuse the bias and gelu. does not work with OC = 50257 (last layer)
|
|
// https://docs.nvidia.com/cuda/cublas/#cublasltmatmul
|
|
// https://github.com/NVIDIA/CUDALibrarySamples/blob/master/cuBLASLt/LtSgemm/sample_cublasLt_LtSgemm.cu
|
|
void matmul_forward_cublaslt(float* out,
|
|
float* inp, float* weight, float* bias,
|
|
int B, int T, int C, int OC) {
|
|
int has_bias = (bias != NULL);
|
|
|
|
// check bias alignment
|
|
if(((uintptr_t)bias % 16) != 0) {
|
|
printf("Bias pointer is not aligned (cuBLASLt requirement)!\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
int returnedResults = 0;
|
|
cublasLtMatmulDesc_t operationDesc;
|
|
cublasLtMatmulPreference_t preference;
|
|
cublasLtMatrixLayout_t weightLayout;
|
|
cublasLtMatrixLayout_t inputLayout;
|
|
cublasLtMatrixLayout_t outputLayout;
|
|
cublasLtMatrixLayout_t biasLayout;
|
|
cublasLtMatmulHeuristicResult_t heuristic;
|
|
|
|
// create the operation descriptor
|
|
cublasOperation_t opNoTranspose = CUBLAS_OP_N;
|
|
cublasOperation_t opTranspose = CUBLAS_OP_T;
|
|
cublasLtEpilogue_t epilogueBias = CUBLASLT_EPILOGUE_BIAS;
|
|
cublasCheck(cublasLtMatmulDescCreate(&operationDesc, cublas_compute_type, CUDA_R_32F));
|
|
cublasCheck(cublasLtMatmulDescSetAttribute(operationDesc, CUBLASLT_MATMUL_DESC_TRANSA, &opTranspose, sizeof(opTranspose)));
|
|
cublasCheck(cublasLtMatmulDescSetAttribute(operationDesc, CUBLASLT_MATMUL_DESC_TRANSB, &opNoTranspose, sizeof(opNoTranspose)));
|
|
cublasCheck(cublasLtMatmulDescSetAttribute(operationDesc, CUBLASLT_MATMUL_DESC_EPILOGUE, &epilogueBias, sizeof(epilogueBias)));
|
|
cublasCheck(cublasLtMatmulDescSetAttribute(operationDesc, CUBLASLT_MATMUL_DESC_BIAS_POINTER, &bias, sizeof(bias)));
|
|
|
|
// define matrix layouts
|
|
cublasCheck(cublasLtMatrixLayoutCreate(&weightLayout, CUDA_R_32F, C, OC, C));
|
|
cublasCheck(cublasLtMatrixLayoutCreate(&inputLayout, CUDA_R_32F, C, B*T, C));
|
|
cublasCheck(cublasLtMatrixLayoutCreate(&outputLayout, CUDA_R_32F, OC, B*T, OC));
|
|
cublasCheck(cublasLtMatrixLayoutCreate(&biasLayout, CUDA_R_32F, OC, 1, OC));
|
|
|
|
// create a preference handle with specified max workspace
|
|
cublasCheck(cublasLtMatmulPreferenceCreate(&preference));
|
|
cublasCheck(cublasLtMatmulPreferenceSetAttribute(preference,
|
|
CUBLASLT_MATMUL_PREF_MAX_WORKSPACE_BYTES,
|
|
&cublaslt_workspace_size, sizeof(cublaslt_workspace_size)));
|
|
|
|
// find a suitable algorithm
|
|
cublasCheck(cublasLtMatmulAlgoGetHeuristic(cublaslt_handle, operationDesc,
|
|
weightLayout, inputLayout, outputLayout, outputLayout,
|
|
preference, 1, &heuristic, &returnedResults));
|
|
if (returnedResults == 0) {
|
|
printf("No cuBLASLt algorithm: B: %d, T: %d, C: %d, OC: %d, bias: %d\n", B, T, C, OC, has_bias);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
// call the matmul
|
|
const float alpha = 1.0f, beta = 0.0f;
|
|
cublasCheck(cublasLtMatmul(cublaslt_handle, operationDesc,
|
|
&alpha, weight, weightLayout, inp, inputLayout, &beta,
|
|
out, outputLayout, out, outputLayout, &heuristic.algo,
|
|
cublaslt_workspace, cublaslt_workspace_size, 0));
|
|
|
|
// cleanups
|
|
cublasCheck(cublasLtMatmulPreferenceDestroy(preference));
|
|
cublasCheck(cublasLtMatmulDescDestroy(operationDesc));
|
|
cublasCheck(cublasLtMatrixLayoutDestroy(weightLayout));
|
|
cublasCheck(cublasLtMatrixLayoutDestroy(inputLayout));
|
|
cublasCheck(cublasLtMatrixLayoutDestroy(outputLayout));
|
|
cublasCheck(cublasLtMatrixLayoutDestroy(biasLayout));
|
|
}
|
|
|
|
|
|
void attention_forward(float* out, float* vaccum, float* qkvr, float* preatt, float* att,
|
|
float* inp,
|
|
int B, int T, int C, int NH) {
|
|
const int block_size = 256;
|
|
const int softmax_block_size = 256;
|
|
|
|
// inp is (B, T, 3C) QKV
|
|
// preatt, att are (B, NH, T, T)
|
|
// output is (B, T, C)
|
|
int HS = C / NH; // head size
|
|
|
|
// permute and separate inp from (B, T, 3, NH, HS) to 3X (B, NH, T, HS)
|
|
float *q, *k, *v;
|
|
q = qkvr + 0 * B * T * C;
|
|
k = qkvr + 1 * B * T * C;
|
|
v = qkvr + 2 * B * T * C;
|
|
int total_threads = B * NH * T * HS;
|
|
int num_blocks = CEIL_DIV(total_threads, block_size);
|
|
permute_kernel<<<num_blocks, block_size>>>(q, k, v, inp, B, T, NH, HS);
|
|
|
|
// batched matrix multiply with cuBLAS
|
|
cublasStatus_t stat;
|
|
const float alpha = 1.0f;
|
|
const float beta = 0.0f;
|
|
stat = cublasSgemmStridedBatched(cublas_handle,
|
|
CUBLAS_OP_T, CUBLAS_OP_N,
|
|
T, T, HS,
|
|
&alpha,
|
|
k, HS, T * HS,
|
|
q, HS, T * HS,
|
|
&beta,
|
|
preatt, T, T * T,
|
|
B * NH);
|
|
if (stat != CUBLAS_STATUS_SUCCESS) {
|
|
printf("cublasSgemm failed\n");
|
|
exit(1);
|
|
}
|
|
|
|
// multiply all elements of preatt elementwise by scale
|
|
float scale = 1.0 / sqrtf(HS);
|
|
int grid_size = CEIL_DIV(B * NH * T * 32, softmax_block_size);
|
|
softmax_forward_kernel5<<<grid_size, softmax_block_size>>>(att, scale, preatt, B * NH, T);
|
|
|
|
// new approach: first cuBLAS another batched matmul
|
|
// y = att @ v # (B, nh, T, T) @ (B, nh, T, hs) -> (B, nh, T, hs)
|
|
stat = cublasSgemmStridedBatched(cublas_handle,
|
|
CUBLAS_OP_N, CUBLAS_OP_N,
|
|
HS, T, T,
|
|
&alpha,
|
|
v, HS, T * HS,
|
|
att, T, T * T,
|
|
&beta,
|
|
vaccum, HS, T * HS,
|
|
B * NH);
|
|
if (stat != CUBLAS_STATUS_SUCCESS) {
|
|
printf("cublasSgemm failed\n");
|
|
exit(1);
|
|
}
|
|
|
|
// now unpermute
|
|
// y = y.transpose(1, 2).contiguous().view(B, T, C) # re-assemble all head outputs side by side
|
|
num_blocks = CEIL_DIV(B * T * C, block_size);
|
|
unpermute_kernel<<<num_blocks, block_size>>>(vaccum, out, B, T, NH, HS);
|
|
}
|
|
|
|
void residual_forward(float* out, float* inp1, float* inp2, int N) {
|
|
const int block_size = 256;
|
|
const int grid_size = CEIL_DIV(N, block_size);
|
|
residual_forward_kernel<<<grid_size, block_size>>>(out, inp1, inp2, N);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
|
|
void gelu_forward(float* out, const float* inp, int N) {
|
|
const int block_size = 128;
|
|
const int grid_size = CEIL_DIV(N, block_size);
|
|
gelu_kernel<<<grid_size, block_size>>>(out, inp, N);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
void softmax_forward(float* out, float* inp, int N, int C) {
|
|
int grid_size = N;
|
|
const int block_size = 512;
|
|
size_t shared_mem_size = 2 * block_size / 32 * sizeof(float);
|
|
softmax_forward_kernel7<<<grid_size, block_size, shared_mem_size>>>(out, inp, N, C);
|
|
}
|
|
|
|
void crossentropy_forward(float* losses,
|
|
float* probs, int* targets,
|
|
int B, int T, int V) {
|
|
const int block_size = 128;
|
|
const int N = B * T;
|
|
const int grid_size = CEIL_DIV(N, block_size);
|
|
crossentropy_forward_kernel1<<<grid_size, block_size>>>(losses, probs, targets, B, T, V);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// GPT-2 model definition
|
|
|
|
// the parameters of the model
|
|
#define NUM_PARAMETER_TENSORS 16
|
|
typedef struct {
|
|
float* wte; // (V, C)
|
|
float* wpe; // (maxT, C)
|
|
float* ln1w; // (L, C)
|
|
float* ln1b; // (L, C)
|
|
float* qkvw; // (L, 3*C, C)
|
|
float* qkvb; // (L, 3*C)
|
|
float* attprojw; // (L, C, C)
|
|
float* attprojb; // (L, C)
|
|
float* ln2w; // (L, C)
|
|
float* ln2b; // (L, C)
|
|
float* fcw; // (L, 4*C, C)
|
|
float* fcb; // (L, 4*C)
|
|
float* fcprojw; // (L, C, 4*C)
|
|
float* fcprojb; // (L, C)
|
|
float* lnfw; // (C)
|
|
float* lnfb; // (C)
|
|
} ParameterTensors;
|
|
|
|
|
|
// allocate memory for the parameters and point the individual tensors to the right places
|
|
float* malloc_and_point_parameters(ParameterTensors* params, size_t* param_sizes, int on_device) {
|
|
// on_device: 0 = CPU, 1 = GPU
|
|
// calculate the number of parameters
|
|
size_t num_parameters = 0;
|
|
for (size_t i = 0; i < NUM_PARAMETER_TENSORS; i++) {
|
|
num_parameters += param_sizes[i];
|
|
}
|
|
// malloc all parameters all at once on the device
|
|
float* params_memory;
|
|
if (on_device) {
|
|
cudaCheck(cudaMalloc((void**)¶ms_memory, num_parameters * sizeof(float)));
|
|
} else {
|
|
params_memory = (float*)malloc(num_parameters * sizeof(float));
|
|
}
|
|
// assign all the tensors their place in the array
|
|
float** ptrs[] = {
|
|
¶ms->wte, ¶ms->wpe, ¶ms->ln1w, ¶ms->ln1b, ¶ms->qkvw, ¶ms->qkvb,
|
|
¶ms->attprojw, ¶ms->attprojb, ¶ms->ln2w, ¶ms->ln2b, ¶ms->fcw, ¶ms->fcb,
|
|
¶ms->fcprojw, ¶ms->fcprojb, ¶ms->lnfw, ¶ms->lnfb
|
|
};
|
|
float* params_memory_iterator = params_memory;
|
|
for (size_t i = 0; i < NUM_PARAMETER_TENSORS; i++) {
|
|
*(ptrs[i]) = params_memory_iterator;
|
|
params_memory_iterator += param_sizes[i];
|
|
}
|
|
return params_memory;
|
|
}
|
|
|
|
|
|
#define NUM_ACTIVATION_TENSORS 25
|
|
typedef struct {
|
|
float* encoded; // (B, T, C)
|
|
float* ln1; // (L, B, T, C)
|
|
float* ln1_mean; // (L, B, T)
|
|
float* ln1_rstd; // (L, B, T)
|
|
float* qkv; // (L, B, T, 3*C)
|
|
float* atty; // (L, B, T, C)
|
|
float* preatt; // (L, B, NH, T, T)
|
|
float* att; // (L, B, NH, T, T)
|
|
float* attproj; // (L, B, T, C)
|
|
float* residual2; // (L, B, T, C)
|
|
float* ln2; // (L, B, T, C)
|
|
float* ln2_mean; // (L, B, T)
|
|
float* ln2_rstd; // (L, B, T)
|
|
float* fch; // (L, B, T, 4*C)
|
|
float* fch_gelu; // (L, B, T, 4*C)
|
|
float* fcproj; // (L, B, T, C)
|
|
float* residual3; // (L, B, T, C)
|
|
float* lnf; // (B, T, C)
|
|
float* lnf_mean; // (B, T)
|
|
float* lnf_rstd; // (B, T)
|
|
float* logits; // (B, T, V)
|
|
float* probs; // (B, T, V)
|
|
float* losses; // (B, T)
|
|
// adding these two compared to the CPU .c code, needed for attention kernel as buffers
|
|
float* qkvr; // (L, B, T, 3*C)
|
|
float* v_accum; // (L, B, T, C)
|
|
} ActivationTensors;
|
|
|
|
float* malloc_and_point_activations(ActivationTensors* acts, size_t* act_sizes) {
|
|
size_t num_activations = 0;
|
|
for (size_t i = 0; i < NUM_ACTIVATION_TENSORS; i++) {
|
|
num_activations += act_sizes[i];
|
|
}
|
|
float* acts_memory;
|
|
cudaCheck(cudaMalloc((void**)&acts_memory, num_activations * sizeof(float)));
|
|
float** ptrs[] = {
|
|
&acts->encoded, &acts->ln1, &acts->ln1_mean, &acts->ln1_rstd, &acts->qkv, &acts->atty,
|
|
&acts->preatt, &acts->att, &acts->attproj, &acts->residual2, &acts->ln2, &acts->ln2_mean,
|
|
&acts->ln2_rstd, &acts->fch, &acts->fch_gelu, &acts->fcproj, &acts->residual3, &acts->lnf,
|
|
&acts->lnf_mean, &acts->lnf_rstd, &acts->logits, &acts->probs, &acts->losses,
|
|
&acts->qkvr, &acts->v_accum
|
|
};
|
|
float* acts_memory_iterator = acts_memory;
|
|
for (size_t i = 0; i < NUM_ACTIVATION_TENSORS; i++) {
|
|
*(ptrs[i]) = acts_memory_iterator;
|
|
acts_memory_iterator += act_sizes[i];
|
|
}
|
|
return acts_memory;
|
|
}
|
|
|
|
typedef struct {
|
|
int max_seq_len; // max sequence length, e.g. 1024
|
|
int vocab_size; // vocab size, e.g. 50257
|
|
int num_layers; // number of layers, e.g. 12
|
|
int num_heads; // number of heads in attention, e.g. 12
|
|
int channels; // number of channels, e.g. 768
|
|
} GPT2Config;
|
|
|
|
typedef struct {
|
|
GPT2Config config;
|
|
// the weights of the model, and their sizes
|
|
ParameterTensors params;
|
|
size_t param_sizes[NUM_PARAMETER_TENSORS];
|
|
float* params_memory;
|
|
int num_parameters;
|
|
// gradients of the weights
|
|
ParameterTensors grads;
|
|
float* grads_memory;
|
|
// buffers for the AdamW optimizer
|
|
float* m_memory;
|
|
float* v_memory;
|
|
// the activations of the model, and their sizes
|
|
ActivationTensors acts;
|
|
size_t act_sizes[NUM_ACTIVATION_TENSORS];
|
|
float* acts_memory;
|
|
int num_activations;
|
|
// gradients of the activations
|
|
ActivationTensors grads_acts;
|
|
float* grads_acts_memory;
|
|
// other run state configuration
|
|
int batch_size; // the batch size (B) of current forward pass
|
|
int seq_len; // the sequence length (T) of current forward pass
|
|
int* inputs; // the input tokens for the current forward pass
|
|
int* targets; // the target tokens for the current forward pass
|
|
float mean_loss; // after a forward pass with targets, will be populated with the mean loss
|
|
float* cpu_losses; // CPU buffer to copy the losses to, allocated with cudaMallocHost
|
|
} GPT2;
|
|
|
|
|
|
void gpt2_build_from_checkpoint(GPT2 *model, const char* checkpoint_path) {
|
|
|
|
// read in model from a checkpoint file
|
|
FILE *model_file = fopen(checkpoint_path, "rb");
|
|
if (model_file == NULL) { printf("Error opening model file\n"); exit(1); }
|
|
int model_header[256];
|
|
fread(model_header, sizeof(int), 256, model_file);
|
|
if (model_header[0] != 20240326) { printf("Bad magic model file"); exit(1); }
|
|
if (model_header[1] != 1) { printf("Bad version in model file"); exit(1); }
|
|
|
|
// read in hyperparameters
|
|
int maxT, V, L, NH, C;
|
|
model->config.max_seq_len = maxT = model_header[2];
|
|
model->config.vocab_size = V = model_header[3];
|
|
model->config.num_layers = L = model_header[4];
|
|
model->config.num_heads = NH = model_header[5];
|
|
model->config.channels = C = model_header[6];
|
|
printf("[GPT-2]\n");
|
|
printf("max_seq_len: %d\n", maxT);
|
|
printf("vocab_size: %d\n", V);
|
|
printf("num_layers: %d\n", L);
|
|
printf("num_heads: %d\n", NH);
|
|
printf("channels: %d\n", C);
|
|
|
|
// allocate space for all the parameters and read them in
|
|
model->param_sizes[0] = V * C; // wte
|
|
model->param_sizes[1] = maxT * C; // wpe
|
|
model->param_sizes[2] = L * C; // ln1w
|
|
model->param_sizes[3] = L * C; // ln1b
|
|
model->param_sizes[4] = L * (3 * C) * C; // qkvw
|
|
model->param_sizes[5] = L * (3 * C); // qkvb
|
|
model->param_sizes[6] = L * C * C; // attprojw
|
|
model->param_sizes[7] = L * C; // attprojb
|
|
model->param_sizes[8] = L * C; // ln2w
|
|
model->param_sizes[9] = L * C; // ln2b
|
|
model->param_sizes[10] = L * (4 * C) * C; // fcw
|
|
model->param_sizes[11] = L * (4 * C); // fcb
|
|
model->param_sizes[12] = L * C * (4 * C); // fcprojw
|
|
model->param_sizes[13] = L * C; // fcprojb
|
|
model->param_sizes[14] = C; // lnfw
|
|
model->param_sizes[15] = C; // lnfb
|
|
|
|
// cound the number of paramaters
|
|
size_t num_parameters = 0;
|
|
for (size_t i = 0; i < NUM_PARAMETER_TENSORS; i++) {
|
|
num_parameters += model->param_sizes[i];
|
|
}
|
|
printf("num_parameters: %zu\n", num_parameters);
|
|
model->num_parameters = num_parameters;
|
|
|
|
// create memory for model parameters on the device
|
|
model->params_memory = malloc_and_point_parameters(&model->params, model->param_sizes, 1);
|
|
|
|
// read in all the parameters from file and copy them to device
|
|
float* params_memory_cpu = (float*)malloc(num_parameters * sizeof(float));
|
|
fread(params_memory_cpu, sizeof(float), num_parameters, model_file);
|
|
cudaCheck(cudaMemcpy(model->params_memory, params_memory_cpu, num_parameters * sizeof(float), cudaMemcpyHostToDevice));
|
|
free(params_memory_cpu);
|
|
fclose(model_file);
|
|
|
|
// other inits
|
|
model->acts_memory = NULL;
|
|
model->grads_memory = NULL;
|
|
model->m_memory = NULL;
|
|
model->v_memory = NULL;
|
|
model->grads_acts_memory = NULL;
|
|
model->inputs = NULL;
|
|
model->targets = NULL;
|
|
model->batch_size = 0;
|
|
model->seq_len = 0;
|
|
model->mean_loss = -1.0f; // -1.0f will designate no loss
|
|
}
|
|
|
|
void gpt2_forward(GPT2 *model, int* inputs, int* targets, int B, int T) {
|
|
// targets are optional and could be NULL
|
|
|
|
// ensure the model was initialized or error out
|
|
if (model->params_memory == NULL) {
|
|
printf("Error: model was not initialized properly.\n");
|
|
exit(1);
|
|
}
|
|
|
|
// convenience parameters
|
|
int V = model->config.vocab_size;
|
|
int L = model->config.num_layers;
|
|
int NH = model->config.num_heads;
|
|
int C = model->config.channels;
|
|
|
|
// allocate space for all the activations if needed (done here, lazily)
|
|
if(model->acts_memory == NULL) {
|
|
// record the current B,T as well
|
|
model->batch_size = B;
|
|
model->seq_len = T;
|
|
// and now allocate the space
|
|
model->act_sizes[0] = B * T * C; // encoded
|
|
model->act_sizes[1] = L * B * T * C; // ln1
|
|
model->act_sizes[2] = L * B * T; // ln1_mean
|
|
model->act_sizes[3] = L * B * T; // ln1_rstd
|
|
model->act_sizes[4] = L * B * T * 3*C; // qkv
|
|
model->act_sizes[5] = L * B * T * C; // atty
|
|
model->act_sizes[6] = L * B * NH * T * T; // preatt
|
|
model->act_sizes[7] = L * B * NH * T * T; // att
|
|
model->act_sizes[8] = L * B * T * C; // attproj
|
|
model->act_sizes[9] = L * B * T * C; // residual2
|
|
model->act_sizes[10] = L * B * T * C; // ln2
|
|
model->act_sizes[11] = L * B * T; // ln2_mean
|
|
model->act_sizes[12] = L * B * T; // ln2_rstd
|
|
model->act_sizes[13] = L * B * T * 4*C; // fch
|
|
model->act_sizes[14] = L * B * T * 4*C; // fch_gelu
|
|
model->act_sizes[15] = L * B * T * C; // fcproj
|
|
model->act_sizes[16] = L * B * T * C; // residual3
|
|
model->act_sizes[17] = B * T * C; // lnf
|
|
model->act_sizes[18] = B * T; // lnf_mean
|
|
model->act_sizes[19] = B * T; // lnf_rstd
|
|
model->act_sizes[20] = B * T * V; // logits
|
|
model->act_sizes[21] = B * T * V; // probs
|
|
model->act_sizes[22] = B * T; // losses
|
|
model->act_sizes[23] = L * B * T * 3*C; // qkvr
|
|
model->act_sizes[24] = L * B * T * C; // v_accum
|
|
size_t num_activations = 0;
|
|
for (size_t i = 0; i < NUM_ACTIVATION_TENSORS; i++) {
|
|
num_activations += model->act_sizes[i];
|
|
}
|
|
printf("num_activations: %zu\n", num_activations);
|
|
model->num_activations = num_activations;
|
|
model->acts_memory = malloc_and_point_activations(&model->acts, model->act_sizes);
|
|
// also create memory for caching inputs and targets
|
|
cudaCheck(cudaMalloc((void**)&model->inputs, B * T * sizeof(int)));
|
|
cudaCheck(cudaMalloc((void**)&model->targets, B * T * sizeof(int)));
|
|
cudaCheck(cudaMallocHost((void**)&model->cpu_losses, B * T * sizeof(float)));
|
|
} else {
|
|
// validate B,T is no larger than what was previously allocated
|
|
// in principle, we could re-allocate a larger chunk of memory, for now we just error out
|
|
if (B > model->batch_size || T > model->seq_len) {
|
|
printf("Error: batch size or sequence length is inadequately large\n");
|
|
printf("Model: B=%d T=%d, Desired: B=%d T=%d\n", model->batch_size, model->seq_len, B, T);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
// copy inputs/targets to the model
|
|
cudaCheck(cudaMemcpy(model->inputs, inputs, B * T * sizeof(int), cudaMemcpyHostToDevice));
|
|
if (targets != NULL) {
|
|
cudaCheck(cudaMemcpy(model->targets, targets, B * T * sizeof(int), cudaMemcpyHostToDevice));
|
|
}
|
|
|
|
// forward pass
|
|
ParameterTensors params = model->params; // for brevity
|
|
ActivationTensors acts = model->acts;
|
|
float* residual;
|
|
encoder_forward(acts.encoded, model->inputs, params.wte, params.wpe, B, T, C); // encoding goes into residual[0]
|
|
|
|
for (int l = 0; l < L; l++) {
|
|
|
|
residual = l == 0 ? acts.encoded : acts.residual3 + (l-1) * B * T * C;
|
|
|
|
// get the pointers of the weights for this layer
|
|
float* l_ln1w = params.ln1w + l * C;
|
|
float* l_ln1b = params.ln1b + l * C;
|
|
float* l_qkvw = params.qkvw + l * 3*C * C;
|
|
float* l_qkvb = params.qkvb + l * 3*C;
|
|
float* l_attprojw = params.attprojw + l * C * C;
|
|
float* l_attprojb = params.attprojb + l * C;
|
|
float* l_ln2w = params.ln2w + l * C;
|
|
float* l_ln2b = params.ln2b + l * C;
|
|
float* l_fcw = params.fcw + l * 4*C * C;
|
|
float* l_fcb = params.fcb + l * 4*C;
|
|
float* l_fcprojw = params.fcprojw + l * C * 4*C;
|
|
float* l_fcprojb = params.fcprojb + l * C;
|
|
|
|
// get the pointers of the activations for this layer
|
|
float* l_ln1 = acts.ln1 + l * B * T * C;
|
|
float* l_ln1_mean = acts.ln1_mean + l * B * T;
|
|
float* l_ln1_rstd = acts.ln1_rstd + l * B * T;
|
|
float* l_qkv = acts.qkv + l * B * T * 3*C;
|
|
float* l_qkvr = acts.qkvr + l * B * T * 3*C;
|
|
float* l_atty = acts.atty + l * B * T * C;
|
|
float* l_preatt = acts.preatt + l * B * NH * T * T;
|
|
float* l_att = acts.att + l * B * NH * T * T;
|
|
float* l_v_accum = acts.v_accum + l * B * T * C;
|
|
float* l_attproj = acts.attproj + l * B * T * C;
|
|
float* l_residual2 = acts.residual2 + l * B * T * C;
|
|
float* l_ln2 = acts.ln2 + l * B * T * C;
|
|
float* l_ln2_mean = acts.ln2_mean + l * B * T;
|
|
float* l_ln2_rstd = acts.ln2_rstd + l * B * T;
|
|
float* l_fch = acts.fch + l * B * T * 4*C;
|
|
float* l_fch_gelu = acts.fch_gelu + l * B * T * 4*C;
|
|
float* l_fcproj = acts.fcproj + l * B * T * C;
|
|
float* l_residual3 = acts.residual3 + l * B * T * C;
|
|
|
|
// now do the forward pass
|
|
layernorm_forward(l_ln1, l_ln1_mean, l_ln1_rstd, residual, l_ln1w, l_ln1b, B, T, C);
|
|
matmul_forward_cublaslt(l_qkv, l_ln1, l_qkvw, l_qkvb, B, T, C, 3*C);
|
|
attention_forward(l_atty, l_v_accum, l_qkvr, l_preatt, l_att, l_qkv, B, T, C, NH);
|
|
matmul_forward_cublaslt(l_attproj, l_atty, l_attprojw, l_attprojb, B, T, C, C);
|
|
residual_forward(l_residual2, residual, l_attproj, B*T*C);
|
|
layernorm_forward(l_ln2, l_ln2_mean, l_ln2_rstd, l_residual2, l_ln2w, l_ln2b, B, T, C);
|
|
matmul_forward_cublaslt(l_fch, l_ln2, l_fcw, l_fcb, B, T, C, 4*C);
|
|
gelu_forward(l_fch_gelu, l_fch, B*T*4*C);
|
|
matmul_forward_cublaslt(l_fcproj, l_fch_gelu, l_fcprojw, l_fcprojb, B, T, 4*C, C);
|
|
residual_forward(l_residual3, l_residual2, l_fcproj, B*T*C);
|
|
}
|
|
|
|
residual = acts.residual3 + (L-1) * B * T * C; // last residual is in residual3
|
|
layernorm_forward(acts.lnf, acts.lnf_mean, acts.lnf_rstd, residual, params.lnfw, params.lnfb, B, T, C);
|
|
matmul_forward_cublas(acts.logits, acts.lnf, params.wte, NULL, B, T, C, V);
|
|
softmax_forward(acts.probs, acts.logits, B*T, V);
|
|
|
|
// also forward the cross-entropy loss function if we have the targets
|
|
if (targets != NULL) {
|
|
crossentropy_forward(acts.losses, acts.probs, model->targets, B, T, V);
|
|
|
|
// for convenience also evaluate the mean loss
|
|
// move the (B,T) losses to CPU
|
|
cudaCheck(cudaMemcpy(model->cpu_losses, acts.losses, B * T * sizeof(float), cudaMemcpyDeviceToHost));
|
|
float mean_loss = 0.0f;
|
|
for (int i=0; i<B*T; i++) { mean_loss += model->cpu_losses[i]; }
|
|
mean_loss /= B*T;
|
|
model->mean_loss = mean_loss;
|
|
|
|
} else {
|
|
// if we don't have targets, we don't have a loss
|
|
model->mean_loss = -1.0f;
|
|
}
|
|
}
|
|
|
|
void gpt2_zero_grad(GPT2 *model) {
|
|
if (model->grads_acts_memory != NULL) { cudaCheck(cudaMemset(model->grads_acts_memory, 0, model->num_activations * sizeof(float))); }
|
|
if (model->grads_memory != NULL) { cudaCheck(cudaMemset(model->grads_memory, 0, model->num_parameters * sizeof(float))); }
|
|
}
|
|
|
|
void gpt2_backward(GPT2 *model) {
|
|
|
|
// double check we forwarded previously, with targets
|
|
if (model->mean_loss == -1.0f) {
|
|
printf("Error: must forward with targets before backward\n");
|
|
exit(1);
|
|
}
|
|
|
|
// lazily allocate the memory for gradients of the weights and activations, if needed
|
|
if (model->grads_memory == NULL) {
|
|
model->grads_memory = malloc_and_point_parameters(&model->grads, model->param_sizes, 1);
|
|
model->grads_acts_memory = malloc_and_point_activations(&model->grads_acts, model->act_sizes);
|
|
gpt2_zero_grad(model);
|
|
}
|
|
|
|
// convenience shortcuts
|
|
int B = model->batch_size;
|
|
int T = model->seq_len;
|
|
// int V = model->config.vocab_size;
|
|
// int L = model->config.num_layers;
|
|
// int NH = model->config.num_heads;
|
|
// int C = model->config.channels;
|
|
|
|
// backward pass: go in the reverse order of the forward pass, and call backward() functions
|
|
// ParameterTensors params = model->params; // for brevity
|
|
// ParameterTensors grads = model->grads;
|
|
// ActivationTensors acts = model->acts;
|
|
ActivationTensors grads_acts = model->grads_acts;
|
|
|
|
// we kick off the chain rule by filling in dlosses with 1.0f/(B*T)
|
|
// technically this is a small, inline backward() pass of calculating
|
|
// total, final loss as the mean over all losses over all (B,T) positions in the batch
|
|
float dloss_mean = 1.0f / (B*T);
|
|
cudaCheck(cudaMemset(grads_acts.losses, dloss_mean, B*T * sizeof(float)));
|
|
|
|
// crossentropy_softmax_backward(grads_acts.logits, grads_acts.losses, acts.probs, model->targets, B, T, V);
|
|
}
|
|
|
|
void gpt2_free(GPT2 *model) {
|
|
cudaCheck(cudaFree(model->params_memory));
|
|
cudaCheck(cudaFree(model->grads_memory));
|
|
cudaCheck(cudaFree(model->m_memory));
|
|
cudaCheck(cudaFree(model->v_memory));
|
|
cudaCheck(cudaFree(model->acts_memory));
|
|
cudaCheck(cudaFree(model->grads_acts_memory));
|
|
cudaCheck(cudaFree(model->inputs));
|
|
cudaCheck(cudaFree(model->targets));
|
|
cudaFreeHost(model->cpu_losses);
|
|
}
|
|
|
|
#ifndef TESTING
|
|
// if we are TESTING (see test_gpt2.cu), we'll skip the int main below
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// data loader lite
|
|
// returns random batches of data from a file of integers
|
|
|
|
typedef struct {
|
|
// hyperparameters
|
|
int B;
|
|
int T;
|
|
// input handling and its state
|
|
FILE* tokens_file;
|
|
long file_size;
|
|
long current_position;
|
|
// output memory
|
|
int* batch;
|
|
int* inputs;
|
|
int* targets;
|
|
// convenience variables
|
|
int num_batches;
|
|
} DataLoader;
|
|
|
|
void dataloader_init(DataLoader *loader, const char* filename, int B, int T) {
|
|
loader->B = B;
|
|
loader->T = T;
|
|
|
|
// open the input file for reading
|
|
loader->tokens_file = fopen(filename, "rb");
|
|
if (loader->tokens_file == NULL) {
|
|
printf("Error opening tokens file\n");
|
|
exit(1);
|
|
}
|
|
|
|
// determine the file size
|
|
fseek(loader->tokens_file, 0, SEEK_END);
|
|
loader->file_size = ftell(loader->tokens_file);
|
|
fseek(loader->tokens_file, 0, SEEK_SET);
|
|
if (loader->file_size < (B * T + 1) * sizeof(int)) {
|
|
printf("Error: file size is too small for the batch size and sequence length\n");
|
|
exit(1);
|
|
}
|
|
loader->current_position = 0; // start at the beginning
|
|
|
|
// allocate space for B*T + 1 integers to store the inputs and targets
|
|
// Using CUDA CPU pinned memory for faster PCI Express transfers to GPU
|
|
// See: https://developer.nvidia.com/blog/how-optimize-data-transfers-cuda-cc/
|
|
cudaMallocHost((void**)&loader->batch, (B * T + 1) * sizeof(int));
|
|
loader->inputs = loader->batch;
|
|
loader->targets = loader->batch + 1; // targets are shifted by one
|
|
loader->num_batches = loader->file_size / (B * T * sizeof(int));
|
|
}
|
|
|
|
void dataloader_reset(DataLoader *loader) {
|
|
loader->current_position = 0;
|
|
}
|
|
|
|
void dataloader_next_batch(DataLoader *loader) {
|
|
int B = loader->B;
|
|
int T = loader->T;
|
|
// if we are at the end of the file, loop back to the beginning
|
|
if (loader->current_position + (B*T+1) * sizeof(int) > loader->file_size) {
|
|
loader->current_position = 0;
|
|
}
|
|
// read the B*T+1 integers from the file into batch
|
|
fseek(loader->tokens_file, loader->current_position, SEEK_SET);
|
|
fread(loader->batch, sizeof(int), B*T+1, loader->tokens_file);
|
|
// advance the current position by B*T integers
|
|
loader->current_position += B*T * sizeof(int);
|
|
}
|
|
|
|
void dataloader_free(DataLoader *loader) {
|
|
fclose(loader->tokens_file);
|
|
cudaFreeHost(loader->batch);
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// sampler
|
|
|
|
#define GPT2_EOT 50256
|
|
|
|
unsigned int random_u32(unsigned long long *state) {
|
|
// xorshift rng: https://en.wikipedia.org/wiki/Xorshift#xorshift.2A
|
|
*state ^= *state >> 12;
|
|
*state ^= *state << 25;
|
|
*state ^= *state >> 27;
|
|
return (*state * 0x2545F4914F6CDD1Dull) >> 32;
|
|
}
|
|
float random_f32(unsigned long long *state) { // random float32 in [0,1)
|
|
return (random_u32(state) >> 8) / 16777216.0f;
|
|
}
|
|
|
|
int sample_mult(float* probabilities, int n, float coin) {
|
|
// sample index from probabilities (they must sum to 1!)
|
|
// coin is a random number in [0, 1), usually from random_f32()
|
|
float cdf = 0.0f;
|
|
for (int i = 0; i < n; i++) {
|
|
cdf += probabilities[i];
|
|
if (coin < cdf) {
|
|
return i;
|
|
}
|
|
}
|
|
return n - 1; // in case of rounding errors
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// main training loop
|
|
int main() {
|
|
|
|
// set up the device
|
|
int deviceIdx = 0;
|
|
cudaCheck(cudaSetDevice(deviceIdx));
|
|
cudaDeviceProp deviceProp;
|
|
cudaGetDeviceProperties(&deviceProp, deviceIdx);
|
|
printf("[System]\n");
|
|
printf("Device %d: %s\n", deviceIdx, deviceProp.name);
|
|
|
|
// setup cuBLAS and cuBLASLt
|
|
cublasCheck(cublasCreate(&cublas_handle));
|
|
cublasCheck(cublasLtCreate(&cublaslt_handle));
|
|
// TF32 precision is equivalent to torch.set_float32_matmul_precision('high')
|
|
int enable_tf32 = deviceProp.major >= 8 ? 1 : 0;
|
|
printf("enable_tf32: %d\n", enable_tf32);
|
|
cublas_compute_type = enable_tf32 ? CUBLAS_COMPUTE_32F_FAST_TF32 : CUBLAS_COMPUTE_32F;
|
|
cublasMath_t cublas_math_mode = enable_tf32 ? CUBLAS_TF32_TENSOR_OP_MATH : CUBLAS_DEFAULT_MATH;
|
|
cublasCheck(cublasSetMathMode(cublas_handle, cublas_math_mode));
|
|
// setup the (global) cuBLASLt workspace
|
|
cudaCheck(cudaMalloc(&cublaslt_workspace, cublaslt_workspace_size));
|
|
|
|
// build the GPT-2 model from a checkpoint
|
|
GPT2 model;
|
|
gpt2_build_from_checkpoint(&model, "gpt2_124M.bin");
|
|
|
|
// build the DataLoaders from tokens files. for now use tiny_shakespeare if available, else tiny_stories
|
|
const char* tiny_stories_train = "data/TinyStories_train.bin";
|
|
const char* tiny_stories_val = "data/TinyStories_val.bin";
|
|
const char* tiny_shakespeare_train = "data/tiny_shakespeare_train.bin";
|
|
const char* tiny_shakespeare_val = "data/tiny_shakespeare_val.bin";
|
|
const char* train_tokens = access(tiny_shakespeare_train, F_OK) != -1 ? tiny_shakespeare_train : tiny_stories_train;
|
|
const char* val_tokens = access(tiny_shakespeare_val, F_OK) != -1 ? tiny_shakespeare_val : tiny_stories_val;
|
|
int B = 4;
|
|
int T = 1024;
|
|
DataLoader train_loader;
|
|
dataloader_init(&train_loader, train_tokens, B, T);
|
|
printf("train dataset num_batches: %d\n", train_loader.num_batches);
|
|
DataLoader val_loader;
|
|
dataloader_init(&val_loader, val_tokens, B, T);
|
|
printf("val dataset num_batches: %d\n", val_loader.num_batches);
|
|
int val_num_batches = 10;
|
|
printf("batch size: %d\n", B);
|
|
printf("sequence length: %d\n", T);
|
|
printf("val_num_batches: %d\n", val_num_batches);
|
|
|
|
// some memory for generating samples from the model
|
|
unsigned long long rng_state = 1337;
|
|
const int genT = 64;
|
|
int* gen_tokens = (int*)calloc(B * genT, sizeof(int)); // init with all zero token
|
|
float* cpu_probs = (float*)malloc(model.config.vocab_size * sizeof(float));
|
|
|
|
// train
|
|
struct timespec start, end;
|
|
for (int step = 0; step <= 40; step++) {
|
|
|
|
// once in a while estimate the validation loss
|
|
if (step % 10 == 0) {
|
|
float val_loss = 0.0f;
|
|
dataloader_reset(&val_loader);
|
|
for (int i = 0; i < val_num_batches; i++) {
|
|
dataloader_next_batch(&val_loader);
|
|
gpt2_forward(&model, val_loader.inputs, val_loader.targets, B, T);
|
|
val_loss += model.mean_loss;
|
|
}
|
|
val_loss /= val_num_batches;
|
|
printf("val loss %f\n", val_loss);
|
|
}
|
|
|
|
// once in a while do model inference to print generated text
|
|
if (step > 0 && step % 20 == 0) {
|
|
gen_tokens[0] = GPT2_EOT; // the GPT-2 EOT token kicks off the generation
|
|
for (int t = 1; t < genT; t++) {
|
|
// note that inference is very wasteful here because for each token
|
|
// we re-calculate the forward pass for all of (B,T) from scratch
|
|
// but the inference here is just for sanity checking anyway
|
|
// and we're only actually using b=0 (i.e. the first row) of all B rows
|
|
gpt2_forward(&model, gen_tokens, NULL, B, T);
|
|
float* probs = model.acts.probs + (t-1) * model.config.vocab_size;
|
|
float coin = random_f32(&rng_state);
|
|
// move probs back to CPU and sample
|
|
cudaCheck(cudaMemcpy(cpu_probs, probs, model.config.vocab_size * sizeof(float), cudaMemcpyDeviceToHost));
|
|
int next_token = sample_mult(cpu_probs, model.config.vocab_size, coin);
|
|
gen_tokens[t] = next_token;
|
|
}
|
|
printf("generated:\n");
|
|
for (int t = 0; t < genT; t++) {
|
|
printf("%d ", gen_tokens[t]);
|
|
}
|
|
printf("\n");
|
|
}
|
|
|
|
// do a training step
|
|
clock_gettime(CLOCK_MONOTONIC, &start);
|
|
dataloader_next_batch(&train_loader);
|
|
gpt2_forward(&model, train_loader.inputs, train_loader.targets, B, T);
|
|
// gpt2_zero_grad(&model);
|
|
// gpt2_backward(&model);
|
|
// gpt2_update(&model, 1e-4f, 0.9f, 0.999f, 1e-8f, 0.0f, step+1);
|
|
cudaCheck(cudaDeviceSynchronize()); // finish all CUDA work to get correct precise timings
|
|
clock_gettime(CLOCK_MONOTONIC, &end);
|
|
double time_elapsed_s = (end.tv_sec - start.tv_sec) + (end.tv_nsec - start.tv_nsec) / 1e9;
|
|
printf("step %d: train loss %f (took %f ms)\n", step, model.mean_loss, time_elapsed_s * 1000);
|
|
}
|
|
|
|
// free
|
|
dataloader_free(&train_loader);
|
|
dataloader_free(&val_loader);
|
|
gpt2_free(&model);
|
|
free(cpu_probs);
|
|
free(gen_tokens);
|
|
cudaCheck(cudaFree(cublaslt_workspace));
|
|
cublasCheck(cublasDestroy(cublas_handle));
|
|
cublasCheck(cublasLtDestroy(cublaslt_handle));
|
|
|
|
return 0;
|
|
}
|
|
#endif
|