4、数据并行训练:数据并行原理、梯度同步机制、AllReduce详解、数据加载瓶颈
数据并行,说白了就是「人多力量大」在分布式训练里的体现。你想想看,一张卡显存有限,装不下大模型,那就把数据切成几份,每张卡拿一份去算。我刚开始接触分布式训练时,觉得这玩意儿不就是多卡跑同一个模型嘛,有啥难的?结果一上手,梯度同步、通信开销、数据加载,处处是坑。
4.1 数据并行原理
数据并行的核心思想很简单:每个GPU都保存一份完整的模型副本,但只处理一部分数据。训练时,每张卡拿到不同的mini-batch,各自做前向和反向传播,算出梯度。然后大家把梯度汇总一下,平均之后更新各自的模型参数。
嗯,这里要注意:模型参数是同步更新的。也就是说,每张卡上的模型参数在任何时刻都是一样的。我见过有人误以为数据并行是每张卡学不同的特征,最后再合并——不是这样的,那是模型并行干的事。
举个例子,假设你有4张卡,batch size是1024。每张卡分到256个样本。每张卡独立计算梯度,然后通过AllReduce把4份梯度加起来求平均,最后用这个平均梯度更新模型。这样,4张卡合起来的效果,就相当于用1024的batch size在单卡上训练。
4.2 梯度同步机制
梯度同步是数据并行的核心,也是最容易出问题的地方。我遇到过最头疼的情况:8卡训练,loss死活不降,查了半天发现有一张卡的梯度没同步过来。
目前主流的梯度同步机制有两种:
- 同步式(Sync SGD):所有卡算完梯度后,统一做一次AllReduce,再一起更新参数。优点是梯度一致性好,收敛稳定。缺点是慢卡拖累整体,也就是「木桶效应」。
- 异步式(Async SGD):每张卡算完梯度就更新参数,不用等别人。优点是快,没有等待开销。缺点是梯度可能过时,收敛不稳定,甚至不收敛。
我个人习惯用同步式。虽然慢一点,但心里踏实。异步式我踩过坑——有一次训练一个BERT模型,loss曲线像心电图一样上下乱跳,最后发现是异步更新导致的梯度冲突。
4.3 AllReduce详解
AllReduce是梯度同步的核心算法。它的目标很简单:把N张卡上的梯度向量加起来,然后广播给所有人。但实现方式不同,效率天差地别。
常见的AllReduce算法有:
| 算法 | 通信量 | 特点 |
|---|---|---|
| Ring AllReduce | 2(N-1) * 数据量 / N | 带宽利用率高,适合大模型 |
| Tree AllReduce | 2 * log(N) * 数据量 | 延迟低,适合小数据量 |
| NCCL AllReduce | 优化实现 | NVIDIA官方库,GPU直连 |
Ring AllReduce是目前最常用的。它的思路是:把N张卡连成一个环,每张卡只和相邻的卡通信。先做scatter-reduce,把数据分成N份,每张卡负责累加其中一份。然后再做allgather,把累加结果广播给所有人。
我画了一张图,帮你理解Ring AllReduce的过程:
你看,Ring AllReduce的通信量是2(N-1)/N * 数据量。当N很大时,接近2倍数据量。而Tree AllReduce是2*log(N)倍。所以卡数多时,Ring更优;卡数少时,Tree更优。
torch.distributed.init_process_group(backend='nccl') 就行。
4.4 数据加载瓶颈
数据加载是分布式训练中最容易被忽视的瓶颈。你想想看,GPU算得飞快,但数据从硬盘读到内存,再从内存传到GPU,这个过程如果慢了,GPU就得干等着。
我遇到过最夸张的一次:8卡V100训练ResNet-50,GPU利用率只有30%。查了半天,发现数据加载线程只有1个,CPU根本来不及喂数据。后来改成4个worker,利用率直接飙到90%以上。
常见的数据加载瓶颈及解决方案:
- 磁盘IO慢:用SSD代替HDD,或者把数据放到内存里(tmpfs)。
- 数据预处理太慢:用多进程预处理,或者提前做好数据增强,存成预处理后的格式。
- 数据传输开销:用NVIDIA DALI库,直接在GPU上做数据预处理,省去CPU到GPU的拷贝。
- 数据不均匀:用分布式采样器(DistributedSampler),确保每张卡拿到的数据不重复。
我建议你在训练脚本里加上数据加载时间的监控。如果发现GPU空闲时间占比超过10%,就该优化数据加载了。一个简单的办法:用 torch.utils.data.DataLoader 的 num_workers 参数,一般设为CPU核心数的2-4倍。
嗯,最后说一句:数据并行不是万能的。当模型大到一张卡都放不下时,就得考虑模型并行或流水线并行了。但那是后面章节的内容,咱们先把数据并行吃透再说。
pin_memory=True,以为能加速。结果发现内存占用暴涨,导致OOM。后来才知道,pin_memory会锁定内存页,用太多会影响系统性能。建议只在数据量不大时使用。
想要更多资料,联系可可