深度学习训练慢这件事,几乎每个跑过CNN的人都有一肚子苦水。一个ResNet-50在单卡上跑ImageNet,动辄两三天才能收敛,中间调个参、改个网络结构,重新训练的时间成本直接让人崩溃。最近PAISoar这个方案在圈子里被反复提起,号称能把训练速度拉高两个数量级,也就是一百倍。这个数字听起来很夸张,但拆开看它的技术路径,其实是有据可循的。我花了些时间把它的核心思路、实现细节和落地要点梳理了一遍,结合自己在分布式训练上踩过的坑,写成这篇东西,给正在被训练速度折磨的朋友一个参考。不管你是刚配好PyTorch GPU环境的新手,还是已经在多机多卡上折腾过的老手,应该都能从中找到能直接用的东西。
1. 为什么深度学习训练需要一百倍加速
1.1 训练慢到底慢在哪里
先说清楚问题,才知道加速方案在解决什么。深度学习训练的时间开销,本质上由三块构成:前向传播、反向传播、参数更新。对于CNN这类模型,前向和反向的计算量都集中在卷积层上,一个典型的卷积层做一次前向,计算量是特征图尺寸乘以卷积核尺寸再乘以输入输出通道数。以VGG-16为例,单张224x224的图片过一次前向,浮点运算量大约在155亿次这个量级,反向传播差不多是前向的两倍。也就是说一张图一个完整训练步,接近500亿次浮点运算。
单张GPU的算力是有限的。一块主流训练卡,FP32算力大概在十几到几十TFLOPS之间,理论上跑VGG-16一个batch(假设batch size为64)需要的时间可以算出来,但实际跑起来远达不到理论峰值。原因在于GPU的计算单元经常在等数据,等的是从内存搬过来的权重和特征图。这就是所谓的访存瓶颈。算力再强,数据喂不上来,计算单元就空转。
除了单卡的计算和访存瓶颈,还有几个隐藏的时间黑洞。数据加载和预处理经常成为瓶颈,尤其是做数据增强的时候,CPU解码图片、做随机裁剪翻转的速度跟不上GPU消费的速度,GPU就在那里干等。梯度同步在多卡场景下也是大头,每张卡算完梯度要和其他卡做all-reduce,通信时间随卡数增加而增长。还有checkpoint保存、日志写入这些看似不起眼的操作,在大规模训练里累积起来也很可观。
1.2 一百倍这个数字是怎么算出来的
一百倍不是凭空喊的,它通常是多个维度优化效果的乘积。单卡层面,通过混合精度训练可以把FP32换成FP16,理论算力翻倍甚至更多,因为很多GPU的FP16吞吐是FP32的两倍。再加上算子融合、显存优化,单卡效率提升两三倍是常见的。
分布式层面,数据并行把batch分散到多张卡上,理想情况下N张卡带来接近N倍的吞吐。但通信开销会吃掉一部分,实际加速比通常在0.7到0.9之间。如果用上更高效的通信策略和梯度压缩,这个比例还能往上提。
再叠加流水线并行、张量并行这些模型并行技术,把大模型切开放到不同设备上,又能突破单卡显存限制,让更大的batch和更大的模型跑起来。把这些乘在一起,从单卡FP32的基线到一个高度优化的分布式FP16训练系统,两个数量级的提升是能摸到的。PAISoar的价值就在于它把这套复杂的优化组合封装成了相对易用的方案,而不是让每个团队从零去搭。
注意:一百倍是特定基线下的对比结果,通常是对比未优化的单卡FP32训练。如果你的基线已经是调优过的多卡混合精度训练,再想提升一百倍是不现实的,这时候能再提升两三倍就已经很可观了。
2. PAISoar 的核心设计思路拆解
2.1 它到底是个什么东西
PAISoar不是一个单一的算法,而是一套面向深度学习训练的加速系统。它的定位介于底层框架和上层应用之间,向下对接PyTorch、TensorFlow这类训练框架,向上提供一套配置化的加速能力。你可以把它理解成一个训练加速中间件,把混合精度、分布式通信、显存管理、数据流水线这些优化点打包在一起,通过配置文件或者少量代码改动就能启用。
这种设计的好处是侵入性低。你不需要重写模型定义,也不需要手动管理每张卡上的梯度同步,只需要在启动脚本里加上PAISoar的入口,或者在训练代码里引入它的封装层。对于已经有成熟训练代码的团队来说,迁移成本是可控的。这也是它比从零自研分布式训练系统更实际的地方。
从技术栈上看,PAISoar依赖几个关键组件:NCCL做GPU间通信,CUDA做底层算子,可能还会用到cuDNN做卷积加速。它做的事情是在这些底层能力之上做调度和编排,让它们协同工作而不是各自为战。
2.2 混合精度为什么是加速的第一杠杆
混合精度训练是PAISoar加速体系里最基础也最有效的一环。核心思想很简单:前向和反向计算用FP16,因为FP16的算力吞吐更高、显存占用更小;但权重更新和梯度累加用FP32,保证数值稳定性。
为什么FP16能快?两个原因。第一,GPU的FP16计算单元吞吐通常是FP32的两倍,同样的计算量用FP16跑理论上时间减半。第二,FP16的显存占用是FP32的一半,这意味着同样显存能放更大的batch,或者放更大的模型。batch变大之后,GPU的利用率会提升,因为计算单元更不容易空转。
但FP16有个致命问题:数值范围窄。FP16能表示的最小正数大约是6e-8,最大约65504。梯度在反向传播中可能变得非常小,小到FP16直接下溢成零,训练就崩了。PAISoar用的解决方案是损失缩放(loss scaling)。具体做法是在计算loss的时候乘上一个大的缩放因子,比如1024,这样梯度也被放大了同样倍数,不会下溢。更新权重之前再把梯度除回来。这个缩放因子可以是固定的,也可以是动态调整的,动态调整更稳,PAISoar默认走的是动态策略。
实际操作中,混合精度不是简单把模型转成half就完事。有些算子对精度敏感,比如softmax、layer norm、loss计算,这些必须保持FP32。PAISoar内部维护了一个算子精度白名单,自动决定哪些算子用FP16、哪些用FP32。你自己写自定义算子的时候要注意,如果发现训练不稳定,先检查是不是某个自定义算子被错误地用了FP16。
2.3 分布式策略的选择逻辑
PAISoar支持数据并行和模型并行两种模式,实际用哪种取决于你的模型大小和硬件条件。
数据并行是最常用的。每张卡上放一份完整的模型副本,把batch切成N份,每张卡算自己那份的梯度,然后所有卡做all-reduce把梯度平均。这种方式实现简单,适合模型能单卡放下的场景。它的瓶颈在通信,卡越多,all-reduce的数据量越大。PAISoar在数据并行上做了梯度分桶和通信重叠,就是把梯度分成若干桶,一个桶在通信的时候另一个桶在计算,把通信时间藏到计算时间后面。
模型并行适合单卡放不下的大模型。把模型的不同层放到不同卡上,数据在卡之间流水线式传递。这种方式通信量相对小,但实现复杂,而且容易出现负载不均衡,有的卡算得快有的卡算得慢,快的等慢的。PAISoar提供了自动切分策略,根据每层的计算量和参数量来决定怎么切,尽量减少流水线气泡。
混合并行是把两者结合,在节点内用张量并行(把单层切开),节点间用数据并行。这是目前训练超大模型的主流做法。PAISoar支持这种组合,但配置起来更复杂,需要对模型结构和集群拓扑都有清晰的认识。
| 并行方式 | 适用场景 | 通信开销 | 实现难度 |
|---|---|---|---|
| 数据并行 | 模型单卡可放下 | 中高,随卡数增长 | 低 |
| 模型并行 | 单卡放不下的大模型 | 中,流水线气泡 | 高 |
| 混合并行 | 超大模型+大规模集群 | 高,需精细调优 | 很高 |
2.4 数据流水线为什么不能忽视
很多人优化训练速度只盯着GPU,忽略了数据加载。实际上在图像训练里,数据预处理经常是隐藏的瓶颈。一张JPEG图片解码成RGB数组,再做随机裁剪、翻转、归一化,这一套下来CPU开销不小。如果DataLoader的worker数量不够,或者预处理逻辑写得太重,GPU就会周期性空转等数据。
PAISoar在数据流水线这块做了几件事。一是预取,在当前batch还在GPU上计算的时候,CPU已经在准备下一个batch的数据了。二是把部分预处理放到GPU上做,比如归一化和简单的几何变换,GPU做这些比CPU快得多。三是支持数据格式优化,比如把图片预先转成更高效的二进制格式,避免每次训练都重新解码JPEG。
这里有个经验:如果你的GPU利用率曲线是锯齿状的,忽高忽低,大概率是数据加载的问题。用nvidia-smi看GPU利用率,如果经常掉到50%以下再冲上去,就该检查DataLoader了。把num_workers调大,开pin_memory,往往能立刻看到改善。
3. 核心细节解析与实操要点
3.1 环境准备与依赖安装
PAISoar的运行依赖比较明确。基础环境需要CUDA和cuDNN,版本要和你的GPU驱动匹配。NCCL是必须的,做多卡通信。Python侧需要PyTorch,版本建议不要太老,新版本对混合精度和分布式支持更好。
安装流程大致是这样。先确认驱动和CUDA版本,用nvidia-smi看驱动支持的CUDA最高版本,然后装对应版本的CUDA Toolkit。cuDNN要注册下载,解压后把库文件放到CUDA目录下。NCCL如果是多卡训练必须装,单卡可以跳过。然后装PyTorch,去官网选对应CUDA版本的安装命令。最后装PAISoar本身,通常是通过包管理器或者从源码编译。
# 检查GPU和驱动 nvidia-smi # 确认CUDA版本 nvcc --version # 安装PyTorch(以CUDA 11.8为例) pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装PAISoar pip install paisoar注意:CUDA版本、PyTorch版本、PAISoar版本三者必须匹配。我见过太多因为版本不匹配导致的诡异报错,比如能跑起来但速度没提升,或者多卡通信直接hang住。装之前先查兼容性矩阵,别凭感觉装。
环境变量也要配好。CUDA_VISIBLE_DEVICES控制用哪些卡,NCCL相关的环境变量影响通信效率。比如NCCL_IB_DISABLE控制是否禁用InfiniBand,如果你的集群没有IB网络,要设成1,否则NCCL会尝试用IB然后超时。
3.2 混合精度训练的关键参数
启用混合精度训练,PAISoar提供了封装好的接口。核心是amp(自动混合精度)模块,autocast上下文管理器负责在前向传播时自动把算子转成FP16,GradScaler负责损失缩放和梯度更新。
import torch from paisoar import amp model = MyCNN().cuda() optimizer = torch.optim.SGD(model.parameters(), lr=0.01) scaler = amp.GradScaler() for data, target in dataloader: optimizer.zero_grad() with amp.autocast(): output = model(data) loss = criterion(output, target) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()这段代码里几个点值得说。autocast的上下文范围要包住前向传播和loss计算,但不要包住backward,backward由scaler处理。scaler.scale把loss放大,backward的时候梯度也被放大。scaler.step在更新前检查梯度有没有出现inf或nan,如果有就跳过这一步并降低缩放因子。scaler.update调整缩放因子,如果连续多步没有溢出就增大,有溢出就减小。
初始缩放因子默认是65536,这个值对大多数模型够用。如果你的模型特别容易溢出,可以调小到4096或者1024。反过来如果训练很稳定但速度没提升,可能是缩放因子太小导致频繁触发梯度检查,可以适当调大。
还有一个容易忽略的点:有些操作在autocast下会自动转FP16,但你可能希望它保持FP32。比如计算loss的时候,如果loss本身很小,FP16下可能精度不够。这时候可以用amp.autocast(enabled=False)局部禁用,或者用torch.cuda.amp.custom_fwd/custom_bwd装饰器指定某个函数的精度。
3.3 分布式训练的启动与配置
PAISoar的分布式训练启动方式有两种:单机多卡和多机多卡。单机多卡用torchrun或者PAISoar自己的launcher,多机多卡需要配置节点间的通信。
单机多卡的启动命令大概是这样:
# 单机8卡训练 paisoar launch --nproc_per_node=8 train.py \ --batch_size=256 \ --use_amp \ --dist_backend=nccl多机多卡需要指定主节点地址和端口,以及每个节点的rank:
# 节点0(主节点) paisoar launch --nproc_per_node=8 --nnodes=2 --node_rank=0 \ --master_addr=192.168.1.100 --master_port=29500 train.py # 节点1 paisoar launch --nproc_per_node=8 --nnodes=2 --node_rank=1 \ --master_addr=192.168.1.100 --master_port=29500 train.py配置里有几个参数直接影响性能。batch_size是全局batch,会被平均分到每张卡上。如果全局batch是256、8张卡,每张卡实际处理32。这里要注意学习率要跟着batch调整,线性缩放规则是batch翻倍学习率也翻倍,但这不是绝对的,大batch有时候需要warmup。
dist_backend选nccl,这是GPU间通信最快的后端。gloo是CPU用的,别选错。如果集群有InfiniBand网络,NCCL会自动用IB,速度比以太网快一个数量级。没有IB的话,至少保证节点间是万兆网,千兆网做分布式训练通信会成为严重瓶颈。
3.4 梯度累积与batch size的权衡
显存不够放不下大batch的时候,梯度累积是个实用的技巧。原理是不每个batch都更新权重,而是累积几个batch的梯度再更新一次。这样等效于放大了batch size,但显存占用不变。
accumulation_steps = 4 for i, (data, target) in enumerate(dataloader): with amp.autocast(): output = model(data) loss = criterion(output, target) / accumulation_steps scaler.scale(loss).backward() if (i + 1) % accumulation_steps == 0: scaler.step(optimizer) scaler.update() optimizer.zero_grad()注意loss要除以累积步数,这样梯度的量级才和真实大batch一致。还有optimizer.zero_grad()的位置,要在更新之后清,不能在每个batch都清。
梯度累积和混合精度配合的时候有个细节:scaler.step和scaler.update只在真正更新权重的那一步调用,中间累积的步骤只做backward。如果搞错了,缩放因子会被频繁调整,训练可能不稳定。
提示:梯度累积虽然省显存,但会增加训练步数,总时间不一定减少。它的价值在于让你能用更大的等效batch,从而可能用更高的学习率、获得更稳的收敛。如果单纯为了省显存,不如考虑梯度检查点(gradient checkpointing),用计算换显存。
4. 实操过程与核心环节实现
4.1 从单卡到多卡的完整迁移流程
假设你有一个单卡训练脚本,想迁移到PAISoar的多卡加速。步骤是这样的。
第一步,把模型包装成分布式版本。PAISoar提供了DistributedDataParallel的封装,比PyTorch原生的DDP多了些优化,比如梯度分桶和通信重叠。
import torch.distributed as dist from paisoar import DistributedDataParallel as DDP dist.init_process_group(backend='nccl') local_rank = int(os.environ['LOCAL_RANK']) torch.cuda.set_device(local_rank) model = MyCNN().cuda() model = DDP(model, device_ids=[local_rank])第二步,DataLoader要加DistributedSampler,保证每张卡拿到不同的数据子集,不会重复训练。
from torch.utils.data.distributed import DistributedSampler sampler = DistributedSampler(dataset, num_replicas=world_size, rank=rank) dataloader = DataLoader(dataset, batch_size=per_gpu_batch, sampler=sampler)第三步,只在主进程做日志和checkpoint保存,避免多张卡同时写文件冲突。
if rank == 0: torch.save(model.state_dict(), 'checkpoint.pth') print(f'Epoch {epoch}, loss {loss.item()}')第四步,调整学习率和batch size。全局batch变成per_gpu_batch乘以卡数,学习率按线性规则调整,但通常要加warmup,前几个epoch让学习率从很小的值慢慢升上去,避免一开始梯度太大导致发散。
这套流程走下来,8卡训练相比单卡,理想情况吞吐提升接近8倍。实际因为通信开销,可能在6到7倍之间。如果提升不到5倍,就要检查通信是不是瓶颈了。
4.2 通信优化的几个实操手段
通信是分布式训练的主要开销来源。PAISoar在通信上做了几层优化,理解这些优化有助于你调参。
梯度分桶是最基础的。默认情况下,每个参数的梯度单独做all-reduce,小梯度的通信延迟占比高。分桶是把多个小梯度拼成一个大buffer再通信,减少通信次数。桶的大小可以配,太小了通信次数多,太大了内存占用高。经验值是25MB左右一个桶,PAISoar默认会动态调整。
通信重叠是更高级的优化。反向传播是从后往前算梯度的,后面的层先算完梯度,可以先开始通信,同时前面的层还在计算。这样通信和计算并行,总时间缩短。PAISoar通过把梯度分桶并按反向传播顺序排列,实现了这种重叠。
梯度压缩是另一个方向。把FP32梯度压缩成FP16甚至INT8再通信,通信量减少,但会引入精度损失。PAISoar支持梯度压缩,但默认不开,因为对某些模型可能影响收敛。如果你的模型对精度不敏感,可以试试,通信量减半,速度提升明显。
| 优化手段 | 通信量变化 | 精度影响 | 适用场景 |
|---|---|---|---|
| 梯度分桶 | 不变 | 无 | 所有分布式训练 |
| 通信重叠 | 不变 | 无 | 所有分布式训练 |
| FP16梯度压缩 | 减半 | 轻微 | 大模型、通信瓶颈明显 |
| INT8梯度压缩 | 减至1/4 | 中等 | 对精度不敏感的场景 |
4.3 显存优化的实战技巧
显存不够是训练大模型时的常见问题。除了混合精度能省一半显存,还有几个手段。
激活检查点(activation checkpointing)是拿计算换显存。正常前向传播会把每层的激活值存下来供反向传播用,这很占显存。激活检查点只存部分层的激活,反向传播时重新计算没存的那些。显存能省很多,但计算量增加约30%。PAISoar支持按层配置,对显存占用大的层开启检查点,小的层不开。
梯度累积前面说过了,省显存但增加步数。还有ZeRO优化,把优化器状态、梯度、参数分片到不同卡上,每张卡只存一部分。ZeRO-1只分片优化器状态,ZeRO-2再分片梯度,ZeRO-3连参数也分片。分片越彻底显存越省,但通信量越大。PAISoar集成了ZeRO,配置里选stage就行。
实际调的时候,先用混合精度,不够再开激活检查点,还不够上ZeRO。ZeRO-3最省显存但通信开销大,如果集群网络好可以用,网络差的话可能得不偿失。
4.4 性能监控与瓶颈定位
优化不能瞎调,得先知道瓶颈在哪。PAISoar提供了性能分析工具,能输出每个算子的耗时、通信占比、GPU利用率。
最基础的监控是nvidia-smi,看GPU利用率和显存占用。利用率长期低于80%说明有瓶颈,要么是数据加载慢,要么是通信等待。显存占用接近上限说明batch不能再大了。
更细的分析用PyTorch Profiler或者PAISoar自带的profiler。它能告诉你时间花在哪些算子上,是卷积慢还是全连接慢,是前向慢还是反向慢。如果发现某个算子异常慢,可能是实现有问题,或者精度设置不对。
通信分析看NCCL的日志。设置NCCL_DEBUG=INFO,能看到每次all-reduce的耗时和数据量。如果通信时间占比超过30%,说明通信是瓶颈,要考虑梯度压缩或者换更快的网络。
# 开启NCCL调试日志 export NCCL_DEBUG=INFO export NCCL_DEBUG_SUBSYS=ALL # 运行训练,观察通信耗时 paisoar launch --nproc_per_node=8 train.py提示:性能分析本身有开销,profiler开启后训练会变慢。建议只在排查问题时开,定位到瓶颈后就关掉。另外profiler的输出文件可能很大,注意磁盘空间。
5. 常见问题与排查技巧实录
5.1 训练不收敛或loss变nan
混合精度训练最常见的问题就是loss变nan。原因通常是梯度溢出,FP16放不下。排查步骤是这样。
先看scaler的缩放因子。如果缩放因子一直在下降,说明频繁溢出。可以手动把初始缩放因子调小,比如从65536降到4096。如果调小后还是溢出,说明模型本身有问题,不是精度的事。
然后检查哪些算子用了FP16。有些算子对精度敏感,比如计算方差、做归一化,这些必须FP32。PAISoar的autocast有默认的白名单,但自定义算子可能没被正确识别。用amp.autocast(enabled=False)把可疑的部分包起来,看是否解决。
还有可能是学习率太大。混合精度下梯度的量级和FP32不完全一样,原来的学习率可能偏大。试试把学习率减半,或者加warmup。
如果以上都试了还不行,退回FP32训练,确认模型本身能收敛。如果FP32也nan,那就是模型或数据的问题,和混合精度无关。
5.2 多卡训练速度不升反降
多卡比单卡还慢,这听起来离谱但确实会发生。原因通常是通信开销超过了并行收益。
先检查网络。节点间是千兆网的话,通信会成为严重瓶颈。8卡跨节点训练,梯度同步的数据量可能达到几百MB,千兆网传一次要好几秒,计算才几百毫秒,通信占了大头。这种情况要么换万兆网或InfiniBand,要么减少跨节点通信,比如节点内用张量并行、节点间用数据并行。
再检查batch size。如果每张卡的batch太小,比如只有8,计算量不足以掩盖通信延迟。增大per-gpu batch,或者用梯度累积增加等效batch。
还要检查是不是所有卡都在工作。有时候某张卡因为显存不足或者数据问题卡住了,其他卡在等它。用nvidia-smi看每张卡的利用率,如果有的卡利用率明显低,就是负载不均衡。
5.3 显存溢出(OOM)的应急处理
OOM是训练大模型的家常便饭。应急处理有几个方向。
立刻能做的:减小batch size,这是最直接的。开混合精度,显存直接减半。开激活检查点,显存再降一截。
稍微麻烦点的:用ZeRO分片,把优化器状态和梯度分散到多卡。清理不必要的缓存,torch.cuda.empty_cache()能释放一些碎片显存。检查有没有张量没释放,比如在循环里累积了loss但没有detach。
长期的:优化模型结构,减少参数量。用更高效的算子,比如深度可分离卷积替代普通卷积。或者上模型并行,把模型切开放到不同卡上。
| 问题现象 | 可能原因 | 排查手段 | 解决方案 |
|---|---|---|---|
| loss变nan | 梯度溢出 | 看scaler缩放因子 | 调小初始缩放因子 |
| 多卡变慢 | 通信瓶颈 | NCCL日志看通信耗时 | 换网络或梯度压缩 |
| OOM | 显存不足 | nvidia-smi看占用 | 减batch或开ZeRO |
| GPU利用率低 | 数据加载慢 | 看DataLoader耗时 | 加worker或预取 |
| 训练卡住 | 某卡异常 | 逐卡看利用率 | 检查数据和显存 |
5.4 数据加载成为瓶颈的识别与解决
数据加载瓶颈的典型表现是GPU利用率周期性下降。用nvidia-smi -l 1每秒刷新,如果利用率在30%到90%之间反复横跳,基本可以确定是数据问题。
解决思路是让CPU准备数据的速度跟上GPU消费的速度。增加DataLoader的num_workers,一般设成CPU核心数。开pin_memory,把数据放到锁页内存,传到GPU更快。用prefetch_factor控制预取多少个batch。
如果预处理逻辑太重,考虑把部分操作移到GPU上。比如归一化,CPU做要遍历所有像素,GPU一个kernel就搞定。还有图片解码,如果数据集是JPEG,每次训练都解码很浪费,可以预先转成numpy数组或者LMDB格式,训练时直接读。
注意:num_workers不是越大越好。设太大反而会因为进程切换开销导致变慢。一般从4开始试,逐步加到8、16,看哪个值GPU利用率最高。另外Windows上num_workers大于0可能有问题,Linux上没这个限制。
6. 不同规模场景下的配置建议
6.1 单机单卡的个人开发者
个人开发者通常只有一张卡,显存有限,预算也有限。这种场景下,混合精度是性价比最高的优化,几乎无成本就能提速接近一倍。开AMP,把batch size调到显存允许的最大值,GPU利用率能上去。
数据加载用num_workers=4到8,pin_memory=True。如果数据集不大,可以整个加载到内存里,避免每次读磁盘。模型方面,用预训练权重做迁移学习,比从头训练快得多,收敛也快。
这种场景不需要分布式,PAISoar的分布式功能用不上,但它的混合精度和显存优化模块是独立的,可以单独用。
6.2 单机多卡的小团队
小团队一般有一台8卡服务器。这种场景下,数据并行是首选。用PAISoar的DDP封装,8卡跑起来,吞吐提升6到7倍。注意学习率要按全局batch调整,加warmup。
通信在单机内走NVLink或者PCIe,速度比跨节点快得多,通信开销占比小。这时候瓶颈往往在数据加载,8张卡同时消费数据,CPU压力大。num_workers要相应增加,或者用更高效的数据格式。
显存方面,8卡加起来显存总量大了,可以跑更大的模型。如果单卡放不下,可以试试ZeRO-2,把优化器状态和梯度分片,每张卡显存占用降低。
6.3 多机多卡的大规模训练
多机多卡是真正考验工程能力的地方。网络是核心,InfiniBand比以太网快一个数量级,有条件一定要上。没有IB的话,至少万兆网,千兆网做大规模训练基本不可行。
并行策略要仔细设计。节点内用张量并行或流水线并行,节点间用数据并行。通信要分层,节点内走NVLink,节点间走IB。PAISoar支持这种分层通信配置,但需要根据集群拓扑调。
容错也很重要。大规模训练跑几天,难免有节点出问题。PAISoar支持checkpoint自动保存和恢复,出问题后从最近的checkpoint继续,不用从头来。checkpoint的频率要权衡,太频繁影响训练速度,太稀疏出问题后损失大。一般每几个epoch存一次。
学习率方面,超大batch需要更精细的调参。线性缩放规则在batch特别大的时候会失效,需要用平方根缩放或者LARS、LAMB这类自适应优化器。PAISoar集成了这些优化器,配置里选就行。
7. 我踩过的坑和几条实在建议
混合精度不是万能的。我遇到过模型开了AMP之后收敛变慢的情况,排查发现是某个自定义的归一化层在FP16下数值不稳定。解决办法是把这个层强制用FP32,其他层保持FP16。所以开AMP之后一定要对比收敛曲线,不能只看速度。
分布式训练的调试成本很高。单卡能跑通的代码,多卡不一定能跑通。建议先在单卡上把逻辑调对,再迁移到多卡。迁移的时候先跑通2卡,再上8卡,逐步增加,出问题容易定位。
通信优化要基于实际测量。不要凭感觉觉得通信是瓶颈就去加梯度压缩,先用profiler测一下通信占比。如果通信只占10%,压缩到5%也就提升5%,但精度可能受损,不划算。
显存优化是有代价的。激活检查点省显存但增加计算,ZeRO-3省显存但增加通信。用之前想清楚瓶颈到底是显存还是速度,别为了省显存把速度搭进去。
最后,加速的收益要放在整个训练周期里看。如果数据加载和预处理占了总时间的一半,光优化GPU计算只能提升剩下的那一半。先找到真正的瓶颈,再针对性优化,比盲目堆技术有效得多。PAISoar这类工具的价值在于它把常用的优化都准备好了,你只需要按需开启,但前提是你知道自己需要什么。