CUDA + MPI 异构并行:多 GPU 分布式编程实战

深入讲解 CUDA 与 MPI 混合并行编程模型,涵盖 GPUDirect RDMA、NCCL 通信库与大规模 GPU 集群实践,配合完整代码示例。

现代超算系统已经从纯 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

程序的关键设计点:

  1. rank-GPU 映射:避免多进程抢占同一块 GPU 导致 context switch 开销
  2. 锁页内存 cudaMallocHost:加速 H2D/D2H 传输,支持异步传输
  3. 通信缓冲区脱载:仅在需要 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_AllreducencclAllReduceAll-Reduce 梯度同步(深度学习训练)
MPI_AllgatherncclAllGather参数服务器模式参数聚合
MPI_ReducencclReduce非对称归约,指定根进程
MPI_BroadcastncclBroadcast初始参数广播

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/s1.83x
NCCL AllReduce (Ring)~24 GB/s2.0x
NCCL AllReduce (NVLink + IB SHARP)~48 GB/s4.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 GPUFat-Tree20-30%
256-4096 GPUFat-Tree / 3-tier Clos25-35%
4096-65536 GPUDragonfly+15-25%
> 65536 GPUDragonfly+ / 定制< 20%

编程框架与工具链

层次工具作用
底层 MPIOpenMPI / MPICH跨进程通信
GPU 通信NCCL / GlooGPU 集合通信优化
深度学习PyTorch / TensorFlow / JAX自动并行化
作业调度Slurm / PBS / Kubernetes资源分配与任务管理
性能分析Nsight Systems / Nsight Compute内核级 profiling
集群监控Prometheus + Grafana / DCGM实时健康监控

带数据依赖的 stencil 计算中,计算与通信重叠通常可将有效并行效率从 65% 提升到 85% 以上。GPU 异构并行编程的核心是:最小化数据传输、最大化 GPU 内核时间、通过非阻塞通信隐藏延迟。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「hpc」更多文章

  1. Slurm 集群调度系统深度解析与实战
  2. Roofline 性能模型:判定性能瓶颈与优化方向
  3. ROCm HIP GPU 编程实战