mirror of
https://github.com/karpathy/llm.c.git
synced 2026-07-27 20:25:09 -04:00
3319 lines
152 KiB
Text
3319 lines
152 KiB
Text
/*
|
|
GPT-2 Transformer Neural Net trained in raw CUDA
|
|
GPT-2 Transformer Neural Net trained in raw CUDA
|
|
Non-trivial notes to be aware of:
|
|
|
|
We are being clever in the backward pass to conserve memory.
|
|
In particular, all parameters use a += in the backward pass, so we
|
|
can later do gradient accumulation. But all activations have = instead of +=
|
|
because these are faster (just read, no write). This is okay for all activations
|
|
except for those in the residual stream, where the gradients have to add. We make
|
|
sure that those parts work out ok and that we do a += as necessary. E.g.,
|
|
the layernorms are connected to the residuals so we += in layernorm backward.
|
|
|
|
In this file we are using Mixed Precision training, so different activations,
|
|
parameters, grads and buffers may be kept at different precisions, to take
|
|
advantage of the fast low-precision hardware in the latest GPUs (bf16/fp16),
|
|
and fp8 (coming soon^TM).
|
|
|
|
Compile:
|
|
make train_gpt2cu
|
|
|
|
Example launch using bfloat16 on 1 GPU batch size 8, sample/eval every 200 steps:
|
|
Also we're using TinyStories here for example as it is a bigger dataset
|
|
./train_gpt2cu -b 8 -v 200 -s 200 -i data/TinyStories
|
|
|
|
Example launch using bfloat16 on 4 GPUs, same as above:
|
|
mpirun -np 4 ./train_gpt2cu -b 8 -v 200 -s 200 -i data/TinyStories
|
|
|
|
If you'd like to see train_gpt2.cu produce identical results to
|
|
`python train_gpt2.py`, you can run it like this:
|
|
make train_gpt2cu && ./train_gpt2cu -b 4 -t 64 -l 1e-4 -v 200 -s 200 -a 1 -x 10 -f 0
|
|
make train_gpt2cu PRECISION=FP32 && ./train_gpt2cu -b 4 -t 64 -l 1e-4 -v 200 -s 200 -a 1 -x 10 -f 0
|
|
This reads & runs in fp32, B=4, T=64, LR=1e-4, val/sample never (200),
|
|
-a 1 is "overfit single batch", -x 10 is 10 iterations, and -f 0 disables tf32
|
|
*/
|
|
|
|
#include <unistd.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdarg.h>
|
|
#include <string>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <vector>
|
|
#include <algorithm>
|
|
#include <functional>
|
|
#include <unordered_map>
|
|
// GPU / CUDA related
|
|
#include <cuda_runtime.h>
|
|
#include <cublas_v2.h>
|
|
#include <cublasLt.h>
|
|
#include <nvtx3/nvToolsExt.h>
|
|
#include <cuda_profiler_api.h>
|
|
// Multi-GPU related
|
|
#ifdef MULTI_GPU
|
|
#include <mpi.h>
|
|
#include <nccl.h>
|
|
#endif
|
|
// our own utilities
|
|
// defines: fopenCheck, freadCheck, fcloseCheck, fseekCheck, mallocCheck
|
|
#include "utils.h"
|
|
// defines: tokenizer_init, tokenizer_decode, tokenizer_free
|
|
#include "tokenizer.h"
|
|
// defines: dataloader_init, dataloader_reset, dataloader_next_batch, dataloader_free
|
|
// defines: evalloader_init, evalloader_reset, evalloader_next_batch, evalloader_free
|
|
#include "dataloader.h"
|
|
// defines: manual_seed, normal_
|
|
// numerically identical to PyTorch's torch.manual_seed and torch.normal
|
|
#include "rand.h"
|
|
// ----------------------------------------------------------------------------
|
|
// CUDA precision settings
|
|
|
|
enum PrecisionMode {
|
|
PRECISION_FP32,
|
|
PRECISION_FP16,
|
|
PRECISION_BF16
|
|
};
|
|
|
|
// Specific configurations based on the enabled precision
|
|
#if defined(ENABLE_FP32)
|
|
typedef float floatX;
|
|
#define CUBLAS_LOWP CUDA_R_32F
|
|
#define PRECISION_MODE PRECISION_FP32
|
|
#ifdef MULTI_GPU
|
|
const ncclDataType_t ncclFloatX = ncclFloat;
|
|
#endif
|
|
|
|
// use fp16 (note: this may require gradient scaler, currently not implemented!)
|
|
#elif defined(ENABLE_FP16)
|
|
typedef half floatX;
|
|
#define CUBLAS_LOWP CUDA_R_16F
|
|
#define PRECISION_MODE PRECISION_FP16
|
|
#ifdef MULTI_GPU
|
|
const ncclDataType_t ncclFloatX = ncclHalf;
|
|
#endif
|
|
|
|
#else // Default to bfloat16
|
|
typedef __nv_bfloat16 floatX;
|
|
#define CUBLAS_LOWP CUDA_R_16BF
|
|
#define PRECISION_MODE PRECISION_BF16
|
|
#ifdef MULTI_GPU
|
|
const ncclDataType_t ncclFloatX = ncclBfloat16;
|
|
#endif
|
|
#endif
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// CUDA utils
|
|
|
|
// Profiler utils
|
|
class NvtxRange {
|
|
public:
|
|
NvtxRange(const char* s) { nvtxRangePush(s); }
|
|
NvtxRange(const std::string& base_str, int number) {
|
|
std::string range_string = base_str + " " + std::to_string(number);
|
|
nvtxRangePush(range_string.c_str());
|
|
}
|
|
~NvtxRange() { nvtxRangePop(); }
|
|
};
|
|
#define NVTX_RANGE_FN() NvtxRange nvtx_range(__FUNCTION__)
|
|
|
|
// try to make sure that 2 blocks fit on A100/H100 to maximise latency tolerance
|
|
// this needs to be defines rather than queried to be used for __launch_bounds__
|
|
#if __CUDA_ARCH__ == 800 || __CUDA_ARCH__ >= 900
|
|
#define MAX_1024_THREADS_BLOCKS 2
|
|
#else
|
|
#define MAX_1024_THREADS_BLOCKS 1
|
|
#endif
|
|
|
|
// WarpSize is not a compile time constant, this allows the compiler to optimize
|
|
#define WARP_SIZE 32U
|
|
|
|
// cuBLAS workspace. Hardcoding to 32MiB but only Hopper needs 32, for others 4 is OK
|
|
const size_t cublaslt_workspace_size = 32 * 1024 * 1024;
|
|
void* cublaslt_workspace = NULL;
|
|
cublasComputeType_t cublas_compute = CUBLAS_COMPUTE_32F;
|
|
cublasLtHandle_t cublaslt_handle;
|
|
cublasHandle_t cublas_handle;
|
|
cudaDeviceProp deviceProp;
|
|
|
|
// 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__); }
|
|
|
|
#ifdef MULTI_GPU
|
|
void nccl_check(ncclResult_t status, const char *file, int line) {
|
|
if (status != ncclSuccess) {
|
|
printf("[NCCL ERROR] at file %s:%d:\n%s\n", file, line, ncclGetErrorString(status));
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
#define ncclCheck(err) (nccl_check(err, __FILE__, __LINE__))
|
|
|
|
void mpi_check(int status, const char *file, int line) {
|
|
if (status != MPI_SUCCESS) {
|
|
char mpi_error[4096];
|
|
int mpi_error_len = 0;
|
|
assert(MPI_Error_string(status, &mpi_error[0], &mpi_error_len) == MPI_SUCCESS);
|
|
printf("[MPI ERROR] at file %s:%d:\n%.*s\n", file, line, mpi_error_len, mpi_error);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
#define mpiCheck(err) (mpi_check(err, __FILE__, __LINE__))
|
|
#endif
|
|
|
|
// older nvcc does not provide __ldcs and __stcs for bfloat16, despite these actually just being unsigned shorts.
|
|
// we need to be careful here to only define our own versions if none already exist, otherwise the compiler will
|
|
// complain.
|
|
// If not, you easily get "no viable overload" (for sm52) and "function already exists" (sm_80)
|
|
#if defined(ENABLE_BF16) && (__CUDACC_VER_MAJOR__ < 12) && !((__CUDA_ARCH__ >= 800) || !defined(__CUDA_ARCH__))
|
|
__device__ floatX __ldcs(const floatX* address) {
|
|
unsigned short bf = __ldcs(reinterpret_cast<const unsigned short*>(address));
|
|
return __nv_bfloat16_raw{bf};
|
|
}
|
|
|
|
__device__ void __stcs(floatX* address, floatX value) {
|
|
__stcs(reinterpret_cast<unsigned short*>(address), ((__nv_bfloat16_raw)value).x);
|
|
}
|
|
#endif
|
|
|
|
// warp-level reduction for summing values
|
|
__device__ float warpReduceSum(float val) {
|
|
for (int offset = 16; offset > 0; offset /= 2) {
|
|
val += __shfl_xor_sync(0xFFFFFFFF, val, offset);
|
|
}
|
|
return val;
|
|
}
|
|
// 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_xor_sync(0xFFFFFFFF, val, offset));
|
|
}
|
|
return val;
|
|
}
|
|
// requires all 32 threads in the warp to be active, but should work for any block size
|
|
// uses non-dynamic shared memory so every call increases shared memory requirements by 128 bytes
|
|
// the fact it's unique shared memory allows us to avoid an extra __syncthreads() call at the end
|
|
// but if called inside a loop, the shared memory will be implicitly reused, so set final_sync to 1
|
|
using reduction_func_t = float (*) (float);
|
|
template<reduction_func_t warp_reduction>
|
|
__device__ float blockReduce(float val, bool final_sync=false, float out_of_bounds=0.0f) {
|
|
// two reductions of up to 1024 threads:
|
|
// 1) inside warp (shuffle), 2) cross-warp (shared memory), 3) inside warp (shuffle)
|
|
__shared__ float shared_val[WARP_SIZE];
|
|
const int lane_id = threadIdx.x % WARP_SIZE;
|
|
const int warp_id = threadIdx.x / WARP_SIZE;
|
|
const int num_warps = blockDim.x / WARP_SIZE;
|
|
|
|
float warp_val = warp_reduction(val);
|
|
if (lane_id == 0) { shared_val[warp_id] = warp_val; }
|
|
__syncthreads();
|
|
warp_val = (lane_id < num_warps) ? shared_val[lane_id] : out_of_bounds;
|
|
float block_val = warp_reduction(warp_val);
|
|
|
|
if (final_sync) {
|
|
__syncthreads(); // only needed in loops when effectively reusing shared memory etc.
|
|
}
|
|
return block_val;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Packed128 data structure, which forces the compiler to use 128-bit loads/stores
|
|
// in GPUs that support (the LDG.128 and STS.128 instructions)
|
|
// This is a bit similar to the use of float4 in the case of 32-bit floats, but
|
|
// supports arbitrary precision.
|
|
|
|
template<class ElementType>
|
|
struct alignas(16) Packed128 {
|
|
Packed128() = default;
|
|
__device__ explicit Packed128(int4 bits) {
|
|
static_assert(sizeof(bits) == sizeof(payload), "Size mismatch.");
|
|
memcpy(&payload, &bits, sizeof(bits));
|
|
}
|
|
__device__ ElementType& operator[](int index) {
|
|
return payload[index];
|
|
}
|
|
__device__ const ElementType& operator[](int index) const {
|
|
return payload[index];
|
|
}
|
|
__device__ int4 get_bits() const {
|
|
int4 bits;
|
|
static_assert(sizeof(bits) == sizeof(payload), "Size mismatch.");
|
|
memcpy(&bits, &payload, sizeof(bits));
|
|
return bits;
|
|
}
|
|
static constexpr const size_t size = sizeof(int4) / sizeof(ElementType);
|
|
ElementType payload[size];
|
|
};
|
|
|
|
// load a Packed128 from an aligned memory address
|
|
template<class ElementType>
|
|
__device__ Packed128<ElementType> load128(const ElementType* address) {
|
|
return Packed128<ElementType>{*reinterpret_cast<const int4*>(address)};
|
|
}
|
|
// load a Packed128 from an aligned memory address with streaming cache hint
|
|
template<class ElementType>
|
|
__device__ Packed128<ElementType> load128cs(const ElementType* address) {
|
|
return Packed128<ElementType>{__ldcs(reinterpret_cast<const int4*>(address))};
|
|
}
|
|
// store a Packed128 to an aligned memory address
|
|
template<class ElementType>
|
|
__device__ void store128(ElementType* target, Packed128<ElementType> value) {
|
|
*reinterpret_cast<int4*>(target) = value.get_bits();
|
|
}
|
|
// store a Packed128 to an aligned memory address with streaming cache hint
|
|
template<class ElementType>
|
|
__device__ void store128cs(ElementType* target, Packed128<ElementType> value) {
|
|
__stcs(reinterpret_cast<int4*>(target), value.get_bits());
|
|
}
|
|
// store a Packed128 to an aligned memory address while caching in L2 but bypassing L1
|
|
template<class ElementType>
|
|
__device__ void store128cg(ElementType* target, Packed128<ElementType> value) {
|
|
__stcg(reinterpret_cast<int4*>(target), value.get_bits());
|
|
}
|
|
|
|
// short-form typedefs
|
|
typedef Packed128<float> f128;
|
|
typedef Packed128<floatX> x128;
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Random Number Generatiom
|
|
|
|
// Simple xorshift RNG
|
|
__device__ __host__ 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;
|
|
}
|
|
__device__ __host__ float random_f32(unsigned long long *state) { // random float32 in [0,1)
|
|
return (random_u32(state) >> 8) / 16777216.0f;
|
|
}
|
|
|
|
// SquirrelNoise5 - Squirrel's Raw Noise utilities (version 5)
|
|
// This gives us a random number from threadIdx/blockIdx + a single seed for the entire GPU
|
|
// todo - possibly overkill and we don't need such high quality random numbers? (tbd)
|
|
// http://eiserloh.net/noise/SquirrelNoise5.hpp
|
|
__device__ __host__ constexpr unsigned int SquirrelNoise5(int positionX, unsigned int seed)
|
|
{
|
|
constexpr unsigned int SQ5_BIT_NOISE1 = 0xd2a80a3f; // 11010010101010000000101000111111
|
|
constexpr unsigned int SQ5_BIT_NOISE2 = 0xa884f197; // 10101000100001001111000110010111
|
|
constexpr unsigned int SQ5_BIT_NOISE3 = 0x6C736F4B; // 01101100011100110110111101001011
|
|
constexpr unsigned int SQ5_BIT_NOISE4 = 0xB79F3ABB; // 10110111100111110011101010111011
|
|
constexpr unsigned int SQ5_BIT_NOISE5 = 0x1b56c4f5; // 00011011010101101100010011110101
|
|
unsigned int mangledBits = (unsigned int) positionX;
|
|
mangledBits *= SQ5_BIT_NOISE1;
|
|
mangledBits += seed;
|
|
mangledBits ^= (mangledBits >> 9);
|
|
mangledBits += SQ5_BIT_NOISE2;
|
|
mangledBits ^= (mangledBits >> 11);
|
|
mangledBits *= SQ5_BIT_NOISE3;
|
|
mangledBits ^= (mangledBits >> 13);
|
|
mangledBits += SQ5_BIT_NOISE4;
|
|
mangledBits ^= (mangledBits >> 15);
|
|
mangledBits *= SQ5_BIT_NOISE5;
|
|
mangledBits ^= (mangledBits >> 17);
|
|
return mangledBits;
|
|
}
|
|
__device__ __host__ constexpr unsigned int Get2dNoiseUint(int indexX, int indexY, unsigned int seed)
|
|
{
|
|
constexpr int PRIME_NUMBER = 198491317; // Large prime number with non-boring bits
|
|
return SquirrelNoise5(indexX + (PRIME_NUMBER * indexY), seed);
|
|
}
|
|
|
|
// stochastic rounding built on top of Squirel Noise above (with seed updated per step via xorshift)
|
|
__device__ __forceinline__ void stochastic_rounding(float in, __nv_bfloat16 *out, unsigned int seed) {
|
|
// todo - is this stochastic rounding *too good*? can we cut any corners?
|
|
unsigned int random = Get2dNoiseUint(threadIdx.x, blockIdx.x, seed);
|
|
unsigned int threshold = random & 0xFFFF;
|
|
unsigned int float_bits = __float_as_uint(in);
|
|
unsigned int rounded_bits = float_bits & 0x0000FFFF;
|
|
float_bits = (rounded_bits > threshold) ? (float_bits | 0xFFFF) : (float_bits & ~0xFFFF);
|
|
*out = __float2bfloat16_rn(__uint_as_float(float_bits));
|
|
}
|
|
__device__ __forceinline__ void stochastic_rounding(float in, half *out, unsigned int random) {
|
|
*out = (float)in; // todo - implement this...
|
|
}
|
|
__device__ __forceinline__ void stochastic_rounding(float in, float *out, unsigned int random) {
|
|
*out = in; // dummy function for when floatX is float (FP32 mode)
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// MPI / multi-processing setup
|
|
|
|
// Parameters specific to training on multiple GPUs.
|
|
typedef struct {
|
|
int process_rank; // Rank of this process among all MPI processes. 0 if no multi-GPU.
|
|
int num_processes; // Total number of processes. 1 if no multi-GPU.
|
|
int local_device_idx; // This process GPU index on current machine. 0 if no multi-GPU.
|
|
|
|
// Zero Redundancy Optimizer stage - https://fairscale.readthedocs.io/en/stable/deep_dive/oss_sdp_fsdp.html
|
|
// 0-Disabled
|
|
// 1-Optimizer State Sharding (OSS)
|
|
// 2-Optimizer + Gradient State Sharding (SDP)
|
|
// 3-Optimizer + Gradient + Horizontal Model Sharding (FSDP)
|
|
int zero_stage;
|
|
size_t shard_num_parameters;
|
|
size_t shard_offset;
|
|
#ifdef MULTI_GPU
|
|
ncclComm_t nccl_comm; // NCCL communication primitive, used for collective multi-GPU work.
|
|
#endif
|
|
} MultiGpuConfig;
|
|
|
|
// one global variable to hold the multi-GPU configuration for this process
|
|
MultiGpuConfig multi_gpu_config;
|
|
|
|
#ifdef MULTI_GPU
|
|
// Determine which GPU this process should use.
|
|
// Processes on the same machines use different GPU indicies. Processes on other machines don't.
|
|
// Copied from NCCL examples: https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/examples.html#example-2-one-device-per-process-or-thread
|
|
int multi_gpu_get_local_device_idx(int process_rank, int num_processes) {
|
|
char hostname[1024];
|
|
hostname[1023] = '\0';
|
|
// All processes on the same machine will share the same hostname.
|
|
gethostname(hostname, 1023);
|
|
for (int i=0; i < 1024; i++) {
|
|
if (hostname[i] == '.') {
|
|
hostname[i] = '\0';
|
|
break;
|
|
}
|
|
}
|
|
uint64_t hostname_hash = 5381;
|
|
for (int c = 0; hostname[c] != '\0'; c++){ hostname_hash = ((hostname_hash << 5) + hostname_hash) ^ hostname[c]; }
|
|
|
|
// Distribute all hostname hashes to all processes.
|
|
uint64_t* all_hostsname_hashes = (uint64_t*)malloc(num_processes * sizeof(uint64_t));
|
|
all_hostsname_hashes[process_rank] = hostname_hash;
|
|
mpiCheck(MPI_Allgather(MPI_IN_PLACE, 0, MPI_DATATYPE_NULL, all_hostsname_hashes, sizeof(uint64_t), MPI_BYTE, MPI_COMM_WORLD));
|
|
|
|
// Identify which GPU we need to use.
|
|
int local_device_idx = 0;
|
|
for (int current_process = 0; current_process < num_processes; ++current_process) {
|
|
if (current_process == process_rank) {
|
|
// Found my gpu, local_device_idx now has my target GPU index.
|
|
break;
|
|
}
|
|
if (all_hostsname_hashes[current_process] == all_hostsname_hashes[process_rank]) {
|
|
// This process ID runs on the same machine, but it's not me, skip this GPU
|
|
local_device_idx++;
|
|
}
|
|
}
|
|
|
|
free(all_hostsname_hashes);
|
|
return local_device_idx;
|
|
}
|
|
#endif
|
|
|
|
MultiGpuConfig multi_gpu_config_init(int *argc, char ***argv) {
|
|
#ifdef MULTI_GPU
|
|
// Initialize MPI.
|
|
MultiGpuConfig result;
|
|
mpiCheck(MPI_Init(argc, argv));
|
|
mpiCheck(MPI_Comm_rank(MPI_COMM_WORLD, &result.process_rank));
|
|
mpiCheck(MPI_Comm_size(MPI_COMM_WORLD, &result.num_processes));
|
|
result.local_device_idx = multi_gpu_get_local_device_idx(result.process_rank, result.num_processes);
|
|
cudaCheck(cudaSetDevice(result.local_device_idx));
|
|
ncclUniqueId nccl_id;
|
|
if (result.process_rank == 0) {
|
|
ncclCheck(ncclGetUniqueId(&nccl_id));
|
|
}
|
|
mpiCheck(MPI_Bcast((void *)&nccl_id, sizeof(nccl_id), MPI_BYTE, 0, MPI_COMM_WORLD));
|
|
ncclCheck(ncclCommInitRank(&result.nccl_comm, result.num_processes, nccl_id, result.process_rank));
|
|
return result;
|
|
#else
|
|
printf("Multi-GPU support is disabled. Using a single GPU.\n");
|
|
cudaCheck(cudaSetDevice(0));
|
|
MultiGpuConfig result;
|
|
result.process_rank = 0;
|
|
result.num_processes = 1;
|
|
result.local_device_idx = 0;
|
|
return result;
|
|
#endif
|
|
}
|
|
|
|
void multi_gpu_config_free(const MultiGpuConfig* multi_gpu_config) {
|
|
#ifdef MULTI_GPU
|
|
ncclCheck(ncclCommDestroy(multi_gpu_config->nccl_comm));
|
|
mpiCheck(MPI_Finalize());
|
|
#endif
|
|
}
|
|
|
|
// convenience function that only prints if the rank of process is zero
|
|
void printf0(const char *format, ...) {
|
|
if (multi_gpu_config.process_rank == 0) {
|
|
va_list args;
|
|
va_start(args, format);
|
|
vprintf(format, args);
|
|
va_end(args);
|
|
}
|
|
}
|
|
|
|
void set_zero_configs(MultiGpuConfig* multi_gpu_config, int zero_stage, size_t total_parameters) {
|
|
|
|
multi_gpu_config->zero_stage = 0;
|
|
multi_gpu_config->shard_num_parameters = total_parameters;
|
|
multi_gpu_config->shard_offset = 0;
|
|
|
|
// Check the Zero Stage and define sharding parameters
|
|
if (zero_stage == 0) {
|
|
printf0("| Zero Optimization is disabled |\n");
|
|
}
|
|
else if (zero_stage == 1) {
|
|
if (total_parameters % multi_gpu_config->num_processes != 0) {
|
|
printf0("| Zero Optimization is disabled, Can't equally partition parameters |\n");
|
|
multi_gpu_config->zero_stage = 0;
|
|
}
|
|
else {
|
|
printf0("| Zero Stage1 is enabled |\n");
|
|
multi_gpu_config->zero_stage = 1;
|
|
multi_gpu_config->shard_num_parameters = total_parameters / multi_gpu_config->num_processes;
|
|
multi_gpu_config->shard_offset = multi_gpu_config->process_rank * multi_gpu_config->shard_num_parameters;
|
|
}
|
|
}
|
|
else{
|
|
printf0("| Disabling Zero Optimization, Zero Stage2 and Stage3 are not yet supported |\n");
|
|
multi_gpu_config->zero_stage = 0;
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// cuDNN path
|
|
#ifdef ENABLE_CUDNN
|
|
// functions defined in cudnn_att.cu
|
|
void create_cudnn();
|
|
void destroy_cudnn();
|
|
void attention_forward_cudnn(floatX* out, // output: (B, T, NH, HS)
|
|
float* stats, // output for backward pass: (B, NH, T)
|
|
floatX* inp, // input: (B, T, 3, NH, HS) QKV
|
|
int B, int T, int NH, int C);
|
|
|
|
void attention_backward_cudnn(floatX* dqkvr, // output
|
|
floatX* dout, floatX* qkvr, floatX* o, float* stats, // inputs
|
|
int B, int T, int NH, int C);
|
|
#else
|
|
void create_cudnn() {}
|
|
void destroy_cudnn() {}
|
|
#endif // ENABLE_CUDNN
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// all the kernels
|
|
|
|
__global__ void encoder_forward_kernel3(floatX* out,
|
|
const int* inp, const floatX* wte, const floatX* wpe,
|
|
int B, int T, int C) {
|
|
int idx = (blockIdx.x * blockDim.x + threadIdx.x) * x128::size;
|
|
int N = B * T * C;
|
|
if (idx >= N) { return; }
|
|
|
|
int bt = idx / C;
|
|
int b = bt / T;
|
|
int t = bt % T;
|
|
int c = idx % C;
|
|
|
|
int ix = inp[b * T + t];
|
|
|
|
floatX* out_btc = out + b * T * C + t * C + c;
|
|
const floatX* wte_ix = wte + ix * C + c;
|
|
const floatX* wpe_tc = wpe + t * C + c;
|
|
|
|
x128 packed_out;
|
|
x128 wte128 = load128cs(wte_ix);
|
|
x128 wpe128 = load128cs(wpe_tc);
|
|
for (int k = 0; k < x128::size; k++) {
|
|
packed_out[k] = (floatX)((float)wte128[k] + (float)wpe128[k]);
|
|
}
|
|
store128(out_btc, packed_out);
|
|
}
|
|
|
|
template <int BLOCK_SIZE=256>
|
|
__global__ void wte_backward_kernel(floatX* dwte,
|
|
const int4* bucket_info, const int* workload_indices, const floatX* dout, const int* inp,
|
|
unsigned int seed, int B, int T, int C) {
|
|
// In order to be deterministic, we preprocess the inputs on the cpu into "buckets"
|
|
// Each bucket corresponds to (WARP_SIZE * x128::size) channels for a single vocabulary token
|
|
// Each thread handles x128::size channels, e.g. 256 per warp for BF16
|
|
// Each block handles (BLOCK_SIZE / WARP_SIZE) elements in a single bucket in parallel
|
|
// If a bucket has less than 8 elements, some warps will return immediately
|
|
// If a bucket has more than 8 elements, we will loop over all of them
|
|
// The buckets are sorted on the CPU so the largest buckets start 1st
|
|
int bucket = blockIdx.x;
|
|
int warp_id = threadIdx.x / WARP_SIZE;
|
|
int lane_id = threadIdx.x % WARP_SIZE;
|
|
int c_per_warp = WARP_SIZE * x128::size;
|
|
|
|
int bucket_start_idx = bucket_info[bucket].x;
|
|
int bucket_size = bucket_info[bucket].y;
|
|
int bucket_ix = bucket_info[bucket].z;
|
|
int c = bucket_info[bucket].w * c_per_warp + (lane_id * x128::size);
|
|
|
|
// Each thread handles "x128::size" channels, so at fp8, each warp would handle 512 channels
|
|
// If C is not a multiple of this (e.g. 768), some buckets/c_groups cannot use the entire warp
|
|
if (c >= C) { return; }
|
|
// Exit early if this is a small bucket and this warp doesn't have any items to process
|
|
if (warp_id >= bucket_size) { return; }
|
|
|
|
float accum[x128::size] = {0.0f};
|
|
__shared__ float accum_shared[x128::size * BLOCK_SIZE];
|
|
|
|
for(int item = warp_id; item < bucket_size; item += BLOCK_SIZE/WARP_SIZE) {
|
|
int bt = workload_indices[bucket_start_idx + item];
|
|
int b = bt / T;
|
|
int t = bt % T;
|
|
|
|
const floatX* dout_btc = dout + b * T * C + t * C + c;
|
|
x128 packed_inp1 = load128cs(dout_btc);
|
|
for (int k = 0; k < packed_inp1.size; k++) {
|
|
accum[k] += (float)packed_inp1[k];
|
|
}
|
|
}
|
|
|
|
if (warp_id != 0) {
|
|
// we accumulate into warp 0, so only the other warps need to write to shared memory
|
|
for (int k = 0; k < x128::size; k++) {
|
|
accum_shared[threadIdx.x + k * BLOCK_SIZE] = accum[k];
|
|
}
|
|
return; // only warp 0 is needed after writing to shared memory
|
|
}
|
|
|
|
// Read dwte for warp 0 even if other warps are not finished yet to maximise latency tolerance
|
|
floatX* dwte_ix = dwte + bucket_ix * C + c;
|
|
x128 packed_in_out = load128(dwte_ix);
|
|
|
|
// note: threads which have returned are considered synchronised by CUDA so no risk of deadlock
|
|
__syncthreads();
|
|
|
|
// Accumulate into warp 0's registers by reading the values of the other warps in shared memory
|
|
for (int i = threadIdx.x+WARP_SIZE; i < min(BLOCK_SIZE, bucket_size*WARP_SIZE); i += WARP_SIZE) {
|
|
for (int k = 0; k < x128::size; k++) {
|
|
accum[k] += accum_shared[i + k * BLOCK_SIZE];
|
|
}
|
|
}
|
|
|
|
// Add the result to dwte and write back to global memory (read-modify-write)
|
|
for (unsigned int k = 0; k < x128::size; k++) {
|
|
// We use stochastic rounding to go from FP32 to BF16 but the seed should be deterministic
|
|
stochastic_rounding(accum[k] + (float)packed_in_out[k], &packed_in_out[k], seed + k);
|
|
}
|
|
store128(dwte_ix, packed_in_out);
|
|
}
|
|
|
|
__global__ void wpe_backward_kernel(floatX* dwpe,
|
|
const floatX* dout, const int* inp,
|
|
int B, int T, int C, unsigned int seed) {
|
|
// Each thread handles x128::size "channel positions", e.g. 256 per warp for BF16
|
|
// For gpt2-124M BF16, C=768 and T=1024, so 3 warps per channel and 3072 warps in total
|
|
// For each "channel position" we sum the gradients for every batch at that C/T element
|
|
// This way each dwte element is only updated once, and the kernel is fully deterministic!
|
|
// The previous kernel was not deterministic, as batches were aggregated with atomicAdd
|
|
int idx = (blockIdx.x * blockDim.x + threadIdx.x) * x128::size;
|
|
if (idx >= T * C) { return; }
|
|
|
|
// if C is not a multiple of WARP_SIZE*x128::size, it's OK for some warps to handle multiple t
|
|
int t = idx / C;
|
|
int c = idx % C;
|
|
float accum[x128::size] = {0.0f};
|
|
|
|
for (int b = 0; b < B; b++) {
|
|
x128 packed_dout = load128cs(dout + (b * T * C) + (t * C) + c); // will never be read again
|
|
for (int k = 0; k < x128::size; k++) {
|
|
accum[k] += (float)packed_dout[k];
|
|
}
|
|
}
|
|
|
|
floatX* dwpe_tc = dwpe + (t * C) + c;
|
|
x128 packed_dwpe = load128(dwpe_tc);
|
|
for (unsigned int k = 0; k < x128::size; k++) {
|
|
// We use stochastic rounding to go from FP32 to BF16 but the seed should be deterministic
|
|
stochastic_rounding(accum[k] + (float)packed_dwpe[k], &packed_dwpe[k], seed + k);
|
|
}
|
|
store128(dwpe_tc, packed_dwpe);
|
|
}
|
|
|
|
__global__ void layernorm_forward_kernel3(floatX* __restrict__ out, floatX* __restrict__ mean, floatX* __restrict__ rstd,
|
|
const floatX* __restrict__ inp, const floatX* __restrict__ weight,
|
|
const floatX* __restrict__ bias, int N, int C) {
|
|
int lane_id = threadIdx.x % WARP_SIZE;
|
|
int warp_id = threadIdx.x / WARP_SIZE;
|
|
int num_warps = blockDim.x / WARP_SIZE;
|
|
|
|
int idx = blockIdx.x * num_warps + warp_id;
|
|
if(idx >= N) { return; } // guard
|
|
|
|
// the row of input that this group of threads is responsible for
|
|
const floatX* x = inp + idx * C;
|
|
|
|
// mean
|
|
float sum = 0.0f;
|
|
for (int i = lane_id; i < C; i += WARP_SIZE) {
|
|
sum += (float)x[i];
|
|
}
|
|
sum = warpReduceSum(sum);
|
|
float m = sum / C;
|
|
if(lane_id == 0 && mean != nullptr) {
|
|
__stcs(mean + idx, (floatX)m);
|
|
}
|
|
|
|
// rstd
|
|
sum = 0.0f;
|
|
for (int i = lane_id; i < C; i += WARP_SIZE) {
|
|
float diff = (float)x[i] - m;
|
|
sum += diff * diff;
|
|
}
|
|
sum = warpReduceSum(sum);
|
|
float s = rsqrtf(sum / C + 1e-5f);
|
|
if(lane_id == 0 && rstd != nullptr) {
|
|
__stcs(rstd + idx, (floatX)s);
|
|
}
|
|
|
|
// final normalization and scaling by weight/bias
|
|
floatX* o = out + idx * C;
|
|
for (int c = lane_id; 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 * ((float)__ldcs(x+c) - m);
|
|
__stcs(o+c, (floatX)(n * (float)weight[c] + (float)bias[c]));
|
|
}
|
|
}
|
|
|
|
__global__ void fused_residual_forward_kernel5(floatX* residual, floatX* normed, floatX* mean, floatX* rstd,
|
|
const floatX* inp1, const floatX* inp2,
|
|
const floatX* weight, const floatX* bias,
|
|
int N, int C) {
|
|
assert(blockDim.x == WARP_SIZE);
|
|
|
|
// load weights and biases into shared memory
|
|
// do this before we allow any threads to exit!
|
|
extern __shared__ char* params[];
|
|
// load128/store128 sometimes generated multiple instructions when the types here were floatX*, so
|
|
// let's keep everything as x128
|
|
x128* s_weight = reinterpret_cast<x128*>(params);
|
|
x128* s_bias = reinterpret_cast<x128*>(params) + (C / x128::size);
|
|
x128* s_res = reinterpret_cast<x128*>(params) + ((2 + threadIdx.y) * C / x128::size);
|
|
|
|
int sidx = (threadIdx.x + WARP_SIZE * threadIdx.y) * x128::size;
|
|
for(int i = sidx; i < C; i += blockDim.y * WARP_SIZE * x128::size) {
|
|
s_weight[i/x128::size] = load128(weight + i);
|
|
s_bias[i/x128::size] = load128(bias + i);
|
|
}
|
|
__syncthreads();
|
|
|
|
int idx = blockIdx.x * blockDim.y + threadIdx.y;
|
|
if(idx > N) return;
|
|
|
|
// adjust pointers to current token
|
|
residual += C * idx;
|
|
normed += C * idx;
|
|
inp1 += C * idx;
|
|
inp2 += C * idx;
|
|
|
|
const float eps = 1e-5f;
|
|
float sum = 0.0f;
|
|
for(int c = threadIdx.x * x128::size; c < C; c += WARP_SIZE * x128::size) {
|
|
const x128 in1 = load128cs(inp1 + c);
|
|
const x128 in2 = load128cs(inp2 + c);
|
|
x128 out;
|
|
for(int k = 0; k < x128::size; ++k) {
|
|
out[k] = (float)in1[k] + (float)in2[k];
|
|
sum += (float)out[k];
|
|
}
|
|
store128cs(residual + c, out);
|
|
s_res[c / x128::size] = out;
|
|
}
|
|
|
|
sum = warpReduceSum(sum);
|
|
float m = sum / C;
|
|
float v = 0.f;
|
|
|
|
for(int c = threadIdx.x * x128::size; c < C; c += WARP_SIZE * x128::size) {
|
|
const x128 res = s_res[c / x128::size];
|
|
for(int k = 0; k < x128::size; ++k) {
|
|
v += ((float)res[k] - m) * ((float)res[k] - m);
|
|
}
|
|
}
|
|
|
|
v = warpReduceSum(v) / C;
|
|
float s = rsqrtf(v + eps);
|
|
|
|
for(int c = threadIdx.x * x128::size; c < C; c += WARP_SIZE * x128::size) {
|
|
const x128 res = s_res[c / x128::size];
|
|
const x128 w = s_weight[c / x128::size];
|
|
const x128 b = s_bias[c / x128::size];
|
|
x128 out;
|
|
for(int k = 0; k < x128::size; ++k) {
|
|
float n = s * ((float)res[k] - m); // normalized output
|
|
float o = n * (float)w[k] + (float)b[k]; // scale and shift it
|
|
out[k] = o;
|
|
}
|
|
|
|
store128cs(normed + c, out);
|
|
}
|
|
// cache the mean and rstd for the backward pass later
|
|
if(threadIdx.x == 0) {
|
|
mean[idx] = m;
|
|
rstd[idx] = s;
|
|
}
|
|
}
|
|
|
|
|
|
// inputs floatX, outputs FP32 (for current FP32-only activation path for this WIP)
|
|
__global__ void permute_kernel(floatX* q, floatX* k, floatX* v,
|
|
const floatX* 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;
|
|
if (idx >= B * NH * N * d) { return; }
|
|
|
|
// Q[b][nh_][n][d_] = inp[b][n][0][nh_][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 permute_kernel_backward(floatX* dinp,
|
|
const floatX* dq, const floatX* dk, const floatX* dv,
|
|
int B, int N, int NH, int d) {
|
|
int idx = blockIdx.x * blockDim.x + threadIdx.x;
|
|
if (idx >= B * NH * N * d) { return; }
|
|
|
|
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_;
|
|
dinp[inp_idx] = dq[idx];
|
|
dinp[inp_idx + NH * d] = dk[idx];
|
|
dinp[inp_idx + 2 * (NH * d)] = dv[idx];
|
|
}
|
|
|
|
__global__ void unpermute_kernel(floatX* inp, floatX *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) { return; }
|
|
|
|
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]);
|
|
}
|
|
|
|
__global__ void unpermute_kernel_backward(floatX* dinp, const floatX *dout, int B, int N, int NH, int d) {
|
|
int idx = blockIdx.x * blockDim.x + threadIdx.x;
|
|
if (idx >= B * NH * N * d) { return; }
|
|
|
|
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_;
|
|
dinp[idx] = (floatX)dout[other_idx];
|
|
}
|
|
|
|
__global__ void softmax_forward_kernel5(floatX* out, float inv_temperature, const floatX* 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);
|
|
int lane_id = threadIdx.x % WARP_SIZE;
|
|
int warp_id = threadIdx.x / WARP_SIZE;
|
|
int num_warps = blockDim.x / WARP_SIZE;
|
|
|
|
// micro-optimization: we iterate backwards so that
|
|
// after the softmax backward operation completes, the cache retains the
|
|
// part of the matrix close to the upper left corner, which benefits the
|
|
// matmul operation that immediately follows.
|
|
// int idx = blockIdx.x * warp.meta_group_size() + warp.meta_group_rank(); // forward order
|
|
int idx = (gridDim.x - blockIdx.x - 1) * num_warps + warp_id; // backward order
|
|
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 floatX* x = inp + idx * T;
|
|
|
|
// not INF, so we don't get NaNs accidentally when subtracting two values.
|
|
const float flt_max = 340282346638528859811704183484516925440.0f; // to avoid including float.h
|
|
float maxval = -flt_max;
|
|
float sumval = 0.0f;
|
|
|
|
const floatX* x_aligned = reinterpret_cast<const floatX*>(__builtin_assume_aligned(x, 16));
|
|
for (int i = lane_id; i < pos_by_4; i += WARP_SIZE) {
|
|
float regarray[4];
|
|
for (int k = 0; k < 4; ++k) {
|
|
regarray[k] = (float)x_aligned[4*i + k];
|
|
}
|
|
float old_maxval = maxval;
|
|
for(int k = 0; k < 4; ++k) {
|
|
maxval = fmaxf(maxval, regarray[k]);
|
|
}
|
|
sumval *= expf(inv_temperature * (old_maxval - maxval));
|
|
for(int k = 0; k < 4; ++k) {
|
|
sumval += expf(inv_temperature * (regarray[k] - maxval));
|
|
}
|
|
}
|
|
|
|
if(4*pos_by_4 + lane_id <= own_pos) {
|
|
float old_maxval = maxval;
|
|
maxval = fmaxf(maxval, (float)x[4*pos_by_4 + lane_id]);
|
|
sumval *= expf(inv_temperature * (old_maxval - maxval));
|
|
sumval += expf(inv_temperature * ((float)x[4*pos_by_4 + lane_id] - maxval));
|
|
}
|
|
|
|
float global_maxval = warpReduceMax(maxval);
|
|
sumval *= expf(inv_temperature * (maxval - global_maxval));
|
|
|
|
float sum = warpReduceSum(sumval);
|
|
float norm = 1.f / sum;
|
|
|
|
// divide the whole row by the sum
|
|
for (int i = lane_id; i <= own_pos; i += WARP_SIZE) {
|
|
// recalculation is faster than doing the round-trip through memory.
|
|
float ev = expf(inv_temperature * ((float)__ldcs(x + i) - global_maxval));
|
|
__stcs(out + idx * T + i, (floatX)(ev * norm));
|
|
}
|
|
}
|
|
|
|
__global__ void residual_forward_kernel(floatX* out, const floatX* inp1, const floatX* inp2) {
|
|
int idx = (blockIdx.x * blockDim.x + threadIdx.x) * x128::size;
|
|
|
|
x128 packed_out;
|
|
x128 packed_inp1 = load128cs(inp1 + idx);
|
|
x128 packed_inp2 = load128cs(inp2 + idx);
|
|
for (int k = 0; k < packed_inp1.size; k++) {
|
|
packed_out[k] = (floatX)((float)packed_inp1[k] + (float)packed_inp2[k]);
|
|
}
|
|
store128(out + idx, packed_out);
|
|
}
|
|
|
|
#define GELU_SCALING_FACTOR sqrtf(2.0f / M_PI)
|
|
__global__ void gelu_forward_kernel2(floatX* out, const floatX* inp) {
|
|
int idx = (blockIdx.x * blockDim.x + threadIdx.x) * x128::size;
|
|
|
|
x128 packed_out;
|
|
x128 packed_inp = load128cs(inp + idx); // load and do not keep in cache
|
|
for(int k = 0; k < packed_inp.size; ++k) {
|
|
float xi = (float)packed_inp[k];
|
|
float cube = 0.044715f * xi * xi * xi;
|
|
packed_out[k] = (floatX)(0.5f * xi * (1.0f + tanhf(GELU_SCALING_FACTOR * (xi + cube))));
|
|
}
|
|
// store instead of storecs (without cache streaming) in case it is useful for the
|
|
// data to be in the cache for the next operation after this GeLU
|
|
store128(out + idx, packed_out);
|
|
}
|
|
|
|
__global__ void gelu_backward_kernel(floatX* dinp, const floatX* inp, const floatX* dout) {
|
|
int idx = (blockIdx.x * blockDim.x + threadIdx.x) * x128::size;
|
|
|
|
x128 packed_dinp;
|
|
x128 packed_inp = load128cs(inp + idx);
|
|
x128 packed_dout = load128cs(dout + idx);
|
|
for (int k = 0; k < packed_inp.size; ++k) {
|
|
float x = (float)packed_inp[k];
|
|
float cube = 0.044715f * x * x * x;
|
|
float tanh_arg = GELU_SCALING_FACTOR * (x + cube);
|
|
float tanh_out = tanhf(tanh_arg);
|
|
float coshf_out = coshf(tanh_arg);
|
|
float sech_out = 1.0f / (coshf_out * coshf_out);
|
|
float local_grad = 0.5f * (1.0f + tanh_out) + x * 0.5f * sech_out * GELU_SCALING_FACTOR * (1.0f + 3.0f * 0.044715f * x * x);
|
|
packed_dinp[k] = (floatX)(local_grad * (float)packed_dout[k]);
|
|
}
|
|
store128(dinp + idx, packed_dinp);
|
|
}
|
|
|
|
template<typename OutFloat, bool UseAuxBuffer>
|
|
__global__ void matmul_backward_bias_kernel9(OutFloat* dbias, const floatX* dout, int B, int T, int OC,
|
|
std::bool_constant<UseAuxBuffer>) {
|
|
constexpr const int bdx = 4;
|
|
constexpr const int bdy = WARP_SIZE / bdx;
|
|
assert(blockDim.x == bdx);
|
|
assert(blockDim.y == bdy);
|
|
|
|
int warp_d = (int)threadIdx.x;
|
|
int warp_c = (int)threadIdx.y;
|
|
int block_d = (int)threadIdx.z;
|
|
|
|
const int OC_per_warp = bdy * x128::size; // 64 at BF16
|
|
|
|
int local_oc = warp_c * x128::size;
|
|
int global_oc = blockIdx.x * OC_per_warp + local_oc;
|
|
|
|
int local_bt = warp_d + bdx * block_d;
|
|
int bt_per_block = bdx * blockDim.z;
|
|
|
|
float accumulators[x128::size];
|
|
for (int k = 0; k < x128::size; k++) {
|
|
accumulators[k] = 0.0f;
|
|
}
|
|
|
|
if(global_oc < OC) {
|
|
// sum up over all bt within registers
|
|
for (int idx = blockIdx.y * bt_per_block + local_bt; idx < B * T; idx += gridDim.y * bt_per_block) {
|
|
x128 packed_dout = load128(dout + global_oc + idx*OC);
|
|
for (int k = 0; k < x128::size; k++) {
|
|
accumulators[k] += (float)packed_dout[k];
|
|
}
|
|
}
|
|
}
|
|
|
|
__shared__ float sub_results[x128::size][WARP_SIZE][bdy];
|
|
|
|
// reduce within-warp results
|
|
for (int k = 0; k < x128::size; k++) {
|
|
float v = accumulators[k];
|
|
v += __shfl_down_sync(0xffffffff, v, 1, 4);
|
|
v += __shfl_down_sync(0xffffffff, v, 2, 4);
|
|
if(warp_d == 0) {
|
|
sub_results[k][block_d][warp_c] = v;
|
|
}
|
|
}
|
|
__syncthreads();
|
|
|
|
// block-wide reductions
|
|
for (int k = block_d; k < x128::size; k += blockDim.z) {
|
|
float a = 0.f;
|
|
for (int r = warp_d; r < blockDim.z; r += bdx) {
|
|
float v = sub_results[k][r][warp_c];
|
|
v += __shfl_down_sync(0xffffffff, v, 1, 4);
|
|
v += __shfl_down_sync(0xffffffff, v, 2, 4);
|
|
a += v;
|
|
}
|
|
if(warp_d == 0 && global_oc < OC) {
|
|
if constexpr (!UseAuxBuffer) {
|
|
dbias[global_oc + k] = (OutFloat)(a + (float)dbias[global_oc + k]);
|
|
} else {
|
|
dbias[global_oc + k + blockIdx.y * OC] = a;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
__global__ void reduce_add_sum_kernel(floatX* dst, const float* src, size_t n, size_t m) {
|
|
const size_t idx = (blockIdx.x * blockDim.x + threadIdx.x) * f128::size;
|
|
assert(n % x128::size == 0);
|
|
if (idx < n) {
|
|
f128 acc;
|
|
for(int k = 0; k < f128::size; ++k) {
|
|
acc[k] = 0.f;
|
|
}
|
|
|
|
for(int l = 0; l < m; ++l) {
|
|
f128 s = load128(src + idx + n * l);
|
|
for(int k = 0; k < f128::size; ++k) {
|
|
acc[k] += s[k];
|
|
}
|
|
}
|
|
for(int k = 0; k < f128::size; ++k) {
|
|
dst[idx + k] = (floatX) ((float)dst[idx + k] + acc[k]);
|
|
}
|
|
}
|
|
}
|
|
|
|
__global__ void __launch_bounds__(512, 2) // todo - any warnings on Turing with only 1024 threads?
|
|
layernorm_backward_kernel9(floatX* dinp, floatX* dweight, floatX* dbias, float* scratch,
|
|
const floatX* dout, const floatX* inp, const floatX* weight,
|
|
const floatX* mean, const floatX* rstd,
|
|
int B, int T, int C) {
|
|
extern __shared__ float shared[]; // size = 2*C + 2*block_size + 1
|
|
int warpsInBlock = blockDim.x / WARP_SIZE; //number of warps in block
|
|
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);
|
|
|
|
// the first half of shared memory is bias, second is weight
|
|
float* dbias_shared = shared;
|
|
float* dweight_shared = shared + C;
|
|
float* dbias_tmp_shared = shared + 2 * C;
|
|
float* dweight_tmp_shared = shared + 2 * C + blockDim.x;
|
|
|
|
// init shared memory to zero
|
|
for(int i = threadIdx.x; i < C; i+= blockDim.x){
|
|
dbias_shared[i] = 0.0f;
|
|
dweight_shared[i] = 0.0f;
|
|
}
|
|
unsigned int *tmp_flag = (unsigned int*)(shared + 2*C + 2*blockDim.x);
|
|
__syncthreads();
|
|
|
|
for (int idx = baseIdx; idx < B * T; idx += warpsInGrid) {
|
|
int b = idx / T;
|
|
int t = idx % T;
|
|
|
|
const floatX* dout_bt = dout + b * T * C + t * C;
|
|
const floatX* inp_bt = inp + b * T * C + t * C;
|
|
floatX* dinp_bt = dinp + b * T * C + t * C;
|
|
const float mean_bt = (float)mean[b * T + t];
|
|
const float rstd_bt = (float)rstd[b * T + t];
|
|
|
|
// 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 norm_bti = ((float)inp128_i[k] - mean_bt) * rstd_bt;
|
|
float dnorm_i = (float)weight128_i[k] * (float)dout128_i[k];
|
|
dnorm_mean += dnorm_i;
|
|
dnorm_norm_mean += dnorm_i * norm_bti;
|
|
}
|
|
}
|
|
dnorm_mean = warpReduceSum(dnorm_mean) / C;
|
|
dnorm_norm_mean = warpReduceSum(dnorm_norm_mean) / C;
|
|
|
|
// now iterate again and accumulate all the gradients
|
|
// unfortunately we cannot use the same index for x128 arrays and shared memory
|
|
// as atomics can only be 32-bit rather than 128-bit (at least pre-SM90/Hopper)
|
|
// so this would result in an 8-way bank conflict, and kill performance
|
|
// so instead, we use a shared memory friendly index, and reorder before the final write
|
|
for (int i = 0; i < iterations_C; i++) {
|
|
int global_index = (warpThreadIdx * x128::size) + (i * C_per_iteration);
|
|
int shared_index = warpThreadIdx + (i * C_per_iteration);
|
|
if (global_index >= C) {
|
|
break;
|
|
}
|
|
|
|
x128 dout128 = load128cs(dout_bt + global_index);
|
|
x128 inp128 = load128cs(inp_bt + global_index);
|
|
x128 dinp128 = load128(dinp_bt + global_index);
|
|
x128 weight128 = load128(weight + global_index);
|
|
|
|
for (int x = 0; x < x128::size; x++) {
|
|
float dout_i = (float)dout128[x];
|
|
float norm_bti = ((float)inp128[x] - mean_bt) * rstd_bt;
|
|
float dnorm_i = (float)weight128[x] * dout_i;
|
|
|
|
// sum up the gradients for bias and weight across the entire block
|
|
// this is basically a reduction (but only inter-warp, not intra-warp)
|
|
// doing it this way allows us to avoid using atomics while using many warps
|
|
if (warpId != 0) {
|
|
dbias_tmp_shared[threadIdx.x] = dout_i;
|
|
dweight_tmp_shared[threadIdx.x] = norm_bti * dout_i;
|
|
}
|
|
__syncthreads();
|
|
if (warpId == 0) {
|
|
float dbias_tmp = dout_i;
|
|
float dweight_tmp = norm_bti * dout_i;
|
|
for (int j = 1; j < warpsInBlock; j++) {
|
|
dbias_tmp += dbias_tmp_shared[threadIdx.x + j * WARP_SIZE];
|
|
dweight_tmp += dweight_tmp_shared[threadIdx.x + j * WARP_SIZE];
|
|
}
|
|
// gradient contribution to bias (using shared memory friendly index)
|
|
dbias_shared[shared_index + x*WARP_SIZE] += dbias_tmp;
|
|
// gradient contribution to weight (using shared memory friendly index)
|
|
dweight_shared[shared_index + x*WARP_SIZE] += dweight_tmp;
|
|
}
|
|
__syncthreads();
|
|
|
|
// 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
|
|
dinp128[x] = (floatX)((float)dinp128[x] + dval);
|
|
}
|
|
// 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_dbias = scratch;
|
|
float* scratch_dweight = scratch + C;
|
|
for(int i = threadIdx.x; i < C; i+= blockDim.x) {
|
|
// Write to global memory in the same "shared memory banking friendly" order
|
|
scratch_dbias[i + 2*C*blockIdx.x] = dbias_shared[i];
|
|
scratch_dweight[i + 2*C*blockIdx.x] = dweight_shared[i];
|
|
}
|
|
|
|
// todo - everything below could become a separate kernel for better performance with maybe less code
|
|
// not enough parallelism even inside that single SM... do we need another level of reduction?!
|
|
__syncthreads();
|
|
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
|
|
for(int i = threadIdx.x * f128::size; i < C; i+= blockDim.x * f128::size) {
|
|
f128 dbias_accum(make_int4(0, 0, 0, 0));
|
|
f128 dweight_accum(make_int4(0, 0, 0, 0));
|
|
|
|
for (int read_block_idx = 0; read_block_idx < gridDim.x; read_block_idx++) {
|
|
int offset = i + 2*C*read_block_idx;
|
|
f128 dbias128 = load128(scratch_dbias + offset);
|
|
f128 dweight128 = load128(scratch_dweight + offset);
|
|
for(int k = 0; k < f128::size; k++) {
|
|
dbias_accum[k] += dbias128[k];
|
|
dweight_accum[k] += dweight128[k];
|
|
}
|
|
}
|
|
store128(dbias_shared + i, dbias_accum);
|
|
store128(dweight_shared + i, dweight_accum);
|
|
}
|
|
__syncthreads();
|
|
|
|
// reorder from atomic/shared memory-friendly index to real global memory index
|
|
// and convert from float/FP32 to floatX/BF16 for the final write
|
|
// this is separate also 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 i = warpId; i < iterations_C; i += warpsInBlock) {
|
|
int global_index = (warpThreadIdx * x128::size) + (i * C_per_iteration);
|
|
int shared_index = warpThreadIdx + (i * C_per_iteration);
|
|
if (global_index >= C) {
|
|
break;
|
|
}
|
|
|
|
x128 dbias128 = load128(dbias + global_index);
|
|
x128 dweight128 = load128(dweight + global_index);
|
|
for (int x = 0; x < x128::size; x++) {
|
|
float s_db = dbias_shared[shared_index + x*WARP_SIZE];
|
|
float s_dw = dweight_shared[shared_index + x*WARP_SIZE];
|
|
dbias128[x] = (floatX)(s_db + (float)dbias128[x]);
|
|
dweight128[x] = (floatX)(s_dw + (float)dweight128[x]);
|
|
}
|
|
store128(dbias + global_index, dbias128);
|
|
store128(dweight + global_index, dweight128);
|
|
}
|
|
}
|
|
}
|
|
|
|
__global__ void softmax_autoregressive_backward_kernel(floatX* dpreatt, const floatX* datt, const floatX* att,
|
|
int B, int T, int C, float scale) {
|
|
constexpr const int BlockSize = 256;
|
|
constexpr int T_per_block = 4;
|
|
|
|
// go through blocks in reverse order, so the slowest block starts first
|
|
int t0 = T - 1 - T_per_block*blockIdx.x;
|
|
int idx = blockIdx.y;
|
|
|
|
att += idx * T * T;
|
|
datt += idx * T * T;
|
|
dpreatt += idx * T * T;
|
|
|
|
for(int to = 0; to < T_per_block; ++to) {
|
|
int t = t0 - to;
|
|
if(t < 0) return;
|
|
const floatX* att_bth = att + t * T;
|
|
const floatX* datt_bth = datt + t * T;
|
|
floatX* dpreatt_bth = dpreatt + t * T;
|
|
|
|
float local_sum = 0;
|
|
for (int t2 = threadIdx.x; t2 <= t; t2 += BlockSize) {
|
|
local_sum += (float)att_bth[t2] * (float)datt_bth[t2];
|
|
}
|
|
|
|
local_sum = blockReduce<warpReduceSum>(local_sum);
|
|
|
|
for (int t3 = threadIdx.x; t3 <= t; t3 += BlockSize) {
|
|
// don't touch the cache. Some parts will still be here from the previous loop, and
|
|
// we want to exploit those.
|
|
float acc = (float)__ldcs(att_bth + t3) * ((float)__ldcs(datt_bth + t3) - local_sum);
|
|
__stcs(dpreatt_bth + t3, (floatX)(scale * acc));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Implements linear interpolation using only two floating-point operations (as opposed to three in a naive implementation).
|
|
// Reference: https://developer.nvidia.com/blog/lerp-faster-cuda
|
|
__device__ float lerp(float start, float end, float weight) {
|
|
return fma(weight, end, fma(-weight, start, start));
|
|
}
|
|
|
|
template <typename Tp, typename Tg>
|
|
__global__ void adamw_kernel3(Tp* params_memory, float* master_params_memory, Tg* grads_memory, float* m_memory, float* v_memory, size_t num_parameters,
|
|
float learning_rate, float beta1, float beta2, float beta1_correction, float beta2_correction, float eps, float weight_decay,
|
|
float grad_scale, unsigned int seed) {
|
|
int idx = blockIdx.x * blockDim.x + threadIdx.x;
|
|
if (idx >= num_parameters) { return; } // guard
|
|
|
|
// get the gradient, m, and v for this parameter
|
|
float grad = grad_scale * (float)grads_memory[idx];
|
|
float m = m_memory[idx];
|
|
float v = v_memory[idx];
|
|
// update the first moment (momentum)
|
|
m = lerp(grad, m, beta1);
|
|
m_memory[idx] = m;
|
|
// update the second moment (RMSprop)
|
|
v = lerp(grad * grad, v, beta2);
|
|
v_memory[idx] = v;
|
|
m /= beta1_correction; // m_hat
|
|
v /= beta2_correction; // v_hat
|
|
// fetch the old value of this parameter as a float, from either source
|
|
float old_param = (master_params_memory != NULL) ? master_params_memory[idx] : (float)params_memory[idx];
|
|
// update this parameter
|
|
float param = old_param - (learning_rate * (m / (sqrtf(v) + eps) + weight_decay * old_param));
|
|
// update our low precision version of the parameters using stochastic rounding
|
|
// this will be used in the next forward pass
|
|
// TODO: simply doing `params_memory[i] = (floatX)param;` breaks everything (why?)
|
|
unsigned int random = Get2dNoiseUint(threadIdx.x, blockIdx.x, seed);
|
|
stochastic_rounding(param, ¶ms_memory[idx], random);
|
|
// write the full, float version of the param into our master copy, if we maintain one
|
|
// this will be used in the next update
|
|
if (master_params_memory != NULL) { master_params_memory[idx] = param; }
|
|
}
|
|
|
|
template<class T>
|
|
__global__ void global_norm_squared_kernel(float* out, const T* data, size_t count) {
|
|
// we want as few atomics as possible, so each block tries to do
|
|
// the maximum amount of work (so no fixed chunk, but instead iterating
|
|
// until we run out of data), and then we reduce inside the block
|
|
// and finally have just one atomic per block.
|
|
// out will be updated atomically from all thread blocks. It is a float, so the
|
|
// atomic op is unproblematic
|
|
size_t index = threadIdx.x + blockDim.x * blockIdx.x;
|
|
size_t grid_width = blockDim.x * gridDim.x;
|
|
float accumulator = 0.f;
|
|
for(size_t i = index; i < count; i += grid_width) {
|
|
accumulator += (float)data[i] * (float)data[i];
|
|
}
|
|
// warp-level reduce
|
|
float block_sum = blockReduce<warpReduceSum>(accumulator);
|
|
if(threadIdx.x == 0) {
|
|
atomicAdd(out, block_sum);
|
|
}
|
|
}
|
|
|
|
struct SoftmaxParams {
|
|
float Scale;
|
|
float Offset;
|
|
};
|
|
|
|
__device__ SoftmaxParams prepare_softmax_blockwide3(int64_t idx, const floatX* inp, int V, int P) {
|
|
// same but not float4
|
|
// one row of inp, i.e. inp[idx, :] of shape (V,)
|
|
|
|
const floatX* x = inp + idx * P;
|
|
float thread_maxval = -INFINITY;
|
|
float thread_sumval = 0.0f;
|
|
int i = (V+x128::size-1)/x128::size + threadIdx.x - blockDim.x;
|
|
|
|
// special-case loop to handle the unaligned elements at the end of the array
|
|
// this lets us skip the bounds check in the main loop below, which improves performance
|
|
while ((i+1)*x128::size > V) {
|
|
for(int k = 0; k < x128::size; ++k) {
|
|
if (i*x128::size+k >= V) {
|
|
break; // bounds checking against real V (rather than padded P)
|
|
}
|
|
float v = (float)x[i*x128::size+k];
|
|
float old_maxval = thread_maxval;
|
|
thread_maxval = fmaxf(thread_maxval, v);
|
|
thread_sumval *= expf((old_maxval - thread_maxval));
|
|
thread_sumval += expf(v - thread_maxval);
|
|
}
|
|
i -= blockDim.x;
|
|
}
|
|
|
|
// main loop for the bulk of the iterations (no bounds checking required!)
|
|
for (; i >= 0; i -= blockDim.x) {
|
|
x128 packed_x = load128(x + i * x128::size); // load and keep in cache until fused_classifier loop
|
|
for(int k = 0; k < x128::size; ++k) {
|
|
float v = (float)packed_x[k];
|
|
float old_maxval = thread_maxval;
|
|
thread_maxval = fmaxf(thread_maxval, v);
|
|
thread_sumval *= expf((old_maxval - thread_maxval));
|
|
thread_sumval += expf(v - thread_maxval);
|
|
}
|
|
}
|
|
|
|
// Block Max Reduction -> Maths -> Block Sum Reduction
|
|
float block_maxval = blockReduce<warpReduceMax>(thread_maxval, false, -INFINITY);
|
|
thread_sumval *= expf(thread_maxval - block_maxval);
|
|
float block_sumval = blockReduce<warpReduceSum>(thread_sumval);
|
|
|
|
// return the softmax parameters
|
|
return SoftmaxParams{1.f / block_sumval, block_maxval};
|
|
}
|
|
|
|
// will _update_ logits to logit gradients
|
|
// uses template to decide whether to write logits and probs
|
|
// split both loops in "multiple-of-x128-size" and "bounds-checked remainder" parts
|
|
template <bool WriteLogits = true, bool WriteProbs = false>
|
|
__global__ void __launch_bounds__(1024, MAX_1024_THREADS_BLOCKS)
|
|
fused_classifier_kernel5(floatX* logits, floatX* losses, floatX* probs,
|
|
const float dloss, const int* targets,
|
|
int B, int T, int V, int P) {
|
|
// note: idx is small enough that it easily fits into 32 bit;
|
|
// by making it a long here, we ensure that any offsets calculated with it (e.g., idx * P)
|
|
// are done is 64 bit
|
|
int64_t idx = gridDim.x - (blockIdx.x+1); // reverse order for cache hits on matmul data
|
|
int ix = targets[idx];
|
|
|
|
// softmax (reading B * T * V, same logits read again below, hopefully still in cache)
|
|
SoftmaxParams sp = prepare_softmax_blockwide3(idx, logits, V, P);
|
|
|
|
// calculate the probability needed for the loss and update (single-threaded)
|
|
if(threadIdx.x == 0) {
|
|
float prob = expf((float)logits[idx * P + ix] - sp.Offset) * sp.Scale;
|
|
losses[idx] = (floatX)(-logf(prob));
|
|
}
|
|
|
|
// calculate the gradients directly, saves bandwidth from probs during training
|
|
// but also supports writing probs for inference-only and debugging
|
|
const floatX* logits_vec = logits + idx * P;
|
|
for (int i = threadIdx.x; i < V/x128::size; i += blockDim.x) {
|
|
// this is the 2nd read of logits after the one in prepare_softmax2
|
|
// it will be overwritten by the logits gradients which is when we reduce cache persistence
|
|
x128 packed_logits_vec = load128(logits_vec + i * x128::size); // rely on cs of store128cs
|
|
x128 packed_probs;
|
|
for(int k = 0; k < x128::size; ++k) {
|
|
int element = i*x128::size + k;
|
|
float prob = expf((float)packed_logits_vec[k] - sp.Offset) * sp.Scale;
|
|
packed_probs[k] = (floatX)prob;
|
|
float indicator = (element == ix) ? 1.0f : 0.0f;
|
|
packed_logits_vec[k] = (floatX)((prob - indicator) * dloss);
|
|
}
|
|
if (WriteLogits){
|
|
// reduce cache persistence for the overwritten logits
|
|
// to maximise probability that logits remain in cache between prepare_softmax and here
|
|
store128cs(logits + idx * P + i * x128::size, packed_logits_vec);
|
|
}
|
|
if (WriteProbs) {
|
|
store128(probs + idx * P + i * x128::size, packed_probs);
|
|
}
|
|
}
|
|
|
|
// handle remaining elements after the last multiple of x128::size
|
|
// e.g. if V = 8003, and x128::size = 8, we need to handle the last 3 elements
|
|
int unaligned_start = V & ~(x128::size - 1); // round down to multiple of x128::size
|
|
for (int i = threadIdx.x + unaligned_start; i < V; i++) {
|
|
float prob = expf((float)logits_vec[i] - sp.Offset) * sp.Scale;
|
|
float indicator = (i == ix) ? 1.0f : 0.0f;
|
|
float dlogit = (prob - indicator) * dloss;
|
|
if (WriteLogits){
|
|
__stcs(logits + idx * P + i, (floatX)dlogit);
|
|
}
|
|
if (WriteProbs) {
|
|
probs[idx * P + i] = (floatX)prob;
|
|
}
|
|
}
|
|
}
|
|
|
|
// device functions and the kernel to cast data between types
|
|
template<typename Td, typename Ts>
|
|
__device__ Td cast_value(Ts val);
|
|
|
|
template<>
|
|
__device__ float cast_value<float, float>(float val) {
|
|
return val;
|
|
}
|
|
|
|
template<>
|
|
__device__ float cast_value<float, half>(half val) {
|
|
return __half2float(val);
|
|
}
|
|
|
|
template<>
|
|
__device__ float cast_value<float, __nv_bfloat16>(__nv_bfloat16 val) {
|
|
return __bfloat162float(val);
|
|
}
|
|
|
|
template<typename Td, typename Ts>
|
|
__global__ void copy_and_cast_kernel(Td* dst, const Ts* src, size_t n) {
|
|
int idx = blockIdx.x * blockDim.x + threadIdx.x;
|
|
// need to try grid stride looping for more perf later
|
|
if (idx < n) {
|
|
dst[idx] = cast_value<Td, Ts>(src[idx]);
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// kernel launchers
|
|
|
|
void encoder_forward(floatX* out,
|
|
const int* inp, const floatX* wte, const floatX* wpe,
|
|
int B, int T, int C) {
|
|
NVTX_RANGE_FN();
|
|
const int block_size = 256;
|
|
const int N = B * T * C;
|
|
const int grid_size = CEIL_DIV(N, (int)(block_size * x128::size));
|
|
encoder_forward_kernel3<<<grid_size, block_size>>>(out, inp, wte, wpe, B, T, C);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
// Fully deterministic (see comments in wte_backward_kernel and wpe_backward_kernel for more details)
|
|
void encoder_backward(floatX* dwte, floatX* dwpe, floatX* scratch, // gpu outputs & scratch
|
|
int* workload_indices, int4* bucket_info, // cpu scratch buffers
|
|
const floatX* dout, const int* inp, const int* inputs_cpu, // cpu/gpu inputs
|
|
int B, int T, int C, unsigned int seed) {
|
|
NVTX_RANGE_FN();
|
|
|
|
// Launch wpe kernel first (so it runs on the GPU in parallel with the CPU pre-processing for wte)
|
|
const int block_size = 256;
|
|
const int N = T * C / x128::size;
|
|
const int grid_size = CEIL_DIV(N, block_size);
|
|
wpe_backward_kernel<<<grid_size, block_size, 0>>>(dwpe, dout, inp, B, T, C, seed);
|
|
|
|
// check the GPU scratch buffer is large enough to hold the bucket info and workload indices
|
|
// todo - this is trivially true given hardcoded scratch buffer size here, is this useful?
|
|
int num_c_groups = CEIL_DIV(C, x128::size * WARP_SIZE);
|
|
assert(B*T*num_c_groups * (sizeof(int4)+sizeof(int)) <= B*T*3*C * sizeof(floatX));
|
|
|
|
// Step 1: Sort inputs into buckets
|
|
int total_items = 0;
|
|
std::unordered_map<uint64_t, std::vector<uint64_t>> buckets;
|
|
for (uint64_t bt = 0; bt < B * T; bt++) {
|
|
for (uint64_t c_group = 0; c_group < num_c_groups; c_group++) {
|
|
// todo - passing c_group/inputs_cpu[bt] in data to avoid a second hash lookup is a bit hacky
|
|
uint64_t data = bt + (c_group<<32ULL) + ((uint64_t)inputs_cpu[bt]<<42ULL);
|
|
buckets[c_group + num_c_groups * inputs_cpu[bt]].push_back(data);
|
|
total_items++;
|
|
}
|
|
}
|
|
|
|
// Step 2: Sort buckets by size in descending order
|
|
// this is so the largest buckets are processed first by the GPU
|
|
// otherwise, if they started late, they would still be running with the rest of the GPU idle
|
|
std::vector<std::pair<uint64_t, std::vector<uint64_t>>> sortedBuckets(buckets.begin(), buckets.end());
|
|
std::sort(sortedBuckets.begin(), sortedBuckets.end(), // ugly because we don't have a typedef for the std::pair
|
|
[](const std::pair<uint64_t, std::vector<uint64_t>>& a, const std::pair<uint64_t, std::vector<uint64_t>>& b) {
|
|
return a.second.size() > b.second.size();
|
|
});
|
|
|
|
int num_buckets = buckets.size();
|
|
int bucket_index = 0;
|
|
int workload_index = 0;
|
|
for (const auto& bucket : sortedBuckets) {
|
|
bucket_info[bucket_index].x = workload_index; // bucket start
|
|
bucket_info[bucket_index].y = bucket.second.size(); // bucket size
|
|
bucket_info[bucket_index].z = (bucket.second[0] >> 42ULL) & ((1ULL<<20ULL)-1); // bucket ix
|
|
bucket_info[bucket_index].w = (bucket.second[0] >> 32ULL) & ((1ULL<<10ULL)-1); // bucket c
|
|
|
|
for (uint64_t idx : bucket.second) {
|
|
workload_indices[workload_index++] = (int)(idx & ((1ULL<<31ULL)-1ULL));
|
|
}
|
|
bucket_index++;
|
|
}
|
|
|
|
// Step 3: Copy data from host to device (async until the last one to avoid synchronising CPU/GPU twice)
|
|
// todo - could use CUDA events (even without streams) to avoid CPU/GPU synchronisation completely
|
|
int4* d_bucket_info = (int4*)scratch;
|
|
int* d_workload_indices = (int*)(scratch + B*T*num_c_groups * sizeof(int4));
|
|
cudaMemcpyAsync(d_bucket_info, bucket_info, num_buckets * sizeof(int4), cudaMemcpyHostToDevice);
|
|
cudaMemcpy(d_workload_indices, workload_indices, total_items * sizeof(int), cudaMemcpyHostToDevice);
|
|
|
|
// Launch wte kernel
|
|
// todo - profile block sizes on more content (depends on number of buckets and on GPU?)
|
|
wte_backward_kernel<256><<<num_buckets, 256>>>(dwte, d_bucket_info, d_workload_indices, dout, inp, seed, B, T, C);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
void layernorm_forward(floatX* out, floatX* mean, floatX* rstd,
|
|
floatX* inp, const floatX* weight, const floatX* bias,
|
|
int B, int T, int C) {
|
|
NVTX_RANGE_FN();
|
|
const int block_size = 512;
|
|
const int N = B * T;
|
|
const int grid_size = CEIL_DIV(N * WARP_SIZE, block_size);
|
|
layernorm_forward_kernel3<<<grid_size, block_size>>>(out, mean, rstd, inp, weight, bias, N, C);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
// https://docs.nvidia.com/cuda/cublas/#cublasltmatmul
|
|
void matmul_forward_cublaslt(floatX* out,
|
|
floatX* inp, floatX* weight, floatX* bias,
|
|
int B, int T, int C, int OC) {
|
|
NVTX_RANGE_FN();
|
|
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);
|
|
}
|
|
|
|
// these need to be in FP16 if and only if alpha/beta are CUBLAS_COMPUTE_16F
|
|
const float alpha = 1.0f, beta = 0.0f;
|
|
|
|
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 = has_bias ? CUBLASLT_EPILOGUE_BIAS : CUBLASLT_EPILOGUE_DEFAULT;
|
|
|
|
cublasCheck(cublasLtMatmulDescCreate(&operationDesc, cublas_compute, CUDA_R_32F)); // FP16 if CUBLAS_COMPUTE_16F
|
|
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, CUBLAS_LOWP, C, OC, C));
|
|
cublasCheck(cublasLtMatrixLayoutCreate(&inputLayout, CUBLAS_LOWP, C, B*T, C));
|
|
cublasCheck(cublasLtMatrixLayoutCreate(&outputLayout, CUBLAS_LOWP, OC, B*T, OC));
|
|
cublasCheck(cublasLtMatrixLayoutCreate(&biasLayout, CUBLAS_LOWP, 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
|
|
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(floatX* out, floatX* qkvr, floatX* att,
|
|
floatX* inp,
|
|
int B, int T, int C, int NH) {
|
|
NVTX_RANGE_FN();
|
|
// Note: `inp` is not needed for backward pass, so we re-use it as a scratch buffer.
|
|
// Its contents will be overwritten by this function.
|
|
const int block_size = 256;
|
|
const float alpha = 1.0f, beta = 0.0f;
|
|
|
|
// 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)
|
|
floatX *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);
|
|
|
|
|
|
floatX* preatt = inp;
|
|
cublasCheck(cublasGemmStridedBatchedEx(cublas_handle,
|
|
CUBLAS_OP_T, CUBLAS_OP_N,
|
|
T, T, HS, &alpha,
|
|
k, CUBLAS_LOWP, HS, T * HS,
|
|
q, CUBLAS_LOWP, HS, T * HS,
|
|
&beta, preatt, CUBLAS_LOWP, T, T * T,
|
|
B * NH, cublas_compute, CUBLAS_GEMM_DEFAULT));
|
|
|
|
// multiply all elements of preatt elementwise by scale
|
|
float scale = 1.0 / sqrtf(HS);
|
|
int grid_size = CEIL_DIV(B * NH * T * 32, block_size);
|
|
softmax_forward_kernel5<<<grid_size, block_size>>>(att, scale, preatt, B * NH, T);
|
|
|
|
// new approach: first cuBLAS another batched matmul
|
|
floatX* vaccum = inp;
|
|
// y = att @ v # (B, nh, T, T) @ (B, nh, T, hs) -> (B, nh, T, hs)
|
|
cublasCheck(cublasGemmStridedBatchedEx(cublas_handle,
|
|
CUBLAS_OP_N, CUBLAS_OP_N,
|
|
HS, T, T, &alpha,
|
|
v, CUBLAS_LOWP, HS, T * HS,
|
|
att, CUBLAS_LOWP, T, T * T,
|
|
&beta, vaccum, CUBLAS_LOWP, HS, T * HS,
|
|
B * NH, cublas_compute, CUBLAS_GEMM_DEFAULT));
|
|
|
|
// 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);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
void residual_forward(floatX* out, const floatX* inp1, const floatX* inp2, int N) {
|
|
NVTX_RANGE_FN();
|
|
const int block_size = 256;
|
|
assert(N % block_size == 0);
|
|
const int grid_size = CEIL_DIV(N, block_size * x128::size);
|
|
residual_forward_kernel<<<grid_size, block_size>>>(out, inp1, inp2);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
void fused_residual_forward5(floatX* residual, floatX* normed, floatX* mean, floatX* rstd,
|
|
const floatX* inp1, const floatX* inp2,
|
|
const floatX* weight, const floatX* bias,
|
|
int N, int C) {
|
|
const int block_size = 256;
|
|
int block_y = block_size / WARP_SIZE;
|
|
const int grid_size = CEIL_DIV(N, block_y);
|
|
size_t smem = (2 + block_y) * C * sizeof(floatX);
|
|
|
|
// in order to use more than 48 KiB of smem, need to call cudaFuncSetAttribute
|
|
// this may fail, in which case we fall back to the smem free implementation.
|
|
cudaCheck(cudaGetLastError());
|
|
auto status = cudaFuncSetAttribute(fused_residual_forward_kernel5, cudaFuncAttributeMaxDynamicSharedMemorySize, smem);
|
|
cudaGetLastError();
|
|
if(status == cudaSuccess) {
|
|
fused_residual_forward_kernel5<<<grid_size, dim3(WARP_SIZE, block_y), smem>>>(residual, normed, mean, rstd, inp1, inp2,
|
|
weight, bias, N, C);
|
|
} else {
|
|
residual_forward(residual, inp1, inp2, N*C);
|
|
layernorm_forward(normed, mean, rstd, residual, weight, bias, N, 1, C);
|
|
}
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
|
|
void gelu_forward(floatX* out, const floatX* inp, int N) {
|
|
NVTX_RANGE_FN();
|
|
const int block_size = 512;
|
|
assert(N % block_size == 0);
|
|
const int grid_size = CEIL_DIV(N, block_size * x128::size);
|
|
gelu_forward_kernel2<<<grid_size, block_size>>>(out, inp);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
void gelu_backward(floatX* dinp, const floatX* inp, const floatX* dout, const int N) {
|
|
NVTX_RANGE_FN();
|
|
const int block_size = 128;
|
|
assert(N % block_size == 0);
|
|
const int grid_size = CEIL_DIV(N, block_size * x128::size);
|
|
gelu_backward_kernel<<<grid_size, block_size>>>(dinp, inp, dout);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
void matmul_backward(floatX* dinp, floatX* dweight, floatX* dbias,
|
|
floatX* dout, floatX* inp, floatX* weight,
|
|
float* dbias_buffer,
|
|
int B, int T, int C, int OC) {
|
|
NVTX_RANGE_FN();
|
|
float one = 1.0f, zero = 0.0f;
|
|
|
|
// backward to bias, if given, does a +=
|
|
if (dbias != NULL) {
|
|
// Each warp is responsible for 8 * "x128::size" = 64 OCs at BF16 (OC must be a multiple of 64!)
|
|
// Block size is 1024 | 768 threads (32|24 warps) and we reduce those values into 1 at the end
|
|
|
|
const int block_size = deviceProp.maxThreadsPerMultiProcessor == 1536 ? 768 : 1024;
|
|
|
|
dim3 block_dim = {4, 8, (unsigned)block_size/WARP_SIZE};
|
|
const int OC_per_warp = block_dim.y * x128::size; // 64 at BF16
|
|
const int grid_size_x = CEIL_DIV(OC, OC_per_warp); // e.g. 12 horizontal blocks for 768 OCs at BF16
|
|
const int grid_size_y = max(1, deviceProp.maxThreadsPerMultiProcessor * deviceProp.multiProcessorCount / (block_size * grid_size_x)); // full GPU!
|
|
|
|
// If we have enough OC that we don't need cross-block reductions, we can skip the bias_buffer accumulation
|
|
// and write results directly to the output.
|
|
if(grid_size_y == 1) {
|
|
matmul_backward_bias_kernel9<<<dim3(grid_size_x, grid_size_y), block_dim>>>(dbias, dout, B, T, OC, std::bool_constant<false>{});
|
|
cudaCheck(cudaGetLastError());
|
|
} else {
|
|
// kernel 9 overwrites temp buffer, so no need to memset
|
|
matmul_backward_bias_kernel9<<<dim3(grid_size_x, grid_size_y), block_dim>>>(dbias_buffer, dout, B, T, OC, std::bool_constant<true>{});
|
|
cudaCheck(cudaGetLastError());
|
|
reduce_add_sum_kernel<<<CEIL_DIV(OC, 256 * f128::size), 256>>>(dbias, dbias_buffer, OC, grid_size_y);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
}
|
|
|
|
// backward to input, uses = in the backward pass (set the gradient)
|
|
cublasCheck(cublasGemmEx(cublas_handle, CUBLAS_OP_N, CUBLAS_OP_N, C, B*T, OC, &one,
|
|
weight, CUBLAS_LOWP, C, dout, CUBLAS_LOWP, OC, &zero,
|
|
dinp, CUBLAS_LOWP, C, cublas_compute, CUBLAS_GEMM_DEFAULT_TENSOR_OP));
|
|
// backward to weight, uses += in the backward pass (accumulate the gradient) by setting alpha=one
|
|
cublasCheck(cublasGemmEx(cublas_handle, CUBLAS_OP_N, CUBLAS_OP_T, C, OC, B*T, &one,
|
|
inp, CUBLAS_LOWP, C, dout, CUBLAS_LOWP, OC, &one,
|
|
dweight, CUBLAS_LOWP, C, cublas_compute, CUBLAS_GEMM_DEFAULT_TENSOR_OP));
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
void layernorm_backward(floatX* dinp, floatX* dweight, floatX* dbias, float* scratch,
|
|
const floatX* dout, const floatX* inp, const floatX* weight, const floatX* mean, const floatX* rstd,
|
|
int B, int T, int C) {
|
|
NVTX_RANGE_FN();
|
|
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;
|
|
size_t shared_mem_size = (2*C + 2*block_size + 1) * sizeof(float); // see kernel
|
|
|
|
cudaMemset(scratch, 0, 1 * sizeof(float)); // only need to reset the flag to 0
|
|
layernorm_backward_kernel9<<<grid_size, block_size, shared_mem_size>>>(dinp, dweight, dbias, scratch, dout, inp, weight, mean, rstd, B, T, C);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
|
|
// the sequence of transformations in this compound op is:
|
|
// inp (B,T,3C) -> qkvr (B,T,3C) -> preatt (B,NH,T,T) -> att (B,NH,T,T) -> vaccum (B,T,C) -> out (B,T,C)
|
|
void attention_backward(floatX* dinp, floatX* dqkvr, floatX* dpreatt, floatX* datt, floatX* scratch,
|
|
const floatX* dout,
|
|
const floatX* qkvr, const floatX* att,
|
|
int B, int T, int C, int NH) {
|
|
NVTX_RANGE_FN();
|
|
const int block_size = 256;
|
|
int HS = C / NH; // head size
|
|
const float alpha = 1.0f, beta = 0.0f;
|
|
|
|
// unpack convenience pointers into q, k, v
|
|
const floatX *q, *k, *v;
|
|
q = qkvr + 0 * B * T * C;
|
|
k = qkvr + 1 * B * T * C;
|
|
v = qkvr + 2 * B * T * C;
|
|
floatX *dq, *dk, *dv;
|
|
dq = dqkvr + 0 * B * T * C;
|
|
dk = dqkvr + 1 * B * T * C;
|
|
dv = dqkvr + 2 * B * T * C;
|
|
|
|
// backward through the unpermute operation
|
|
int num_blocks = CEIL_DIV(B * T * C, block_size);
|
|
unpermute_kernel_backward<<<num_blocks, block_size>>>(scratch, dout, B, T, NH, HS);
|
|
// backward into datt
|
|
cublasCheck(cublasGemmStridedBatchedEx(cublas_handle, CUBLAS_OP_T, CUBLAS_OP_N, T, T, HS, &alpha,
|
|
v, CUBLAS_LOWP, HS, T * HS, scratch, CUBLAS_LOWP, HS, T * HS, &beta,
|
|
datt, CUBLAS_LOWP, T, T * T, B * NH, cublas_compute, CUBLAS_GEMM_DEFAULT));
|
|
// backward into dv
|
|
cublasCheck(cublasGemmStridedBatchedEx(cublas_handle, CUBLAS_OP_N, CUBLAS_OP_T, HS, T, T, &alpha,
|
|
scratch, CUBLAS_LOWP, HS, T * HS, att, CUBLAS_LOWP, T, T * T, &beta,
|
|
dv, CUBLAS_LOWP, HS, T * HS, B * NH, cublas_compute, CUBLAS_GEMM_DEFAULT));
|
|
// backward into preatt
|
|
int hs = C / NH; // head size
|
|
float scale = 1.0f / sqrtf(hs);
|
|
softmax_autoregressive_backward_kernel<<<dim3(T / 4, B * NH), 256, 256>>>(dpreatt, datt, att, B, T, C, scale);
|
|
// backward into q
|
|
cublasCheck(cublasGemmStridedBatchedEx(cublas_handle, CUBLAS_OP_N, CUBLAS_OP_N, HS, T, T, &alpha,
|
|
k, CUBLAS_LOWP, HS, T * HS, dpreatt, CUBLAS_LOWP, T, T * T, &beta,
|
|
dq, CUBLAS_LOWP, HS, T * HS, B * NH, cublas_compute, CUBLAS_GEMM_DEFAULT));
|
|
// backward into k
|
|
cublasCheck(cublasGemmStridedBatchedEx(cublas_handle, CUBLAS_OP_N, CUBLAS_OP_T, HS, T, T, &alpha,
|
|
q, CUBLAS_LOWP, HS, T * HS, dpreatt, CUBLAS_LOWP, T, T * T, &beta,
|
|
dk, CUBLAS_LOWP, HS, T * HS, B * NH, cublas_compute, CUBLAS_GEMM_DEFAULT));
|
|
// backward into inp
|
|
num_blocks = CEIL_DIV(B * NH * T * HS, block_size);
|
|
permute_kernel_backward<<<num_blocks, block_size>>>(dinp, dq, dk, dv, B, T, NH, HS);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
// replaces logits with logit gradients
|
|
template <typename Type>
|
|
void fused_classifier(Type* logits, Type* losses,
|
|
const float dloss, const int* targets,
|
|
int B, int T, int V, int P) {
|
|
NVTX_RANGE_FN();
|
|
const int block_size = 1024;
|
|
const int N = B * T;
|
|
const int grid_size = N;
|
|
fused_classifier_kernel5<<<grid_size, block_size, 512>>>(logits, losses, (floatX*)NULL, dloss, targets, B, T, V, P);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
template<typename T>
|
|
void global_norm_squared(float* out, const T* values, size_t count) {
|
|
const int block_size = 512;
|
|
// launch just enough blocks to fill the grid. deliberately no DIV_CEIL.
|
|
// having one block less than possible is a tiny performance hit, having
|
|
// one block too many is catastrophic, since it only can start once all the other
|
|
// blocks finish. anyway, I think cuda_threads_per_SM should be a multiple of 512
|
|
// on all gpus, so the division really is going to be exact.
|
|
const int grid_size = deviceProp.maxThreadsPerMultiProcessor * deviceProp.multiProcessorCount / block_size;
|
|
assert(grid_size > 0); // gives a better error than letting the call below fail
|
|
// initialize out with zero
|
|
cudaCheck(cudaMemset(out, 0, sizeof(float)));
|
|
global_norm_squared_kernel<<<grid_size, block_size>>>(out, values, count);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// GPT-2 model definition
|
|
|
|
typedef struct {
|
|
int max_seq_len; // max sequence length, e.g. 1024
|
|
int vocab_size; // vocab size, e.g. 50257
|
|
int padded_vocab_size; // padded to e.g. %128==0, 50304
|
|
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;
|
|
|
|
// the parameters of the model
|
|
constexpr const int NUM_PARAMETER_TENSORS = 16;
|
|
typedef struct {
|
|
floatX* wte; // (V, C)
|
|
floatX* wpe; // (maxT, C)
|
|
floatX* ln1w; // (L, C)
|
|
floatX* ln1b; // (L, C)
|
|
floatX* qkvw; // (L, 3*C, C)
|
|
floatX* qkvb; // (L, 3*C)
|
|
floatX* attprojw; // (L, C, C)
|
|
floatX* attprojb; // (L, C)
|
|
floatX* ln2w; // (L, C)
|
|
floatX* ln2b; // (L, C)
|
|
floatX* fcw; // (L, 4*C, C)
|
|
floatX* fcb; // (L, 4*C)
|
|
floatX* fcprojw; // (L, C, 4*C)
|
|
floatX* fcprojb; // (L, C)
|
|
floatX* lnfw; // (C)
|
|
floatX* lnfb; // (C)
|
|
} ParameterTensors;
|
|
static_assert(sizeof(ParameterTensors) == NUM_PARAMETER_TENSORS * sizeof(void*), "Inconsistent sizes!");
|
|
|
|
void fill_in_parameter_sizes(size_t* param_sizes, size_t* param_sizeof, GPT2Config config) {
|
|
size_t Vp = config.padded_vocab_size;
|
|
size_t C = config.channels;
|
|
size_t maxT = config.max_seq_len;
|
|
size_t L = config.num_layers;
|
|
param_sizes[0] = Vp * C; // wte
|
|
param_sizes[1] = maxT * C; // wpe
|
|
param_sizes[2] = L * C; // ln1w
|
|
param_sizes[3] = L * C; // ln1b
|
|
param_sizes[4] = L * (3 * C) * C; // qkvw
|
|
param_sizes[5] = L * (3 * C); // qkvb
|
|
param_sizes[6] = L * C * C; // attprojw
|
|
param_sizes[7] = L * C; // attprojb
|
|
param_sizes[8] = L * C; // ln2w
|
|
param_sizes[9] = L * C; // ln2b
|
|
param_sizes[10] = L * (4 * C) * C; // fcw
|
|
param_sizes[11] = L * (4 * C); // fcb
|
|
param_sizes[12] = L * C * (4 * C); // fcprojw
|
|
param_sizes[13] = L * C; // fcprojb
|
|
param_sizes[14] = C; // lnfw
|
|
param_sizes[15] = C; // lnfb
|
|
|
|
// populate the parameter sizes in bytes (all the same for now, keeping for future use)
|
|
for (int i = 0; i < NUM_PARAMETER_TENSORS; i++) {
|
|
param_sizeof[i] = sizeof(floatX);
|
|
}
|
|
}
|
|
|
|
// allocate memory for the parameters and point the individual tensors to the right places
|
|
void* malloc_and_point_parameters(ParameterTensors* params, size_t* param_elements, size_t *param_sizeof) {
|
|
// calculate the total number of parameters and bytes across all tensors
|
|
size_t num_parameters = 0;
|
|
size_t num_parameters_bytes = 0;
|
|
for (int i = 0; i < NUM_PARAMETER_TENSORS; i++) {
|
|
num_parameters += param_elements[i];
|
|
num_parameters_bytes += param_elements[i] * param_sizeof[i];
|
|
}
|
|
// malloc all parameters all at once on the device
|
|
void* params_memory;
|
|
cudaCheck(cudaMalloc((void**)¶ms_memory, num_parameters_bytes));
|
|
// assign all the tensors their place in the array
|
|
floatX** 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
|
|
};
|
|
char* params_memory_iterator = (char*)params_memory;
|
|
for (int i = 0; i < NUM_PARAMETER_TENSORS; i++) {
|
|
*(ptrs[i]) = (floatX*)params_memory_iterator;
|
|
params_memory_iterator += param_elements[i] * param_sizeof[i];
|
|
}
|
|
return params_memory;
|
|
}
|
|
|
|
#define NUM_ACTIVATION_TENSORS 21
|
|
typedef struct {
|
|
floatX* encoded; // (B, T, C)
|
|
floatX* ln1; // (L, B, T, C)
|
|
floatX* ln1_mean; // (L, B, T)
|
|
floatX* ln1_rstd; // (L, B, T)
|
|
floatX* atty; // (L, B, T, C)
|
|
floatX* att; // (L, B, NH, T, T) (smaller with cuDNN)
|
|
floatX* attproj; // (L, B, T, C)
|
|
floatX* residual2; // (L, B, T, C)
|
|
floatX* ln2; // (L, B, T, C)
|
|
floatX* ln2_mean; // (L, B, T)
|
|
floatX* ln2_rstd; // (L, B, T)
|
|
floatX* fch; // (L, B, T, 4*C)
|
|
floatX* fch_gelu; // (L, B, T, 4*C)
|
|
floatX* fcproj; // (L, B, T, C)
|
|
floatX* residual3; // (L, B, T, C)
|
|
floatX* lnf; // (B, T, C)
|
|
floatX* lnf_mean; // (B, T)
|
|
floatX* lnf_rstd; // (B, T)
|
|
floatX* losses; // (B, T)
|
|
// adding these two compared to the CPU .c code, needed for attention kernel as buffers
|
|
floatX* qkvr; // (L, B, T, 3*C)
|
|
// in inference mode, this buffer will store the logits
|
|
// in training mode, this buffer will contain the *gradients* of the logits.
|
|
// during the processing of transformer blocks, we will also use this as a
|
|
// general scratchpad buffer. Allocation is made large enough to hold (B, T, 3C),
|
|
// (B, NH, T, T), and (B, T, V) shaped tensors.
|
|
floatX* output;
|
|
} ActivationTensors;
|
|
|
|
void fill_in_activation_sizes(size_t* act_sizes, size_t B, size_t T, GPT2Config config, int recompute) {
|
|
size_t Vp = config.padded_vocab_size;
|
|
size_t L = config.num_layers;
|
|
size_t NH = config.num_heads;
|
|
size_t C = config.channels;
|
|
act_sizes[0] = B * T * C; // encoded
|
|
act_sizes[1] = L * B * T * C; // ln1
|
|
act_sizes[2] = L * B * T; // ln1_mean
|
|
act_sizes[3] = L * B * T; // ln1_rstd
|
|
act_sizes[4] = L * B * T * C; // atty
|
|
#ifdef ENABLE_CUDNN
|
|
// FP32 stats tensor for cuDNN to be passed to backward pass
|
|
act_sizes[5] = L * B * NH * T * (sizeof(float) / sizeof(floatX));
|
|
#else
|
|
act_sizes[5] = L * B * NH * T * T; // att
|
|
#endif
|
|
act_sizes[6] = L * B * T * C; // attproj
|
|
act_sizes[7] = L * B * T * C; // residual2
|
|
act_sizes[8] = L * B * T * C; // ln2
|
|
act_sizes[9] = L * B * T; // ln2_mean
|
|
act_sizes[10] = L * B * T; // ln2_rstd
|
|
act_sizes[11] = L * B * T * 4*C; // fch
|
|
// if recompute >= 1 then we will recompute gelu_forward during backward and use this as scratch buffer
|
|
act_sizes[12] = (recompute == 0) ? L * B * T * 4*C : B * T * 4*C;
|
|
act_sizes[13] = L * B * T * C; // fcproj
|
|
act_sizes[14] = L * B * T * C; // residual3
|
|
act_sizes[15] = B * T * C; // lnf
|
|
act_sizes[16] = B * T; // lnf_mean
|
|
act_sizes[17] = B * T; // lnf_rstd
|
|
act_sizes[18] = B * T; // losses
|
|
act_sizes[19] = L * B * T * 3*C; // qkvr
|
|
act_sizes[20] = B * T * max(3*C, max(NH*T, Vp)); // output / scratch
|
|
}
|
|
|
|
// Backward pass is conceptually quite different from forward, because we can discard
|
|
// the activations of a layer as soon as we're done with it. This lets us aggressively
|
|
// reuse memory, so that we need far fewer tensors for backward state.
|
|
#ifdef ENABLE_CUDNN
|
|
#define NUM_BACKWARD_TENSORS 2
|
|
#else
|
|
#define NUM_BACKWARD_TENSORS 3
|
|
#endif
|
|
|
|
typedef struct {
|
|
floatX* bt4c; // (B, T, 4*C)
|
|
floatX* residual3; // (B, T, C)
|
|
#ifndef ENABLE_CUDNN
|
|
floatX* preatt; // (B, NH, T, T)
|
|
#endif
|
|
} GradActTensors;
|
|
|
|
void fill_in_grad_act_sizes(size_t* act_sizes, size_t B, size_t T, GPT2Config config) {
|
|
size_t C = config.channels;
|
|
act_sizes[0] = B * T * 4 * C; // bt4c
|
|
act_sizes[1] = B * T * C; // residual3
|
|
|
|
#ifndef ENABLE_CUDNN
|
|
size_t NH = config.num_heads;
|
|
act_sizes[2] = B * NH * T * T; // preatt
|
|
#endif
|
|
}
|
|
|
|
void* malloc_and_point(floatX** targets[], const size_t* act_sizes, size_t n) {
|
|
size_t num_activations = 0;
|
|
for (size_t i = 0; i < n; i++) {
|
|
num_activations += act_sizes[i];
|
|
}
|
|
void* acts_memory;
|
|
cudaCheck(cudaMalloc((void**)&acts_memory, num_activations * sizeof(floatX)));
|
|
char* acts_memory_iterator = (char*)acts_memory;
|
|
for (size_t i = 0; i < n; i++) {
|
|
*(targets[i]) = (floatX*)acts_memory_iterator;
|
|
acts_memory_iterator += act_sizes[i] * sizeof(floatX);
|
|
}
|
|
return acts_memory;
|
|
}
|
|
|
|
void* malloc_and_point_activations(ActivationTensors* acts, const size_t* act_sizes) {
|
|
floatX** ptrs[] = {
|
|
&acts->encoded, &acts->ln1, &acts->ln1_mean, &acts->ln1_rstd, &acts->atty,
|
|
&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->losses, &acts->qkvr, &acts->output
|
|
};
|
|
return malloc_and_point(ptrs, act_sizes, NUM_ACTIVATION_TENSORS);
|
|
}
|
|
|
|
void* malloc_and_point_backward(GradActTensors* acts, const size_t* act_sizes) {
|
|
floatX** ptrs[] = {
|
|
&acts->bt4c, &acts->residual3,
|
|
#ifndef ENABLE_CUDNN
|
|
&acts->preatt,
|
|
#endif
|
|
};
|
|
return malloc_and_point(ptrs, act_sizes, NUM_BACKWARD_TENSORS);
|
|
}
|
|
|
|
typedef struct {
|
|
GPT2Config config;
|
|
// the weights of the model, and their sizes
|
|
ParameterTensors params;
|
|
size_t param_elements[NUM_PARAMETER_TENSORS];
|
|
size_t param_sizeof[NUM_PARAMETER_TENSORS];
|
|
void* params_memory;
|
|
size_t num_parameters;
|
|
size_t num_parameters_bytes;
|
|
// gradients of the weights
|
|
ParameterTensors grads;
|
|
void* grads_memory;
|
|
// buffers for the AdamW optimizer
|
|
float* m_memory;
|
|
float* v_memory;
|
|
float* master_weights; // is NULL unless fp32 weights is enabled.
|
|
// the activations of the model, and their sizes
|
|
ActivationTensors acts;
|
|
size_t act_sizes[NUM_ACTIVATION_TENSORS];
|
|
void* acts_memory;
|
|
size_t num_activations;
|
|
// gradients of the activations
|
|
GradActTensors grads_acts;
|
|
size_t num_grad_acts;
|
|
void* 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 accumulated_mean_loss; // Mean loss after aggregating it on all GPUs
|
|
floatX* cpu_losses; // CPU buffer to copy the losses to, allocated with cudaMallocHost
|
|
float* cpu_losses_fp32; // same but fp32
|
|
unsigned long long rng_state; // the RNG state for seeding stochastic rounding etc.
|
|
int use_master_weights;
|
|
int recompute;
|
|
// todo - if other functions need cpu scratch buffers in the future, reuse as generic scratch?
|
|
int* workload_indices; // encoder_backward, B*T*num_c_groups (int)
|
|
int4* bucket_info; // encoder_backward, B*T*num_c_groups (int4) - size for worst case
|
|
} GPT2;
|
|
|
|
void gpt2_write_to_checkpoint(GPT2 *model, const char* checkpoint_path) {
|
|
// write the model to a checkpoint file
|
|
printf0("Writing model to %s\n", checkpoint_path);
|
|
FILE *model_file = fopenCheck(checkpoint_path, "wb");
|
|
// write the header first
|
|
int model_header[256];
|
|
model_header[0] = 20240326;
|
|
assert(PRECISION_MODE == PRECISION_FP32 || PRECISION_MODE == PRECISION_BF16);
|
|
model_header[1] = PRECISION_MODE == PRECISION_FP32 ? 3 : 5;
|
|
model_header[2] = model->config.max_seq_len;
|
|
model_header[3] = model->config.vocab_size;
|
|
model_header[4] = model->config.num_layers;
|
|
model_header[5] = model->config.num_heads;
|
|
model_header[6] = model->config.channels;
|
|
model_header[7] = model->config.padded_vocab_size;
|
|
fwrite(model_header, sizeof(int), 256, model_file);
|
|
// write the parameters
|
|
void* params_memory_cpu = (void*)mallocCheck(model->num_parameters_bytes);
|
|
cudaCheck(cudaMemcpy(params_memory_cpu, model->params_memory, model->num_parameters_bytes, cudaMemcpyDeviceToHost));
|
|
fwrite(params_memory_cpu, 1, model->num_parameters_bytes, model_file);
|
|
free(params_memory_cpu);
|
|
// close file, we're done
|
|
fcloseCheck(model_file);
|
|
}
|
|
|
|
void gpt2_build_from_checkpoint(GPT2 *model, const char* checkpoint_path) {
|
|
|
|
if (PRECISION_MODE == PRECISION_FP16) {
|
|
// TODO for later perhaps, would require us dynamically converting the
|
|
// model weights from fp32 to fp16 online, here in this function, or writing
|
|
// the fp16 weights directly from Python, which we only do for fp32/bf16 atm.
|
|
fprintf(stderr, "build_from_checkpoint() does not support fp16 right now.\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
// read in model from a checkpoint file
|
|
FILE *model_file = fopenCheck(checkpoint_path, "rb");
|
|
int model_header[256];
|
|
freadCheck(model_header, sizeof(int), 256, model_file);
|
|
if (model_header[0] != 20240326) { printf("Bad magic model file\n"); exit(EXIT_FAILURE); }
|
|
int version = model_header[1];
|
|
if (!(version == 3 || version == 5)) {
|
|
// 3 = fp32, padded vocab
|
|
// 5 = bf16, padded vocab, layernorms also in bf16
|
|
fprintf(stderr, "Bad version in model file\n");
|
|
fprintf(stderr, "---> HINT: try to re-run `python train_gpt2.py`\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
if (PRECISION_MODE == PRECISION_BF16 && version != 5) {
|
|
fprintf(stderr, "Precision is configured as BF16 but model at %s is not.\n", checkpoint_path);
|
|
fprintf(stderr, "---> HINT: are you sure you're loading a _bf16.bin file?\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
if (PRECISION_MODE == PRECISION_FP32 && version != 3) {
|
|
fprintf(stderr, "Precision is configured as FP32 but model at %s is not.\n", checkpoint_path);
|
|
fprintf(stderr, "---> HINT: to turn on FP32 you have to compile like: `make train_gpt2cu PRECISION=FP32`\n");
|
|
fprintf(stderr, "---> HINT: are you sure you're loading a .bin file without any _bf16 in the name?\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
// read in hyperparameters
|
|
model->config.max_seq_len = model_header[2];
|
|
model->config.vocab_size = model_header[3];
|
|
model->config.num_layers = model_header[4];
|
|
model->config.num_heads = model_header[5];
|
|
model->config.channels = model_header[6];
|
|
model->config.padded_vocab_size = model_header[7];
|
|
|
|
// allocate space for all the parameters and read them in
|
|
fill_in_parameter_sizes(model->param_elements, model->param_sizeof, model->config);
|
|
|
|
model->num_parameters = 0;
|
|
model->num_parameters_bytes = 0;
|
|
for (int i = 0; i < NUM_PARAMETER_TENSORS; i++) {
|
|
model->num_parameters += model->param_elements[i];
|
|
model->num_parameters_bytes += model->param_elements[i] * model->param_sizeof[i];
|
|
}
|
|
|
|
// create memory for model parameters on the device
|
|
model->params_memory = malloc_and_point_parameters(&model->params, model->param_elements, model->param_sizeof);
|
|
|
|
// read in all the parameters from file and copy them to device
|
|
void* params_memory_cpu = (void*)mallocCheck(model->num_parameters_bytes);
|
|
freadCheck(params_memory_cpu, 1, model->num_parameters_bytes, model_file);
|
|
cudaCheck(cudaMemcpy(model->params_memory, params_memory_cpu, model->num_parameters_bytes, cudaMemcpyHostToDevice));
|
|
free(params_memory_cpu);
|
|
fcloseCheck(model_file);
|
|
|
|
// other inits
|
|
model->acts_memory = NULL;
|
|
model->grads_memory = NULL;
|
|
model->m_memory = NULL;
|
|
model->v_memory = NULL;
|
|
model->master_weights = NULL;
|
|
model->grads_acts_memory = NULL;
|
|
model->inputs = NULL;
|
|
model->targets = NULL;
|
|
model->cpu_losses = NULL;
|
|
model->cpu_losses_fp32 = NULL;
|
|
model->workload_indices = NULL;
|
|
model->bucket_info = NULL;
|
|
model->batch_size = 0;
|
|
model->seq_len = 0;
|
|
model->mean_loss = -1.0f; // -1.0f will designate no loss
|
|
model->rng_state = 13371337;
|
|
model->use_master_weights = 1; // keep master weights copy in float for optim update?
|
|
model->recompute = 1; // default to recompute gelu during backward
|
|
}
|
|
|
|
void gpt2_build_from_random(GPT2 *model, int depth) {
|
|
// init random (training from scratch)
|
|
|
|
// parameterize the size of gpt2 based only on the depth of the model (num_layers)
|
|
model->config.num_layers = depth;
|
|
// follows GPT-2 sizes
|
|
int channels, num_heads;
|
|
if (depth == 6) { channels = 384; num_heads = 6; } // gpt2-tiny (30M)
|
|
else if (depth == 12) { channels = 768; num_heads = 12; } // gpt2 (124M)
|
|
else if (depth == 24) { channels = 1024; num_heads = 16; } // gpt2-medium (350M)
|
|
else if (depth == 36) { channels = 1280; num_heads = 20; } // gpt2-large (774M)
|
|
else if (depth == 48) { channels = 1600; num_heads = 25; } // gpt2-xl (1558M)
|
|
else { fprintf(stderr, "Unsupported depth for now\n"); exit(EXIT_FAILURE); }
|
|
model->config.channels = channels;
|
|
model->config.num_heads = num_heads;
|
|
model->config.max_seq_len = 1024;
|
|
model->config.vocab_size = 50257;
|
|
model->config.padded_vocab_size = 50304; // padded to 128
|
|
|
|
// fill in all the parameter tensor dimensions and types
|
|
fill_in_parameter_sizes(model->param_elements, model->param_sizeof, model->config);
|
|
model->num_parameters = 0;
|
|
model->num_parameters_bytes = 0;
|
|
for (int i = 0; i < NUM_PARAMETER_TENSORS; i++) {
|
|
model->num_parameters += model->param_elements[i];
|
|
model->num_parameters_bytes += model->param_elements[i] * model->param_sizeof[i];
|
|
}
|
|
// create memory for model parameters on the device
|
|
model->params_memory = malloc_and_point_parameters(&model->params, model->param_elements, model->param_sizeof);
|
|
|
|
// allocate and random init the memory for all the parameters with GPT-2 schema
|
|
// weights ~N(0, 0.02), biases 0, c_proj weights ~N(0, 0.02/(2*L)**0.5)
|
|
// NOTE: assuming all parameters are of the type floatX, could be relaxed later
|
|
mt19937_state init_rng;
|
|
manual_seed(&init_rng, 42);
|
|
floatX* params_memory_cpu = (floatX*)mallocCheck(model->num_parameters_bytes);
|
|
memset(params_memory_cpu, 0, model->num_parameters_bytes);
|
|
// fill in all the weights with random values
|
|
float residual_scale = 1.0f / sqrtf(2.0f * model->config.num_layers);
|
|
// we have to init all these tensors exactly in the order that PyTorch initializes them
|
|
// so that we can match them up and get correctness and exactly the same initial conditions
|
|
size_t L = model->config.num_layers;
|
|
size_t offset = 0;
|
|
for (int l = 0; l < L; l++) {
|
|
offset = 0;
|
|
for (int i = 0; i < NUM_PARAMETER_TENSORS; i++) {
|
|
// the layernorm parameters are all initialized to 1
|
|
if (l == 0 && (i == 2 || i == 8 || i == 14)) { // only at l = 0 to init these just once
|
|
for (size_t j = 0; j < model->param_elements[i]; j++) {
|
|
params_memory_cpu[offset + j] = 1.0f;
|
|
}
|
|
}
|
|
// weights tensors are handled here
|
|
if ((l == 0 && (i == 0 || i == 1)) // only at l = 0, init the wte and wpe tensors
|
|
|| i == 4 || i == 6 || i == 10 || i == 12) {
|
|
int n = model->param_elements[i];
|
|
size_t layer_offset = 0;
|
|
if (i == 0) {
|
|
// for wte tensor (padded vocab) override to init V instead of Vp rows
|
|
n = model->config.vocab_size * model->config.channels;
|
|
}
|
|
if (i == 4 || i == 6 || i == 10 || i == 12) {
|
|
// weight tensors, we are only initializing layer l
|
|
assert(n % L == 0);
|
|
n = n / L;
|
|
layer_offset = l * n;
|
|
}
|
|
// in GPT-2, the projections back into the residual stream are additionally
|
|
// scaled by 1/sqrt(2*L) for training stability
|
|
float scale = (i == 6 || i == 12) ? 0.02f * residual_scale : 0.02f;
|
|
// okay let's draw the random numbers and write them
|
|
float *fp32_buffer = (float*)mallocCheck(n * sizeof(float));
|
|
normal_(fp32_buffer, n, 0.0f, scale, &init_rng);
|
|
for (size_t j = 0; j < n; j++) {
|
|
params_memory_cpu[offset + layer_offset + j] = (floatX)fp32_buffer[j];
|
|
}
|
|
free(fp32_buffer);
|
|
}
|
|
offset += model->param_elements[i];
|
|
}
|
|
}
|
|
|
|
// copy them to GPU
|
|
cudaCheck(cudaMemcpy(model->params_memory, params_memory_cpu, model->num_parameters_bytes, cudaMemcpyHostToDevice));
|
|
free(params_memory_cpu);
|
|
|
|
// other inits and defaults
|
|
model->acts_memory = NULL;
|
|
model->grads_memory = NULL;
|
|
model->m_memory = NULL;
|
|
model->v_memory = NULL;
|
|
model->master_weights = NULL;
|
|
model->grads_acts_memory = NULL;
|
|
model->inputs = NULL;
|
|
model->targets = NULL;
|
|
model->cpu_losses = NULL;
|
|
model->cpu_losses_fp32 = NULL;
|
|
model->batch_size = 0;
|
|
model->seq_len = 0;
|
|
model->mean_loss = -1.0f; // -1.0f designates no loss
|
|
model->rng_state = 13371337;
|
|
model->use_master_weights = 1; // keep master weights copy in float for optim update?
|
|
model->recompute = 1; // default to recompute gelu during backward
|
|
}
|
|
|
|
void gpt2_forward(GPT2 *model, int* inputs, int* targets, size_t B, size_t T, int grad_accum_steps=1) {
|
|
NVTX_RANGE_FN();
|
|
// targets are optional and could be NULL
|
|
// in this function we must be careful and use size_t instead of int, otherwise
|
|
// we could overflow int. E.g. l * B * NH * T * T overflows int at B 16.
|
|
|
|
// ensure the model was initialized or error out
|
|
if (model->params_memory == NULL) {
|
|
printf("Error: model was not initialized properly.\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
// convenience parameters
|
|
size_t V = model->config.vocab_size;
|
|
size_t Vp = model->config.padded_vocab_size;
|
|
size_t L = model->config.num_layers;
|
|
size_t NH = model->config.num_heads;
|
|
size_t C = model->config.channels;
|
|
|
|
// validate inputs, all indices must be in the range [0, V)
|
|
for(int i = 0; i < B * T; i++) {
|
|
assert(0 <= inputs[i] && inputs[i] < V);
|
|
if (targets != NULL) {
|
|
assert(0 <= targets[i] && targets[i] < V);
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
// allocate the space
|
|
fill_in_activation_sizes(model->act_sizes, B, T, model->config, model->recompute);
|
|
size_t num_activations = 0;
|
|
for (size_t i = 0; i < NUM_ACTIVATION_TENSORS; i++) {
|
|
num_activations += model->act_sizes[i];
|
|
}
|
|
model->num_activations = num_activations;
|
|
printf0("allocating %d MiB for activations\n", (int)round(num_activations * sizeof(floatX) / (1024 * 1024)));
|
|
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(floatX)));
|
|
cudaCheck(cudaMallocHost((void**)&model->cpu_losses_fp32, B * T * sizeof(float)));
|
|
} else {
|
|
// validate B,T is consistent with how we've allocated the memory before
|
|
// in principle we could get more clever here in the future, for now this is safest
|
|
if (B != model->batch_size || T != model->seq_len) {
|
|
printf("Model: B=%d T=%d, Desired: B=%d T=%d\n", model->batch_size, model->seq_len, (int)B, (int)T);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
|
|
// copy inputs/targets to the model
|
|
// todo - inputs is copied on default stream so this synchronises CPU/GPU for now
|
|
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;
|
|
encoder_forward(acts.encoded, model->inputs, params.wte, params.wpe, B, T, C); // encoding goes into residual[0]
|
|
|
|
// first layernorm isn't fused
|
|
layernorm_forward(acts.ln1, acts.ln1_mean, acts.ln1_rstd, acts.encoded, params.ln1w, params.ln1b, B, T, C);
|
|
|
|
for (int l = 0; l < L; l++) {
|
|
NvtxRange layer_range("Layer", l);
|
|
|
|
floatX* residual = l == 0 ? acts.encoded : acts.residual3 + (l-1) * B * T * C;
|
|
|
|
// get the pointers of the weights for this layer
|
|
floatX* l_qkvw = params.qkvw + l * 3*C * C;
|
|
floatX* l_qkvb = params.qkvb + l * 3*C;
|
|
floatX* l_attprojw = params.attprojw + l * C * C;
|
|
floatX* l_attprojb = params.attprojb + l * C;
|
|
floatX* l_ln2w = params.ln2w + l * C;
|
|
floatX* l_ln2b = params.ln2b + l * C;
|
|
floatX* l_fcw = params.fcw + l * 4*C * C;
|
|
floatX* l_fcb = params.fcb + l * 4*C;
|
|
floatX* l_fcprojw = params.fcprojw + l * C * 4*C;
|
|
floatX* l_fcprojb = params.fcprojb + l * C;
|
|
|
|
// get the pointers of the activations for this layer
|
|
floatX* l_ln1 = acts.ln1 + l * B * T * C;
|
|
floatX* l_qkvr = acts.qkvr + l * B * T * 3*C;
|
|
floatX* l_atty = acts.atty + l * B * T * C;
|
|
floatX* l_attproj = acts.attproj + l * B * T * C;
|
|
floatX* l_residual2 = acts.residual2 + l * B * T * C;
|
|
floatX* l_ln2 = acts.ln2 + l * B * T * C;
|
|
floatX* l_ln2_mean = acts.ln2_mean + l * B * T;
|
|
floatX* l_ln2_rstd = acts.ln2_rstd + l * B * T;
|
|
floatX* l_fch = acts.fch + l * B * T * 4*C;
|
|
// reuse the same activation buffer at each layer, as we'll re-compute the gelu during backward
|
|
// very useful because we dramatically reduce VRAM usage, and may be able to fit larger batch size
|
|
floatX* l_fch_gelu = (model->recompute == 0) ? acts.fch_gelu + l * B * T * 4*C : acts.fch_gelu;
|
|
floatX* l_fcproj = acts.fcproj + l * B * T * C;
|
|
floatX* l_residual3 = acts.residual3 + l * B * T * C;
|
|
|
|
// now do the forward pass
|
|
#ifdef ENABLE_CUDNN
|
|
float* l_att = (float*)acts.att + l * B * NH * T; // cuDNN needs a smaller FP32 tensor
|
|
matmul_forward_cublaslt(l_qkvr, l_ln1, l_qkvw, l_qkvb, B, T, C, 3*C);
|
|
attention_forward_cudnn(l_atty, (float*)l_att, l_qkvr, B, T, NH, C);
|
|
#else
|
|
floatX* l_att = acts.att + l * B * NH * T * T;
|
|
// these are only needed as scratchpads for the forward pass, but
|
|
// need not be stored for backward
|
|
floatX* scratch = (floatX*)acts.output;
|
|
matmul_forward_cublaslt(scratch, l_ln1, l_qkvw, l_qkvb, B, T, C, 3*C);
|
|
attention_forward(l_atty, l_qkvr, l_att, scratch, B, T, C, NH);
|
|
#endif
|
|
|
|
matmul_forward_cublaslt(l_attproj, l_atty, l_attprojw, l_attprojb, B, T, C, C);
|
|
fused_residual_forward5(l_residual2, l_ln2, l_ln2_mean, l_ln2_rstd, residual, l_attproj, 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);
|
|
|
|
// OK, fusion across blocks.
|
|
if(l+1 != L) {
|
|
floatX* l_ln1 = acts.ln1 + (l + 1) * B * T * C;
|
|
floatX* l_ln1_mean = acts.ln1_mean + (l + 1) * B * T;
|
|
floatX* l_ln1_rstd = acts.ln1_rstd + (l + 1) * B * T;
|
|
const floatX* l_ln1w = params.ln1w + (l + 1) * C;
|
|
const floatX* l_ln1b = params.ln1b + (l + 1) * C;
|
|
fused_residual_forward5(l_residual3, l_ln1, l_ln1_mean, l_ln1_rstd, l_residual2, l_fcproj, l_ln1w, l_ln1b,
|
|
B * T, C);
|
|
} else {
|
|
fused_residual_forward5(l_residual3, acts.lnf, acts.lnf_mean, acts.lnf_rstd, l_residual2, l_fcproj,
|
|
params.lnfw, params.lnfb,
|
|
B * T, C);
|
|
}
|
|
}
|
|
|
|
matmul_forward_cublaslt(acts.output, acts.lnf, params.wte, NULL, B, T, C, Vp);
|
|
|
|
// also forward the cross-entropy loss function if we have the targets
|
|
if (targets != NULL) {
|
|
NvtxRange classifier_and_loss_range("classifier_and_loss");
|
|
// fused classifier: does the forward pass and first part of the backward pass
|
|
const float dloss = 1.0f / (B * T * grad_accum_steps); // results in the uniform average loss over all elements
|
|
fused_classifier(acts.output, acts.losses, dloss, model->targets, B, T, V, Vp);
|
|
// for convenience also evaluate the mean loss (TODO re-think this compute+sync point)
|
|
cudaCheck(cudaMemcpy(model->cpu_losses, acts.losses, B * T * sizeof(floatX), cudaMemcpyDeviceToHost));
|
|
float mean_loss = 0.0f;
|
|
for (int i = 0; i < B*T; i++) {
|
|
float loss = (float)(model->cpu_losses[i]);
|
|
model->cpu_losses_fp32[i] = loss;
|
|
mean_loss += loss;
|
|
}
|
|
mean_loss /= B*T*grad_accum_steps;
|
|
model->mean_loss = mean_loss;
|
|
} else {
|
|
// if we don't have targets, we don't have loss
|
|
model->mean_loss = -1.0f;
|
|
}
|
|
}
|
|
|
|
void gpt2_zero_grad(GPT2 *model) {
|
|
NVTX_RANGE_FN();
|
|
if (model->grads_memory != NULL) {
|
|
cudaCheck(cudaMemset(model->grads_memory, 0, model->num_parameters * sizeof(floatX)));
|
|
}
|
|
}
|
|
|
|
void gpt2_backward(GPT2 *model, int* inputs) {
|
|
NVTX_RANGE_FN();
|
|
// double check we forwarded previously, with targets
|
|
if (model->mean_loss == -1.0f) {
|
|
printf("Error: must forward with targets before backward\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
// lazily allocate the memory for gradients of the weights and activations, if needed
|
|
if (model->grads_memory == NULL) {
|
|
// allocate buffers for weight gradients
|
|
printf0("allocating %d MiB for parameter gradients\n", (int)round(model->num_parameters * sizeof(floatX) / (1024 * 1024)));
|
|
model->grads_memory = malloc_and_point_parameters(&model->grads, model->param_elements, model->param_sizeof);
|
|
// we're going to be clever for the activations backward pass. we don't need to exactly
|
|
// mirror the forward pass activations and we will save memory.
|
|
size_t bw_act_sizes[NUM_ACTIVATION_TENSORS];
|
|
fill_in_grad_act_sizes(bw_act_sizes, model->batch_size, model->seq_len, model->config);
|
|
// count up and allocate the space
|
|
model->num_grad_acts = 0;
|
|
for (size_t i = 0; i < NUM_BACKWARD_TENSORS; i++) {
|
|
model->num_grad_acts += bw_act_sizes[i];
|
|
}
|
|
printf0("allocating %d MiB for activation gradients\n", (int)round(model->num_grad_acts * sizeof(floatX) / (1024 * 1024)));
|
|
model->grads_acts_memory = malloc_and_point_backward(&model->grads_acts, bw_act_sizes);
|
|
// init gradients of parameters and activations to zero
|
|
gpt2_zero_grad(model);
|
|
// initialise cpu scratch buffers for encoder backward
|
|
size_t num_c_groups = model->config.channels / (WARP_SIZE * x128::size);
|
|
assert((size_t)(model->batch_size * model->seq_len) * num_c_groups < (1ULL<<31ULL)); // todo - maybe an issue for llama3-400B(?)
|
|
model->workload_indices = (int*)mallocCheck(sizeof(int) * model->batch_size * model->seq_len * num_c_groups);
|
|
model->bucket_info = (int4*)mallocCheck(sizeof(int4) * model->batch_size * model->seq_len * num_c_groups);
|
|
}
|
|
|
|
// convenience shortcuts, size_t instead of int so that pointer arithmetics don't overflow
|
|
size_t B = model->batch_size;
|
|
size_t T = model->seq_len;
|
|
size_t Vp = model->config.padded_vocab_size;
|
|
size_t L = model->config.num_layers;
|
|
size_t NH = model->config.num_heads;
|
|
size_t 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;
|
|
GradActTensors grads_acts = model->grads_acts;
|
|
|
|
// reset residual stream gradients (put here to work with gradient accumulation)
|
|
cudaCheck(cudaMemset(model->grads_acts.residual3, 0, B * T * C * sizeof(floatX)));
|
|
|
|
// re-use the output buffer of the forward pass as a scratchpad during backward pass
|
|
float* scratchF = (float*)acts.output;
|
|
floatX* scratchX = (floatX*)acts.output;
|
|
|
|
// we kick off the chain rule by filling in dlosses with 1.0f/(B*T)
|
|
// this was done in the fused classifier kernel as last step of forward pass
|
|
// technically that 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
|
|
// next: backward the classifier matmul
|
|
matmul_backward(grads_acts.bt4c, grads.wte, NULL, acts.output, acts.lnf, params.wte, NULL, B, T, C, Vp);
|
|
// backward the final layernorm
|
|
floatX* residual = acts.residual3 + (L-1) * B * T * C; // last residual is in residual3
|
|
floatX* dresidual = (floatX*)grads_acts.residual3; // the main buffer holding the gradient in the backward pass
|
|
layernorm_backward(dresidual, grads.lnfw, grads.lnfb, scratchF, grads_acts.bt4c, residual, params.lnfw, acts.lnf_mean, acts.lnf_rstd, B, T, C);
|
|
|
|
// now backward all the layers
|
|
for (int l = L-1; l >= 0; l--) {
|
|
NvtxRange layer_range("Layer", l);
|
|
|
|
residual = l == 0 ? acts.encoded : acts.residual3 + (l-1) * B * T * C;
|
|
|
|
// get the pointers of the weights for this layer
|
|
floatX* l_ln1w = params.ln1w + l * C;
|
|
floatX* l_qkvw = params.qkvw + l * 3*C * C;
|
|
floatX* l_attprojw = params.attprojw + l * C * C;
|
|
floatX* l_ln2w = params.ln2w + l * C;
|
|
floatX* l_fcw = params.fcw + l * 4*C * C;
|
|
floatX* l_fcprojw = params.fcprojw + l * C * 4*C;
|
|
// get the pointers of the gradients of the weights for this layer
|
|
floatX* dl_ln1w = grads.ln1w + l * C;
|
|
floatX* dl_ln1b = grads.ln1b + l * C;
|
|
floatX* dl_qkvw = grads.qkvw + l * 3*C * C;
|
|
floatX* dl_qkvb = grads.qkvb + l * 3*C;
|
|
floatX* dl_attprojw = grads.attprojw + l * C * C;
|
|
floatX* dl_attprojb = grads.attprojb + l * C;
|
|
floatX* dl_ln2w = grads.ln2w + l * C;
|
|
floatX* dl_ln2b = grads.ln2b + l * C;
|
|
floatX* dl_fcw = grads.fcw + l * 4*C * C;
|
|
floatX* dl_fcb = grads.fcb + l * 4*C;
|
|
floatX* dl_fcprojw = grads.fcprojw + l * C * 4*C;
|
|
floatX* dl_fcprojb = grads.fcprojb + l * C;
|
|
// get the pointers of the activations for this layer
|
|
floatX* l_ln1 = acts.ln1 + l * B * T * C;
|
|
floatX* l_ln1_mean = acts.ln1_mean + l * B * T;
|
|
floatX* l_ln1_rstd = acts.ln1_rstd + l * B * T;
|
|
floatX* l_qkvr = acts.qkvr + l * B * T * 3*C;
|
|
floatX* l_atty = acts.atty + l * B * T * C;
|
|
floatX* l_residual2 = acts.residual2 + l * B * T * C;
|
|
floatX* l_ln2 = acts.ln2 + l * B * T * C;
|
|
floatX* l_ln2_mean = acts.ln2_mean + l * B * T;
|
|
floatX* l_ln2_rstd = acts.ln2_rstd + l * B * T;
|
|
floatX* l_fch = acts.fch + l * B * T * 4*C;
|
|
floatX* l_fch_gelu = (model->recompute == 0) ? acts.fch_gelu + l * B * T * 4*C : acts.fch_gelu;
|
|
// get the pointers of the gradients of the activations for this layer
|
|
// notice that there is no l *, because we just have a single copy, and keep
|
|
// re-using this memory in every Transformer block as we calculate backward pass
|
|
|
|
// we need a B x T x C buffer; thankfully, the forward activation for lnf isn't needed anymore,
|
|
// so we can co-opt it here.
|
|
floatX* dl_btc = (floatX*)acts.lnf;
|
|
floatX* dl_bt4c = (floatX*)grads_acts.bt4c;
|
|
|
|
// start the backward pass for this layer
|
|
if(model->recompute >= 1) {
|
|
// recompute >= 1 means we recompute gelu. in this case,
|
|
// l_fch_gelu is just a buffer, so re-compute the gelu from l_fch here
|
|
gelu_forward(l_fch_gelu, l_fch, B*T*4*C);
|
|
}
|
|
matmul_backward(dl_bt4c, dl_fcprojw, dl_fcprojb, dresidual, l_fch_gelu, l_fcprojw, scratchF, B, T, 4*C, C);
|
|
gelu_backward(dl_bt4c, l_fch, dl_bt4c, B*T*4*C);
|
|
matmul_backward(dl_btc, dl_fcw, dl_fcb, dl_bt4c, l_ln2, l_fcw, scratchF, B, T, C, 4 * C);
|
|
// layernorm backward does += to the dresidual, so it correctly accumulates grad from the MLP block above
|
|
layernorm_backward(dresidual, dl_ln2w, dl_ln2b, scratchF, dl_btc, l_residual2, l_ln2w, l_ln2_mean, l_ln2_rstd, B, T, C);
|
|
matmul_backward(dl_btc, dl_attprojw, dl_attprojb, dresidual, l_atty, l_attprojw, scratchF, B, T, C, C);
|
|
|
|
#ifdef ENABLE_CUDNN
|
|
float* l_att = (float*)acts.att + l * B * NH * T; // cuDNN needs a smaller FP32 tensor
|
|
attention_backward_cudnn(dl_bt4c, dl_btc, l_qkvr, l_atty, (float*)l_att, B, T, NH, C);
|
|
#else
|
|
floatX* l_att = acts.att + l * B * NH * T * T;
|
|
// we need B x T x (4)C buffers. l_atty and l_fch aren't needed anymore at this point, so reuse their memory
|
|
floatX* buffer_a = l_atty;
|
|
floatX* buffer_b = l_fch; // this is B x T x 4C, so even larger than what we need
|
|
floatX* dl_preatt = (floatX*)grads_acts.preatt; // dedicated scratchpad allocation
|
|
attention_backward(dl_bt4c, buffer_b, dl_preatt, scratchX, buffer_a, dl_btc, l_qkvr, l_att, B, T, C, NH);
|
|
#endif
|
|
|
|
// QKV parameter gradients
|
|
matmul_backward(dl_btc, dl_qkvw, dl_qkvb, dl_bt4c, l_ln1, l_qkvw, scratchF, B, T, C, 3 * C);
|
|
// layernorm backward does += to dresidual, so it correctly accumulates gradient for the Attention block above
|
|
layernorm_backward(dresidual, dl_ln1w, dl_ln1b, scratchF, dl_btc, residual, l_ln1w, l_ln1_mean, l_ln1_rstd, B, T, C);
|
|
}
|
|
encoder_backward(grads.wte, grads.wpe, scratchX, model->workload_indices, model->bucket_info,
|
|
dresidual, model->inputs, inputs, B, T, C, random_u32(&model->rng_state));
|
|
}
|
|
|
|
// Compute sum of a single CPU value across all GPU processes. No-op when multi-GPU is disabled.
|
|
float multi_gpu_cpu_float_sum(float value) {
|
|
#ifdef MULTI_GPU
|
|
// note MPI doesn't support all reduce with mean, only sum
|
|
float result;
|
|
mpiCheck(MPI_Allreduce(&value, &result, 1, MPI_FLOAT, MPI_SUM, MPI_COMM_WORLD));
|
|
return result;
|
|
#else
|
|
return value;
|
|
#endif
|
|
}
|
|
|
|
// Averages out the loss and gradients across all GPUs. No-op when multi-GPU is disabled.
|
|
// todo - this version only works if all the parameters are the same size (floatX)
|
|
void gpt2_multi_gpu_accumulate(GPT2* model, MultiGpuConfig* multi_gpu_config) {
|
|
#ifdef MULTI_GPU
|
|
NVTX_RANGE_FN();
|
|
if (multi_gpu_config->num_processes == 1) { return; }
|
|
// Average all losses.
|
|
model->accumulated_mean_loss = multi_gpu_cpu_float_sum(model->mean_loss) / multi_gpu_config->num_processes;
|
|
if(multi_gpu_config->zero_stage == 0) {
|
|
// no ZERO == standard DDP: Average all gradients.
|
|
ncclCheck(ncclAllReduce(model->grads_memory, model->grads_memory,
|
|
model->num_parameters,
|
|
ncclFloatX, ncclAvg,
|
|
multi_gpu_config->nccl_comm, 0));
|
|
} else if (multi_gpu_config->zero_stage == 1) {
|
|
// ZERO-1: Get average gradient for local shard
|
|
floatX* local_grads_memory = (floatX*) model->grads_memory + multi_gpu_config->shard_offset;
|
|
ncclCheck(ncclReduceScatter(model->grads_memory, local_grads_memory,
|
|
multi_gpu_config->shard_num_parameters,
|
|
ncclFloatX, ncclAvg,
|
|
multi_gpu_config->nccl_comm, 0));
|
|
}
|
|
#endif
|
|
}
|
|
|
|
float gpt2_update(GPT2 *model, float learning_rate, float beta1, float beta2, float eps, float weight_decay, float grad_clip, int t, MultiGpuConfig* multi_gpu_config) {
|
|
NVTX_RANGE_FN();
|
|
size_t num_parameters = multi_gpu_config->shard_num_parameters;
|
|
floatX* params_memory = (floatX*)model->params_memory + multi_gpu_config->shard_offset;
|
|
floatX* grads_memory = (floatX*)model->grads_memory + multi_gpu_config->shard_offset;
|
|
|
|
if (model->m_memory == NULL) {
|
|
printf0("allocating %zu MiB for AdamW optimizer state m\n", (num_parameters * sizeof(float)) >> 20);
|
|
printf0("allocating %zu MiB for AdamW optimizer state v\n", (num_parameters * sizeof(float)) >> 20);
|
|
cudaCheck(cudaMalloc((void**)&model->m_memory, num_parameters * sizeof(float)));
|
|
cudaCheck(cudaMalloc((void**)&model->v_memory, num_parameters * sizeof(float)));
|
|
cudaCheck(cudaMemset(model->m_memory, 0, num_parameters * sizeof(float)));
|
|
cudaCheck(cudaMemset(model->v_memory, 0, num_parameters * sizeof(float)));
|
|
if (model->use_master_weights == 1) {
|
|
printf0("allocating %zu MiB for master copy of params\n", (num_parameters * sizeof(float)) >> 20);
|
|
cudaCheck(cudaMalloc((void**)&model->master_weights, num_parameters * sizeof(float)));
|
|
copy_and_cast_kernel<<<CEIL_DIV(num_parameters, 512), 512>>>(model->master_weights, params_memory, num_parameters);
|
|
cudaCheck(cudaGetLastError());
|
|
}
|
|
}
|
|
|
|
// gradient clipping
|
|
// repurposing this buffer (which isn't needed now) to write grad norm into it
|
|
float* grad_norm_squared = (float*)model->acts.output;
|
|
global_norm_squared(grad_norm_squared, (floatX*)model->grads_memory, model->num_parameters);
|
|
// transfer the gradient norm to CPU
|
|
float grad_norm_squared_cpu = 0.0f;
|
|
cudaCheck(cudaMemcpy(&grad_norm_squared_cpu, grad_norm_squared, sizeof(float), cudaMemcpyDeviceToHost));
|
|
if(!isfinite(grad_norm_squared_cpu)) {
|
|
// may happen due to some issue (e.g. overflow?)
|
|
// TODO: later may want to keep a global counter of instabilities like this
|
|
printf0("[WARNING]: grad norm is not finite, skipping AdamW update\n");
|
|
return -1.0f;
|
|
}
|
|
float grad_norm_cpu = sqrtf(grad_norm_squared_cpu);
|
|
float grad_scale = (grad_norm_cpu > grad_clip) ? grad_clip / grad_norm_cpu : 1.0f;
|
|
|
|
// AdamW update
|
|
int block_size = 512;
|
|
float beta1_correction = 1.0f - powf(beta1, t);
|
|
float beta2_correction = 1.0f - powf(beta2, t);
|
|
unsigned int seed = random_u32(&model->rng_state);
|
|
|
|
// individually call the adamw_kernel3 on all parameter tensors separately
|
|
floatX* params_memory_iter = params_memory;
|
|
float* master_weights_iter = model->master_weights;
|
|
floatX* grads_memory_iter = grads_memory;
|
|
float* m_memory_iter = (float*)model->m_memory;
|
|
float* v_memory_iter = (float*)model->v_memory;
|
|
for (int i = 0; i < NUM_PARAMETER_TENSORS; i++) {
|
|
size_t num_parameters = model->param_elements[i];
|
|
int num_blocks = CEIL_DIV(num_parameters, block_size);
|
|
// we only want to weight decay the 2D tensors and leave all 1D tensors alone
|
|
// in particular this also decays the embedding weights, but this is ok:
|
|
// - the token embeddings are weight shared and participate in the final projection to logits
|
|
// - the position embeddings actively participate at every forward/backward pass
|
|
float wd = (i == 0 || i == 1 || i == 4 || i == 6 || i == 10 || i == 12) ? weight_decay : 0.0f;
|
|
adamw_kernel3<<<num_blocks, block_size>>>(params_memory_iter, master_weights_iter, grads_memory_iter,
|
|
m_memory_iter, v_memory_iter, num_parameters,
|
|
learning_rate, beta1, beta2, beta1_correction, beta2_correction, eps, wd,
|
|
grad_scale, seed);
|
|
params_memory_iter += num_parameters;
|
|
if (master_weights_iter != NULL) { master_weights_iter += num_parameters; }
|
|
grads_memory_iter += num_parameters;
|
|
m_memory_iter += num_parameters;
|
|
v_memory_iter += num_parameters;
|
|
}
|
|
cudaCheck(cudaGetLastError());
|
|
return grad_norm_cpu;
|
|
}
|
|
|
|
void gpt2_multi_gpu_gather(GPT2 *model, MultiGpuConfig* multi_gpu_config)
|
|
{
|
|
#ifdef MULTI_GPU
|
|
if (multi_gpu_config->num_processes == 1) { return; } // 1 process => noop
|
|
if (multi_gpu_config->zero_stage == 1) {
|
|
// gather updated shards of model->params_memory from each process
|
|
ncclCheck(ncclAllGather((floatX*)model->params_memory + multi_gpu_config->shard_offset, (floatX*)model->params_memory,
|
|
multi_gpu_config->shard_num_parameters, ncclFloatX,
|
|
multi_gpu_config->nccl_comm, 0));
|
|
}
|
|
cudaCheck(cudaGetLastError());
|
|
#endif
|
|
}
|
|
|
|
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->master_weights));
|
|
cudaCheck(cudaFree(model->acts_memory));
|
|
cudaCheck(cudaFree(model->grads_acts_memory));
|
|
cudaCheck(cudaFree(model->inputs));
|
|
cudaCheck(cudaFree(model->targets));
|
|
cudaFreeHost(model->cpu_losses);
|
|
cudaFreeHost(model->cpu_losses_fp32);
|
|
free(model->workload_indices);
|
|
free(model->bucket_info);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// common init & free code for train/test/profile
|
|
void common_start(bool override_enable_tf32 = true, bool print_device_info = true) {
|
|
cudaGetDeviceProperties(&deviceProp, multi_gpu_config.local_device_idx);
|
|
if (print_device_info) {
|
|
printf("[System]\n");
|
|
printf("Device %d: %s\n", multi_gpu_config.local_device_idx, deviceProp.name);
|
|
}
|
|
|
|
// set up cuBLAS and cuBLASLt (and cuDNN if enabled)
|
|
cublasCheck(cublasCreate(&cublas_handle));
|
|
cublasCheck(cublasLtCreate(&cublaslt_handle));
|
|
cudaCheck(cudaMalloc(&cublaslt_workspace, cublaslt_workspace_size));
|
|
|
|
// TF32 precision is equivalent to torch.set_float32_matmul_precision('high')
|
|
bool enable_tf32 = PRECISION_MODE == PRECISION_FP32 && deviceProp.major >= 8 && override_enable_tf32;
|
|
cublasCheck(cublasSetMathMode(cublas_handle, enable_tf32 ? CUBLAS_TF32_TENSOR_OP_MATH : CUBLAS_DEFAULT_MATH));
|
|
cublas_compute = enable_tf32 ? CUBLAS_COMPUTE_32F_FAST_TF32 : CUBLAS_COMPUTE_32F;
|
|
|
|
create_cudnn();
|
|
}
|
|
|
|
void common_free(GPT2 &model) {
|
|
gpt2_free(&model);
|
|
cudaCheck(cudaFree(cublaslt_workspace));
|
|
cublasCheck(cublasDestroy(cublas_handle));
|
|
cublasCheck(cublasLtDestroy(cublaslt_handle));
|
|
destroy_cudnn();
|
|
}
|
|
|
|
#ifndef TESTING
|
|
// if we are TESTING (see test_gpt2.cu), we'll skip everything below this point
|
|
// ----------------------------------------------------------------------------
|
|
// sampler: takes probabilities and samples integers from them
|
|
|
|
int sample_softmax(const float* logits, int n, float coin) {
|
|
// sample index from logits (converted to probabilities using softmax)
|
|
// coin is a random number in [0, 1), usually from random_f32()
|
|
double norm = 0;
|
|
for (int i = 0; i < n; i++) {
|
|
norm += expf(logits[i]);
|
|
}
|
|
// instead of dividing all exp(logits), we can just multiply coin.
|
|
coin *= norm;
|
|
float cdf = 0.0f;
|
|
for (int i = 0; i < n; i++) {
|
|
cdf += expf(logits[i]);
|
|
if (coin < cdf) {
|
|
return i;
|
|
}
|
|
}
|
|
return n - 1; // in case of rounding errors
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Logger lite, will probably grow/change some over time
|
|
|
|
void create_dir_if_not_exists(const char *dir) {
|
|
if (dir == NULL) { return; }
|
|
struct stat st = {0};
|
|
if (stat(dir, &st) == -1) {
|
|
if (mkdir(dir, 0700) == -1) {
|
|
printf0("ERROR: could not create directory: %s\n", dir);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
printf0("created directory: %s\n", dir);
|
|
}
|
|
}
|
|
|
|
typedef struct {
|
|
FILE *logfile;
|
|
int flush_every; // every how many steps to flush the log
|
|
} Logger;
|
|
|
|
void logger_init(Logger *logger, const char *log_dir, int process_rank) {
|
|
logger->flush_every = 10;
|
|
logger->logfile = NULL;
|
|
if (log_dir != NULL && process_rank == 0) {
|
|
char output_log_file[256];
|
|
assert(strlen(log_dir) < 200); // being a bit lazy, can relax later maybe
|
|
snprintf(output_log_file, 256, "%s/main.log", log_dir);
|
|
logger->logfile = fopenCheck(output_log_file, "w");
|
|
}
|
|
}
|
|
|
|
void logger_log_eval(Logger *logger, int step, float val) {
|
|
if (logger->logfile != NULL) {
|
|
fprintf(logger->logfile, "s:%d eval:%.4f\n", step, val);
|
|
}
|
|
}
|
|
|
|
void logger_log_val(Logger *logger, int step, float val_loss) {
|
|
if (logger->logfile != NULL) {
|
|
fprintf(logger->logfile, "s:%d tel:%.4f\n", step, val_loss);
|
|
}
|
|
}
|
|
|
|
void logger_log_train(Logger *logger, int step, float train_loss) {
|
|
if (logger->logfile != NULL) {
|
|
fprintf(logger->logfile, "s:%d trl:%.4f\n", step, train_loss);
|
|
if (step % logger->flush_every == 0) { fflush(logger->logfile); }
|
|
}
|
|
}
|
|
|
|
void logger_free(Logger *logger) {
|
|
if (logger->logfile != NULL) { fclose(logger->logfile); }
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// CLI, poor man's argparse
|
|
|
|
void error_usage() {
|
|
fprintf(stderr, "Usage: ./train_gpt2cu [options]\n");
|
|
fprintf(stderr, "Options:\n");
|
|
fprintf(stderr, " -i <string> train data filename pattern (default = dev/data/tinyshakespeare/tiny_shakespeare_train.bin)\n");
|
|
fprintf(stderr, " -j <string> val data filename pattern (default = dev/data/tinyshakespeare/tiny_shakespeare_val.bin)\n");
|
|
fprintf(stderr, " -e <string> input from model at this filename (default = gpt2_124M_bf16.bin)\n");
|
|
fprintf(stderr, " -o <string> output log dir (default = NULL, no logging)\n");
|
|
fprintf(stderr, " -b <int> (per-GPU, micro) batch size B (default = 4)\n");
|
|
fprintf(stderr, " -t <int> sequence length T (default = 1024)\n");
|
|
fprintf(stderr, " -d <int> total desired batch size (default = B * T * num_processes, i.e. no grad accumulation\n");
|
|
fprintf(stderr, " -l <float> learning rate (default = 3e-4f)\n");
|
|
fprintf(stderr, " -u <int> learning rate warmup iterations (default = 0, no warmup)\n");
|
|
fprintf(stderr, " -q <float> learning rate decay: final fraction, at end of training (default = 1.0 (no decay))\n");
|
|
fprintf(stderr, " -c <float> weight decay (default = 0.0f)\n");
|
|
fprintf(stderr, " -x <int> max_steps of optimization to run (-1 (default) = disable, run 1 epoch)\n");
|
|
fprintf(stderr, " -v <int> val_loss_every, how often we evaluate val loss (default = 20)\n");
|
|
fprintf(stderr, " -m <int> val_max_batches, up to how many val batches to estimate val loss? (default = 20)\n");
|
|
fprintf(stderr, " -s <int> sample_every, how often we inference the model (default = 20)\n");
|
|
fprintf(stderr, " -g <int> genT, how many steps of inference we do (default = 64)\n");
|
|
fprintf(stderr, " -a <int> overfit a single batch? 0/1. useful for debugging\n");
|
|
fprintf(stderr, " -f <int> enable_tf32 override (default: 1, set to 0 to disable tf32)\n");
|
|
fprintf(stderr, " -w <int> keep f32 copy of weights for the optimizer? (default: 1)\n");
|
|
fprintf(stderr, " -z <int> zero_stage, Zero Optimization Stage, 0,1,2,3 (default = 0)\n");
|
|
fprintf(stderr, " -r <int> recompute: saves memory at cost of speed. (default = 1), 0 = none. 1 = recompute gelu\n");
|
|
fprintf(stderr, " -h <int> hellaswag eval run? (default = 0)\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// main training loop
|
|
int main(int argc, char *argv[]) {
|
|
multi_gpu_config = multi_gpu_config_init(&argc, &argv);
|
|
|
|
// read in the (optional) command line arguments
|
|
const char* train_data_pattern = "dev/data/tinyshakespeare/tiny_shakespeare_train.bin";
|
|
const char* val_data_pattern = "dev/data/tinyshakespeare/tiny_shakespeare_val.bin";
|
|
const char* load_filename = "gpt2_124M_bf16.bin"; // bf16 weights of the model
|
|
const char* output_log_dir = NULL;
|
|
int B = 4; // batch size
|
|
int T = 1024; // sequence length max
|
|
int total_batch_size = -1; // will be calculated down below later, if not provided
|
|
float learning_rate = 3e-4f;
|
|
int warmup_iterations = 0;
|
|
float final_learning_rate_frac = 1.0f; // final fraction of learning rate, at end of training
|
|
float weight_decay = 0.0f;
|
|
int val_loss_every = 20; // every how many steps do we eval validation loss?
|
|
int val_max_batches = 20; // how many batches max do we eval for validation loss?
|
|
int sample_every = 20; // every how many steps to do inference?
|
|
int genT = 64; // number of steps of inference we will do
|
|
int overfit_single_batch = 0; // useful for debugging, 1 = only load a single data batch once
|
|
int max_steps = -1;
|
|
int override_enable_tf32 = 1;
|
|
int use_master_weights = 1;
|
|
int recompute = 1; // recompute during backward setting, 0 = none, 1 = recompute gelu
|
|
int zero_stage = 0; // Zero Optimization Stage for Multi-GPU training
|
|
float grad_clip = 1.0f;
|
|
int hellaswag_eval = 0;
|
|
for (int i = 1; i < argc; i+=2) {
|
|
if (i + 1 >= argc) { error_usage(); } // must have arg after flag
|
|
if (argv[i][0] != '-') { error_usage(); } // must start with dash
|
|
if (strlen(argv[i]) != 2) { error_usage(); } // must be -x (one dash, one letter)
|
|
// read in the args
|
|
if (argv[i][1] == 'i') { train_data_pattern = argv[i+1]; }
|
|
else if (argv[i][1] == 'j') { val_data_pattern = argv[i+1]; }
|
|
else if (argv[i][1] == 'e') { load_filename = argv[i+1]; }
|
|
else if (argv[i][1] == 'o') { output_log_dir = argv[i+1]; }
|
|
else if (argv[i][1] == 'b') { B = atoi(argv[i+1]); } // Per-GPU (micro) batch size
|
|
else if (argv[i][1] == 't') { T = atoi(argv[i+1]); }
|
|
else if (argv[i][1] == 'd') { total_batch_size = atoi(argv[i+1]); }
|
|
else if (argv[i][1] == 'l') { learning_rate = atof(argv[i+1]); }
|
|
else if (argv[i][1] == 'u') { warmup_iterations = atoi(argv[i+1]); }
|
|
else if (argv[i][1] == 'q') { final_learning_rate_frac = atof(argv[i+1]); }
|
|
else if (argv[i][1] == 'c') { weight_decay = atof(argv[i+1]); }
|
|
else if (argv[i][1] == 'x') { max_steps = atoi(argv[i+1]); }
|
|
else if (argv[i][1] == 'v') { val_loss_every = atoi(argv[i+1]); }
|
|
else if (argv[i][1] == 'm') { val_max_batches = atoi(argv[i+1]); }
|
|
else if (argv[i][1] == 's') { sample_every = atoi(argv[i+1]); }
|
|
else if (argv[i][1] == 'g') { genT = atoi(argv[i+1]); }
|
|
else if (argv[i][1] == 'a') { overfit_single_batch = atoi(argv[i+1]); }
|
|
else if (argv[i][1] == 'f') { override_enable_tf32 = atoi(argv[i+1]); }
|
|
else if (argv[i][1] == 'w') { use_master_weights = atoi(argv[i+1]); }
|
|
else if (argv[i][1] == 'c') { grad_clip = atof(argv[i+1]); }
|
|
else if (argv[i][1] == 'z') { zero_stage = atoi(argv[i+1]); }
|
|
else if (argv[i][1] == 'r') { recompute = atoi(argv[i+1]); }
|
|
else if (argv[i][1] == 'h') { hellaswag_eval = atoi(argv[i+1]); }
|
|
else { error_usage(); }
|
|
}
|
|
// should do a bit more error checking here
|
|
assert(warmup_iterations >= 0);
|
|
// check if output_log_dir has a "." in it, because this behavior changed May 24, 2024. take out later
|
|
if (output_log_dir != NULL && strstr(output_log_dir, ".") != NULL) {
|
|
fprintf(stderr, "-o (output_log_dir) has a '.', are you specifying a file instead of dir?\n");
|
|
fprintf(stderr, "(note that this option changed recently, -o used to be file, became dir.)\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
// calculate a sensible default for total batch size by assuming no gradient accumulation
|
|
if (total_batch_size == -1) { total_batch_size = B * T * multi_gpu_config.num_processes; }
|
|
// if we're only overfitting a single batch for debugging, let's overfit the first batch
|
|
// from val instead of train split, because val is smaller and faster. (train_gpt2.py does the same)
|
|
if (overfit_single_batch == 1) { train_data_pattern = val_data_pattern; }
|
|
printf0("+-----------------------+----------------------------------------------------+\n");
|
|
printf0("| Parameter | Value |\n");
|
|
printf0("+-----------------------+----------------------------------------------------+\n");
|
|
printf0("| train data pattern | %-50s |\n", train_data_pattern);
|
|
printf0("| val data pattern | %-50s |\n", val_data_pattern);
|
|
printf0("| output log dir | %-50s |\n", output_log_dir == NULL ? "NULL" : output_log_dir);
|
|
printf0("| micro batch size B | %-50d |\n", B);
|
|
printf0("| sequence length T | %-50d |\n", T);
|
|
printf0("| total batch size | %-50d |\n", total_batch_size);
|
|
printf0("| learning rate (LR) | %-50e |\n", learning_rate);
|
|
printf0("| warmup iterations | %-50d |\n", warmup_iterations);
|
|
printf0("| final LR fraction | %-50e |\n", final_learning_rate_frac);
|
|
printf0("| weight decay | %-50e |\n", weight_decay);
|
|
printf0("| grad_clip | %-50e |\n", grad_clip);
|
|
printf0("| max_steps | %-50d |\n", max_steps);
|
|
printf0("| val_loss_every | %-50d |\n", val_loss_every);
|
|
printf0("| val_max_batches | %-50d |\n", val_max_batches);
|
|
printf0("| sample_every | %-50d |\n", sample_every);
|
|
printf0("| genT | %-50d |\n", genT);
|
|
printf0("| overfit_single_batch | %-50d |\n", overfit_single_batch);
|
|
printf0("| use_master_weights | %-50s |\n", use_master_weights ? "enabled" : "disabled");
|
|
printf0("| recompute | %-50d |\n", recompute);
|
|
printf0("+-----------------------+----------------------------------------------------+\n");
|
|
|
|
common_start(override_enable_tf32, false); // common init code for train/test/profile
|
|
|
|
const char* precision_str = (PRECISION_MODE == PRECISION_FP32)
|
|
? (cublas_compute == CUBLAS_COMPUTE_32F_FAST_TF32 ? "TF32" : "FP32")
|
|
: (PRECISION_MODE == PRECISION_FP16 ? "FP16" : "BF16");
|
|
|
|
printf0("| device | %-50s |\n", deviceProp.name);
|
|
printf0("| precision | %-50s |\n", precision_str);
|
|
printf0("+-----------------------+----------------------------------------------------+\n");
|
|
|
|
// build the GPT-2 model
|
|
GPT2 model;
|
|
// if load_filename is of the form "dX" where X is an integer (e.g. d12), then we build
|
|
// a random model with the depth of the model specified by X (e.g. 12). otherwise interpret
|
|
// this variable as a checkpoint filename, and load that checkpoint
|
|
assert(strlen(load_filename) >= 2);
|
|
if (load_filename[0] == 'd') {
|
|
int depth = atoi(load_filename + 1);
|
|
if (depth > 1 && depth <= 1000) { // we're not going to train models this big right? heh
|
|
gpt2_build_from_random(&model, depth);
|
|
} else {
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
} else {
|
|
gpt2_build_from_checkpoint(&model, load_filename);
|
|
}
|
|
|
|
model.use_master_weights = use_master_weights;
|
|
model.recompute = recompute;
|
|
printf0("| load_filename | %-50s |\n", load_filename);
|
|
printf0("| max_sequence_length T | %-50d |\n", model.config.max_seq_len);
|
|
printf0("| vocab_size V | %-50d |\n", model.config.vocab_size);
|
|
printf0("| padded_vocab_size Vp | %-50d |\n", model.config.padded_vocab_size);
|
|
printf0("| num_layers L | %-50d |\n", model.config.num_layers);
|
|
printf0("| num_heads NH | %-50d |\n", model.config.num_heads);
|
|
printf0("| channels C | %-50d |\n", model.config.channels);
|
|
printf0("| num_parameters | %-50zu |\n", model.num_parameters);
|
|
printf0("+-----------------------+----------------------------------------------------+\n");
|
|
|
|
// build DataLoaders for both train and val
|
|
DataLoader train_loader, val_loader;
|
|
dataloader_init(&train_loader, train_data_pattern, B, T, multi_gpu_config.process_rank, multi_gpu_config.num_processes);
|
|
dataloader_init(&val_loader, val_data_pattern, B, T, multi_gpu_config.process_rank, multi_gpu_config.num_processes);
|
|
int train_num_batches = (max_steps == -1) ? train_loader.num_batches : max_steps; // default = 1 epoch
|
|
int val_num_batches = train_loader.num_batches < val_max_batches ? train_loader.num_batches : val_max_batches;
|
|
printf0("| train_num_batches | %-50d |\n", train_num_batches);
|
|
printf0("| val_num_batches | %-50d |\n", val_num_batches);
|
|
printf0("+-----------------------+----------------------------------------------------+\n");
|
|
|
|
// build an EvalLoader for HellaSwag
|
|
EvalLoader eval_loader;
|
|
const char* hellaswag_path = "dev/data/hellaswag/hellaswag_val.bin";
|
|
const char hellaswag_available = access(hellaswag_path, F_OK) == 0;
|
|
const char run_hellaswag = hellaswag_eval && hellaswag_available;
|
|
if (run_hellaswag) {
|
|
evalloader_init(&eval_loader, hellaswag_path, B, T, multi_gpu_config.process_rank, multi_gpu_config.num_processes);
|
|
}
|
|
printf0("| run hellaswag | %-50s |\n", run_hellaswag ? "yes" : "no");
|
|
printf0("+-----------------------+----------------------------------------------------+\n");
|
|
|
|
// pretty print in a table the multi-gpu configuration as well
|
|
set_zero_configs(&multi_gpu_config, zero_stage, model.num_parameters);
|
|
printf0("| num_processes | %-50d |\n", multi_gpu_config.num_processes);
|
|
printf0("| zero_stage | %-50d |\n", multi_gpu_config.zero_stage);
|
|
printf0("+-----------------------+----------------------------------------------------+\n");
|
|
|
|
// prints outside of pretty table to here and below
|
|
if (!hellaswag_available) {
|
|
printf0("HellaSwag eval not found at %s, skipping its evaluation\n", hellaswag_path);
|
|
printf0("You can run `python dev/data/hellaswag.py` to export and use it with `-h 1`.\n");
|
|
}
|
|
// more prints related to allocations from gpt2_build_from_checkpoint down here to not mess up our table above
|
|
printf0("num_parameters: %zu => bytes: %zu\n", model.num_parameters, model.num_parameters_bytes);
|
|
printf0("allocated %d MiB for model parameters\n", (int)round(model.num_parameters_bytes / (1024 * 1024)));
|
|
|
|
// figure out gradient accumulation from the desired total batch size
|
|
int tokens_per_fwdbwd = B * T * multi_gpu_config.num_processes; // one micro-batch processes this many tokens
|
|
assert(total_batch_size % tokens_per_fwdbwd == 0);
|
|
int grad_accum_steps = total_batch_size / tokens_per_fwdbwd;
|
|
printf0("batch_size B=%d * seq_len T=%d * num_processes=%d and total_batch_size=%d\n",
|
|
B, T, multi_gpu_config.num_processes, total_batch_size);
|
|
printf0("=> setting grad_accum_steps=%d\n", grad_accum_steps);
|
|
|
|
// set up logging
|
|
create_dir_if_not_exists(output_log_dir);
|
|
Logger logger;
|
|
logger_init(&logger, output_log_dir, multi_gpu_config.process_rank);
|
|
|
|
// set up the Tokenizer
|
|
Tokenizer tokenizer;
|
|
tokenizer_init(&tokenizer, "gpt2_tokenizer.bin");
|
|
|
|
// some memory for generating samples from the model
|
|
unsigned long long rng_state = 1337;
|
|
int* gen_tokens = (int*)mallocCheck(B * T * sizeof(int));
|
|
floatX* cpu_logits_raw = (floatX*)mallocCheck(model.config.vocab_size * sizeof(floatX));
|
|
float* cpu_logits = (float*)mallocCheck(model.config.vocab_size * sizeof(float));
|
|
|
|
// train
|
|
cudaEvent_t start, end;
|
|
cudaCheck(cudaEventCreate(&start));
|
|
cudaCheck(cudaEventCreate(&end));
|
|
cudaCheck(cudaProfilerStart());
|
|
double total_sum_iteration_time_s = 0.0;
|
|
float ema_tokens_per_second = 0.0f;
|
|
for (int step = 0; step <= train_num_batches; step++) {
|
|
NvtxRange step_range("Train step", step);
|
|
|
|
int last_step = step == train_num_batches;
|
|
|
|
// once in a while estimate the validation loss
|
|
if (step % val_loss_every == 0 || last_step) {
|
|
NvtxRange validation_range("validation");
|
|
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;
|
|
val_loss = multi_gpu_cpu_float_sum(val_loss) / multi_gpu_config.num_processes;
|
|
printf0("val loss %f\n", val_loss);
|
|
logger_log_val(&logger, step, val_loss);
|
|
}
|
|
|
|
// once in a while estimate HellaSwag accuracy
|
|
if (run_hellaswag &&
|
|
((step > 0 && step % val_loss_every == 0) || last_step)) {
|
|
NvtxRange evaluation_range("evaluation");
|
|
float eval_acc_norm = 0.0f;
|
|
evalloader_reset(&eval_loader);
|
|
for (int i = 0; i < eval_loader.num_batches; i++) {
|
|
if (i % 10 == 0) { printf("evaluating HellaSwag: %d/%d\r", i, eval_loader.num_batches); }
|
|
evalloader_next_batch(&eval_loader);
|
|
gpt2_forward(&model, eval_loader.inputs, eval_loader.targets, B, T);
|
|
int correct = evalloader_stat_losses(&eval_loader, model.cpu_losses_fp32);
|
|
eval_acc_norm += (float)correct;
|
|
}
|
|
// careful because not all ranks may have the exact same allocation of number of examples
|
|
eval_acc_norm = multi_gpu_cpu_float_sum(eval_acc_norm);
|
|
printf0("HellaSwag: %d/%d = %f\n", (int)eval_acc_norm, eval_loader.num_examples, eval_acc_norm / eval_loader.num_examples);
|
|
logger_log_eval(&logger, step, eval_acc_norm / eval_loader.num_examples);
|
|
}
|
|
|
|
// once in a while do model inference to print generated text
|
|
if (multi_gpu_config.process_rank == 0 && (step > 0 && (step % sample_every) == 0 || last_step)) {
|
|
NvtxRange generation_range("generation");
|
|
// fill up gen_tokens with the <|endoftext|> token, which kicks off the generation
|
|
int eot_token = tokenizer.eot_token;
|
|
for(int i = 0; i < B * T; ++i) {
|
|
gen_tokens[i] = eot_token;
|
|
}
|
|
// now sample from the model autoregressively
|
|
printf("generating:\n---\n");
|
|
for (int t = 1; t < genT; t++) {
|
|
NvtxRange generation_range("Generation step", t);
|
|
// note that inference is very wasteful here because for each token
|
|
// we re-calculate the forward pass for all of (B,T) positions from scratch
|
|
// but the inference here is just for sanity checking anyway
|
|
// and we can maybe optimize a bit more later, with careful tests
|
|
gpt2_forward(&model, gen_tokens, NULL, B, T);
|
|
// furthermore, below we're only using b=0 (i.e. the first row) of all B rows
|
|
// we're in principle running B "inference streams" in parallel here
|
|
// only using position 0 because it's a bit faster (copy less probs from GPU -> CPU)
|
|
// get the V-dimensional vector probs[0, t-1, :]
|
|
floatX* logits = model.acts.output + (t - 1) * model.config.padded_vocab_size;
|
|
// move probs back to CPU and sample (note we only move the first vocab_size logits, ignoring the padding)
|
|
cudaCheck(cudaMemcpy(cpu_logits_raw, logits, model.config.vocab_size * sizeof(floatX), cudaMemcpyDeviceToHost));
|
|
// convert to FP32 into cpu_logits (this does nothing useful if floatX == float)
|
|
for (int i = 0; i < model.config.vocab_size; i++) {
|
|
cpu_logits[i] = (float)cpu_logits_raw[i];
|
|
}
|
|
|
|
float coin = random_f32(&rng_state);
|
|
int next_token = sample_softmax(cpu_logits, model.config.vocab_size, coin);
|
|
gen_tokens[t] = next_token;
|
|
// print the generated token, either using the Tokenizer or a fallback
|
|
if (tokenizer.init_ok) {
|
|
const char* token_str = tokenizer_decode(&tokenizer, next_token);
|
|
safe_printf(token_str);
|
|
} else {
|
|
// fall back to printing the token id
|
|
printf("%d ", next_token);
|
|
}
|
|
fflush(stdout);
|
|
}
|
|
printf("\n---\n");
|
|
}
|
|
|
|
// bit confusing: we want to make sure to eval and sample on 0th iteration
|
|
// but also after the very last iteration. so we loop for step <= train_num_batches
|
|
// instead of just < train_num_batches (one extra due to <=), only to do
|
|
// the validation/sampling one last time, and then we break right here as we're done.
|
|
if (last_step) { break; }
|
|
|
|
// --------------- TRAINING SECTION BEGIN -----------------
|
|
// do one training step, doing forward/backward/update on total_batch_size tokens
|
|
cudaEventRecord(start);
|
|
// gradient accumulation loop over micro-batches
|
|
float lossf = 0.0f; // for getting the mean loss over the accumulation steps
|
|
for (int micro_step = 0; micro_step < grad_accum_steps; micro_step++) {
|
|
// fetch the next data batch
|
|
// and if we're overfitting a single batch, we'll only call this a single time
|
|
if (overfit_single_batch == 0 ||
|
|
(overfit_single_batch == 1 && step == 0 && micro_step == 0)) {
|
|
dataloader_next_batch(&train_loader);
|
|
}
|
|
// forward pass. note that we pass in grad_accum_steps, which scales down the loss
|
|
gpt2_forward(&model, train_loader.inputs, train_loader.targets, B, T, grad_accum_steps);
|
|
lossf += model.mean_loss; // the mean_loss was normalized by grad_accum_steps inside gpt2_forward
|
|
// backward pass. all model params accumulate gradients with += inside this inner loop
|
|
gpt2_backward(&model, train_loader.inputs);
|
|
}
|
|
// override the mean loss, accounting for the gradient accumulation loop
|
|
// this is esp important to do here in multigpu update below, where model.mean_loss gets allreduced
|
|
model.mean_loss = lossf;
|
|
// update the parameters
|
|
gpt2_multi_gpu_accumulate(&model, &multi_gpu_config);
|
|
// learning rate schedule: warmup linearly to max LR, then cosine decay to LR * final_learning_rate_frac
|
|
float step_learning_rate = learning_rate;
|
|
if (step < warmup_iterations) {
|
|
step_learning_rate = learning_rate * ((float)(step + 1)) / warmup_iterations;
|
|
} else {
|
|
float decay_ratio = ((float)(step - warmup_iterations)) / (train_num_batches - warmup_iterations);
|
|
assert(0.0f <= decay_ratio && decay_ratio <= 1.0f);
|
|
float coeff = 0.5f * (1.0f + cosf(M_PI * decay_ratio)); // coeff starts at 1 and goes to 0
|
|
assert(0.0f <= coeff && coeff <= 1.0f);
|
|
float min_lr = learning_rate * final_learning_rate_frac;
|
|
step_learning_rate = min_lr + coeff * (learning_rate - min_lr);
|
|
}
|
|
// update the model parameters
|
|
float grad_norm = gpt2_update(&model, step_learning_rate, 0.9f, 0.95f, 1e-8f, weight_decay, grad_clip, step+1, &multi_gpu_config);
|
|
gpt2_multi_gpu_gather(&model, &multi_gpu_config);
|
|
// zero out the gradients for the next iteration
|
|
gpt2_zero_grad(&model);
|
|
cudaEventRecord(end);
|
|
cudaCheck(cudaEventSynchronize(end)); // wait for the end event to finish to get correct timings
|
|
// --------------- TRAINING SECTION END -------------------
|
|
// everything that follows now is just diagnostics, prints, logging, etc.
|
|
|
|
// todo - move or double-buffer all of this timing logic to avoid idling the GPU at this point!
|
|
float time_elapsed_ms;
|
|
cudaCheck(cudaEventElapsedTime(&time_elapsed_ms, start, end));
|
|
size_t tokens_processed = (size_t)multi_gpu_config.num_processes * B * T * grad_accum_steps;
|
|
float tokens_per_second = tokens_processed / time_elapsed_ms * 1000.0f;
|
|
float bias_corrected_ema_tokens_per_second = tokens_per_second; // by default set to non-ema version
|
|
if (step > 0) { // consider the first batch to be a warmup (e.g. cuBLAS/cuDNN initialisation)
|
|
total_sum_iteration_time_s += time_elapsed_ms / 1000.0f;
|
|
// smooth out the tok/s with an exponential moving average, and bias correct just like in AdamW
|
|
ema_tokens_per_second = 0.95f * ema_tokens_per_second + 0.05f * tokens_per_second;
|
|
bias_corrected_ema_tokens_per_second = ema_tokens_per_second / (1.0f - powf(0.95f, step));
|
|
}
|
|
float accumulated_loss = multi_gpu_config.num_processes == 1 ? model.mean_loss : model.accumulated_mean_loss;
|
|
printf0("step %4d/%d: train loss %f norm %.4f lr %.2e (%.2f ms, %.0f tok/s)\n",
|
|
step + 1, train_num_batches, accumulated_loss, grad_norm, step_learning_rate,
|
|
time_elapsed_ms, bias_corrected_ema_tokens_per_second);
|
|
logger_log_train(&logger, step, model.mean_loss);
|
|
|
|
// disable the profiler after 3 steps of optimization
|
|
if (step == 3) { cudaProfilerStop(); }
|
|
}
|
|
// add a total average, for optimizations that are only mild improvements (excluding 1st batch as warmup)
|
|
printf0("total average iteration time: %f ms\n", total_sum_iteration_time_s / (train_num_batches-1) * 1000);
|
|
|
|
// free and destroy everything
|
|
cudaCheck(cudaEventDestroy(end));
|
|
cudaCheck(cudaEventDestroy(start));
|
|
if (run_hellaswag) { evalloader_free(&eval_loader); }
|
|
dataloader_free(&train_loader);
|
|
dataloader_free(&val_loader);
|
|
tokenizer_free(&tokenizer);
|
|
free(cpu_logits_raw);
|
|
free(cpu_logits);
|
|
free(gen_tokens);
|
|
logger_free(&logger);
|
|
multi_gpu_config_free(&multi_gpu_config);
|
|
common_free(model);
|
|
return 0;
|
|
}
|
|
#endif
|