序列并行:解决超长序列的显存瓶颈

张量并行(TP)将模型参数切分到多个GPU,流水线并行(PP)将模型层切分到多个GPU,但两者都没有解决序列长度带来的显存问题。

当序列长度达到128K甚至1M时,即使使用TP和PP,每个GPU上存储的激活值仍然是 O(seq_len × hidden_dim)。序列并行(Sequence Parallelism, SP)正是为了解决这个问题——将序列维度也切分到多个GPU上。

序列并行的核心思想

基本切分

将序列维度切分到N个GPU上,每个GPU只处理 seq_len/N 个token:

原始序列: [t0, t1, t2, ..., t_{L-1}]
GPU 0:     [t0, t1, ..., t_{L/N-1}]
GPU 1:     [t_{L/N}, ..., t_{2L/N-1}]
...
GPU N-1:   [t_{L-L/N}, ..., t_{L-1}]

通信需求

序列切分后,LayerNorm和Dropout等逐token操作可以独立进行(无需通信),但注意力计算需要完整的序列信息——每个token的Query需要与所有token的Key/Value计算注意力。

这是序列并行的核心挑战:如何在不重新聚合完整序列的前提下完成注意力计算?

DeepSpeed-Ulysses

原理

DeepSpeed-Ulysses使用All-to-All通信来实现序列并行注意力:

class UlyssesAttention(nn.Module):
    def __init__(self, d_model, n_heads, n_gpus):
        super().__init__()
        self.n_gpus = n_gpus
        self.local_heads = n_heads // n_gpus
        # 每个GPU只存储部分注意力头的参数
        self.W_q = nn.Linear(d_model, self.local_heads * (d_model // n_heads))
        self.W_k = nn.Linear(d_model, self.local_heads * (d_model // n_heads))
        self.W_v = nn.Linear(d_model, self.local_heads * (d_model // n_heads))
        self.W_o = nn.Linear(self.local_heads * (d_model // n_heads), d_model)
    
    def forward(self, x):
        # x的形状: [batch, local_seq_len, d_model]
        # 每个GPU持有序列的一部分
        
        # 1. 本地计算QKV
        Q_local = self.W_q(x)  # [batch, local_seq, local_heads * head_dim]
        K_local = self.W_k(x)
        V_local = self.W_v(x)
        
        # 2. All-to-All通信:重新排列序列和头维度
        # 从 [batch, local_seq, local_heads, head_dim]
        # 到 [batch, global_seq, local_heads, head_dim]
        Q_global = all_to_all(Q_local, split_dim=1, concat_dim=2)
        K_global = all_to_all(K_local, split_dim=1, concat_dim=2)
        V_global = all_to_all(V_local, split_dim=1, concat_dim=2)
        
        # 3. 本地注意力计算(现在每个GPU有完整的序列,但只有部分头)
        attn_out = flash_attention(Q_global, K_global, V_global)
        
        # 4. All-to-All通信:将结果按序列切分回去
        attn_local = all_to_all(attn_out, split_dim=2, concat_dim=1)
        
        # 5. 输出投影
        output = self.W_o(attn_local)
        return output

通信分析

Ulysses需要2次All-to-All通信(前向)+ 2次All-to-All(反向)。

All-to-All的通信量:每个GPU发送/接收 batch × seq_len × hidden_dim / n_gpus 个元素。

n_gpus 较大时,通信量线性增长,成为瓶颈。

Megatron SP(Sequence Parallelism)

原理

Megatron-LM的序列并行与DeepSpeed-Ulysses不同,它不切分注意力计算,而是切分LayerNorm和Dropout:

class MegatronSP(nn.Module):
    def forward(self, x):
        # x按序列维度切分:[batch, local_seq, d_model]
        
        # 1. LayerNorm:本地独立计算(无需通信!)
        x_norm = layer_norm(x)  # 逐token操作,天然支持序列并行
        
        # 2. QKV投影:本地计算
        Q = self.W_q(x_norm)
        K = self.W_k(x_norm)
        V = self.W_v(x_norm)
        
        # 3. 注意力:需要All-Gather恢复完整序列
        Q_full = all_gather(Q, dim=1)  # [batch, seq, local_heads * head_dim]
        K_full = all_gather(K, dim=1)
        V_full = all_gather(V, dim=1)
        
        # 4. 注意力计算
        attn_out = attention(Q_full, K_full, V_full)
        
        # 5. Reduce-Scatter:按序列切分回去
        attn_local = reduce_scatter(attn_out, dim=1)
        
        # 6. 输出投影
        output = self.W_o(attn_local)
        
        # 7. Dropout:本地独立计算
        output = dropout(output)
        
        return output + x  # 残差连接

关键洞察

Megatron SP的核心洞察是:LayerNorm和Dropout是逐token操作,天然支持序列并行。这些操作在TP中会在所有GPU上重复计算,而SP将它们切分到不同GPU,节省了显存。

注意力计算虽然需要All-Gather,但只占整体计算的一小部分(当FFN较大时)。

Ulysses vs Megatron SP

特性DeepSpeed-UlyssesMegatron SP
切分维度注意力头序列维度
通信模式All-to-AllAll-Gather + Reduce-Scatter
LayerNorm重复计算切分(节省显存)
适用场景注意力密集型FFN密集型
通信量O(seq_len × d / N)O(seq_len × d / N)

组合使用

两者可以组合——Ulysses切分注意力头,Megatron SP切分序列维度:

# 组合配置
parallel_config = {
    "tensor_model_parallel_size": 4,    # TP=4
    "pipeline_model_parallel_size": 2,  # PP=2
    "sequence_parallel_size": 4,        # SP=4 (Megatron方式)
    "ulysses_parallel_size": 2,         # UP=2 (Ulysses方式)
}
# 总GPU数 = 4 × 2 × 4 × 2 = 64

Ring Attention与序列并行

Ring Attention(之前文章介绍过)也是一种序列并行方案,但它通过环形通信避免实例化完整注意力矩阵。

Ring Attention + SP

将Ring Attention与Megatron SP结合:

  1. 序列切分:SP将序列切分到多个GPU(Megatron方式)
  2. 环形通信:Ring Attention在切分后的序列上做环形注意力

这种组合可以将序列长度扩展到极长(1M+ tokens)。

2026年序列并行进展

混合序列并行

根据模型不同层的特点,动态选择Ulysses或Megatron SP:

  • 浅层:使用Megatron SP(LayerNorm占比大)
  • 深层:使用Ulysses(注意力头数多)

动态序列并行

根据当前序列长度动态调整是否启用SP:

def dynamic_sp(model, input_ids, seq_parallel_threshold=8192):
    """动态序列并行"""
    seq_len = input_ids.shape[1]
    
    if seq_len > seq_parallel_threshold:
        # 长序列:启用序列并行
        return model.forward_sp(input_ids)
    else:
        # 短序列:标准计算(避免通信开销)
        return model.forward(input_ids)

序列并行 + MoE

在MoE模型中,序列并行切分序列后,专家路由需要在全局序列上操作。这需要额外的All-to-All通信来同步路由决策。

实践建议

  1. 启用条件:序列长度 > 8K 时考虑启用SP
  2. 与TP/PP配合:SP通常作为第三维并行(在TP和PP之外)
  3. 通信优化:使用NCCL的All-Gather和Reduce-Scatter,确保通信-计算重叠
  4. 梯度检查点:SP下梯度检查点仍然有效,但需要注意检查点保存的是本地序列部分

结语

序列并行是超长序列训练的关键技术。DeepSpeed-Ulysses和Megatron SP提供了两种不同的切分思路,两者可以组合使用以发挥最大效用。随着上下文长度从32K向1M+迈进,序列并行将成为大模型训练的标配。

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。