Files
2026-07-13 12:37:59 +08:00

199 lines
7.4 KiB
Plaintext

/*
A simple test of NCCL capabilities.
Fills a vector with 1s on the first GPU, 2s on the second, etc.
Then aggregates the values in the resulting vectors.
Compile example:
nvcc -lmpi -lnccl -I/usr/lib/x86_64-linux-gnu/openmpi/include -L/usr/lib/x86_64-linux-gnu/openmpi/lib/ -lcublas -lcublasLt nccl_all_reduce.cu -o nccl_all_reduce
Run on 2 local GPUs (set -np to a different value to change GPU count):
mpirun -np 2 ./nccl_all_reduce
*/
#include "common.h"
#include <assert.h>
#include <cuda_runtime.h>
#include <mpi.h>
#include <nccl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
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__))
// Sets a vector to a predefined value
__global__ void set_vector(float *data, int N, float value) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
// Check for out-of-bounds access
if (i < N) {
data[i] = value;
}
}
size_t cdiv(size_t a, size_t b) { return (a + b - 1) / b; }
// Parameters specific to training on multiple GPUs.
typedef struct {
int process_rank; // Rank of this process among all MPI processes on all hosts. 0 if no multi-GPU.
int num_processes; // Total number of processes on all hosts. 1 if no multi-GPU.
int local_device_idx; // This process GPU index on current machine. 0 if no multi-GPU.
ncclComm_t nccl_comm; // NCCL communication primitive, used for collective mutli-GPU work.
} MultiGpuConfig;
// 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;
}
MultiGpuConfig multi_gpu_config_init(int *argc, char ***argv) {
// 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);
printf("[Process rank %d] Using GPU %d\n", result.process_rank, result.local_device_idx);
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;
}
void multi_gpu_config_free(const MultiGpuConfig* multi_gpu_config) {
ncclCommDestroy(multi_gpu_config->nccl_comm);
mpiCheck(MPI_Finalize());
}
float get_mean(float *arr, size_t size, int process_rank) {
double sum = 0.0;
for (size_t i = 0; i < size; ++i) {
sum += arr[i];
}
return sum / size;
}
int main(int argc, char **argv) {
// Some constants
const size_t all_reduce_buffer_size = 32 * 1024 * 1024;
const size_t threads_per_block = 1024;
MultiGpuConfig multi_gpu_config = multi_gpu_config_init(&argc, &argv);
// Allocating buffers on each of the devices.
float *all_reduce_buffer;
cudaCheck(
cudaMalloc(&all_reduce_buffer, all_reduce_buffer_size * sizeof(float)));
int n_blocks = cdiv(all_reduce_buffer_size, threads_per_block);
// Set the allocated memory to a defined value.
set_vector<<<n_blocks, threads_per_block>>>(
all_reduce_buffer, all_reduce_buffer_size,
(float)(multi_gpu_config.process_rank + 1));
cudaCheck(cudaGetLastError());
float *all_reduce_buffer_host =
(float *)malloc(all_reduce_buffer_size * sizeof(float));
cudaCheck(cudaMemcpy(all_reduce_buffer_host, all_reduce_buffer,
sizeof(float) * all_reduce_buffer_size,
cudaMemcpyDeviceToHost));
printf("[Process rank %d] average value before all reduce is %.6f\n", multi_gpu_config.process_rank,
get_mean(all_reduce_buffer_host, all_reduce_buffer_size,
multi_gpu_config.process_rank));
float *all_reduce_buffer_recv;
cudaCheck(cudaMalloc(&all_reduce_buffer_recv,
all_reduce_buffer_size * sizeof(float)));
ncclCheck(ncclAllReduce(
(const void *)all_reduce_buffer, (void *)all_reduce_buffer_recv,
all_reduce_buffer_size, ncclFloat, ncclSum, multi_gpu_config.nccl_comm, 0));
cudaCheck(cudaMemcpy(all_reduce_buffer_host, all_reduce_buffer_recv,
sizeof(float) * all_reduce_buffer_size,
cudaMemcpyDeviceToHost));
float all_reduce_mean_value = get_mean(all_reduce_buffer_host, all_reduce_buffer_size, multi_gpu_config.process_rank);
printf("[Process rank %d] average value after all reduce is %.6f\n", multi_gpu_config.process_rank, all_reduce_mean_value);
float expected_all_reduce_mean_value = 0.0;
for (int i = 0; i != multi_gpu_config.num_processes; ++i) {
expected_all_reduce_mean_value += i + 1;
}
if (abs(expected_all_reduce_mean_value - all_reduce_mean_value) > 1e-5) {
printf("[Process rank %d] ERROR: Unexpected all reduce value: %.8f, expected %.8f\n", multi_gpu_config.process_rank, all_reduce_mean_value, expected_all_reduce_mean_value);
} else {
printf("[Process rank %d] Checked against expected mean value. All good!\n", multi_gpu_config.process_rank);
}
free(all_reduce_buffer_host);
cudaCheck(cudaFree(all_reduce_buffer));
cudaCheck(cudaFree(all_reduce_buffer_recv));
multi_gpu_config_free(&multi_gpu_config);
}