Merge branch 'karpathy:master' into feature/tests

This commit is contained in:
rosslwheeler 2024-06-24 23:05:55 -07:00 committed by GitHub
commit 2e32e25e37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1026 additions and 143 deletions

View file

@ -110,3 +110,13 @@ jobs:
- name: Execute testing program fp32 with cuDNN
run: ./test_gpt2fp32cu
unit-tests-gpu:
runs-on: ubicloud-gpu-standard-1-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Test Device<->File IO
run: cd dev/test && nvcc -o device_file_io device_file_io.cu && ./device_file_io

View file

@ -188,27 +188,37 @@ else
endif
endif
# Check if OpenMPI and NCCL are available, include them if so, for multi-GPU training
# Check if NCCL is available, include if so, for multi-GPU training
ifeq ($(NO_MULTI_GPU), 1)
$(info → Multi-GPU (OpenMPI + NCCL) is manually disabled)
$(info → Multi-GPU (NCCL) is manually disabled)
else
ifneq ($(OS), Windows_NT)
# Detect if running on macOS or Linux
ifeq ($(SHELL_UNAME), Darwin)
$(info ✗ Multi-GPU on CUDA on Darwin is not supported, skipping OpenMPI + NCCL support)
else ifeq ($(shell [ -d /usr/lib/x86_64-linux-gnu/openmpi/lib/ ] && [ -d /usr/lib/x86_64-linux-gnu/openmpi/include/ ] && echo "exists"), exists)
$(info ✓ OpenMPI found, OK to train with multiple GPUs)
NVCC_INCLUDES += -I/usr/lib/x86_64-linux-gnu/openmpi/include
NVCC_LDFLAGS += -L/usr/lib/x86_64-linux-gnu/openmpi/lib/
NVCC_LDLIBS += -lmpi -lnccl
NVCC_FLAGS += -DMULTI_GPU
$(info ✗ Multi-GPU on CUDA on Darwin is not supported, skipping NCCL support)
else ifeq ($(shell dpkg -l | grep -q nccl && echo "exists"), exists)
$(info ✓ NCCL found, OK to train with multiple GPUs)
NVCC_LDLIBS += -lnccl
else
$(info ✗ OpenMPI is not found, disabling multi-GPU support)
$(info ---> On Linux you can try install OpenMPI with `sudo apt install openmpi-bin openmpi-doc libopenmpi-dev`)
$(info ✗ NCCL is not found, disabling multi-GPU support)
$(info ---> On Linux you can try install NCCL with `sudo apt install libnccl2 libnccl-dev`)
endif
endif
endif
ifeq ($(NO_USE_MPI), 1)
$(info → MPI is manually disabled)
else ifeq ($(shell [ -d /usr/lib/x86_64-linux-gnu/openmpi/lib/ ] && [ -d /usr/lib/x86_64-linux-gnu/openmpi/include/ ] && echo "exists"), exists)
$(info ✓ MPI enabled)
NVCC_INCLUDES += -I/usr/lib/x86_64-linux-gnu/openmpi/include
NVCC_LDFLAGS += -L/usr/lib/x86_64-linux-gnu/openmpi/lib/
NVCC_LDLIBS += -lmpi
NVCC_FLAGS += -DUSE_MPI
NVCC_FLAGS += -DMULTI_GPU
else
$(info ✗ MPI not found)
endif
# Precision settings, default to bf16 but ability to override
PRECISION ?= BF16
VALID_PRECISIONS := FP32 FP16 BF16

View file

@ -134,7 +134,9 @@ sudo apt-get -y install libcudnn9-dev-cuda-12
On top of this you need the [cuDNN frontend](https://github.com/NVIDIA/cudnn-frontend/tree/main), but this is just header files. Simply clone the repo to your disk. The Makefile currently looks for it in either your home directory or the current directory. If you have put it elsewhere, add `CUDNN_FRONTEND_PATH=/path/to/your/cudnn-frontend/include` to the `make` command-line.
**multi-GPU training using MPI and NCCL**. Make sure you install MPI and NCCL, e.g. on Linux:
## multi-GPU training
Make sure you install MPI and NCCL, e.g. on Linux:
```bash
sudo apt install openmpi-bin openmpi-doc libopenmpi-dev
@ -149,6 +151,23 @@ make train_gpt2cu
mpirun -np <number of GPUs> ./train_gpt2cu
```
or simply run one of our scripts under `./scripts/`.
## multi-node training
Make sure you've installed `NCCL` following instructions from [multi-GPU](#multi-gpu-training) section.
There are 3 ways we currently support that allow you to run multi-node training:
1) Use OpenMPI to exchange nccl id and initialize NCCL. See e.g. `./scripts/multi_node/run_gpt2_124M_mpi.sh` script for details.
2) Use shared file system to init NCCL. See `./scripts/multi_node/run_gpt2_124M_fs.sbatch` script for details.
3) Use TCP sockets to init NCCL. See `./scripts/multi_node/run_gpt2_124M_tcp.sbatch` script for details.
Note:
* If you're running in a slurm environment and your slurm doesn't support PMIx (which we assume will be a common situation given that `slurm-wlm` dropped PMIx support) you will have to use FS (2) or TCP (3) approach. To test whether your slurm supports PMIx run: `srun --mpi=list` and see whether you get `pmix` in the output.
* If you don't have slurm set up, you can kick off a multi-node run using `mpirun` - MPI (1).
None of these 3 methods is superior, we just offer you options so that you can run in your specific environment.
## experiments / sweeps
Just as an example process to sweep learning rates on a machine with 4 GPUs on TinyStories. Run a shell script `sweep.sh` (after you of course `chmod u+x sweep.sh`):

View file

@ -0,0 +1,64 @@
/*
Tests device <-> file IO functions
compile and run as (from dev/test directory)
nvcc -o device_file_io device_file_io.cu && ./device_file_io
*/
#include "../../llmc/cuda_common.h"
#include <vector>
#include <random>
#include <cstdio>
#include <algorithm>
void test(size_t nelem, size_t wt_buf_size, size_t rd_buf_size) {
float* data;
cudaCheck(cudaMalloc(&data, nelem*sizeof(float)));
// generate random array
std::vector<float> random_data(nelem);
std::mt19937 rng(42);
std::uniform_real_distribution<float> dist(-100.f, 100.f);
std::generate(random_data.begin(), random_data.end(), [&](){ return dist(rng); });
cudaCheck(cudaMemcpy(data, random_data.data(), random_data.size()*sizeof(float), cudaMemcpyHostToDevice));
cudaStream_t stream;
cudaStreamCreate(&stream);
FILE* tmp = fopenCheck("tmp.bin", "w");
device_to_file(tmp, data, nelem * sizeof(float), wt_buf_size, stream);
fcloseCheck(tmp);
float* reload;
cudaCheck(cudaMalloc(&reload, nelem*sizeof(float)));
tmp = fopenCheck("tmp.bin", "r");
file_to_device(reload, tmp, nelem * sizeof(float), rd_buf_size, stream);
fcloseCheck(tmp);
std::vector<float> cmp(nelem);
cudaCheck(cudaMemcpy(cmp.data(), reload, nelem * sizeof(float), cudaMemcpyDeviceToHost));
for(int i = 0; i < nelem; ++i) {
if(random_data[i] != cmp[i]) {
fprintf(stderr, "FAIL: Mismatch at position %d: %f vs %f\n", i, random_data[i], cmp[i]);
remove("tmp.bin");
exit(EXIT_FAILURE);
}
}
cudaCheck(cudaFree(reload));
cudaCheck(cudaFree(data));
remove("tmp.bin");
}
int main() {
test(1025, 10000, 10000); // buffers larger than data
test(1025, 1024, 513); // different and smaller
test(500, 500*sizeof(float),
500*sizeof(float)); // exact match
test(125'000, 10000, 10000); // large array
}

View file

@ -15,6 +15,8 @@ Common utilities for CUDA code.
#include <cuda_bf16.h>
#include <cuda_fp16.h>
#include "utils.h"
// ----------------------------------------------------------------------------
// Global defines and settings
@ -116,4 +118,87 @@ class NvtxRange {
};
#define NVTX_RANGE_FN() NvtxRange nvtx_range(__FUNCTION__)
// ----------------------------------------------------------------------------
// Utilities to Read & Write between CUDA memory <-> files
// copy num_bytes from device pointer src into file dest, using double buffering running on the given stream.
inline void device_to_file(FILE* dest, void* src, size_t num_bytes, size_t buffer_size, cudaStream_t stream) {
// allocate pinned buffer for faster, async transfer
char* buffer_space;
cudaCheck(cudaMallocHost(&buffer_space, 2*buffer_size));
// split allocation in two
void* read_buffer = buffer_space;
void* write_buffer = buffer_space + buffer_size;
// prime the read buffer; first copy means we have to wait
char* gpu_read_ptr = (char*)src;
size_t copy_amount = std::min(buffer_size, num_bytes);
cudaCheck(cudaMemcpyAsync(read_buffer, gpu_read_ptr, copy_amount, cudaMemcpyDeviceToHost, stream));
cudaCheck(cudaStreamSynchronize(stream));
size_t rest_bytes = num_bytes - copy_amount;
size_t write_buffer_size = copy_amount;
gpu_read_ptr += copy_amount;
std::swap(read_buffer, write_buffer);
// now the main loop; as long as there are bytes left
while(rest_bytes > 0) {
// initiate next read
copy_amount = std::min(buffer_size, rest_bytes);
cudaCheck(cudaMemcpyAsync(read_buffer, gpu_read_ptr, copy_amount, cudaMemcpyDeviceToHost, stream));
// while this is going on, transfer the write buffer to disk
fwriteCheck(write_buffer, 1, write_buffer_size, dest);
cudaCheck(cudaStreamSynchronize(stream)); // wait for both buffers to be ready.
std::swap(read_buffer, write_buffer);
rest_bytes -= copy_amount;
write_buffer_size = copy_amount;
gpu_read_ptr += copy_amount;
}
// make sure to write the last remaining write buffer
fwriteCheck(write_buffer, 1, write_buffer_size, dest);
cudaCheck(cudaFreeHost(buffer_space));
}
// copy num_bytes from file src into device pointer dest, using double buffering running on the given stream.
inline void file_to_device(void* dest, FILE* src, size_t num_bytes, size_t buffer_size, cudaStream_t stream) {
// allocate pinned buffer for faster, async transfer
// from the docs (https://developer.download.nvidia.com/compute/DevZone/docs/html/C/doc/html/group__CUDART__HIGHLEVEL_ge439496de696b166ba457dab5dd4f356.html)
// WC memory is a good option for buffers that will be written by the CPU and read by the device via mapped pinned memory or host->device transfers.
char* buffer_space;
cudaCheck(cudaMallocHost(&buffer_space, 2*buffer_size, cudaHostAllocWriteCombined));
// split allocation in two
void* read_buffer = buffer_space;
void* write_buffer = buffer_space + buffer_size;
// prime the read buffer;
char* gpu_write_ptr = (char*)dest;
size_t copy_amount = std::min(buffer_size, num_bytes);
freadCheck(read_buffer, 1, copy_amount, src);
size_t rest_bytes = num_bytes - copy_amount;
size_t write_buffer_size = copy_amount;
std::swap(read_buffer, write_buffer);
// now the main loop; as long as there are bytes left
while(rest_bytes > 0) {
// initiate next read
copy_amount = std::min(buffer_size, rest_bytes);
cudaCheck(cudaMemcpyAsync(gpu_write_ptr, write_buffer, write_buffer_size, cudaMemcpyHostToDevice, stream));
gpu_write_ptr += write_buffer_size;
// while this is going on, read from disk
freadCheck(read_buffer, 1, copy_amount, src);
cudaCheck(cudaStreamSynchronize(stream)); // wait for both buffers to be ready.
std::swap(read_buffer, write_buffer);
rest_bytes -= copy_amount;
write_buffer_size = copy_amount;
}
// copy the last remaining write buffer to gpu
cudaCheck(cudaMemcpyAsync(gpu_write_ptr, write_buffer, write_buffer_size, cudaMemcpyHostToDevice, stream));
cudaCheck(cudaStreamSynchronize(stream));
cudaCheck(cudaFreeHost(buffer_space));
}
#endif // CUDA_COMMON_H

View file

@ -160,14 +160,14 @@ __device__ inline float blockReduce(float val, bool final_sync=false, float out_
// 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)
__device__ __host__ constexpr unsigned int SquirrelNoise5(unsigned 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;
unsigned int mangledBits = positionX;
mangledBits *= SQ5_BIT_NOISE1;
mangledBits += seed;
mangledBits ^= (mangledBits >> 9);
@ -183,8 +183,11 @@ __device__ __host__ constexpr unsigned int SquirrelNoise5(int positionX, unsigne
}
__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);
constexpr unsigned int PRIME_NUMBER = 198491317u; // Large prime number with non-boring bits
unsigned int x = static_cast<unsigned int>(indexX);
unsigned int y = static_cast<unsigned int>(indexY);
return SquirrelNoise5(x + (PRIME_NUMBER * y), seed);
}
// stochastic rounding built on top of Squirel Noise above (with seed updated per step via xorshift)

100
llmc/schedulers.h Normal file
View file

@ -0,0 +1,100 @@
/*
Implements various learning rate schedulers.
*/
#ifndef SCHEDULERS_H
#define SCHEDULERS_H
#include <assert.h>
#include <math.h>
#include <string.h>
typedef struct {
const char* type;
float learning_rate;
int warmup_iterations;
int train_num_batches;
float final_learning_rate_frac;
} LearningRateScheduler;
void lr_scheduler_init(LearningRateScheduler *scheduler, const char* scheduler_type, float learning_rate, int warmup_iterations, int train_num_batches, float final_learning_rate_frac) {
scheduler->type = scheduler_type;
scheduler->learning_rate = learning_rate;
scheduler->warmup_iterations = warmup_iterations;
scheduler->train_num_batches = train_num_batches;
scheduler->final_learning_rate_frac = final_learning_rate_frac;
}
// cosine: warmup linearly to max LR, then cosine decay to LR * final_learning_rate_frac
float get_learning_rate_cosine(LearningRateScheduler *scheduler, int step) {
float lr = scheduler->learning_rate;
if (step < scheduler->warmup_iterations) {
lr = scheduler->learning_rate * ((float)(step + 1)) / scheduler->warmup_iterations;
} else {
float decay_ratio = ((float)(step - scheduler->warmup_iterations)) / (scheduler->train_num_batches - scheduler->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 = scheduler->learning_rate * scheduler->final_learning_rate_frac;
lr = min_lr + coeff * (scheduler->learning_rate - min_lr);
}
return lr;
}
// linear: warmup linearly to max LR, then decay linearly to LR * final_learning_rate_frac
float get_learning_rate_linear(LearningRateScheduler *scheduler, int step) {
float lr = scheduler->learning_rate;
if (step < scheduler->warmup_iterations) {
lr = scheduler->learning_rate * ((float)(step + 1)) / scheduler->warmup_iterations;
} else {
float decay_ratio = ((float)(step - scheduler->warmup_iterations)) / (scheduler->train_num_batches - scheduler->warmup_iterations);
assert(0.0f <= decay_ratio && decay_ratio <= 1.0f);
float min_lr = scheduler->learning_rate * scheduler->final_learning_rate_frac;
lr = scheduler->learning_rate - decay_ratio * (scheduler->learning_rate - min_lr);
}
return lr;
}
// constant
float get_learning_rate_constant(LearningRateScheduler *scheduler, int step) {
return scheduler->learning_rate;
}
// wsd schedule: warmup linearly, keep constant, last 20% decay using 1 - sqrt decay to final_frac (should be 0.0)
// https://arxiv.org/abs/2405.18392
float get_learning_rate_wsd(LearningRateScheduler *scheduler, int step) {
int decay_point = (int)(0.8f * scheduler->train_num_batches);
float max_lr = scheduler->learning_rate;
float lr = max_lr;
if (step < scheduler->warmup_iterations) {
float decay_ratio = ((float)(step + 1)) / scheduler->warmup_iterations;
lr = max_lr * decay_ratio;
} else if (step < decay_point) {
// noop, keep lr constant
} else {
float decay_ratio = ((float)(step - decay_point)) / (scheduler->train_num_batches - decay_point);
assert(0.0f <= decay_ratio && decay_ratio <= 1.0f);
float min_lr = max_lr * scheduler->final_learning_rate_frac;
return min_lr + (1.0f - sqrtf(decay_ratio)) * (max_lr - min_lr);
}
return lr;
}
// return the learning rate at a given step
float get_learning_rate(LearningRateScheduler *scheduler, int step) {
float step_learning_rate;
if (strcmp(scheduler->type, "cosine") == 0) {
step_learning_rate = get_learning_rate_cosine(scheduler, step);
} else if (strcmp(scheduler->type, "linear") == 0) {
step_learning_rate = get_learning_rate_linear(scheduler, step);
} else if (strcmp(scheduler->type, "constant") == 0) {
step_learning_rate = get_learning_rate_constant(scheduler, step);
} else if (strcmp(scheduler->type, "wsd") == 0) {
step_learning_rate = get_learning_rate_wsd(scheduler, step);
} else {
fprintf(stderr, "Unknown learning rate scheduler type: %s\n", scheduler->type);
exit(EXIT_FAILURE);
}
return step_learning_rate;
}
#endif // SCHEDULERS_H

View file

@ -21,7 +21,7 @@
// simple replace fopen, fread, fclose, fseek
// with fopenCheck, freadCheck, fcloseCheck, fseekCheck
FILE *fopen_check(const char *path, const char *mode, const char *file, int line) {
extern inline FILE *fopen_check(const char *path, const char *mode, const char *file, int line) {
FILE *fp = fopen(path, mode);
if (fp == NULL) {
fprintf(stderr, "Error: Failed to open file '%s' at %s:%d\n", path, file, line);
@ -39,7 +39,7 @@ FILE *fopen_check(const char *path, const char *mode, const char *file, int line
#define fopenCheck(path, mode) fopen_check(path, mode, __FILE__, __LINE__)
void fread_check(void *ptr, size_t size, size_t nmemb, FILE *stream, const char *file, int line) {
extern inline void fread_check(void *ptr, size_t size, size_t nmemb, FILE *stream, const char *file, int line) {
size_t result = fread(ptr, size, nmemb, stream);
if (result != nmemb) {
if (feof(stream)) {
@ -61,7 +61,7 @@ void fread_check(void *ptr, size_t size, size_t nmemb, FILE *stream, const char
#define freadCheck(ptr, size, nmemb, stream) fread_check(ptr, size, nmemb, stream, __FILE__, __LINE__)
void fclose_check(FILE *fp, const char *file, int line) {
extern inline void fclose_check(FILE *fp, const char *file, int line) {
if (fclose(fp) != 0) {
fprintf(stderr, "Error: Failed to close file at %s:%d\n", file, line);
fprintf(stderr, "Error details:\n");
@ -73,7 +73,7 @@ void fclose_check(FILE *fp, const char *file, int line) {
#define fcloseCheck(fp) fclose_check(fp, __FILE__, __LINE__)
void fseek_check(FILE *fp, long off, int whence, const char *file, int line) {
extern inline void fseek_check(FILE *fp, long off, int whence, const char *file, int line) {
if (fseek(fp, off, whence) != 0) {
fprintf(stderr, "Error: Failed to seek in file at %s:%d\n", file, line);
fprintf(stderr, "Error details:\n");
@ -87,10 +87,32 @@ void fseek_check(FILE *fp, long off, int whence, const char *file, int line) {
#define fseekCheck(fp, off, whence) fseek_check(fp, off, whence, __FILE__, __LINE__)
extern inline void fwrite_check(void *ptr, size_t size, size_t nmemb, FILE *stream, const char *file, int line) {
size_t result = fwrite(ptr, size, nmemb, stream);
if (result != nmemb) {
if (feof(stream)) {
fprintf(stderr, "Error: Unexpected end of file at %s:%d\n", file, line);
} else if (ferror(stream)) {
fprintf(stderr, "Error: File write error at %s:%d\n", file, line);
} else {
fprintf(stderr, "Error: Partial write at %s:%d. Expected %zu elements, wrote %zu\n",
file, line, nmemb, result);
}
fprintf(stderr, "Error details:\n");
fprintf(stderr, " File: %s\n", file);
fprintf(stderr, " Line: %d\n", line);
fprintf(stderr, " Expected elements: %zu\n", nmemb);
fprintf(stderr, " Written elements: %zu\n", result);
exit(EXIT_FAILURE);
}
}
#define fwriteCheck(ptr, size, nmemb, stream) fwrite_check(ptr, size, nmemb, stream, __FILE__, __LINE__)
// ----------------------------------------------------------------------------
// malloc error-handling wrapper util
void *malloc_check(size_t size, const char *file, int line) {
extern inline void *malloc_check(size_t size, const char *file, int line) {
void *ptr = malloc(size);
if (ptr == NULL) {
fprintf(stderr, "Error: Memory allocation failed at %s:%d\n", file, line);
@ -108,7 +130,7 @@ void *malloc_check(size_t size, const char *file, int line) {
// ----------------------------------------------------------------------------
// I/O ops
void create_dir_if_not_exists(const char *dir) {
extern inline void create_dir_if_not_exists(const char *dir) {
if (dir == NULL) { return; }
struct stat st = {0};
if (stat(dir, &st) == -1) {
@ -120,7 +142,7 @@ void create_dir_if_not_exists(const char *dir) {
}
}
int find_max_step(const char* output_log_dir) {
extern inline int find_max_step(const char* output_log_dir) {
// find the DONE file in the log dir with highest step count
if (output_log_dir == NULL) { return -1; }
DIR* dir;

View file

@ -5,6 +5,12 @@ Utilities for ZeRO sharding
#ifndef LLMC_ZERO_CUH
#define LLMC_ZERO_CUH
#ifdef _WIN32
#include <winsock2.h>
#else
#include <arpa/inet.h>
#endif
#include <cuda_runtime_api.h>
#include <stdint.h>
#include <stdlib.h>
@ -12,8 +18,10 @@ Utilities for ZeRO sharding
#include <stddef.h>
#ifdef MULTI_GPU
#include <mpi.h>
#include <nccl.h>
#ifdef USE_MPI
#include <mpi.h>
#endif
#endif
// ----------------------------------------------------------------------------
@ -36,6 +44,7 @@ void nccl_check(ncclResult_t status, const char *file, int line) {
}
#define ncclCheck(err) (nccl_check(err, __FILE__, __LINE__))
#ifdef USE_MPI
void mpi_check(int status, const char *file, int line) {
if (status != MPI_SUCCESS) {
char mpi_error[4096];
@ -46,15 +55,14 @@ void mpi_check(int status, const char *file, int line) {
}
}
#define mpiCheck(err) (mpi_check(err, __FILE__, __LINE__))
#endif
#endif // MULTI_GPU
// ----------------------------------------------------------------------------
// 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 process_rank; // Rank of this process among all 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.
@ -69,70 +77,381 @@ typedef struct {
ncclComm_t nccl_comm; // NCCL communication primitive, used for collective multi-GPU work.
cudaStream_t nccl_stream; // CUDA Stream to perform NCCL operations.
cudaEvent_t compute_nccl_sync; // Event used to synchronize NCCL with the compute
float* unified_buffer;
#endif
} MultiGpuConfig;
#ifdef MULTI_GPU
#ifdef _WIN32
void send_nccl_id_to_clients_windows(ncclUniqueId *nccl_id, SOCKET client_sockets[], int num_clients) {
for (int i = 0; i < num_clients; ++i) {
if (send(client_sockets[i], (const char *)nccl_id, sizeof(*nccl_id), 0) == SOCKET_ERROR) {
printf("Failed to send nccl_id");
WSACleanup();
exit(EXIT_FAILURE);
}
closesocket(client_sockets[i]);
}
}
#else
void send_nccl_id_to_clients(ncclUniqueId *nccl_id, int client_sockets[], int num_clients) {
for (int i = 0; i < num_clients; ++i) {
if (send(client_sockets[i], nccl_id, sizeof(*nccl_id), 0) == -1) {
printf("Failed to send nccl_id");
exit(EXIT_FAILURE);
}
close(client_sockets[i]);
}
}
#endif
#ifdef _WIN32
// Same as get_nccl_id_via_tcp but for Windows
ncclUniqueId get_nccl_id_via_tcp_windows(MultiGpuConfig* result, const char* server_ip) {
ncclUniqueId nccl_id;
int SERVER_PORT = 12345; // hardcoded an arbitrary port number between 1024 and 49151 (registered ports)
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("WSAStartup failed");
exit(EXIT_FAILURE);
}
if (result->process_rank == 0) {
ncclCheck(ncclGetUniqueId(&nccl_id));
int MAX_CLIENTS = result->num_processes - 1;
SOCKET client_sockets[MAX_CLIENTS];
int num_clients = 0;
SOCKET server_socket, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
// Step 1) create a server TCP socket
if ((server_socket = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
printf("Socket failed");
WSACleanup();
exit(EXIT_FAILURE);
}
// Step 2) set the server address and port
address.sin_family = AF_INET; // IPv4
address.sin_addr.s_addr = inet_addr(server_ip);
address.sin_port = htons(SERVER_PORT);
// Step 3) bind the socket to the address and port
if (bind(server_socket, (struct sockaddr *)&address, sizeof(address)) == SOCKET_ERROR) {
printf("Bind failed");
closesocket(server_socket);
WSACleanup();
exit(EXIT_FAILURE);
}
// Step 4) MAX_CLIENTS specifies the maximum number of clients that can be queued for this server
if (listen(server_socket, MAX_CLIENTS) == SOCKET_ERROR) {
printf("Listen failed");
closesocket(server_socket);
WSACleanup();
exit(EXIT_FAILURE);
}
// Step 5) accept connections from clients
printf("Waiting for clients to connect...\n");
while (num_clients < MAX_CLIENTS) {
if ((new_socket = accept(server_socket, (struct sockaddr *)&address, &addrlen)) == INVALID_SOCKET) {
printf("Accept failed");
closesocket(server_socket);
WSACleanup();
exit(EXIT_FAILURE);
}
client_sockets[num_clients++] = new_socket;
printf("Client %d connected\n", num_clients);
}
// Step 6) send the NCCL ID to all clients
send_nccl_id_to_clients_windows(&nccl_id, client_sockets, num_clients);
printf("NCCL ID sent to all clients\n");
closesocket(server_socket);
} else {
int num_connection_attempts = 5;
int time_to_sleep = 2;
SOCKET client_socket;
struct sockaddr_in serv_addr;
// Step 1) create a client TCP socket
if ((client_socket = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
printf("Socket creation error");
WSACleanup();
exit(EXIT_FAILURE);
}
// Step 2) set the server address and port
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERVER_PORT);
if (inet_pton(AF_INET, server_ip, &serv_addr.sin_addr) <= 0) {
printf("Invalid address or address not supported");
closesocket(client_socket);
WSACleanup();
exit(EXIT_FAILURE);
}
// Step 3) Try to connect to the server - retry up to `num_connection_attempts` times if the connection fails
while (connect(client_socket, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR) {
printf("%d Connection failed, retrying in %d seconds\n", result->process_rank, time_to_sleep);
if (--num_connection_attempts == 0) {
printf("Failed to connect to the server\n");
closesocket(client_socket);
WSACleanup();
exit(EXIT_FAILURE);
}
Sleep(time_to_sleep * 1000);
}
// Step 4) receive the NCCL ID from the server
if (recv(client_socket, (char *)&nccl_id, sizeof(nccl_id), 0) <= 0) {
printf("Failed to receive nccl_id");
closesocket(client_socket);
WSACleanup();
exit(EXIT_FAILURE);
}
printf("Received NCCL ID\n");
closesocket(client_socket);
}
WSACleanup();
return nccl_id;
}
#else
ncclUniqueId get_nccl_id_via_tcp(MultiGpuConfig* result, const char* server_ip) {
ncclUniqueId nccl_id;
int SERVER_PORT = 12345; // hardcoded an arbitrary port number between 1024 and 49151 (registered ports)
if (result->process_rank == 0) {
ncclCheck(ncclGetUniqueId(&nccl_id));
int MAX_CLIENTS = result->num_processes - 1;
int client_sockets[MAX_CLIENTS];
int num_clients = 0;
int server_socket, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
int opt = 1;
// Step 1) create a server TCP socket
if ((server_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("Socket failed");
exit(EXIT_FAILURE);
}
// Step 2) set socket options
// SOL_SOCKET - means that option is configured at socket level
// SO_REUSEADDR - allows to bind to an address which is in a TIME_WAIT state (already used by another socket) - useful when restarting the server
// SO_REUSEPORT - allows to bind to the same port multiple times
if (setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)) < 0) {
printf("Setsockopt failed");
exit(EXIT_FAILURE);
}
// Step 3) set the server address and port
address.sin_family = AF_INET; // IPv4
address.sin_addr.s_addr = inet_addr(server_ip); // alternatively use INADDR_ANY to bind to all interfaces, currently we only allow ethernet
address.sin_port = htons(SERVER_PORT);
// Step 4) bind the socket to the address and port
if (bind(server_socket, (struct sockaddr *)&address, sizeof(address)) < 0) {
printf("Bind failed");
exit(EXIT_FAILURE);
}
// Step 5) MAX_CLIENTS specifies the maximum number of clients that can be queued for this server
if (listen(server_socket, MAX_CLIENTS) < 0) {
printf("Listen failed");
exit(EXIT_FAILURE);
}
// Step 6) accept connections from clients
printf("Waiting for clients to connect...\n");
while (num_clients < MAX_CLIENTS) {
if ((new_socket = accept(server_socket, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
printf("Accept failed");
exit(EXIT_FAILURE);
}
client_sockets[num_clients++] = new_socket;
printf("Client %d connected\n", num_clients);
}
// Step 7) send the NCCL ID to all clients
send_nccl_id_to_clients(&nccl_id, client_sockets, num_clients);
printf("NCCL ID sent to all clients\n");
close(server_socket);
} else {
int num_connection_attempts = 5;
int time_to_sleep = 2;
int client_socket;
struct sockaddr_in serv_addr;
// Step 1) create a client TCP socket
if ((client_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("Socket creation error");
exit(EXIT_FAILURE);
}
// Step 2) set the server address and port
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERVER_PORT);
if (inet_pton(AF_INET, server_ip, &serv_addr.sin_addr) <= 0) {
printf("Invalid address or address not supported");
exit(EXIT_FAILURE);
}
// Step 3) Try to connect to the server - retry up to `num_connection_attempts` times if the connection fails
while (connect(client_socket, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
printf("%d Connection failed, retrying in %d seconds\n", result->process_rank, time_to_sleep);
if (--num_connection_attempts == 0) {
printf("Failed to connect to the server\n");
exit(EXIT_FAILURE);
}
sleep(time_to_sleep);
}
// Step 4) receive the NCCL ID from the server
if (recv(client_socket, &nccl_id, sizeof(nccl_id), 0) <= 0) {
printf("Failed to receive nccl_id");
exit(EXIT_FAILURE);
}
printf("Received NCCL ID\n");
close(client_socket);
}
return nccl_id;
}
#endif
ncclUniqueId get_nccl_id_via_fs(MultiGpuConfig* result, char* fs_path) {
// Works assuming that the filesystem is shared among all processes
ncclUniqueId nccl_id;
FILE* idFile;
static char filename[1024];
snprintf(filename, sizeof(filename), "%s/ncclUniqueId.sync", fs_path);
if (result->process_rank != 0) { // client processse should wait for the server to write to the file
// This is a naive and not 100% robust way to synchronize the processes but it should work almost always
sleep(2);
}
if (result->process_rank == 0) {
ncclCheck(ncclGetUniqueId(&nccl_id));
idFile = fopen(filename, "wb");
assert(idFile != NULL);
fwrite(&nccl_id, sizeof(nccl_id), 1, idFile);
fclose(idFile);
} else {
// Other ranks wait until the file is available and read the unique ID
do {
sleep(1); // 1 second
idFile = fopen(filename, "rb");
if (idFile != NULL) break;
} while (idFile == NULL);
freadCheck(&nccl_id, sizeof(nccl_id), 1, idFile);
fclose(idFile);
}
return nccl_id;
}
#ifdef USE_MPI
// 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;
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 = 5381u;
for (int c = 0; hostname[c] != '\0'; c++){ hostname_hash = ((hostname_hash << 5u) + hostname_hash) ^ hostname[c]; }
uint64_t hostname_hash = 5381u;
for (int c = 0; hostname[c] != '\0'; c++){ hostname_hash = ((hostname_hash << 5u) + 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));
// 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++;
}
}
// 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;
free(all_hostsname_hashes);
return local_device_idx;
}
#endif
MultiGpuConfig multi_gpu_config_init(int *argc, char ***argv) {
#endif
MultiGpuConfig multi_gpu_config_init(int num_processes, int process_rank, int gpus_per_node, char* server_ip, char* fs_path, char* init_method) {
#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));
// Get nccl_id using MPI, TCP, or FS (file system synchronization) methods
// On newer slurm versions (slurm-wlm package) PMIx is disabled so we can not use MPI for NCCL init in multi node setup
if (strcmp(init_method, "mpi") == 0) {
#ifdef USE_MPI
mpiCheck(MPI_Init(NULL, NULL));
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);
if (result.process_rank == 0) {
ncclCheck(ncclGetUniqueId(&nccl_id));
}
mpiCheck(MPI_Bcast(&nccl_id, sizeof(nccl_id), MPI_BYTE, 0, MPI_COMM_WORLD));
#else
printf("MPI support is disabled. Please enable MPI support to use MPI-based NCCL-init method.\n");
exit(EXIT_FAILURE);
#endif
} else {
result.process_rank = process_rank;
result.num_processes = num_processes;
result.local_device_idx = process_rank % gpus_per_node;
if (strcmp(init_method, "tcp") == 0) {
#ifdef _WIN32
nccl_id = get_nccl_id_via_tcp_windows(&result, server_ip);
#else
nccl_id = get_nccl_id_via_tcp(&result, server_ip);
#endif
} else if (strcmp(init_method, "fs") == 0) {
nccl_id = get_nccl_id_via_fs(&result, fs_path);
} else {
printf("Invalid NCCL-init method\n");
exit(EXIT_FAILURE);
}
}
mpiCheck(MPI_Bcast((void *)&nccl_id, sizeof(nccl_id), MPI_BYTE, 0, MPI_COMM_WORLD));
cudaCheck(cudaSetDevice(result.local_device_idx));
ncclCheck(ncclCommInitRank(&result.nccl_comm, result.num_processes, nccl_id, result.process_rank));
cudaCheck(cudaStreamCreate(&result.nccl_stream));
// event without timing for maximum performance
cudaCheck(cudaEventCreate(&result.compute_nccl_sync, cudaEventDisableTiming));
nvtxNameCudaStreamA(result.nccl_stream, "nccl stream");
nvtxNameCudaEventA(result.compute_nccl_sync, "nccl compute sync");
cudaCheck(cudaMallocManaged(&result.unified_buffer, sizeof(float)));
return result;
#else
printf("Multi-GPU support is disabled. Using a single GPU.\n");
@ -150,15 +469,19 @@ void multi_gpu_config_free(MultiGpuConfig* multi_gpu_config) {
ncclCheck(ncclCommDestroy(multi_gpu_config->nccl_comm));
cudaCheck(cudaStreamDestroy(multi_gpu_config->nccl_stream));
cudaCheck(cudaEventDestroy(multi_gpu_config->compute_nccl_sync));
cudaCheck(cudaFree(multi_gpu_config->unified_buffer));
#ifdef USE_MPI
mpiCheck(MPI_Finalize());
#endif
#endif
}
void multi_gpu_barrier(const MultiGpuConfig* multi_gpu_config) {
#ifdef MULTI_GPU
if (multi_gpu_config->num_processes > 1) {
mpiCheck(MPI_Barrier(MPI_COMM_WORLD));
ncclCheck(ncclAllReduce(multi_gpu_config->unified_buffer, multi_gpu_config->unified_buffer, sizeof(float), ncclFloat, ncclSum, multi_gpu_config->nccl_comm, multi_gpu_config->nccl_stream));
}
cudaCheck(cudaDeviceSynchronize());
#endif
}

View file

@ -28,7 +28,13 @@ the profile.ncu-rep from a cloud box to local to pretty view.
#include "train_gpt2.cu"
int main(int argc, char *argv[]) {
multi_gpu_config = multi_gpu_config_init(&argc, &argv);
char nccl_init_method[256] = "mpi"; // "tcp" or "fs" or "mpi"
int num_processes = -1; // doesn't matter when using MPI
int process_rank = -1; // doesn't matter when using MPI
int gpus_per_node = -1; // doesn't matter when using MPI
char server_ip[256] = ""; // doesn't matter when using MPI
char fs_path[256] = ""; // doesn't matter when using MPI
multi_gpu_config = multi_gpu_config_init(num_processes, process_rank, gpus_per_node, server_ip, fs_path, nccl_init_method);
common_start(true, true);
// build the GPT-2 model from a checkpoint
@ -55,7 +61,7 @@ int main(int argc, char *argv[]) {
// do a training step
gpt2_forward(&model, x, y, B, T);
gpt2_zero_grad(&model);
gpt2_backward(&model, x, true);
gpt2_backward_and_reduce(&model, x, true);
gpt2_update(&model, 1e-4f, 0.9f, 0.999f, 1e-8f, 0.0f, 1.f, 1, &multi_gpu_config);
cudaCheck(cudaDeviceSynchronize()); // finish all CUDA work to get correct precise timings

View file

@ -0,0 +1,85 @@
#!/bin/bash
#SBATCH --job-name=llmc-multinode # job name
#SBATCH --output=/home/ubuntu/llm.c/scripts/multi_node/%x_%j_%t.log # output file
#SBATCH --error=/home/ubuntu/llm.c/scripts/multi_node/%x_%j_%t.err # error file
#SBATCH --partition=llmc # Specify the GPU partition
#SBATCH --ntasks=16 # total number of processes to launch on all nodes
#SBATCH --nodes=2 # total number of nodes
#SBATCH --ntasks-per-node=8 # assuming each node has 8 gpus
#SBATCH --gres=gpu:8 # request 8 gpus from each node
# NOTE: change the above slurm arguments to match your system!
# Run with `sbatch <path_to_this_script.sh>`
make train_gpt2cu USE_CUDNN=1 NO_USE_MPI=1
# NOTE: change the following to match your system
binary_path="/home/ubuntu/llm.c/train_gpt2cu"
out_dir="/ephemeral/data/fineweb/log_gpt2_124M_multi"
train_data_path='/ephemeral/data/fineweb/bin_10B/fineweb_train_*.bin'
val_data_path='/ephemeral/data/fineweb/bin_10B/fineweb_val_*.bin'
sync_fs_path=$out_dir # needs to be a shared filesystem path that all nodes can access
# In case the file system is shared this is a no-op.
# Otherwise, we need to copy the binary to all nodes.
current_user=$USER
hosts=$(scontrol show hostnames $SLURM_JOB_NODELIST) # get the hostnames of the allocated nodes
current_host=$(hostname)
for host in $hosts; do
if [ $host == $current_host ]; then
continue
fi
echo "copying $binary_path to $current_user@$host"
scp -r $binary_path $current_user@$host:$binary_path
done
# Use this for NCCL debugging if you run into issues
# export NCCL_DEBUG=INFO
# export NCCL_DEBUG_SUBSYS=ALL
export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
# Optimization flags
export NCCL_NET_GDR_LEVEL=2 # use GPUDirect RDMA - allows for direct memory access between GPUs across different nodes by bypassing the CPU
export NCCL_IB_DISABLE=0 # use InfiniBand if available
# NOTE: change the following environment variables to match your system - or comment them out if you don't need them
export NCCL_SOCKET_IFNAME=ens17
export OMPI_MCA_btl_tcp_if_include=ens17
export NCCL_P2P_LEVEL=PXB
if [ -z "$SLURM_JOB_ID" ]; then
echo "Make sure you're running in a SLURM environment. Did you forget to run with sbatch? Aborting."
exit 1
else
DATESTRING=`date "+%Y-%m-%dT%H:%M:%S"`
echo "Running in a SLURM environment (job ID: $SLURM_JOB_ID, user: $current_user)"
echo "Running on hosts: $(echo $(scontrol show hostname))"
echo "$DATESTRING"
fi
srun -l -u bash -c "
$binary_path \
-i '$train_data_path' \
-j '$val_data_path' \
-o $out_dir \
-v 250 -s 20000 -g 144 \
-h 1 \
-b 64 -t 1024 \
-d 2097152 \
-r 0 \
-z 1 \
-c 0.1 \
-l 0.0006 \
-q 0.0 \
-u 700 \
-n 5000 \
-y 1 \
-e d12 \
-pn \$SLURM_NTASKS \
-pr \$SLURM_PROCID \
-pg \$SLURM_NTASKS_PER_NODE \
-pf $sync_fs_path \
-pi "fs" \
"
echo "$DATESTRING"

View file

@ -0,0 +1,49 @@
make train_gpt2cu USE_CUDNN=1
# NOTE: change the following to match your system
binary_path="/home/ubuntu/llm.c/train_gpt2cu"
out_dir="/ephemeral/data/fineweb/log_gpt2_124M_multi"
train_data_path='/ephemeral/data/fineweb/bin_10B/fineweb_train_*.bin'
val_data_path='/ephemeral/data/fineweb/bin_10B/fineweb_val_*.bin'
# You can find these names either in `/etc/hosts`` file or in the terminal (user@host:~$).
host1="h100-node-1-0" # master and worker node
host2="h100-node-1-1" # worker node
# In case the file system is shared this is a no-op.
# Otherwise, we need to copy the binary to all nodes.
scp -r $binary_path $USER@$host2:$binary_path
# Use this for NCCL debugging if you run into issues
# export NCCL_DEBUG=INFO
# export NCCL_DEBUG_SUBSYS=ALL
export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
# Optimization flags
export NCCL_NET_GDR_LEVEL=2 # use GPUDirect RDMA - allows for direct memory access between GPUs across different nodes by bypassing the CPU
export NCCL_IB_DISABLE=0 # use InfiniBand if available
# NOTE: change the following environment variables to match your system - or comment them out if you don't need them
export NCCL_SOCKET_IFNAME=ens17
export OMPI_MCA_btl_tcp_if_include=ens17
export NCCL_P2P_LEVEL=PXB
mpirun -np 16 --host $host1:8,$host2:8 \
$binary_path \
-i "$train_data_path" \
-j "$val_data_path" \
-o $out_dir \
-v 250 -s 20000 -g 144 \
-h 1 \
-b 64 -t 1024 \
-d 2097152 \
-r 0 \
-z 1 \
-c 0.1 \
-l 0.0006 \
-q 0.1 \
-u 700 \
-n 1000 \
-y 0 \
-e d12 \
-pi "mpi" \

View file

@ -0,0 +1,86 @@
#!/bin/bash
#SBATCH --job-name=llmc-multinode # job name
#SBATCH --output=/home/ubuntu/llm.c/scripts/multi_node/%x_%j_%t.log # output file
#SBATCH --error=/home/ubuntu/llm.c/scripts/multi_node/%x_%j_%t.err # error file
#SBATCH --partition=llmc # Specify the GPU partition
#SBATCH --ntasks=16 # total number of processes to launch on all nodes
#SBATCH --nodes=2 # total number of nodes
#SBATCH --ntasks-per-node=8 # assuming each node has 8 gpus
#SBATCH --gres=gpu:8 # request 8 gpus from each node
# NOTE: change the above slurm arguments to match your system!
# Run with `sbatch <path_to_this_script.sh>`
make train_gpt2cu USE_CUDNN=1 NO_USE_MPI=1
# NOTE: change the following to match your system
binary_path="/home/ubuntu/llm.c/train_gpt2cu"
out_dir="/ephemeral/data/fineweb/log_gpt2_124M_multi"
train_data_path='/ephemeral/data/fineweb/bin_10B/fineweb_train_*.bin'
val_data_path='/ephemeral/data/fineweb/bin_10B/fineweb_val_*.bin'
# NOTE: change the server_ip to the IP address of the machine that is running process zero
server_ip="10.0.1.220"
# In case the file system is shared this is a no-op.
# Otherwise, we need to copy the binary to all nodes.
current_user=$USER
hosts=$(scontrol show hostnames $SLURM_JOB_NODELIST) # get the hostnames of the allocated nodes
current_host=$(hostname)
for host in $hosts; do
if [ $host == $current_host ]; then
continue
fi
echo "copying $binary_path to $current_user@$host"
scp -r $binary_path $current_user@$host:$binary_path
done
# Use this for NCCL debugging if you run into issues
# export NCCL_DEBUG=INFO
# export NCCL_DEBUG_SUBSYS=ALL
export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
# Optimization flags
export NCCL_NET_GDR_LEVEL=2 # use GPUDirect RDMA - allows for direct memory access between GPUs across different nodes by bypassing the CPU
export NCCL_IB_DISABLE=0 # use InfiniBand if available
# NOTE: change the following environment variables to match your system - or comment them out if you don't need them
export NCCL_SOCKET_IFNAME=ens17
export OMPI_MCA_btl_tcp_if_include=ens17
export NCCL_P2P_LEVEL=PXB
if [ -z "$SLURM_JOB_ID" ]; then
echo "Make sure you're running in a SLURM environment. Did you forget to run with sbatch? Aborting."
exit 1
else
DATESTRING=`date "+%Y-%m-%dT%H:%M:%S"`
echo "Running in a SLURM environment (job ID: $SLURM_JOB_ID, user: $current_user)"
echo "Running on hosts: $(echo $(scontrol show hostname))"
echo "$DATESTRING"
fi
srun -l -u bash -c "
$binary_path \
-i '$train_data_path' \
-j '$val_data_path' \
-o $out_dir \
-v 250 -s 20000 -g 144 \
-h 1 \
-b 64 -t 1024 \
-d 2097152 \
-r 0 \
-z 1 \
-c 0.1 \
-l 0.0006 \
-q 0.0 \
-u 700 \
-n 5000 \
-y 1 \
-e d12 \
-pn \$SLURM_NTASKS \
-pr \$SLURM_PROCID \
-pg \$SLURM_NTASKS_PER_NODE \
-ps $server_ip \
-pi "tcp" \
"
echo "$DATESTRING"

View file

@ -89,7 +89,13 @@ float* float_cpu_malloc_and_point_parameters(FloatParameterTensors* params, size
}
int main(int argc, char *argv[]) {
multi_gpu_config = multi_gpu_config_init(&argc, &argv);
char nccl_init_method[256] = "mpi"; // "tcp" or "fs" or "mpi"
int num_processes = -1; // doesn't matter when using MPI
int process_rank = -1; // doesn't matter when using MPI
int gpus_per_node = -1; // doesn't matter when using MPI
char server_ip[256] = ""; // doesn't matter when using MPI
char fs_path[256] = ""; // doesn't matter when using MPI
multi_gpu_config = multi_gpu_config_init(num_processes, process_rank, gpus_per_node, server_ip, fs_path, nccl_init_method);
common_start(false, true);
// set the right paths
@ -166,7 +172,7 @@ int main(int argc, char *argv[]) {
// copy logits to CPU so we can compare them
floatX* logits_cpu_raw = (floatX*)mallocCheck(B * T * Vp * sizeof(floatX));
float* logits_cpu = (float*)mallocCheck(B * T * Vp * sizeof(float));
cudaMemcpy(logits_cpu_raw, model.acts.output, B * T * Vp * sizeof(floatX), cudaMemcpyDeviceToHost);
cudaCheck(cudaMemcpy(logits_cpu_raw, model.acts.output, B * T * Vp * sizeof(floatX), cudaMemcpyDeviceToHost));
for (int i = 0; i < B * T * Vp; i++) {
logits_cpu[i] = (float)logits_cpu_raw[i];
}
@ -212,7 +218,7 @@ int main(int argc, char *argv[]) {
clock_gettime(CLOCK_MONOTONIC, &start);
gpt2_forward(&model, x, y, B, T);
gpt2_zero_grad(&model);
gpt2_backward(&model, x, true);
gpt2_backward_and_reduce(&model, x, true);
clock_gettime(CLOCK_MONOTONIC, &end);
double time_elapsed_s = (end.tv_sec - start.tv_sec) + (end.tv_nsec - start.tv_nsec) / 1e9;
@ -328,7 +334,7 @@ int main(int argc, char *argv[]) {
dataloader_next_batch(&loader);
gpt2_forward(&model, loader.inputs, loader.targets, B, T);
gpt2_zero_grad(&model);
gpt2_backward(&model, loader.inputs, true);
gpt2_backward_and_reduce(&model, loader.inputs, true);
gpt2_update(&model, 1e-4f, 0.9f, 0.95f, 1e-8f, 0.0f, 1.0f, step+11, &multi_gpu_config);
losses[step] = model.mean_loss;
tokens[step] = loader.inputs[0];
@ -343,7 +349,7 @@ int main(int argc, char *argv[]) {
dataloader_next_batch(&loader);
gpt2_forward(&model, loader.inputs, loader.targets, B, T);
gpt2_zero_grad(&model);
gpt2_backward(&model, loader.inputs, true);
gpt2_backward_and_reduce(&model, loader.inputs, true);
gpt2_update(&model, 1e-4f, 0.9f, 0.95f, 1e-8f, 0.0f, 1.0f, step+11, &multi_gpu_config);
if(loader.inputs[0] != tokens[step]) {

View file

@ -2,6 +2,10 @@
GPT-2 Transformer Neural Net training loop. See README.md for usage.
*/
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#endif
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
@ -21,6 +25,8 @@ GPT-2 Transformer Neural Net training loop. See README.md for usage.
#include "llmc/dataloader.h"
// defines: manual_seed, normal_ (same as torch.manual_seed and torch.normal)
#include "llmc/rand.h"
// defines: lr_scheduler_init, get_learning_rate
#include "llmc/schedulers.h"
// defines: sample_softmax, random_f32
#include "llmc/sampler.h"
// defines: logger_init, logger_log_eval, logger_log_val, logger_log_train
@ -71,6 +77,8 @@ cudaDeviceProp deviceProp; // fills in common_start()
cudaStream_t main_stream;
// one global variable to hold the multi-GPU configuration for this process
MultiGpuConfig multi_gpu_config;
// buffer size to use for device <-> disk io
constexpr const size_t IO_BUF_SIZE = 32 * 1024 * 1024;
// convenience function that only prints if the rank of process is zero
void printf0(const char *format, ...) {
@ -95,7 +103,6 @@ void set_zero_configs(MultiGpuConfig* multi_gpu_config, int zero_stage, size_t t
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;
}
@ -381,12 +388,10 @@ void gpt2_write_to_checkpoint(GPT2 *model, const char* checkpoint_path) {
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);
fwriteCheck(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);
device_to_file(model_file, model->params_memory, model->num_parameters_bytes,
IO_BUF_SIZE, main_stream);
// close file, we're done
fcloseCheck(model_file);
}
@ -449,10 +454,8 @@ void gpt2_build_from_checkpoint(GPT2 *model, const char* checkpoint_path) {
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);
file_to_device(model->params_memory, model_file, model->num_parameters_bytes,
IO_BUF_SIZE, main_stream);
fcloseCheck(model_file);
// only return from this function once we are certain the params are ready on the GPU
@ -720,7 +723,7 @@ void gpt2_zero_grad(GPT2 *model) {
cudaCheck(cudaDeviceSynchronize());
}
void gpt2_backward(GPT2 *model, int* inputs, bool last_step) {
void gpt2_backward_and_reduce(GPT2 *model, int* inputs, bool last_step) {
NVTX_RANGE_FN();
// double check we forwarded previously, with targets
if (model->mean_loss == -1.0f) {
@ -894,12 +897,15 @@ void gpt2_backward(GPT2 *model, int* inputs, bool last_step) {
}
// 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) {
float multi_gpu_cpu_float_sum(float value, MultiGpuConfig* multi_gpu_config) {
#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;
if (multi_gpu_config->num_processes == 1) return value;
float* unified_buffer = multi_gpu_config->unified_buffer;
*unified_buffer = value;
ncclCheck(ncclAllReduce(unified_buffer, unified_buffer, sizeof(float), ncclFloat, ncclSum, multi_gpu_config->nccl_comm, multi_gpu_config->nccl_stream));
cudaCheck(cudaDeviceSynchronize());
return *unified_buffer;
#else
return value;
#endif
@ -913,7 +919,7 @@ void gpt2_multi_gpu_loss_reduce(GPT2* model, MultiGpuConfig* multi_gpu_config) {
// If there's only one process, there is nothing to do
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;
model->accumulated_mean_loss = multi_gpu_cpu_float_sum(model->mean_loss, multi_gpu_config) / multi_gpu_config->num_processes;
#endif
cudaCheck(cudaDeviceSynchronize());
}
@ -991,7 +997,7 @@ float gpt2_update(GPT2 *model, float learning_rate, float beta1, float beta2, fl
global_norm_squared_aggregate(grad_norm_squared, max_num_block_sums, main_stream);
cudaCheck(cudaMemcpy(&grad_norm_squared_cpu, grad_norm_squared, sizeof(float), cudaMemcpyDeviceToHost));
// further sum the (partial) squared norm across all GPUs (see comment ^1 above)
grad_norm_squared_cpu = multi_gpu_cpu_float_sum(grad_norm_squared_cpu);
grad_norm_squared_cpu = multi_gpu_cpu_float_sum(grad_norm_squared_cpu, multi_gpu_config);
} else {
// in regular DDP, backward has averaged the gradients across all GPUs
// so each GPU can compute the squared norm over the whole grad vector, with no added comms needed
@ -1178,30 +1184,25 @@ void save_state(const char* filename, int step, GPT2* model, DataLoader* loader)
// dataloader state, start at 30 to leave some padding
*((size_t*)&state_header[30]) = loader->current_shard_idx; // shard of the dataset
*((size_t*)&state_header[32]) = loader->current_sample_idx; // position in shard
fwrite(state_header, sizeof(int), 256, state_file);
fwriteCheck(state_header, sizeof(int), 256, state_file);
// write AdamW m, v, and master_weights here (they are all float)
size_t shard_num_parameters = multi_gpu_config.shard_num_parameters;
float* cpu_buffer = (float*)mallocCheck(shard_num_parameters * sizeof(float));
cudaCheck(cudaMemcpy(cpu_buffer, model->m_memory, shard_num_parameters * sizeof(float), cudaMemcpyDeviceToHost));
fwrite(cpu_buffer, sizeof(float), shard_num_parameters, state_file);
cudaCheck(cudaMemcpy(cpu_buffer, model->v_memory, shard_num_parameters * sizeof(float), cudaMemcpyDeviceToHost));
fwrite(cpu_buffer, sizeof(float), shard_num_parameters, state_file);
if (model->use_master_weights == 1) {
cudaCheck(cudaMemcpy(cpu_buffer, model->master_weights, shard_num_parameters * sizeof(float), cudaMemcpyDeviceToHost));
fwrite(cpu_buffer, sizeof(float), shard_num_parameters, state_file);
device_to_file(state_file, model->m_memory, shard_num_parameters * sizeof(float), IO_BUF_SIZE, main_stream);
device_to_file(state_file, model->v_memory, shard_num_parameters * sizeof(float), IO_BUF_SIZE, main_stream);
if(model->use_master_weights) {
device_to_file(state_file, model->master_weights, shard_num_parameters * sizeof(float), IO_BUF_SIZE, main_stream);
}
free(cpu_buffer);
// write dataloader state if we are using the Permuted version of it
if (loader->should_shuffle) {
fwrite(&loader->glob_result.gl_pathc, sizeof(size_t), 1, state_file); // number of shards
fwrite(loader->shard_indices, sizeof(int), loader->glob_result.gl_pathc, state_file);
fwrite(&loader->shard_num_samples, sizeof(size_t), 1, state_file);
fwrite(loader->intra_shard_indices, sizeof(int), loader->shard_num_samples, state_file);
fwrite(&loader->shuffle_rng, sizeof(mt19937_state), 1, state_file);
fwriteCheck(&loader->glob_result.gl_pathc, sizeof(size_t), 1, state_file); // number of shards
fwriteCheck(loader->shard_indices, sizeof(int), loader->glob_result.gl_pathc, state_file);
fwriteCheck(&loader->shard_num_samples, sizeof(size_t), 1, state_file);
fwriteCheck(loader->intra_shard_indices, sizeof(int), loader->shard_num_samples, state_file);
fwriteCheck(&loader->shuffle_rng, sizeof(mt19937_state), 1, state_file);
}
fclose(state_file);
fcloseCheck(state_file);
}
void load_state(int* step, GPT2* model, DataLoader* loader, const char* filename) {
@ -1230,20 +1231,21 @@ void load_state(int* step, GPT2* model, DataLoader* loader, const char* filename
printf0("allocating %zu MiB for AdamW optimizer state v\n", (shard_num_parameters * sizeof(float)) >> 20);
cudaCheck(cudaMalloc((void**)&model->v_memory, shard_num_parameters * sizeof(float)));
}
if(use_master_weights == 1 && !model->use_master_weights) {
printf0("Warning: Master weights are present in state, but not enabled for current run.");
} else if (use_master_weights == 0 && model->use_master_weights) {
printf0("Error: Master weights requested, but not present in state file.");
exit(EXIT_FAILURE);
}
if (model->master_weights == NULL && use_master_weights == 1) {
printf0("allocating %zu MiB for master copy of params\n", (shard_num_parameters * sizeof(float)) >> 20);
cudaCheck(cudaMalloc((void**)&model->master_weights, shard_num_parameters * sizeof(float)));
}
float* cpu_buffer = (float*)mallocCheck(shard_num_parameters * sizeof(float));
freadCheck(cpu_buffer, sizeof(float), shard_num_parameters, state_file);
cudaCheck(cudaMemcpy(model->m_memory, cpu_buffer, shard_num_parameters * sizeof(float), cudaMemcpyHostToDevice));
freadCheck(cpu_buffer, sizeof(float), shard_num_parameters, state_file);
cudaCheck(cudaMemcpy(model->v_memory, cpu_buffer, shard_num_parameters * sizeof(float), cudaMemcpyHostToDevice));
if (use_master_weights == 1) {
freadCheck(cpu_buffer, sizeof(float), shard_num_parameters, state_file);
cudaCheck(cudaMemcpy(model->master_weights, cpu_buffer, shard_num_parameters * sizeof(float), cudaMemcpyHostToDevice));
file_to_device(model->m_memory, state_file, shard_num_parameters * sizeof(float), IO_BUF_SIZE, main_stream);
file_to_device(model->v_memory, state_file, shard_num_parameters * sizeof(float), IO_BUF_SIZE, main_stream);
if(model->use_master_weights) {
file_to_device(model->master_weights, state_file, shard_num_parameters * sizeof(float), IO_BUF_SIZE, main_stream);
}
free(cpu_buffer);
// revive the DataLoader object and its state
loader->should_shuffle = should_shuffle;
@ -1268,7 +1270,7 @@ void load_state(int* step, GPT2* model, DataLoader* loader, const char* filename
dataloader_resume(loader, current_shard_idx, current_sample_idx);
// all done, close state file
fclose(state_file);
fcloseCheck(state_file);
}
@ -1282,7 +1284,7 @@ void load_state(int* step, GPT2* model, DataLoader* loader, const char* filename
// ----------------------------------------------------------------------------
// CLI, poor man's argparse
// unclaimed flags lol: k,p
// unclaimed flags lol: p
void error_usage() {
fprintf(stderr, "Usage: ./train_gpt2cu [options]\n");
@ -1301,6 +1303,7 @@ void error_usage() {
// workload (number of steps)
fprintf(stderr, " -x <int> max_steps of optimization to run (-1 (default) = disable, run 1 epoch)\n");
// optimization
fprintf(stderr, " -k <string> learning rate scheduler (default = cosine)\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");
@ -1319,18 +1322,24 @@ void error_usage() {
// memory management
fprintf(stderr, " -z <int> zero_stage, Zero Optimization Stage, 0,1,2,3 (default = 0)\n");
fprintf(stderr, " -r <int> recompute: less memory but less speed. (default = 1), 0|1|2 = none,gelu,gelu+ln\n");
// multi-node settings
fprintf(stderr, " -pn <int> num_processes (default = 1)\n");
fprintf(stderr, " -pr <int> process_rank (default = 0)\n");
fprintf(stderr, " -pg <int> gpus_per_node (default = 8)\n");
fprintf(stderr, " -pm <string> nccl_init_method: tcp,fs,mpi (default = mpi)\n");
fprintf(stderr, " -ps <string> server_ip - used only when nccl_init_method is tcp (default = -1)\n");
fprintf(stderr, " -pp <string> fs_path - used only when nccl_init_method is fs (default = /tmp)\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* lr_scheduler_type = "cosine";
const char* output_log_dir = NULL;
int checkpoint_every = 0; // write optimization checkpoints every how many steps?
int resume = 0; // resume the optimization, if one is found inside output_log_dir?
@ -1352,10 +1361,17 @@ int main(int argc, char *argv[]) {
int recompute = 1; // recompute during backward setting, 0 = none, 1 = recompute gelu
int zero_stage = 0; // Zero Optimization Stage for Multi-GPU training
int hellaswag_eval = 0;
// multi-node settings
int num_processes = 1; // this should be set by the slurm environment
int process_rank = 0; // this should be set by the slurm environment
int gpus_per_node = 8; // this should be set by the slurm environment
char nccl_init_method[256] = "mpi"; // "tcp" or "fs" or "mpi"
char server_ip[256] = ""; // used if init_method set to "tcp" -> set to your server ip address
char fs_path[256] = ""; // used if init_method set to "fs" -> set to a shared filesystem path
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)
if (!(strlen(argv[i]) == 2 || strlen(argv[i]) == 3)) { error_usage(); } // must be -x[y] (one dash, one or two letters)
// 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]; }
@ -1381,19 +1397,22 @@ int main(int argc, char *argv[]) {
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 if (argv[i][1] == 'k') { lr_scheduler_type = argv[i+1]; }
else if (argv[i][1] == 'p' && argv[i][2] == 'i') { strcpy(nccl_init_method, argv[i+1]); }
else if (argv[i][1] == 'p' && argv[i][2] == 'f') { strcpy(fs_path, argv[i+1]); }
else if (argv[i][1] == 'p' && argv[i][2] == 's') { strcpy(server_ip, argv[i+1]); }
else if (argv[i][1] == 'p' && argv[i][2] == 'n') { num_processes = atoi(argv[i+1]); }
else if (argv[i][1] == 'p' && argv[i][2] == 'r') { process_rank = atoi(argv[i+1]); }
else if (argv[i][1] == 'p' && argv[i][2] == 'g') { gpus_per_node = atoi(argv[i+1]); }
else { error_usage(); }
}
multi_gpu_config = multi_gpu_config_init(num_processes, process_rank, gpus_per_node, server_ip, fs_path, nccl_init_method);
// should do a bit more error checking here
assert(warmup_iterations >= 0);
if (output_log_dir != NULL) {
assert(strlen(output_log_dir) < 400); // careful bunch of hardcoded snprintf around this
}
// check if output_log_dir does not exist or is a file
struct stat info;
if (output_log_dir != NULL && (stat(output_log_dir, &info ) != 0 || !(info.st_mode & S_IFDIR))) {
fprintf(stderr, "-o \"%s\" does not exist or is a file - are you specifying a file instead of dir?\n", output_log_dir);
exit(EXIT_FAILURE);
}
int tokens_per_fwdbwd = B * T * multi_gpu_config.num_processes; // one micro-batch processes this many tokens
// calculate sensible default for total batch size as assuming no gradient accumulation
if (total_batch_size == -1) { total_batch_size = tokens_per_fwdbwd; }
@ -1414,6 +1433,7 @@ int main(int argc, char *argv[]) {
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("| LR scheduler | %-50s |\n", lr_scheduler_type);
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);
@ -1546,6 +1566,11 @@ int main(int argc, char *argv[]) {
Tokenizer tokenizer;
tokenizer_init(&tokenizer, "gpt2_tokenizer.bin");
// set up learning rate scheduler
LearningRateScheduler lr_scheduler;
lr_scheduler_init(&lr_scheduler, lr_scheduler_type, learning_rate,
warmup_iterations, train_num_batches, final_learning_rate_frac);
// some memory for generating samples from the model
int* gen_tokens = (int*)mallocCheck(B * T * sizeof(int));
floatX* cpu_logits_raw = (floatX*)mallocCheck(model.config.vocab_size * sizeof(floatX));
@ -1581,7 +1606,7 @@ int main(int argc, char *argv[]) {
val_loss += model.mean_loss;
}
val_loss /= val_num_batches;
val_loss = multi_gpu_cpu_float_sum(val_loss) / multi_gpu_config.num_processes;
val_loss = multi_gpu_cpu_float_sum(val_loss, &multi_gpu_config) / multi_gpu_config.num_processes;
printf0("val loss %f\n", val_loss);
logger_log_val(&logger, step, val_loss);
}
@ -1600,7 +1625,7 @@ int main(int argc, char *argv[]) {
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);
eval_acc_norm = multi_gpu_cpu_float_sum(eval_acc_norm, &multi_gpu_config);
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);
}
@ -1697,25 +1722,15 @@ int main(int argc, char *argv[]) {
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, micro_step == grad_accum_steps - 1);
gpt2_backward_and_reduce(&model, train_loader.inputs, micro_step == grad_accum_steps - 1);
}
// 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;
// average the loss and the gradients between all processes
gpt2_multi_gpu_loss_reduce(&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);
}
// fetch the next learning rate
float step_learning_rate = get_learning_rate(&lr_scheduler, step);
// update the model parameters
float grad_norm = gpt2_update(&model, step_learning_rate, 0.9f, 0.95f, 1e-8f, weight_decay, 1.0f, step+1, &multi_gpu_config);
// zero out the gradients for the next iteration