/* Utilities for ZeRO sharding */ #ifndef LLMC_ZERO_CUH #define LLMC_ZERO_CUH #include #include #include #include #include #ifdef MULTI_GPU #include #ifdef USE_MPI #include #endif #endif // defines: fcloseCheck, fwriteCheck, scloseCheck, sclosesocketCheck #include "utils.h" // ---------------------------------------------------------------------------- // Multi-GPU related #ifdef MULTI_GPU #if defined(ENABLE_FP32) const ncclDataType_t ncclFloatX = ncclFloat; #elif defined(ENABLE_FP16) const ncclDataType_t ncclFloatX = ncclHalf; #else // Default to bfloat16 const ncclDataType_t ncclFloatX = ncclBfloat16; #endif 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__)) #ifdef USE_MPI 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 #endif // MULTI_GPU // ---------------------------------------------------------------------------- // Parameters specific to training on multiple GPUs. typedef struct { 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. // 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; #ifdef MULTI_GPU 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; // one global variable to hold the multi-GPU configuration for this process // inline, so we can include this header multiple times without getting multiple definitions inline MultiGpuConfig multi_gpu_config; #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); } closesocketCheck(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); } scloseCheck(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"); closesocketCheck(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"); closesocketCheck(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"); closesocketCheck(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"); closesocketCheck(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"); closesocketCheck(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"); closesocketCheck(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"); closesocketCheck(client_socket); WSACleanup(); exit(EXIT_FAILURE); } printf("Received NCCL ID\n"); closesocketCheck(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"); scloseCheck(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"); scloseCheck(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); fwriteCheck(&nccl_id, sizeof(nccl_id), 1, idFile); fcloseCheck(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); fcloseCheck(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; } } 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)); // 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 #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 MultiGpuConfig result; ncclUniqueId 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); } } 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"); 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(MultiGpuConfig* config) { #ifdef MULTI_GPU ncclCheck(ncclCommDestroy(config->nccl_comm)); cudaCheck(cudaStreamDestroy(config->nccl_stream)); cudaCheck(cudaEventDestroy(config->compute_nccl_sync)); cudaCheck(cudaFree(config->unified_buffer)); #ifdef USE_MPI mpiCheck(MPI_Finalize()); #endif #endif } void multi_gpu_barrier(const MultiGpuConfig* config) { #ifdef MULTI_GPU if (config->num_processes > 1) { ncclCheck(ncclAllReduce(config->unified_buffer, config->unified_buffer, sizeof(float), ncclFloat, ncclSum, config->nccl_comm, config->nccl_stream)); } cudaCheck(cudaDeviceSynchronize()); #endif } // Offset and size of a tensor shard typedef struct { ptrdiff_t offset; size_t size; } ShardInfo; // Get info about sharding for a tensor of elements many numbers ShardInfo multi_gpu_get_shard_offset(size_t elements, const MultiGpuConfig* config, int shard_at_stage) { const int nproc = config->num_processes; if(config->zero_stage >= shard_at_stage) { if (elements % nproc != 0) { fprintf(stderr, "Number of elements %zu must be a multiple of the number of processes %d\n", elements, nproc); exit(EXIT_FAILURE); } return {(ptrdiff_t) (config->process_rank * (elements / nproc)), elements / nproc}; } else { return {0, elements}; } } // Block NCCL stream until computations on compute_stream are done, then aggregate multiple pointers in an NCCL group. // This can work either as an all-reduce (i.e., no ZeRo), or a reduce-scatter (ZeRO 1). // The awkward `(&pointers)[N]` syntax ensures we are capturing the parameters as sized arrays, so that it becomes impossible // to call this function if pointers and pointers_sizes do not match. template void multi_gpu_async_reduce_gradient( floatX* const (&pointers)[N], const size_t (&pointers_sizes)[N], MultiGpuConfig* config, cudaStream_t compute_stream) { if (config->num_processes == 1) { return; // no multi-GPU, just exit. } #ifdef MULTI_GPU NVTX_RANGE_FN(); // mark an event on the compute stream, and immediately wait on this in the nccl stream // this means that the nccl stream won't start executing before all compute kernels that // have been submitted before this point have finished. // by using an event instead of cudaSyncStream, we avoid having to synchronize the host, and // can enqueue new work to the GPU right away. cudaCheck(cudaEventRecord(config->compute_nccl_sync, compute_stream)); cudaCheck(cudaStreamWaitEvent(config->nccl_stream, config->compute_nccl_sync)); ncclCheck(ncclGroupStart()); // NCCL group: aggregate all pointers in a single NCCL GPU kernel. for (int i = 0; i < N; ++i) { if(config->zero_stage == 0) { ncclCheck(ncclAllReduce( pointers[i], pointers[i], pointers_sizes[i], ncclFloatX, ncclAvg, config->nccl_comm, config->nccl_stream )); } else if(config->zero_stage == 1) { assert(pointers_sizes[i] % config->num_processes == 0); size_t shard_size = pointers_sizes[i] / config->num_processes; ptrdiff_t shard_offset = (ptrdiff_t)shard_size * config->process_rank; ncclCheck(ncclReduceScatter( pointers[i], pointers[i] + shard_offset, shard_size, ncclFloatX, ncclAvg, config->nccl_comm, config->nccl_stream )); } } ncclCheck(ncclGroupEnd()); #endif } // convenience macro that only prints if the rank of process is zero #define printf0(...) if (::multi_gpu_config.process_rank == 0) { printf(__VA_ARGS__); } void set_zero_configs(MultiGpuConfig* config, int zero_stage, size_t total_parameters) { config->zero_stage = 0; config->shard_num_parameters = total_parameters; // 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 % config->num_processes != 0) { printf0("| Zero Optimization is disabled, Can't equally partition parameters |\n"); config->zero_stage = 0; } else { config->zero_stage = 1; config->shard_num_parameters = total_parameters / config->num_processes; } } else{ printf0("| Disabling Zero Optimization, Zero Stage2 and Stage3 are not yet supported |\n"); config->zero_stage = 0; } } // 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, MultiGpuConfig* config) { #ifdef MULTI_GPU if (config->num_processes == 1) return value; float* unified_buffer = config->unified_buffer; *unified_buffer = value; ncclCheck(ncclAllReduce(unified_buffer, unified_buffer, sizeof(float), ncclFloat, ncclSum, config->nccl_comm, config->nccl_stream)); cudaCheck(cudaDeviceSynchronize()); return *unified_buffer; #else return value; #endif } #endif