多节点训练实验环境:从单机模拟到云上真实集群
多节点训练的第一道墙不是算法,而是环境。rank 混淆与网络协商的真实问题,只有真的跨过一次节点才会出现。
PyTorch 训练一章讲清了训练需要的调度语义:成组、最小可用、可恢复、可抢占。但语义学会了,去哪里练?多数工程师手头没有集群,而单机训练(哪怕是单机八卡)有一个结构性盲区:它永远暴露不出只在跨节点时才出现的问题。本章给出四条按成本递增的实验路线,让“练一次真实的多节点 DDP”的门槛降到一杯咖啡钱。
单机训练的盲区:为什么必须真的跨一次节点
两个在单机上永远遇不到的问题:
- rank 与 local_rank 的混淆。单机多卡时,进程的全局 rank(
RANK)与节点内 rank(LOCAL_RANK)恰好一一重合(第 i 卡的进程两个值都是 i),写错也能跑。跨节点后两者分化:双节点各四卡时,节点 1 上的四个进程local_rank是 0–3,rank却是 4–7。凡是把rank当local_rank用的代码(设备选择、数据分片、checkpoint 保存),单机全对,上集群即错。 - 跨节点通信的协商。NCCL 建立集合通信时要协商 socket:动态临时端口、多网卡选路、NAT 与防火墙穿透。单机上这一切都不可见(进程间走共享内存);跨节点后,“
all_reduce无报错但永久挂起”十有八九是网络协商失败。集合通信本身的原理见互连与集合通信。
所以本章的目标不是讲新的训练技术,而是给你一个能把这两类问题真实复现出来的环境。四条路线,成本从免费到几美元:
| 路线 | 成本 | 练到什么 | 练不到什么 |
|---|---|---|---|
| 一台双卡实例模拟双节点 | 约 $0.2–0.5/小时 | rank 映射、DDP 生命周期、双进程协作 | 真实跨节点网络 |
| 云上真实双节点 | 每次实验个位数美元 | 全部:rank、网络、防火墙、配额流程 | 大规模拓扑 |
| 本地双卡 / 单卡 Gloo | $0 | 路线一的免费版 / 纯控制流验证 | 真实网络与算力 |
| 云上 Slurm 集群 | 免费微实例可验证编排 | HPC 形态的作业调度全流程 | (取决于投入) |
路线一:一台双卡实例,模拟两个节点
核心思路:租一台双卡实例,而不是两台单卡。原因很实际:主流 GPU 租赁平台(以 Vast.ai 为例)交付的是共享宿主机上的容器,通常没有 /dev/net/tun,WireGuard/Tailscale 组网走不通;而静态端口转发对付不了 NCCL 的动态临时端口,两台单卡之间打不通的直接后果就是 all_reduce 挂起。一台双卡机器则完全绕开组网问题。
做法是用 CUDA_VISIBLE_DEVICES 把两个进程分别钉在两块卡上,各自以独立节点的身份加入同一个 torchrun 集群(master 走本机回环地址):
# tmux 窗格 1,模拟节点 0
CUDA_VISIBLE_DEVICES=0 torchrun --nnodes=2 --nproc_per_node=1 \
--master_addr=127.0.0.1 --master_port=29500 --node_rank=0 train.py
# tmux 窗格 2,模拟节点 1
CUDA_VISIBLE_DEVICES=1 torchrun --nnodes=2 --nproc_per_node=1 \
--master_addr=127.0.0.1 --master_port=29500 --node_rank=1 train.py这个安排的精妙之处在于设备视图的忠实重演:CUDA_VISIBLE_DEVICES 会把可见卡重新编号,两个“节点”各自只看得见 cuda:0,与真实双节点上每个节点的视角完全一致。因此 rank 映射错误会真实暴露,而跨节点网络问题不会(同主机上 NCCL 走共享内存/P2P)。
一个高频坑:不要在租来的实例里新建干净的虚拟环境,那会隔离平台预装的 torch 与 CUDA。要么直接用系统环境,要么建 venv 时加 --system-site-packages。
路线二:云上真实双节点
要练完整的网络问题,就得开两台真机。以 Oracle Cloud(OCI)为例(其他云流程等价,实例选型参考从免费开始一章的付费选型):
- 配额是第一道关。GPU 配额初始为零:升级为按量付费(PAYG)账号后提交 service limit 提升(作者实测数小时内获批)。控制台里 quota 显示 “critical” 警告常常只是“已用 0/上限 1”的字面提示,不代表异常。单卡 A10(24GB 显存)按需约 $2/小时,双节点跑一两个小时,实验成本是个位数美元。
- 网络两件事。同一 VCN 子网内的入站规则放行子网 CIDR 的全部协议;登进机器后清空 Ubuntu 镜像自带的 iptables 规则(
sudo iptables -F)。“云安全组放行了、主机防火墙还在拦”是双节点 DDP 挂起的经典组合。 - 启动。与路线一相同的
torchrun,只是--master_addr换成节点 0 的私网 IP,两个节点分别用各自的--node_rank。
用完按从免费开始一章的 terminate 纪律清理。配额审批与账单止损的纪律,本章实验与本书所有云实验一致。
路线三:本地与混合(零成本)
- 本地双卡工作站:路线一的免费版,命令原样可用。
- 单卡甚至无卡:把后端从
nccl换成gloo,在 CPU 上验证控制流:rank 分工、数据分片、checkpoint 保存/加载、集合调用顺序。算不了力,但编排逻辑的真实性不打折,成本为零。 - 本地 + 云混合组网(如 Tailscale 打通家里与云上机器):社区常见做法,但依赖各平台的网络条件,本书实验未验证,不作推荐,只作线索。
路线四:Slurm 集群(超算形态)
如果目标是理解 HPC 世界的作业调度,值得亲手搭一次 Slurm。在单机容器里模拟它非常痛苦:slurmctld/slurmd/munged 需要 systemd、节点间需要共享文件系统(NFS)与私网,这三样在 Docker 里都要费大劲伪造。正确姿势是用云厂商的成套工具:AWS ParallelCluster、GCP HPC Toolkit、OCI HPC Cluster Stack,一条流水线给出完整的 Slurm 集群。
省钱技巧:先用免费的 CPU 微型实例把编排流程跑通。Slurm 的调度逻辑在免费微实例与 H100 集群上行为一致;只需把训练脚本的通信后端换成 gloo。其中最关键的一行代码,是从调度器解析 master 地址:
MASTER_ADDR=$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n 1)Slurm 与 Kubernetes 是两条并行的治理路线:本书主线在 K8s(gang 调度见 Volcano、队列治理见 Kueue),但 Slurm 的 partition/job 语义是 gang 调度的历史源头,体验一次能加深对控制面各组件“为什么存在”的理解。
动手实验:单机双卡跑通双节点 DDP
实验目标
用一台双卡实例(或本地双卡机器)模拟两个节点,跑通 --nnodes=2 的 DDP,亲眼看到“两个节点各自只有 cuda:0”与 rank 的分化,这正是单机训练永远给不了你的视角。
前置条件与成本预估
- 一台双卡机器:Vast.ai 上双卡实例约 $0.2–0.5/小时(以平台实时报价为准),或本地双卡工作站($0)。实验全程半小时到一小时,云上预算 1 美元以内。
- 实例自带的 PyTorch 环境可直接使用(勿新建隔离的 venv)。
操作步骤
先写一个最小验证脚本 train.py:
import os
import torch
import torch.distributed as dist
dist.init_process_group(backend="nccl") # torchrun 会注入 RANK/WORLD_SIZE 等环境变量
rank = dist.get_rank() # 全局 rank
local_rank = int(os.environ["LOCAL_RANK"]) # 节点内 rank
world_size = dist.get_world_size()
torch.cuda.set_device(local_rank)
x = torch.ones(8, 8, device="cuda") * (rank + 1)
dist.all_reduce(x) # 两个节点的贡献应该求和后处处可见
print(f"[rank {rank}/{world_size}] local_rank={local_rank}, "
f"device={torch.cuda.get_device_name(0)}, all_reduce check: {x[0, 0].item():.0f}")
dist.destroy_process_group()然后在两个 tmux 窗格里分别执行(命令见路线一):窗格 1 带 CUDA_VISIBLE_DEVICES=0、--node_rank=0;窗格 2 带 CUDA_VISIBLE_DEVICES=1、--node_rank=1。
验证清单
- 两个窗格各打印一行,
world_size均为 2 - 两行的
rank分别是 0 和 1(全局 rank 跨节点编号) - 两行的
local_rank都是 0(各自的节点内编号,这就是单机看不到的分化) - 两行的
device相同(各自只能看见编号为 0 的卡,CUDA_VISIBLE_DEVICES重编号的效果) - 两行的
all_reduce check都是 3(即 1+2,集合通信在两个“节点”间真实完成)
原理速览
CUDA_VISIBLE_DEVICES 在驱动层重写设备的可见编号,进程只看得见被放行的那块卡并把它编为 0,这精确复刻了真实双节点上每个节点的设备视角。torchrun 的 rendezvous 通过 127.0.0.1:29500 把两个进程组织成 world_size=2 的集群;由于两个“节点”实为同主机进程,NCCL 的传输回落到主机内共享内存/P2P,这正是本路线“练 rank 语义、不练网络”的物理原因。
延伸练习与清理
- 延伸一:把脚本后端换成
gloo、设备换成 CPU(device="cpu"),在无 GPU 的笔记本上重跑,验证控制流。 - 延伸二:申请 OCI 配额,按路线二开真双节点重跑本实验,对比
master_addr从回环地址换成私网 IP 后的网络行为。 - 清理:销毁实例(Vast.ai 上 destroy,云厂商 terminate),并按从免费开始一章的清单确认零残留。
总结
多节点训练的练习门槛比多数人想象的低:一台双卡实例加一小时,就能把 rank 映射这个单机盲区真实暴露出来;个位数美元就能练完整的跨节点网络与配额流程;免费微实例就能走通 Slurm 的作业调度。四条路线的共同结论是:平台摩擦(配额、守护进程、内核与网络配置)才是多节点训练真正的工程门槛,这也正是本书把“环境”当作一等公民的原因。下一章Ray 与拓扑把多节点推向生产形态:当节点数继续增长,资源位置与互连拓扑将取代资源数量,成为调度的主要约束。