现代超算系统已经从纯 CPU 集群演进为 GPU 加速卡主导的异构架构。
多 GPU 编程模型
每块 GPU 拥有独立的显存空间,线程块、网格、流的概念构建三层并行体系:
┌─────────────────────────────────────────────────────┐
│ Device (GPU) │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │Grid 0 │ │Grid 1 │ │Grid 2 │ │Grid 3 │ │
│ │ │ │ │ │ │ │ │ │
│ │Block 0 │ │Block 0 │ │Block 0 │ │Block 0 │ │
│ │Block 1 │ │Block 1 │ │Block 1 │ │Block 1 │ │
│ │ ... │ │ ... │ │ ... │ │ ... │ │
│ │────────│ │────────│ │────────│ │────────│ │
│ │Stream 0│ │Stream 1│ │Stream 2│ │Stream 3│ │
│ └────────┘ └────────┘ └────────┘ └────────┘ │
│ Global Memory │
└─────────────────────────────────────────────────────┘
↑ PCIe / NVLink / NVSwitch ↑
┌────┴────────────────────────────┴────┐
│ CUDA Context │
└──────────────────────────────────────┘
单个 MPI 进程通常对应一块 GPU。通过 cudaSetDevice(rank % num_gpus_per_node) 实现进程与 GPU 的一一映射。
MPI + CUDA 混合程序结构
典型的 CUDA+MPI 程序框架如下:
#include <mpi.h>
#include <cuda_runtime.h>
__global__ void kernel(float *d_data, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) d_data[idx] *= 2.0f;
}
int main(int argc, char **argv) {
int rank, size;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);
// 绑定 MPI rank 到本地 GPU
int dev_count;
cudaGetDeviceCount(&dev_count);
int dev_id = rank % dev_count;
cudaSetDevice(dev_id);
int n_local = 1024 * 1024;
size_t bytes = n_local * sizeof(float);
// GPU 显存分配
float *d_data;
cudaMalloc(&d_data, bytes);
// 主机内存分配(建议锁页内存)
float *h_data;
cudaMallocHost(&h_data, bytes); // page-locked
// 初始化数据
for (int i = 0; i < n_local; i++) {
h_data[i] = (float)(i + rank * n_local);
}
// H2D
cudaMemcpy(d_data, h_data, bytes, cudaMemcpyHostToDevice);
// 启动核函数
int threads = 256;
int blocks = (n_local + threads - 1) / threads;
kernel<<<blocks, threads>>>(d_data, n_local);
cudaDeviceSynchronize();
// D2H
cudaMemcpy(h_data, d_data, bytes, cudaMemcpyDeviceToHost);
// ---- MPI 进程间通信 ----
float local_sum = 0.0f;
for (int i = 0; i < n_local; i++) local_sum += h_data[i];
float global_sum;
MPI_Allreduce(&local_sum, &global_sum, 1, MPI_FLOAT,
MPI_SUM, MPI_COMM_WORLD);
if (rank == 0) {
printf("Global sum = %.2f\n", global_sum);
}
// 清理
cudaFree(d_data);
cudaFreeHost(h_data);
MPI_Finalize();
return 0;
}
编译命令:
mpicc -cuda hybrid.cu -o hybrid -I/usr/local/cuda/include \
-L/usr/local/cuda/lib64 -lcudart
程序的关键设计点:
- rank-GPU 映射:避免多进程抢占同一块 GPU 导致 context switch 开销
- 锁页内存
cudaMallocHost:加速 H2D/D2H 传输,支持异步传输 - 通信缓冲区脱载:仅在需要 MPI 通信时才将数据从 GPU 拷回主机
GPUDirect RDMA
未启用 GPUDirect 时,GPU 间数据传输路径:
GPU Mem → CPU Mem → MPI_Send (CPU驱动) → Network → CPU Mem → GPU Mem
(D2H) (CPU buffer) (H2D)
启用 GPUDirect RDMA 后,InfiniBand NIC 可以直接读写 GPU 显存:
GPU Mem ────────── RDMA Fetch ──────────→ GPU Mem
↑ ↑
└──── NIC 直接访问显存(绕过 CPU)──────┘
环境检查与开启:
# 1. 检查 nvidia_p2p 模块
lsmod | grep nvidia_p2p
# 2. 启用 CUDA+IB 直接访问
export MPIR_CVAR_ENABLE_GPU=1
export MPICH_GPU_SUPPORT_ENABLED=1 # MPICH 系
export OMPI_MCA_btl_openib_allow_cuda_memory=1 # OpenMPI
# 3. 使用 CUDA-aware MPI:直接在显存指针上调用 MPI
float *d_sendbuf, *d_recvbuf; // 设备指针
cudaMalloc(&d_sendbuf, bytes);
cudaMalloc(&d_recvbuf, bytes);
// ... 填充 d_sendbuf ...
MPI_Send(d_sendbuf, count, MPI_FLOAT, dest, tag, MPI_COMM_WORLD);
CUDA-aware MPI 将 cudaMalloc 分配的指针直接传给 MPI_Send/MPI_Recv,驱动自动利用 GPUDirect 旁路 CPU。性能增益在中小消息(1KB-1MB)时最为显著。
NCCL 通信库
虽然 MPI 管理跨节点进程,NVIDIA Collective Communications Library (NCCL) 专为 GPU 集合通信优化:
MPI 层: 进程 0 进程 1 进程 2 进程 3
│ │ │ │
GPU 层: GPU 0 GPU 1 GPU 2 GPU 3
│─────────────┼─────────────┼─────────────│
│ NCCL Ring/Tree │
│ AllReduce / AllGather │
└───────────────────────────┘
NCCL 典型 API 使用:
#include <nccl.h>
ncclComm_t comm;
int localRank = rank % dev_count;
// 使用 ncclGetUniqueId + ncclCommInitRank 在多进程间建立 NCCL 通信子
ncclUniqueId id;
if (rank == 0) ncclGetUniqueId(&id);
MPI_Bcast(&id, sizeof(id), MPI_BYTE, 0, MPI_COMM_WORLD);
ncclCommInitRank(&comm, size, id, rank);
// NCCL 集合通信:直接在显存上操作
ncclAllReduce(d_sendbuf, d_recvbuf, count,
ncclFloat, ncclSum, comm, cudaStreamDefault);
// 同步
ncclCommDestroy(comm);
| MPI 集体操作 | 对应 NCCL API | 适用场景 |
|---|---|---|
MPI_Allreduce | ncclAllReduce | All-Reduce 梯度同步(深度学习训练) |
MPI_Allgather | ncclAllGather | 参数服务器模式参数聚合 |
MPI_Reduce | ncclReduce | 非对称归约,指定根进程 |
MPI_Broadcast | ncclBroadcast | 初始参数广播 |
NCCL 尤其适用于深度学习训练中的梯度同步(DDP/FSDP),其 Ring AllReduce 算法在大规模集群上具有接近线性的扩展效率。
多流并行与计算通信重叠
CUDA Stream 允许独立的 GPU 操作并行执行。在 MPI+CUDA 程序中,利用多流可以实现:
- Stream A:前一迭代的 D2H 数据传输
- Stream B:下一迭代的 H2D 数据传输
- Stream C:当前迭代的核函数计算
cudaStream_t stream_compute, stream_comm;
cudaStreamCreate(&stream_compute);
cudaStreamCreate(&stream_comm);
for (int iter = 0; iter < max_iters; iter++) {
// 启动计算核(stream_compute 上)
compute_kernel<<<grid, block, 0, stream_compute>>>(d_data, ...);
// 同时将上一迭代需要通信的数据 D2H(stream_comm 上)
cudaMemcpyAsync(h_halo, d_halo, halo_bytes,
cudaMemcpyDeviceToHost, stream_comm);
// 更新边界缓冲(stream_compute 完成后)
update_boundary<<<grid, block, 0, stream_compute>>>(d_data, ...);
// 等待通信流完成数据准备
cudaStreamSynchronize(stream_comm);
// MPI 交换边界
MPI_Sendrecv(h_halo_send, count, MPI_FLOAT, ...,
h_halo_recv, count, MPI_FLOAT, ...);
// 将收到的边界数据传回 GPU
cudaMemcpyAsync(d_halo_recv, h_halo_recv, halo_bytes,
cudaMemcpyHostToDevice, stream_compute);
}
通过双缓冲甚至多缓冲技术,可以完全隐藏外推网络延迟。
大规模集群实践
拓扑感知进程映射
# rank-to-node mapping:确保相邻 rank 在物理上靠近
mpirun -np 8 --map-by ppr:4:node:PE=8 --bind-to core \
./cuda_mpi_app
# 配合 NCCL 拓扑自动检测:
# NCCL 自动选择 NVLink > PCIe P2P > IB 的最快路径
避免进程与 GPU 争用
// 严格的 rank-to-GPU 绑定
int ngpus;
cudaGetDeviceCount(&ngpus);
if (ngpus == 0) {
fprintf(stderr, "No GPU found\n");
MPI_Abort(MPI_COMM_WORLD, 1);
}
int gpu_id = local_rank % ngpus;
cudaSetDevice(gpu_id);
内存策略
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 数据集能放入显存 | 全程 GPU 运算,仅在 checkpoint 时 D2H | 最大 GPU 利用率 |
| GPU 显存不足 | Unified Memory (cudaMallocManaged) | 自动页面迁移,简化编程 |
| 需 host staging | 锁页内存 + 双缓冲流 | 减少传输 overhead |
多节点性能基准
在 4 节点 x 8 A100 (NVLink+InfiniBand HDR) 上测量不同通信策略的带宽对比:
| 通信方式 | 有效带宽 | 相对效率 |
|---|---|---|
| 显存 → 锁页内存 → MPI | ~12 GB/s | 基准 |
| CUDA-aware MPI (GPUDirect) | ~22 GB/s | 1.83x |
| NCCL AllReduce (Ring) | ~24 GB/s | 2.0x |
| NCCL AllReduce (NVLink + IB SHARP) | ~48 GB/s | 4.0x |
带数据依赖的 stencil 计算中,计算与通信重叠通常可将有效并行效率从 65% 提升到 85% 以上。GPU 异构并行编程的核心是:最小化数据传输、最大化 GPU 内核时间、通过非阻塞通信隐藏延迟。
深度学习训练中的 MPI+CUDA 模式
大规模 AI 训练(如 GPT、LLaMA)是 CUDA+MPI 的最重要应用场景之一:
# PyTorch DDP 内部使用 NCCL 通信后端
import torch
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
dist.init_process_group(backend='nccl')
model = MyModel().cuda(local_rank)
ddp_model = DDP(model, device_ids=[local_rank])
for batch in dataloader:
loss = ddp_model(batch)
loss.backward()
# DDP 自动执行 AllReduce 梯度同步(NCCL)
optimizer.step()
训练并行模式对比:
| 策略 | 切分维度 | 通信量 | 典型场景 |
|---|---|---|---|
| 数据并行(DDP) | Batch | 每步梯度 AllReduce | 模型可放入单卡 |
| 模型并行 | 层 / 参数 | 每步激活值传输 | 模型 > 单卡显存 |
| 流水线并行 | 层序列 | 每阶段边界传输 | 超大模型 |
| 张量并行 | 层内参数 | 每步 all-reduce | 序列长、层宽 |
| 混合并行(FSDP) | 数据 + 参数分片 | 动态 | 超大规模训练 |
FSDP(Fully Sharded Data Parallel) 将模型参数、梯度和优化器状态分片到所有 GPU,前向/反向传播时按需收集,显著降低单卡显存占用。
功耗管理与散热策略
大规模 GPU 集群的功耗密度远高于传统 CPU 集群:
| 配置 | 单节点功耗 | 散热需求 |
|---|---|---|
| 8x A100 80GB | ~3.5 kW | 液冷或高效风冷 |
| 8x H100 80GB | ~5.2 kW | 液冷为主 |
| 8x MI300X | ~5.5 kW | 液冷必需 |
功耗优化手段:
- 功率上限(Power Cap):
nvidia-smi -pl 250将 A100 从 400W 限制到 250W,性能通常只下降 5-10%,能效大幅提升 - 动态频率调整(DVFS):空闲时自动降频
- MIG(Multi-Instance GPU):将单卡划分为多个独立实例,提高利用率
大规模集群的故障恢复
超算集群中 GPU 故障是常态而非意外:
| 故障类型 | 频率 | 影响 | 应对 |
|---|---|---|---|
| ECC 内存错误 | 常见(每周) | 单卡降速 | 自动重算或重启任务 |
| GPU 掉卡 | 偶发(每月) | 单节点失效 | 热插拔 + 作业迁移 |
| NVLink 故障 | 罕见 | 通信降级 | 回退至 PCIe / IB |
| 网络分区 | 偶发 | 多节点通信中断 | 检查点 + 重跑 |
弹性训练(Elastic Training):
- 定期保存检查点(checkpoint)到并行文件系统
- 作业调度器(如 Slurm)检测到节点故障后重新分配资源
- 训练框架(PyTorch Elastic、Horovod Elastic)支持动态扩缩容
# PyTorch Elastic Launch(容忍节点增减)
torchrun --nnodes=1:4 --nproc_per_node=8 --max_restarts=3 train.py
网络拓扑与集群架构
现代 GPU 集群的网络拓扑直接影响通信效率:
Fat-Tree 拓扑(中小型集群):
Leaf Switch ── 连接 16-32 个节点
Spine Switch ── 互联所有 Leaf,提供非阻塞带宽
Dragonfly+ 拓扑(超大规模):
Group ── 完全连接的节点组(如 32 节点)
Global Link ── 组间稀疏连接,降低交换机数量
特点:直径小、成本低,适合 10万+ GPU 规模
| 集群规模 | 推荐拓扑 | 交换机成本占比 |
|---|---|---|
| < 256 GPU | Fat-Tree | 20-30% |
| 256-4096 GPU | Fat-Tree / 3-tier Clos | 25-35% |
| 4096-65536 GPU | Dragonfly+ | 15-25% |
| > 65536 GPU | Dragonfly+ / 定制 | < 20% |
编程框架与工具链
| 层次 | 工具 | 作用 |
|---|---|---|
| 底层 MPI | OpenMPI / MPICH | 跨进程通信 |
| GPU 通信 | NCCL / Gloo | GPU 集合通信优化 |
| 深度学习 | PyTorch / TensorFlow / JAX | 自动并行化 |
| 作业调度 | Slurm / PBS / Kubernetes | 资源分配与任务管理 |
| 性能分析 | Nsight Systems / Nsight Compute | 内核级 profiling |
| 集群监控 | Prometheus + Grafana / DCGM | 实时健康监控 |
带数据依赖的 stencil 计算中,计算与通信重叠通常可将有效并行效率从 65% 提升到 85% 以上。GPU 异构并行编程的核心是:最小化数据传输、最大化 GPU 内核时间、通过非阻塞通信隐藏延迟。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。