4、多机多卡同步训练实战:多机通信原理、torchrun 启动脚本、环境变量设置、多机训练代码实战
好,咱们终于聊到多机多卡了。
说实话,单机多卡训练,你只要搞定一台机器上的 GPU 通信就行。但多机训练,说白了就是让几台机器像一支军队一样协同作战。我刚开始搞多机训练时,踩过的坑比代码行数还多。今天我把这些经验掰开揉碎了讲给你。
4.1 多机通信原理:机器之间怎么“说话”
多机训练的核心,是让每台机器上的 GPU 能交换梯度。你想想看,如果每台机器只算自己那部分数据,最后不合并梯度,那模型就学歪了。
PyTorch 用的通信后端是 NCCL(NVIDIA Collective Communications Library)。它专门为 GPU 设计,走的是 InfiniBand 或者 RoCE 网络。我个人习惯用 InfiniBand,延迟低、带宽大,但贵。如果你只有千兆以太网,嗯,那训练速度会慢得让你怀疑人生。
通信模式是 AllReduce。每台机器算完梯度后,NCCL 会把所有机器的梯度加起来,再广播回去。这个过程叫“规约-广播”。
关键点:多机通信的瓶颈不在 GPU 算力,而在网络带宽。我曾经在 4 台机器上跑 ResNet-50,千兆网卡只能跑到 40% 的 GPU 利用率,换成 InfiniBand 直接拉满到 95%。
这里我画了一张图,帮你理解多机通信的流程:
我的经验:多机训练时,尽量让每台机器的 GPU 数量一致。我曾经混用 4 卡和 8 卡的机器,结果慢的那台成了瓶颈,整体效率反而下降。保持对称,性能最优。
4.2 torchrun 启动脚本:一键拉起多机训练
PyTorch 官方推荐用 torchrun 来启动分布式训练。它帮你处理了进程管理、环境变量设置、错误重启等脏活累活。
先看一个典型的多机启动命令:
# 在机器 1 上执行(主节点)
torchrun --nnodes=2 --nproc_per_node=4 \
--rdzv_endpoint=192.168.1.100:29500 \
--rdzv_backend=c10d \
train.py
# 在机器 2 上执行(从节点)
torchrun --nnodes=2 --nproc_per_node=4 \
--rdzv_endpoint=192.168.1.100:29500 \
--rdzv_backend=c10d \
train.py
参数说明:
| 参数 | 含义 | 我的建议 |
|---|---|---|
--nnodes |
总机器数 | 写死,不要用变量 |
--nproc_per_node |
每台机器的 GPU 数 | 一般等于 GPU 数量 |
--rdzv_endpoint |
主节点的 IP:端口 | 用内网 IP,别用 127.0.0.1 |
--rdzv_backend |
发现后端 | c10d 就够了 |
注意:所有机器的启动命令必须完全一样!我曾经在机器 2 上把 --nnodes 写成了 1,结果等了半小时才发现进程根本没连上。嗯,这种低级错误犯一次就够了。
4.3 环境变量设置:MASTER_ADDR, MASTER_PORT, WORLD_SIZE, RANK
torchrun 会自动帮你设置这些环境变量,但理解它们很重要。我见过太多人因为不理解这些变量而出错。
| 环境变量 | 含义 | 示例值 |
|---|---|---|
MASTER_ADDR |
主节点 IP | 192.168.1.100 |
MASTER_PORT |
主节点端口 | 29500 |
WORLD_SIZE |
总进程数 = nnodes × nproc_per_node | 8 |
RANK |
当前进程的全局编号 | 0 ~ 7 |
LOCAL_RANK |
当前进程在本地机器上的编号 | 0 ~ 3 |
举个例子:2 台机器,每台 4 卡,那么:
- 机器 1 的进程 RANK 是 0,1,2,3,LOCAL_RANK 是 0,1,2,3
- 机器 2 的进程 RANK 是 4,5,6,7,LOCAL_RANK 是 0,1,2,3
核心逻辑:RANK 是全局唯一的,用于区分不同进程。LOCAL_RANK 是本地唯一的,用于绑定 GPU 设备。你想想看,如果不用 LOCAL_RANK,你怎么知道当前进程该用哪块 GPU?
4.4 多机训练代码实战
好,代码来了。这是一个完整的多机训练脚本,我加了详细的注释。
import torch
import torch.distributed as dist
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, Dataset
from torch.utils.data.distributed import DistributedSampler
# 1. 初始化进程组
def init_distributed():
dist.init_process_group(backend='nccl')
torch.cuda.set_device(dist.get_rank() % torch.cuda.device_count())
# 2. 模拟数据集
class FakeDataset(Dataset):
def __init__(self, size=1000):
self.data = torch.randn(size, 3, 224, 224)
self.labels = torch.randint(0, 10, (size,))
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
return self.data[idx], self.labels[idx]
# 3. 简单模型
class SimpleModel(nn.Module):
def __init__(self):
super().__init__()
self.fc = nn.Linear(3*224*224, 10)
def forward(self, x):
return self.fc(x.view(x.size(0), -1))
# 4. 训练函数
def train():
init_distributed()
rank = dist.get_rank()
local_rank = rank % torch.cuda.device_count()
# 每个进程绑定不同的 GPU
device = torch.device(f'cuda:{local_rank}')
# 模型放到 GPU 上
model = SimpleModel().to(device)
# 包装成分布式模型
model = nn.parallel.DistributedDataParallel(model, device_ids=[local_rank])
# 数据集:每个进程只看到一部分
dataset = FakeDataset()
sampler = DistributedSampler(dataset)
dataloader = DataLoader(dataset, batch_size=32, sampler=sampler)
optimizer = optim.SGD(model.parameters(), lr=0.01)
criterion = nn.CrossEntropyLoss()
for epoch in range(5):
sampler.set_epoch(epoch) # 每个 epoch 打乱数据
for data, target in dataloader:
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
# 只在主进程打印
if rank == 0:
print(f'Epoch {epoch}, Loss: {loss.item():.4f}')
dist.destroy_process_group()
if __name__ == '__main__':
train()
避坑指南:我曾经在代码里忘了调用 sampler.set_epoch(epoch),结果每个 epoch 的数据顺序都一样,模型过拟合得一塌糊涂。记住,分布式训练时,每个 epoch 都要重新打乱数据!
启动方式:
# 机器 1
torchrun --nnodes=2 --nproc_per_node=4 --rdzv_endpoint=192.168.1.100:29500 train.py
# 机器 2
torchrun --nnodes=2 --nproc_per_node=4 --rdzv_endpoint=192.168.1.100:29500 train.py
重要:所有机器的代码必须完全一致!不要试图在机器 1 上改模型结构,机器 2 上改学习率。分布式训练要求所有进程的模型结构、优化器配置完全相同。我曾经犯过这个错,结果梯度对不上,训练直接崩溃。
最后说一句,多机训练调试起来比单机麻烦得多。我的习惯是先在一台机器上用 --nnodes=1 跑通,确认代码没问题,再扩展到多机。这样能省下大量排查网络问题的时间。
好了,多机多卡同步训练的核心内容就这些。记住:通信是瓶颈,环境变量是基础,torchrun 是你的好帮手。动手试试吧!