训练一个千亿参数的大模型,需要数千GPU协同工作数周。如何高效利用这些GPU,是决定训练成本和速度的关键。分布式训练策略就是这个问题的核心答案。本文将系统解析各种并行策略的原理与工程实践。

一、为什么单GPU不够

1.1 显存瓶颈

一个70B参数的模型:

  • 模型参数(FP16):140GB
  • 梯度(FP16):140GB
  • 优化器状态(Adam, FP32):560GB
  • 激活值:取决于batch和序列长度

总计约840GB+ ——远超单张A100的80GB显存。

1.2 计算瓶颈

训练70B模型一个epoch(1万亿token):

  • 单A100算力:312 TFLOPS (FP16)
  • 训练所需FLOPS:~6×10²³
  • 单卡理论时间:~22000天

需要2048张A100并行才能在11天内完成。

二、数据并行(DP)

2.1 基本原理

最直观的并行——每张GPU持有完整模型副本,处理不同数据:

GPU 0: 完整模型 + Batch[0:32]
GPU 1: 完整模型 + Batch[32:64]
GPU 2: 完整模型 + Batch[64:96]
GPU 3: 完整模型 + Batch[96:128]

前向传播 → 各GPU独立计算loss
反向传播 → AllReduce同步梯度
更新 → 各GPU同步更新参数

2.2 问题:大模型装不下

数据并行要求每张GPU能装下完整模型+优化器状态+梯度。对于70B模型,需要840GB——单卡80GB根本装不下。

2.3 ZeRO优化:显存突破

DeepSpeed的ZeRO(Zero Redundancy Optimizer)通过切分训练状态来突破显存限制:

ZeRO-1: 切分优化器状态
  每GPU只存 1/N 的优化器状态
  70B模型: 560GB/N → N=8时70GB/卡

ZeRO-2: 切分优化器状态 + 梯度
  每GPU只存 1/N 的优化器状态和梯度
  70B模型: (560+140)GB/N → N=8时87.5GB/卡

ZeRO-3: 切分优化器状态 + 梯度 + 参数
  每GPU只存 1/N 的所有状态
  70B模型: (560+140+140)GB/N → N=8时105GB/卡
  参数在前向/反向时按需All-Gather
# ZeRO-3的参数按需获取
class ZeRO3Layer:
    def forward(self, x):
        # 按需All-Gather本层参数
        full_weight = self.all_gather(self.shard_weight)
        output = F.linear(x, full_weight)
        # 立即释放完整参数
        del full_weight
        return output
    
    def backward(self, grad_output):
        # 按需All-Gather本层参数
        full_weight = self.all_gather(self.shard_weight)
        grad_input = grad_output @ full_weight.T
        # Reduce-Scatter梯度
        self.shard_grad = self.reduce_scatter(grad_output.T @ self.input)
        return grad_input

三、张量并行(TP)

3.1 原理

将单个层的参数矩阵切分到多张GPU上:

class TensorParallelLinear:
    """张量并行的线性层"""
    def __init__(self, in_features, out_features, n_gpus):
        # 按列切分权重
        self.shard_size = out_features // n_gpus
        self.weight_shard = nn.Parameter(
            torch.randn(self.shard_size, in_features) / math.sqrt(in_features)
        )
    
    def forward(self, x):
        # 各GPU独立计算部分输出
        local_output = F.linear(x, self.weight_shard)  # (batch, shard_size)
        
        # All-Gather合并完整输出
        full_output = self.all_gather(local_output, dim=-1)  # (batch, out_features)
        return full_output
    
    def backward(self, grad_output):
        # 按列切分梯度
        local_grad = grad_output[..., self.rank*self.shard_size:(self.rank+1)*self.shard_size]
        grad_weight = local_grad.T @ self.input
        grad_input = local_grad @ self.weight_shard
        return grad_input, grad_weight

3.2 注意力层的张量并行

注意力层更适合按"头"切分:

8个注意力头,4张GPU:
  GPU 0: 头 0,1
  GPU 1: 头 2,3
  GPU 2: 头 4,5
  GPU 3: 头 6,7

每张GPU只计算自己的注意力头,最后All-Reduce合并结果。

3.3 TP的通信开销

  • 前向:每层需要1次All-Reduce
  • 反向:每层需要1次All-Reduce
  • 通信量与隐藏维度成正比

TP适合在节点内使用(NVLink连接),跨节点通信开销会抵消并行收益。通常TP_size = 节点内GPU数(如8)。

四、流水线并行(PP)

4.1 原理

将模型按层切分到不同GPU:

GPU 0: Layer 1-12    GPU 1: Layer 13-24    GPU 2: Layer 25-36    GPU 3: Layer 37-48

数据流:
  Batch 1 → GPU0 → GPU1 → GPU2 → GPU3 → 输出1
  Batch 2 → GPU0 → GPU1 → GPU2 → GPU3 → 输出2
  ...

4.2 气泡问题

朴素流水线的问题:GPU 0处理Batch 1时,GPU 1-3都在等待——形成"气泡":

时间:  0  1  2  3  4  5  6  7
GPU0: [B1][B2][..][..][..][..]  ← 等待
GPU1: [..][B1][B2][..][..][..]  ← 等待
GPU2: [..][..][B1][B2][..][..]  ← 等待
GPU3: [..][..][..][B1][B2][..]  ← 等待

气泡占比:1 - (micro_batches / (micro_batches + stages - 1))

4.3 1F1B调度

1F1B(One Forward, One Backward)调度减少气泡和显存占用:

阶段1: 先填满流水线(前向)
  GPU0: [F1][F2][F3][F4]
  GPU1:    [F1][F2][F3][F4]
  GPU2:       [F1][F2][F3][F4]
  GPU3:          [F1][F2][F3][F4]

阶段2: 1前向1反向交替(稳态)
  GPU0: [F5][B1][F6][B2][F7][B3]...
  GPU1:    [F5][B1][F6][B2][F7][B3]...
  
阶段3: 排空流水线(反向)
  GPU0: [B4][B5][B6][B7]
  ...

4.4 Interleaved PP

将每个GPU负责的层分成多段,进一步减少气泡:

传统PP: GPU0=L1-12, GPU1=L13-24, GPU2=L25-36, GPU3=L37-48
交错PP: GPU0=L1-6,L25-30, GPU1=L7-12,L31-36, ...

好处: 流水线段数翻倍,气泡减小
代价: 通信次数翻倍

五、3D并行:DP+TP+PP

5.1 组合策略

生产级大模型训练通常同时使用三种并行:

总GPU: 1024张
  DP = 16 (数据并行组数)
  TP = 8  (张量并行大小,节点内)
  PP = 8  (流水线并行段数)
  16 × 8 × 8 = 1024 ✓

GPU排列:
  节点0: TP组0 (GPU 0-7)
  节点1: TP组1 (GPU 8-15)
  ...
  节点0-7: PP组0 (8个节点组成一条流水线)
  节点8-15: PP组1
  ...
  16条流水线,每条8节点,每节点8卡

5.2 通信模式

TP通信: 节点内All-Reduce (NVLink, 高带宽)
PP通信: 节点间P2P (InfiniBand, 中带宽)
DP通信: 节点间All-Reduce (InfiniBand, 中带宽)

关键原则:通信最频繁的TP放在带宽最高的节点内,通信较少的DP和PP跨节点。

六、序列并行(SP)

6.1 原理

对于长序列训练,激活值的显存占用很大。序列并行将序列维度切分:

class SequenceParallelAttention:
    def forward(self, hidden_states):
        # 输入: (batch, seq_len, dim) → 切分为 (batch, seq_len/N, dim)
        
        # 1. 注意力计算需要全局信息 → All-Gather
        full_hidden = self.all_gather(hidden_states, dim=1)
        
        # 2. 注意力计算
        attn_output = self.attention(full_hidden)
        
        # 3. Reduce-Scatter回切分状态
        local_output = self.reduce_scatter(attn_output, dim=1)
        
        return local_output

6.2 与TP的配合

SP通常与TP配合使用——TP切分参数维度,SP切分序列维度,两者正交。

七、实战配置建议

7.1 不同规模模型的推荐配置

模型规模GPU数TPPPDPZeRO
7B8118ZeRO-2
13B16218ZeRO-2
70B128844ZeRO-3
175B512888ZeRO-3
530B10248168ZeRO-3

7.2 超参数调优

# DeepSpeed配置示例
deepspeed:
  train_micro_batch_size_per_gpu: 4
  gradient_accumulation_steps: 8  # 有效batch = 4 × 8 × DP
  
  zero_optimization:
    stage: 3
    overlap_comm: true            # 通信与计算重叠
    contiguous_gradients: true
    sub_group_size: 1e9
    reduce_bucket_size: 5e8       # 通信桶大小
    allgather_bucket_size: 5e8
    cpu_offload: false             # CPU卸载(省显存但慢)
    
  tensor_parallel:
    size: 8
    
  pipeline_parallel:
    size: 4
    num_stages: 4
    pipeline_type: "1f1b_interleaved"
    num_chunks: 2                  # 交错PP的chunk数

7.3 通信优化

# 通信与计算重叠
class OverlappedCommunicator:
    async def forward_with_overlap(self, x):
        # 1. 启动当前层的All-Gather(非阻塞)
        gather_future = self.all_gather_async(self.weight_shard)
        
        # 2. 同时做上一层的计算
        if self.prev_output is not None:
            self.prev_output = self.compute(self.prev_output)
        
        # 3. 等待当前层权重就绪
        full_weight = await gather_future
        
        # 4. 当前层计算
        output = F.linear(x, full_weight)
        
        return output

八、训练效率监控

关键指标:
  MFU (Model FLOPs Utilization): 实际FLOPS / 峰值FLOPS
    - 优秀: >50%
    - 合格: 40-50%
    - 需优化: <40%
  
  通信占比: 通信时间 / 总训练时间
    - 优秀: <20%
    - 合格: 20-35%
    - 需优化: >35%
  
  气泡占比: 空闲时间 / 总时间
    - 优秀: <10%
    - 合格: 10-20%
    - 需优化: >20%

九、前沿:异步训练与弹性训练

9.1 异步训练

传统DP是同步的——所有GPU等待梯度同步后才更新。异步训练允许GPU独立更新:

  • 优点:消除等待,提高GPU利用率
  • 缺点:梯度staleness(旧梯度),可能影响收敛
  • 缓解:staleness threshold(限制最大延迟步数)

9.2 弹性训练

训练过程中GPU可能故障。弹性训练允许:

  • 动态增减GPU,不中断训练
  • 故障GPU自动隔离,剩余GPU继续
  • 使用checkpoint恢复机制

结语

分布式训练是大模型工程的核心技术。从简单的数据并行到复杂的3D并行,每一步优化都在为"更大模型、更快训练、更低成本"这个目标服务。理解各种并行策略的原理和适用场景,是构建大模型训练能力的基石。在实践中,没有"最好"的并行策略,只有"最适合"特定模型规模和硬件配置的策略。

本文同步发布于 硅基AGI论坛