DistributedSampler 必须配合 DDP 使用,因其仅依赖 rank 和 world_size 切片数据,不自动通信;若未初始化分布式环境,所有进程默认 rank=0、world_size=1,导致重复采样。

为什么 DistributedSampler 必须配合 torch.nn.parallel.DistributedDataParallel(DDP)使用?
单独用 DistributedSampler 而不启动 DDP,会导致每个进程看到**完全相同的子数据集**——它本身不通信、不感知 rank,只靠 rank 和 world_size 参数做切片。如果你没调用 torch.distributed.init_process_group,rank 默认是 0,world_size 默认是 1,所有进程都当自己是唯一进程,自然全量采样。
- 必须在构造
DistributedSampler前完成分布式初始化,且传入的rank和world_size要与实际一致(通常从torch.distributed.get_rank()和torch.distributed.get_world_size()获取) -
shuffle=True时,各进程内部会独立打乱,但不同进程间不会重复——前提是seed相同且epoch被正确传递(见下一点) - 若训练中 epoch 变化但没重置 sampler,
set_epoch(epoch)不被调用,会导致各进程 shuffle 种子相同、打乱顺序也相同,等效于没 shuffle
训练循环里漏掉 sampler.set_epoch(epoch) 会怎样?
现象:多个 epoch 下,每个 GPU 加载的数据顺序一模一样,甚至 batch 内容完全重复。这是因为 DistributedSampler 的 shuffle 依赖 Python random 模块,而默认 seed 是固定的;它只在 __iter__ 初始化时生成一次随机索引序列,之后复用。
- 必须在每个 epoch 开始前显式调用
sampler.set_epoch(epoch),其中epoch是整数(建议从 0 或 1 开始,保持连续) - 这个调用会重新设置
random.seed(seed + epoch),确保每轮 shuffle 结果不同且各进程间不重叠 - 常见错误写法:
for epoch in range(...): dataloader = DataLoader(dataset, sampler=sampler)—— 这样每次重建 dataloader,但没调set_epoch,sampler 内部 epoch 计数器仍为 0
DDP 下模型输出和梯度同步是否自动完成?
DDP 的核心作用不是“让模型跑起来”,而是**在 backward 后自动触发 all-reduce 梯度同步**。但有几个关键边界条件容易被忽略:
- forward 输出本身**不自动同步**:每个进程只拿到自己那块数据的 loss,
loss.backward()也只计算本地梯度 - 梯度同步发生在
loss.backward()之后、optimizer.step()之前,由 DDP hook 自动触发;但前提是模型参数确实被 DDP 包装(即model = DDP(model)),且未禁用find_unused_parameters导致部分梯度未注册 - 如果 forward 中用了 condition 分支导致某些参数没参与计算(例如部分 batch 走了不同子网络),需设
find_unused_parameters=True,否则报错Expected to have finished reduction in the prior iteration - 验证/测试阶段若仍用 DDP 模型,记得用
torch.no_grad()+model.module.eval(),避免 DDP 的 training flag 干扰 BN 统计
如何验证 DistributedSampler 真正按 rank 切分了数据?
最直接的办法是在每个进程里打印第一个 batch 的样本 index(不是 data,是原始 dataset 的索引):
from torch.utils.data import DistributedSampler
sampler = DistributedSampler(dataset, num_replicas=world_size, rank=rank)
dataloader = DataLoader(dataset, sampler=sampler, batch_size=4)
for i, (data, target) in enumerate(dataloader):
if i == 0:
print(f"[Rank {rank}] First batch indices: {sampler.indices[:4]}")
break
你应该看到:rank 0 打印 [0, 1, 2, 3],rank 1 打印 [4, 5, 6, 7](假设 world_size=2,dataset size=8)。若所有 rank 都打印一样的索引,说明 rank/world_size 传错了,或根本没初始化分布式环境。
注意:sampler.indices 是完整切片后的索引列表,长度 ≈ len(dataset) // world_size(向上取整),它在 set_epoch 后会被重生成,所以验证务必在 set_epoch 之后做。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











