并行计算模型:数据并行、模型并行、流水线并行、混合并行策略

聊到多GPU协同,绕不开的就是并行计算模型。说白了,就是怎么把一个大模型拆开,让多个GPU一起干活。

我刚开始接触分布式训练时,觉得这事儿挺简单——多卡嘛,一人分一块数据不就完了?后来踩了不少坑才明白,选错并行策略,可能比单卡还慢。嗯,这里面的门道,咱们一个一个说。

数据并行:最直观的思路

数据并行,我个人习惯叫它「数据分片」。每个GPU上都放一份完整的模型副本,然后把训练数据切成小块,分给各个GPU。

举个例子:假设你有4张GPU,一个batch是64条数据。数据并行就是每张卡分16条,各自算梯度,最后把梯度汇总更新模型。

核心流程:

  1. 每张GPU持有完整模型参数
  2. 数据被切分成多个mini-batch,分发给各GPU
  3. 各GPU独立前向传播、反向传播
  4. 通过AllReduce操作同步梯度
  5. 更新模型参数

我在项目中遇到过一个问题:模型太大,单卡放不下。这时候数据并行就不好使了。你想想看,每张卡都要存一份完整的模型,显存开销是线性增长的。

避坑指南:我曾经在训练BERT-large时用了数据并行,结果发现batch size设太大,模型收敛反而变差了。后来才意识到,数据并行虽然能加速,但batch size不是越大越好。建议用梯度累积来模拟大batch,而不是真的把batch size撑到极限。

模型并行:解决显存瓶颈

模型并行,说白了就是把模型切开。每张GPU只负责模型的一部分。

为什么会这样?因为有些模型实在太大了。比如GPT-3,1750亿参数,单卡根本装不下。这时候就得把模型切成几块,分到不同的GPU上。

模型并行有两种常见方式:

类型 做法 适用场景
层间并行 按层切分,GPU1负责前几层,GPU2负责中间层,GPU3负责后几层 层数很深的模型
层内并行 把某一层的参数矩阵切块,分到不同GPU上计算 单层参数量特别大的模型

我建议新手先从层间并行入手。原因很简单:实现起来相对容易,通信开销也小。层内并行虽然更灵活,但通信模式复杂,调试起来很头疼。

注意:模型并行有个天然缺陷——计算资源利用率不高。因为GPU之间是串行依赖的,GPU2必须等GPU1算完才能开始。这会导致大量GPU处于空闲等待状态。我曾经在一个8卡集群上测试,模型并行的GPU利用率只有40%左右,太浪费了。

流水线并行:让GPU忙起来

流水线并行,你可以把它理解成「工厂流水线」。模型还是按层切分,但引入了微批次(micro-batch)的概念。

具体怎么做?把一个大batch再切成更小的微批次。GPU1处理完第一个微批次后,把结果传给GPU2,同时GPU1开始处理第二个微批次。这样所有GPU都能持续工作,不会闲着。

# 流水线并行的伪代码示例
def pipeline_parallel(model_chunks, micro_batches):
    # model_chunks: 切分后的模型片段列表
    # micro_batches: 微批次数据列表
    
    for i, micro_batch in enumerate(micro_batches):
        # 前向传播
        for stage_id, chunk in enumerate(model_chunks):
            if i - stage_id >= 0:  # 流水线填充阶段
                output = chunk(micro_batches[i - stage_id])
        
        # 反向传播(略)
    
    # 梯度更新
    optimizer.step()

我做过一个实验:用4张GPU跑一个12层的Transformer。纯模型并行,吞吐量只有200 samples/s。改成流水线并行后,直接飙到650 samples/s。差距就是这么明显。

经验之谈:流水线并行有个关键参数——微批次数量。我建议设成GPU数量的2-4倍。太少的话,流水线填充和排空阶段的浪费会很明显。太多的话,显存开销会增大。我一般设成GPU数的3倍,效果比较均衡。

混合并行策略:组合拳才是王道

实际工程中,很少只用一种并行策略。说白了,每种策略都有短板,组合起来才能取长补短。

我参与过一个千亿参数模型的训练项目,最终方案是这样的:

  • 数据并行:用于跨节点扩展,每个节点内用数据并行
  • 模型并行:用于单节点内,把模型切分到节点内的多张GPU上
  • 流水线并行:在模型并行基础上,引入微批次提高利用率

这种混合策略,业内叫「3D并行」。听起来高大上,其实核心思想就一句话:在合适的维度上做合适的切分

混合并行的设计原则:

  • 通信量大的维度,尽量在节点内部做(NVLink带宽高)
  • 通信量小的维度,可以跨节点做(以太网带宽有限)
  • 显存瓶颈优先用模型并行解决
  • 计算瓶颈优先用数据并行解决

我曾经踩过一个坑:在混合并行中,把模型并行和数据并行混在一起,结果通信拓扑太复杂,性能反而下降了。后来我学乖了,先画通信拓扑图,算清楚每步通信的数据量和带宽,再决定并行策略。

嗯,这里要特别提醒一下:混合并行的调试难度是指数级上升的。我建议先在小规模集群上验证,确认没问题了再上大规模。别问我怎么知道的——说多了都是泪。

最后说一句:没有银弹。数据并行、模型并行、流水线并行、混合并行,没有哪个是万能的。选型时要考虑模型大小、集群拓扑、带宽、显存等多个因素。我个人的习惯是:先跑个profiling,看看瓶颈在哪,再对症下药。

好了,并行计算模型这块就聊到这儿。下一章咱们聊聊实际部署中的那些坑,我保证都是真金白银换来的经验。