从云原生走向 AI 原生:一套面向未来的架构方法论 → 阅读《AI 原生基础设施》

多节点训练实验环境:从单机模拟到云上真实集群

草稿

多节点训练的第一道墙不是算法,而是环境。rank 混淆与网络协商的真实问题,只有真的跨过一次节点才会出现。

PyTorch 训练一章讲清了训练需要的调度语义:成组、最小可用、可恢复、可抢占。但语义学会了,去哪里练?多数工程师手头没有集群,而单机训练(哪怕是单机八卡)有一个结构性盲区:它永远暴露不出只在跨节点时才出现的问题。本章给出四条按成本递增的实验路线,让“练一次真实的多节点 DDP”的门槛降到一杯咖啡钱。

内容来源
本章四条路线的框架与多个实战要点(租用双卡而非两台单卡的原因、OCI 配额流程、iptables 与安全组排查、Slurm 免费验证法),源自 Henry Wu 的博客文章 Distributed AI Systems: A Practical Guide to GPU Compute and Experimental Environments(其著作《Distributed AI Systems》的配套指南)。本章按本书的实验格式独立改写,命令与结论以本书验证口径为准。

单机训练的盲区:为什么必须真的跨一次节点

两个在单机上永远遇不到的问题:

  1. rank 与 local_rank 的混淆。单机多卡时,进程的全局 rank(RANK)与节点内 rank(LOCAL_RANK)恰好一一重合(第 i 卡的进程两个值都是 i),写错也能跑。跨节点后两者分化:双节点各四卡时,节点 1 上的四个进程 local_rank 是 0–3,rank 却是 4–7。凡是把 ranklocal_rank 用的代码(设备选择、数据分片、checkpoint 保存),单机全对,上集群即错。
  2. 跨节点通信的协商。NCCL 建立集合通信时要协商 socket:动态临时端口、多网卡选路、NAT 与防火墙穿透。单机上这一切都不可见(进程间走共享内存);跨节点后,“all_reduce 无报错但永久挂起”十有八九是网络协商失败。集合通信本身的原理见互连与集合通信

所以本章的目标不是讲新的训练技术,而是给你一个能把这两类问题真实复现出来的环境。四条路线,成本从免费到几美元:

路线成本练到什么练不到什么
一台双卡实例模拟双节点约 $0.2–0.5/小时rank 映射、DDP 生命周期、双进程协作真实跨节点网络
云上真实双节点每次实验个位数美元全部:rank、网络、防火墙、配额流程大规模拓扑
本地双卡 / 单卡 Gloo$0路线一的免费版 / 纯控制流验证真实网络与算力
云上 Slurm 集群免费微实例可验证编排HPC 形态的作业调度全流程(取决于投入)
表 1: 四条多节点实验路线

路线一:一台双卡实例,模拟两个节点

核心思路:租一台双卡实例,而不是两台单卡。原因很实际:主流 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)为例(其他云流程等价,实例选型参考从免费开始一章的付费选型):

  1. 配额是第一道关。GPU 配额初始为零:升级为按量付费(PAYG)账号后提交 service limit 提升(作者实测数小时内获批)。控制台里 quota 显示 “critical” 警告常常只是“已用 0/上限 1”的字面提示,不代表异常。单卡 A10(24GB 显存)按需约 $2/小时,双节点跑一两个小时,实验成本是个位数美元。
  2. 网络两件事。同一 VCN 子网内的入站规则放行子网 CIDR 的全部协议;登进机器后清空 Ubuntu 镜像自带的 iptables 规则(sudo iptables -F)。“云安全组放行了、主机防火墙还在拦”是双节点 DDP 挂起的经典组合。
  3. 启动。与路线一相同的 torchrun,只是 --master_addr 换成节点 0 的私网 IP,两个节点分别用各自的 --node_rank

用完按从免费开始一章的 terminate 纪律清理。配额审批与账单止损的纪律,本章实验与本书所有云实验一致。

路线三:本地与混合(零成本)

  • 本地双卡工作站:路线一的免费版,命令原样可用。
  • 单卡甚至无卡:把后端从 nccl 换成 gloo,在 CPU 上验证控制流:rank 分工、数据分片、checkpoint 保存/加载、集合调用顺序。算不了力,但编排逻辑的真实性不打折,成本为零。
  • 本地 + 云混合组网(如 Tailscale 打通家里与云上机器):社区常见做法,但依赖各平台的网络条件,本书实验未验证,不作推荐,只作线索。

路线四:Slurm 集群(超算形态)

如果目标是理解 HPC 世界的作业调度,值得亲手搭一次 Slurm。在单机容器里模拟它非常痛苦:slurmctld/slurmd/munged 需要 systemd、节点间需要共享文件系统(NFS)与私网,这三样在 Docker 里都要费大劲伪造。正确姿势是用云厂商的成套工具:AWS ParallelClusterGCP HPC ToolkitOCI 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 与拓扑把多节点推向生产形态:当节点数继续增长,资源位置与互连拓扑将取代资源数量,成为调度的主要约束。

参考资料

创建于 2026/09/15 更新于 2026/09/15 3547 字 阅读约 8 分钟