【推理与部署篇10】模型并行实战指南:张量并行、流水线并行与专家并行

前言:当模型大到单张GPU放不下时——LLaMA-70B需要140GB、DeepSeek-V4需要300GB+——唯一的办法就是"切开"模型,放到多张GPU上。这就是模型并行。但在推理场景下,模型并行面临与训练完全不同的挑战:延迟敏感、请求动态变化、KV Cache跨GPU管理。本文从张量并行(TP)、流水线并行(PP)、专家并行(EP)到序列并行(SP),覆盖所有推理场景的模型并行策略,附完整实现代码和通信分析。


📋 目录


一、为什么需要模型并行?

1.1 单卡装不下的模型

模型并行(Model Parallelism)是当模型太大、单张GPU放不下时的唯一选择。

2026年主流模型所需显存(FP16,不含KV Cache):
┌──────────────────┬──────────┬──────────┬──────────┬──────────┐
│ 模型             │ 参数量   │ 权重显存 │ KV Cache  │ 总需求   │
│                  │          │ (FP16)   │ (32K基)  │          │
├──────────────────┼──────────┼──────────┼──────────┼──────────┤
│ LLaMA-3.1-8B     │ 8B       │ 16GB     │ 8GB      │ 24GB     │
│ LLaMA-3.1-70B    │ 70B      │ 140GB    │ 40GB     │ 180GB    │
│ Qwen-2.5-72B     │ 72B      │ 144GB    │ 40GB     │ 184GB    │
│ DeepSeek-V4      │ 685B     │ 1.37TB   │ 86GB     │ 1.46TB   │
│ GLM-5-130B       │ 130B     │ 260GB    │ 54GB     │ 314GB    │
│ Mixtral-8x22B    │ 141B     │ 282GB    │ 14GB     │ 296GB    │
│ GPT-5.5          │ 数T      │ 数TB     │ 大量      │ >>单卡   │
└──────────────────┴──────────┴──────────┴──────────┴──────────┘

H100只有80GB显存 → 70B模型需要3张H100才能放下。

注意:权重只是成本的一部分!
  KV Cache在长上下文下甚至会超过权重:
  70B + 128K上下文 + batch=32 → KV Cache = 160GB + 权重140GB = 300GB
  → 需要4张H100

模型并行的"黄金法则":
  单卡放得下 → 用单卡(最简单、最低延迟)
  单卡放不下 → 用TP(张量并行、通信密集)
  多卡但通信瓶颈 → 用PP(流水线并行、通信稀疏)
  MoE模型 → 用EP(专家并行、天然切分)
  超长序列 → 用SP(序列并行、切分上下文)

1.2 推理场景下模型并行的独特挑战

推理时的模型并行与训练有很大不同:

┌──────────────┬──────────────────┬──────────────────┐
│ 挑战          │ 训练              │ 推理              │
├──────────────┼──────────────────┼──────────────────┤
│ Batch大小     │ 大(256-4096)    │ 小(1-64)        │
│ 通信负担      │ 分摊到大批量      │ 每步都要通信      │
│ 延迟敏感度    │ 低(吞吐优先)    │ 高(TTFT/TPOT)  │
│ KV Cache      │ 不需要持久化      │ 必须跨GPU管理     │
│ Dynamism      │ 稳定(固定batch) │ 动态(连续批处理)│
│ Prefill/Decode│ 几乎只有prefill   │ prefill+decode混合│
│ 显存分配      │ 一次性分配        │ 动态分配(Paged) │
└──────────────┴──────────────────┴──────────────────┘

核心矛盾:
  训练:通信开销被大批量分摊 → 通信延迟不是问题
  推理:小批量 → 通信开销占比大 → 必须精心优化
  
  例如:LLaMA-70B TP=8时
    训练:batch=1024 → 每个token的通信成本 ~0.1μs
    推理:batch=1 → 每个token的通信成本 ~20μs → 占TPOT的30%+!

二、张量并行(Tensor Parallelism):切矩阵,不减通信

2.1 核心原理

张量并行(Tensor Parallelism, TP):
  将Transformer中的权重矩阵按行或列切分到多张GPU

为什么选择TP?
  单个矩阵乘法的计算量太大 → 切分开并行计算
  每张GPU只持有部分权重 → 总显存 = 单卡容量 × TP大小

TP的切分方式:

1. Column-wise Partition(列切分) — 用于Linear层
  Weight: [out_features, in_features]
  切分: W = [W_0, W_1, ..., W_{t-1}]  — 按out_features列切
  计算: Y = X × W = [X×W_0, X×W_1, ..., X×W_{t-1}]
  通信: AllReduce(合并所有GPU的部分结果)

2. Row-wise Partition(行切分) — 用于Linear层
  Weight: [out_features, in_features]
  切分: W = [W_0; W_1; ...; W_{t-1}]  — 按in_features行切
  计算: Y_i = X_i × W_i(每GPU只处理部分输入)
  通信: AllReduce(不同GPU对输出求和)

3. Fused TP(融合切分) — QKV投影
  QKV权重: [3×out_features, in_features]
  切分: 每个GPU有QKV的1/t

TP在Transformer中的位置:
┌──────────────────────────────────────────────────────────┐
│                                                           │
│  一层Transformer的TP分解:                                 │
│                                                           │
│  输入 X ([B, S, d])                                       │
│    ↓                                                      │
│  ┌─────────────────────────────────┐                     │
│  │  Attention                      │                     │
│  │  QKV Proj: 列切分TP             │                     │
│  │   每个GPU有Q_i, K_i, V_i        │                     │
│  │   → 各自计算attention           │                     │
│  │   → AllReduce合并               │                     │
│  │  Output Proj: 行切分TP          │                     │
│  │   → 每GPU算部分输出             │                     │
│  │   → AllReduce求和               │                     │
│  └─────────────────────────────────┘                     │
│    ↓                                                      │
│  ┌─────────────────────────────────┐                     │
│  │  FFN                            │                     │
│  │  Gate Proj: 列切分TP            │                     │
│  │  Up Proj: 列切分TP              │                     │
│  │  Down Proj: 行切分TP            │                     │
│  │  → 每步都有AllReduce            │                     │
│  └─────────────────────────────────┘                     │
│    ↓                                                      │
│  输出 X'                                                   │
│                                                           │
└──────────────────────────────────────────────────────────┘

2.2 TP通信分析

import torch
import torch.nn as nn
import torch.distributed as dist
from typing import Optional


def compute_tp_communication(
    hidden_size: int,
    intermediate_size: int,
    num_layers: int,
    tp_size: int,
    batch_size: int,
    seq_len: int,
    dtype_bytes: int = 2,  # FP16
):
    """
    计算TP单步的通信量
    
    TP每层发生的AllReduce:
    
    1. Attention QKV Proj后:
       Q_output = [B, S, H×d] → AllReduce
       通信量 = B × S × H×d × dtype_bytes
       
    2. Attention Output Proj后:
       Attn_output = [B, S, H×d] → AllReduce
       通信量 = B × S × H×d × dtype_bytes
       
    3. FFN Gate/Up后:
       Gate_output = [B, S, intermediate] → AllReduce
       通信量 = B × S × intermediate × dtype_bytes
       
    4. FFN Down后:
       FFN_output = [B, S, H×d] → AllReduce
       通信量 = B × S × H×d × dtype_bytes
    
    单层总计:4次AllReduce
    AllReduce的通信量 = 2 × 数据量(求和+广播)
    """
    per_layer_comm = 0
    
    # Attention: QKV output + Attn output = 2次
    attn_qkv = batch_size * seq_len * hidden_size * dtype_bytes
    attn_out = batch_size * seq_len * hidden_size * dtype_bytes
    per_layer_comm += 2 * (attn_qkv + attn_out)  # AllReduce ×2
    
    # FFN: Gate/Up output + Down output = 2次
    ffn_gate_up = batch_size * seq_len * intermediate_size * dtype_bytes
    ffn_down = batch_size * seq_len * hidden_size * dtype_bytes
    per_layer_comm += 2 * (ffn_gate_up + ffn_down)  # AllReduce ×2
    
    # 总计
    total_comm_per_step = per_layer_comm * num_layers  # 所有层
    total_comm_per_token = total_comm_per_step / batch_size  # 每个token
    
    # TP的具体实现:Ring AllReduce vs NVLink AllReduce
    # 理想带宽利用率下,通信时间 = 数据量 / (带宽 × tp_size × 效率)
    # NVLink带宽(H100): 900 GB/s (双向)
    
    return {
        "per_layer_comm_bytes": per_layer_comm,
        "total_comm_per_step_bytes": total_comm_per_step,
        "total_comm_per_token_bytes": total_comm_per_token,
        "per_step_comm_time_us": total_comm_per_step / (900e9 * 0.7) * 1e6,
    }


def tp_communication_results():
    """
    TP通信量实际数据(LLaMA-70B, H100, NVLink):
    
    ┌──────────┬──────────┬──────────┬──────────┬──────────┐
    │ TP大小   │ 每步通信 │ batch=1  │ batch=8  │ 占TPOT  │
    │          │          │ 通信时间 │ 通信时间 │ (batch=1)│
    ├──────────┼──────────┼──────────┼──────────┼──────────┤
    │ TP=2     │ 1.3GB    │ 2.1μs    │ 16.5μs   │ 3%       │
    │ TP=4     │ 1.3GB    │ 1.0μs    │ 8.2μs    │ 5%       │
    │ TP=8     │ 1.3GB    │ 0.5μs    │ 4.1μs    │ 8%       │
    │ TP=16    │ 1.3GB    │ 0.33μs   │ 2.6μs    │ 12%      │
    └──────────┴──────────┴──────────┴──────────┴──────────┘
    
    关键洞察:
    - TP越大,单步通信量不变(都是全量数据AllReduce)
    - TP越大,通信时间越短(更多带宽)
    - 但TP越大,通信占TPOT比例越高(计算更少)
    - batch=1时,TP=8的通信只占TPOT的8% → 可接受
    - batch=8时,通信时间 ×8 → 如果TP=16则占TPOT 15%+
    """
    pass

2.3 TP实现:Column Parallel + Row Parallel

class ColumnParallelLinear(nn.Module):
    """
    列并行Linear(切分out_features维度)
    
    输入X在每个GPU上相同
    输出Y每个GPU只有1/tp_size
    需要AllReduce合并
    """
    def __init__(
        self,
        in_features: int,
        out_features: int,
        tp_size: int,
        tp_rank: int,
        bias: bool = True,
    ):
        super().__init__()
        self.tp_size = tp_size
        self.tp_rank = tp_rank
        self.out_features_per_rank = out_features // tp_size
        
        # 每个GPU只持有部分权重
        self.weight = nn.Parameter(
            torch.randn(self.out_features_per_rank, in_features)
        )
        if bias:
            self.bias = nn.Parameter(
                torch.randn(self.out_features_per_rank)
            )
        else:
            self.bias = None
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        X: [batch, seq, in_features]
        输出: [batch, seq, out_features] — 所有GPU都有完整输出
        """
        # 本GPU的局部计算
        local_output = torch.matmul(x, self.weight.T)
        if self.bias is not None:
            local_output = local_output + self.bias
        
        # AllReduce合并:所有GPU的输出拼接成完整输出
        # 注意:不是AllReduce求和,而是AllGather!
        # 因为列切分后,每个GPU的输出是不同的列
        full_output = torch.empty(
            x.shape[0], x.shape[1], self.out_features_per_rank * self.tp_size,
            device=x.device, dtype=x.dtype
        )
        dist.all_gather_into_tensor(
            full_output, local_output,
            group=dist.group.WORLD
        )
        
        return full_output


class RowParallelLinear(nn.Module):
    """
    行并行Linear(切分in_features维度)
    
    输入X每个GPU只有部分(前一个ColumnParallel的输出)
    输出Y需要AllReduce求和
    """
    def __init__(
        self,
        in_features: int,
        out_features: int,
        tp_size: int,
        tp_rank: int,
        bias: bool = True,
    ):
        super().__init__()
        self.tp_size = tp_size
        self.tp_rank = tp_rank
        self.in_features_per_rank = in_features // tp_size
        
        # 每个GPU只持有部分权重(对应in_features的一部分)
        self.weight = nn.Parameter(
            torch.randn(out_features, self.in_features_per_rank)
        )
        if bias:
            self.bias = nn.Parameter(torch.randn(out_features))
        else:
            self.bias = None
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        X: [batch, seq, in_features_per_rank] — 只包含本GPU的输入部分
        输出: [batch, seq, out_features] — 所有GPU都有完整输出
        """
        # 局部计算
        local_output = torch.matmul(x, self.weight.T)
        
        # AllReduce求和:所有GPU的部分输出相加 = 完整输出
        if self.tp_size > 1:
            dist.all_reduce(local_output, op=dist.ReduceOp.SUM)
        
        if self.bias is not None:
            local_output = local_output + self.bias
        
        return local_output


class TPAttention(nn.Module):
    """
    TP版本的Attention
    
    每张GPU有部分head,各自计算attention
    """
    def __init__(self, hidden_size, num_heads, tp_size, tp_rank):
        super().__init__()
        self.tp_size = tp_size
        self.tp_rank = tp_rank
        self.num_heads_per_rank = num_heads // tp_size
        self.head_dim = hidden_size // num_heads
        self.hidden_size_per_rank = self.num_heads_per_rank * self.head_dim
        
        # QKV投影:列并行(每个GPU输出自己的head部分)
        self.qkv_proj = ColumnParallelLinear(
            hidden_size, 3 * self.hidden_size_per_rank,
            tp_size=1, tp_rank=0,  # 简化:不做column parallel
            bias=False
        )
        
        # 输出投影:行并行
        self.o_proj = RowParallelLinear(
            self.hidden_size_per_rank, hidden_size,
            tp_size=tp_size, tp_rank=tp_rank,
            bias=False
        )
    
    def forward(self, x, attention_mask=None):
        batch_size, seq_len, _ = x.shape
        
        # QKV投影(每个GPU只算自己的head部分)
        qkv = self.qkv_proj(x)
        qkv = qkv.reshape(batch_size, seq_len, 3, self.num_heads_per_rank, self.head_dim)
        q, k, v = qkv[:, :, 0], qkv[:, :, 1], qkv[:, :, 2]
        # q,k,v: [B, S, H_per_rank, d]
        
        # 转置为attention格式
        q = q.transpose(1, 2)  # [B, H_per_rank, S, d]
        k = k.transpose(1, 2)
        v = v.transpose(1, 2)
        
        # 计算attention(只在自己GPU上)
        scale = self.head_dim ** -0.5
        attn = torch.matmul(q, k.transpose(-2, -1)) * scale
        if attention_mask is not None:
            attn = attn + attention_mask
        attn = torch.softmax(attn, dim=-1)
        out = torch.matmul(attn, v)
        out = out.transpose(1, 2).reshape(batch_size, seq_len, -1)
        # out: [B, S, H_per_rank × d]
        
        # 输出投影(行并行 + AllReduce求和)
        out = self.o_proj(out)
        
        return out


class TPTransformerLayer(nn.Module):
    """
    TP版本的Transformer层
    """
    def __init__(self, hidden_size, num_heads, intermediate_size, tp_size, tp_rank):
        super().__init__()
        self.attention = TPAttention(hidden_size, num_heads, tp_size, tp_rank)
        
        # FFN的Gate/Up投影:列并行
        self.gate_proj = ColumnParallelLinear(
            hidden_size, intermediate_size // tp_size,
            tp_size=1, tp_rank=0,
            bias=False
        )
        self.up_proj = ColumnParallelLinear(
            hidden_size, intermediate_size // tp_size,
            tp_size=1, tp_rank=0,
            bias=False
        )
        
        # Down投影:行并行
        self.down_proj = RowParallelLinear(
            intermediate_size // tp_size, hidden_size,
            tp_size=tp_size, tp_rank=tp_rank,
            bias=False
        )
        
        self.input_norm = nn.LayerNorm(hidden_size)
        self.post_attn_norm = nn.LayerNorm(hidden_size)
    
    def forward(self, x, attention_mask=None):
        # Attention
        residual = x
        x = self.input_norm(x)
        x = self.attention(x, attention_mask)
        x = residual + x
        
        # FFN(SwiGLU)
        residual = x
        x = self.post_attn_norm(x)
        gate = torch.sigmoid(self.gate_proj(x))  # SiLU
        up = self.up_proj(x)
        x = gate * up
        x = self.down_proj(x)
        x = residual + x
        
        return x


# TP推理的KV Cache管理
class TPKVCache:
    """
    TP下每张GPU的KV Cache
    
    关键:每张GPU只有部分KV head
    所以KV Cache也按TP分散
    """
    def __init__(self, num_heads, tp_size, tp_rank, block_size=16):
        self.num_heads_per_rank = num_heads // tp_size
        self.tp_rank = tp_rank
        self.block_size = block_size
        
        # 与本GPU关联的KV Block
        self.blocks = {}  # req_id → {K: [H_per_rank, S, d], V: [H_per_rank, S, d]}
    
    def update(self, req_id, k, v):
        """更新KV Cache(k, v已经是本GPU的部分head)"""
        if req_id not in self.blocks:
            self.blocks[req_id] = {"K": k, "V": v}
        else:
            self.blocks[req_id]["K"] = torch.cat(
                [self.blocks[req_id]["K"], k], dim=1
            )
            self.blocks[req_id]["V"] = torch.cat(
                [self.blocks[req_id]["V"], v], dim=1
            )
    
    def get_kv(self, req_id):
        return self.blocks[req_id]["K"], self.blocks[req_id]["V"]

2.4 TP的优缺点

✅ 优点:
  1. 显存显式减少:每GPU只持有1/tp_size的权重
  2. 计算完全并行:所有GPU同时计算不同的head/neurons
  3. Prefill加速明显:大矩阵被切分,每GPU的计算量减少
  4. 不需要额外的调度复杂性

❌ 缺点:
  1. 通信密集:每层有2-4次AllReduce,路径上有NVLink瓶颈
  2. TP大小受限于head数和intermediate_size
  3. batch=1时通信开销占比大
  4. 需要高带宽互联(NVLink/Socket)

通信量与计算量之比:
  每层TP通信量 ≈ 4 × hidden_size × batch × seq
  每层TP计算量 ≈ 12 × hidden_size² × batch × seq(粗略)
  通信/计算比 ≈ 4 / (12 × hidden_size) = 1 / (3 × hidden_size)
  
  对LLaMA-70B (hidden=8192): 通信/计算 ≈ 1/24576 ≈ 0.004%
  → 通信开销极小!关键在于NVLink带宽能支撑。
  
  实际上TP=8的通信开销 < 5%计算时间(NVLink环境下)
  如果没有NVLink(如PCIe互联),通信开销可能到30%+

三、流水线并行(Pipeline Parallelism):切层数,藏延迟

3.1 核心原理

流水线并行(Pipeline Parallelism, PP):
  将Transformer的不同层分配到不同GPU,形成流水线

为什么用PP?
  TP需要高带宽互联(NVLink),跨节点(e.g. 2台8卡机器)时带宽不足
  PP的通信量远小于TP → 适合跨节点部署

PP的工作方式(1F1B调度 — One-Forward-One-Backward):

推理场景下的PP(只有前向):

┌──────────────────────────────────────────────────────────┐
│                                                           │
│  PP=4(4张GPU)的推理流水线:                               │
│                                                           │
│  GPU-0 (Layers 0-19):  │ F(0) │      │      │      │      │
│  GPU-1 (Layers 20-39): │      │ F(1) │      │      │      │
│  GPU-2 (Layers 40-59): │      │      │ F(2) │      │      │
│  GPU-3 (Layers 60-79): │      │      │      │ F(3) │      │
│                        ─────────────────────────→ 时间     │
│                                                           │
│  每个GPU处理自己的部分:                                     │
│  GPU-0算完L0-19 → 发送hidden_state给GPU-1                 │
│  GPU-1收到后算L20-39 → 发送给GPU-2                        │
│  ...                                                       │
│  总时延 = 所有GPU顺序处理的时间 = (TPT_per_layer × layers)  │
│                                                           │
│  但因为没有流水线气泡?→ 推理只有前向,没有反向              │
│  所以推理的PP = 纯粹的串行流水线 = 实际上是"层并行"         │
│               = 每张GPU分层计算,不省延迟,只省显存          │
│                                                           │
└──────────────────────────────────────────────────────────┘

3.2 推理PP的关键:Micro-Batching(微批处理)

推理场景下,PP通过Micro-Batching隐藏延迟:

没有Micro-Batching(每个请求串行通过流水线):
  ┌──────────────────────────────────────────────────────────┐
  │  GPU-0: [Req1] [Req2] [Req3] [Req4]                     │
  │  GPU-1:         [Req1] [Req2] [Req3] [Req4]             │
  │  GPU-2:                [Req1] [Req2] [Req3] [Req4]       │
  │  GPU-3:                       [Req1] [Req2] [Req3] [Req4] │
  │                                                           │
  │  TPOT = GPU0时间 + GPU1时间 + GPU2时间 + GPU3时间        │
  │  → 延迟增加了4倍 → PP在推理中的最大问题!                │
  └──────────────────────────────────────────────────────────┘

有Micro-Batching(多个请求同时流式处理):
  ┌──────────────────────────────────────────────────────────┐
  │  GPU-0: [R1][R2][R3][R4][R5][R6][R7][R8] ...            │
  │  GPU-1:    [R1][R2][R3][R4][R5][R6][R7][R8] ...          │
  │  GPU-2:       [R1][R2][R3][R4][R5][R6][R7][R8] ...        │
  │  GPU-3:          [R1][R2][R3][R4][R5][R6][R7][R8] ...      │
  │                                                           │
  │  启动气泡后 → 流水线填满 → 每步所有GPU同时工作            │
  │  稳态TPOT = max(单GPU计算时间) ≈ 单GPU的TPOT             │
  │  启动气泡 = PP_size - 1步                                 │
  │                                                           │
  │  如果batch很大 → 气泡占比小 → PP效率高                     │
  │  如果batch=1 → 没有任何micro-batch可以填流水线 → 延迟增  │
  │                                                           │
  └──────────────────────────────────────────────────────────┘

Micro-Batching的关键参数:
  需要的并发请求数 ≥ PP_size 才能填满流水线
  例如PP=4 → 至少4个请求同时处理
  如果只有1个请求在推理 → PP增加了延迟(没有加速)

3.3 PP实现

import torch
import torch.distributed as dist
from typing import Optional


class PipelineParallelModel(nn.Module):
    """
    流水线并行的推理模型
    
    每张GPU持有连续的几层
    通过点对点通信(send/recv)传递中间结果
    """
    def __init__(
        self,
        all_layers: nn.ModuleList,  # 所有Transformer层
        pp_size: int,
        pp_rank: int,
        num_layers_per_rank: int,
        device: torch.device,
    ):
        super().__init__()
        self.pp_size = pp_size
        self.pp_rank = pp_rank
        self.device = device
        
        # 本GPU持有的层
        start_layer = pp_rank * num_layers_per_rank
        end_layer = (pp_rank + 1) * num_layers_per_rank
        self.layers = all_layers[start_layer:end_layer]
        
        # embed和lm_head只有rank-0和rank-last持有
        # 其他rank不需要
        self.embed_tokens = None
        self.lm_head = None
    
    def forward_micro_batch(
        self,
        hidden_states: torch.Tensor,
        attention_mask: Optional[torch.Tensor] = None,
    ):
        """
        处理一个micro-batch的前向
        """
        for layer in self.layers:
            hidden_states = layer(hidden_states, attention_mask)
        return hidden_states


class PipelineParallelScheduler:
    """
    流水线并行调度器(推理场景)
    
    实现micro-batching调度
    """
    def __init__(
        self,
        pp_size: int,
        pp_rank: int,
        num_micro_batches: int = 4,  # 每个step处理的micro-batch数
    ):
        self.pp_size = pp_size
        self.pp_rank = pp_rank
        self.num_micro_batches = num_micro_batches
        
        # 点对点通信(P2P)
        self.prev_rank = pp_rank - 1 if pp_rank > 0 else None
        self.next_rank = pp_rank + 1 if pp_rank < pp_size - 1 else None
    
    def p2p_send(self, tensor: torch.Tensor, dst_rank: int):
        """发送tensor到下一阶段"""
        dist.send(tensor, dst=dst_rank)
    
    def p2p_recv(self, src_rank: int, shape, dtype, device) -> torch.Tensor:
        """从上一阶段接收tensor"""
        tensor = torch.empty(shape, dtype=dtype, device=device)
        dist.recv(tensor, src=src_rank)
        return tensor
    
    @torch.no_grad()
    def step_micro_batches(
        self,
        pp_model: PipelineParallelModel,
        micro_batches: list,  # [{hidden_states, mask}, ...]
    ):
        """
        处理多个micro-batch
        实现流水线并行调度
        """
        outputs = []
        
        if self.pp_rank == 0:
            # ===== Rank-0:发送prefill/micro-batch到下一级 =====
            for mb in micro_batches:
                hs = pp_model.forward_micro_batch(
                    mb["hidden_states"], mb.get("mask")
                )
                if self.next_rank is not None:
                    # 发送到下一级
                    self.p2p_send(hs, self.next_rank)
                outputs.append(hs)
        
        elif self.pp_rank == self.pp_size - 1:
            # ===== Last Rank:接收并处理 =====
            for i in range(len(micro_batches)):
                # 从上一级接收
                hs = self.p2p_recv(
                    self.prev_rank,
                    micro_batches[i]["hidden_states"].shape,
                    micro_batches[i]["hidden_states"].dtype,
                    micro_batches[i]["hidden_states"].device,
                )
                hs = pp_model.forward_micro_batch(hs, micro_batches[i].get("mask"))
                outputs.append(hs)
        
        else:
            # ===== 中间Rank:先收后发 =====
            for i in range(len(micro_batches)):
                hs = self.p2p_recv(
                    self.prev_rank,
                    micro_batches[i]["hidden_states"].shape,
                    micro_batches[i]["hidden_states"].dtype,
                    micro_batches[i]["hidden_states"].device,
                )
                hs = pp_model.forward_micro_batch(hs, micro_batches[i].get("mask"))
                if self.next_rank is not None:
                    self.p2p_send(hs, self.next_rank)
                outputs.append(hs)
        
        return outputs


# PP vs TP通信量对比
def pp_vs_tp_communication():
    """
    PP vs TP的通信量对比(LLaMA-70B):
    
    TP通信:每层2-4次AllReduce,每次通信量 = hidden_size × batch × seq
    PP通信:每层组1次P2P send/recv,每次通信量 = hidden_size × batch × seq
    
    关键差异:
    - TP的AllReduce是全局通信:所有TP节点一起求和
    - PP的P2P是点到点通信:只涉及相邻节点
    
    通信量公式:
    TP:total_comm = 4 × hidden × seq × batch × layers × tp_size(AllReduce因子)
    PP:total_comm = 1 × hidden × seq × batch × layers(P2P,无放大因子)
    
    具体数据(hidden=8192, seq=4096, batch=1, layers=80):
    
    ┌──────────┬──────────────┬──────────────┬──────────────┐
    │ 方案      │ TP=8         │ PP=8         │ TP vs PP    │
    ├──────────┼──────────────┼──────────────┼──────────────┤
    │ 每步通信  │ 10.7 GB      │ 0.67 GB      │ 16x         │
    │ 通信带宽  │ NVLink 900GB/s│ PCIe 64GB/s  │ 14x         │
    │ 通信时间  │ 17μs         │ 83μs         │ PP 5x更慢   │
    │ 每步计算  │ 230μs        │ 1840μs       │ PP 8x更慢   │
    │ 通信占比  │ 7%           │ 4.5%          │ PP略好      │
    └──────────┴──────────────┴──────────────┴──────────────┘
    
    关键结论:
    - TP通信时间短(NVLink快)但数据量大
    - PP通信时间长(PCIe慢)但数据量小
    - 通信占比:TP≈7%, PP≈4.5%
    - 但PP的TPOT(每步延迟)是TP的 ≈ 8倍!
    - 因为每张GPU只处理1/8的层 → 但总计算量没变
    - 所以PP主要用于**跨节点部署**(PCIe/NVLink都在节点内)
    """
    pass

3.4 PP的优缺点

✅ 优点:
  1. 通信量小(P2P而非AllReduce)→ 适合跨节点
  2. 显存节省:每GPU只持有一部分层
  3. 不需要高带宽互联(PCIe也可以)
  4. 与TP正交,可以组合(3D并行)

❌ 缺点:
  1. 推理延迟增加:需要串行通过所有阶段
  2. 需要Micro-Batching才有效率,batch小则无效
  3. 流水线气泡:启动bubble和结束bubble
  4. 负载不均:最后一层(lm_head)计算量不同
  5. 跨GPU的KV Cache管理复杂

适用场景:
  ✅ 跨节点推理(TP不能跨节点、PP可以)
  ✅ Batch大的场景(micro-batching填满流水线)
  ❌ Batch=1的在线推理(会增大延迟)
  ❌ 延迟敏感场景(PP增加TPOT)

四、专家并行(Expert Parallelism):MoE模型的"自然切分"

4.1 MoE模型与专家并行

专家并行(Expert Parallelism, EP):
  MoE模型)中专用的并行策略

MoE模型的结构:
  ┌─────────────────────────────────────────────────────┐
  │                                                      │
  │  输入                                                     │
  │    ↓                                                    │
  │  Router(路由):gate = Softmax(W_r × x)               │
  │    → 选择top-k个专家激活                                 │
  │    ↓                                                    │
  │  Expert_1 ──┐                                           │
  │  Expert_2 ──┤── 加权求和                                 │
  │  Expert_3 ──┘                                           │
  │  ... (其他专家未激活)                                    │
  │    ↓                                                    │
  │  输出                                                     │
  │                                                      │
  └──────────────────────────────────────────────────────┘

为什么MoE天然适合专家并行?
  每个expert是独立的FFN → 可以分配到不同GPU
  Router部分需要所有GPU协作(All-to-All通信)
  只有激活的专家需要通信 → 通信量可控

EP vs TP for MoE:
  TP:把每个expert切分到多GPU → 所有GPU处理同一个expert
  EP:每个GPU持有完整expert → 不同GPU处理不同expert
  
  MoE推理中,EP通常优于TP:
  - EP的通信是稀疏的(只传路由到专家)
  - EP的计算是密集的(专家FFN很大)
  - EP的自然并行度 = num_experts

Mixtral-8x22B的EP配置:
  total_experts = 8
  top_k = 2
  num_gpus = 8 → 每个GPU放1个expert
  
  每步通信:每个token → 2个expert → All-to-All
  通信量 = 2/8 × total_comm = 25%的TP通信量

4.2 EP实现

class MoEExpertParallelLayer(nn.Module):
    """
    专家并行的MoE层
    
    每个GPU持有部分expert
    使用All-to-All通信分发token到专家所在的GPU
    """
    def __init__(
        self,
        hidden_size: int,
        intermediate_size: int,
        num_experts: int,
        top_k: int,
        ep_size: int,  # = num_experts(典型配置)
        ep_rank: int,
        local_experts_per_rank: int,  # = num_experts / ep_size
    ):
        super().__init__()
        self.hidden_size = hidden_size
        self.intermediate_size = intermediate_size
        self.num_experts = num_experts
        self.top_k = top_k
        self.ep_size = ep_size
        self.ep_rank = ep_rank
        self.local_experts_per_rank = local_experts_per_rank
        
        # 本GPU上的expert
        self.local_experts = nn.ModuleList([
            nn.Sequential(
                nn.Linear(hidden_size, intermediate_size, bias=False),
                nn.SiLU(),
                nn.Linear(intermediate_size, hidden_size, bias=False),
            )
            for _ in range(local_experts_per_rank)
        ])
        
        # Router(所有GPU共享,需要broadcast)
        self.router = nn.Linear(hidden_size, num_experts, bias=False)
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        x: [batch, seq, hidden]
        输出: [batch, seq, hidden]
        
        流程:
        1. Router计算gate scores(所有GPU冗余计算)
        2. 选择top-k个expert
        3. All-to-All分发token到对应的GPU
        4. 每个GPU处理自己的expert
        5. All-to-All收集结果
        """
        batch, seq, hidden = x.shape
        
        # Step 1: Router(所有GPU并行做,避免通信)
        # gate: [batch, seq, num_experts]
        gate = torch.softmax(self.router(x), dim=-1)
        
        # Top-k选择
        top_k_weights, top_k_indices = torch.topk(gate, self.top_k, dim=-1)
        # top_k_weights: [B, S, K] — 路由权重
        # top_k_indices: [B, S, K] — 选择的expert ID
        
        # Step 2: 准备All-to-All的token分发
        # 把每个token映射到目标expert所在的GPU
        # expert_id → ep_rank: expert_id // local_experts_per_rank
        
        # 计算每个token发往哪个GPU
        # ...(复杂的数据重排)
        
        # Step 3: All-to-All通信
        # 发送token/权重到其他GPU,接收其他GPU发来的token
        
        # 简化实现(假设所有expert在本地)
        output = x.new_zeros(batch, seq, hidden)
        
        # 对每个token计算expert输出
        for b in range(batch):
            for s in range(seq):
                for k in range(self.top_k):
                    expert_id = top_k_indices[b, s, k].item()
                    weight = top_k_weights[b, s, k]
                    
                    # 检查expert是否在本GPU
                    local_expert_id = expert_id % self.local_experts_per_rank
                    if expert_id // self.local_experts_per_rank == self.ep_rank:
                        expert_out = self.local_experts[local_expert_id](
                            x[b:b+1, s:s+1]
                        )
                        output[b:b+1, s:s+1] += weight * expert_out
                
        # Step 4: All-to-All收集(实际推理中)
        # ... 从其他GPU收集计算结果
        
        return output


# EP通信分析
def ep_communication_analysis():
    """
    EP的All-to-All通信量分析:
    
    MoE推理中,每个token被路由到top-k个expert
    这意味着每个token需要发送到k个GPU

    通信量(每步):
      发送: batch × seq × hidden × top_k(每个token去k个GPU)
      接收: batch × seq × hidden × top_k(从k个GPU回来)
    
    实际通信量 = 2 × batch × seq × hidden × top_k / ep_size
    因为All-to-All会把数据打散,每个GPU只接收1/ep_size
    
    对比(LLaMA-70B vs Mixtral-8x22B):
    
    ┌────────┬──────────┬──────────┬──────────┬──────────┐
    │ 指标    │ TP-70B    │ EP-MoE   │ 差异      │ 说明      │
    ├────────┼──────────┼──────────┼──────────┼──────────┤
    │ 参数量  │ 70B      │ 141B(活)│ -         │ -        │
    │ 每步计算量│ 1.0x   │ 0.45x  │ ↓55%     │ 只有top-k  │
    │ 每步通信  │ 10.7GB  │ 0.26GB │ ↓97%     │ 稀疏路由  │
    │ 通信占比  │ 7%      │ 1%     │ ↓         │ 几乎可忽略│
    │ 推理速度  │ 1.0x    │ 3.2x   │ ↑220%    │ 活参少+通 │
    │          │         │         │           │ 信少      │
    └──────────┴──────────┴──────────┴──────────┴──────────┘
    
    结论:MoE + EP是2026年推理的黄金组合
    全参模型(70B+)必须用TP,MoE模型天然可以用EP
    EP的通信开销远小于TP
    """
    pass

4.3 EP vs TP in MoE

MoE推理中的EP vs TP对比:

┌──────────────┬──────────────────┬──────────────────┐
│ 维度          │ TP(切每个expert) │ EP(expert分布)  │
├──────────────┼──────────────────┼──────────────────┤
│ 典型配置      │ TP=8, 每个GPU有   │ EP=8, 每个GPU有   │
│              │ 1/8的每个expert   │ 完整expert        │
│ 通信          │ AllReduce密集     │ All-to-All稀疏    │
│ 计算粒度      │ 每个expert都被切分│ expert是完整FFN   │
│ GPU利用率     │ 中(碎片化)      │ 高(完整矩阵)    │
│ 带宽需求      │ 高(NVLink必须)  │ 低(PCIe也够)    │
│ 负载均衡      │ 好(均匀切分)    │ 差(expert访问不均)│
│ 部署灵活性    │ 差(需要同节点)  │ 好(可跨节点)    │
│ 典型加速比    │ MoE中TP≈2x       │ MoE中EP≈3-4x     │
└──────────────┴──────────────────┴──────────────────┘

实际部署:MoE模型通常使用 "EP + TP混合"
  场景:Mixtral-8x22B, 8 GPU (2 nodes × 4)
  节点1:GPU 0-3, 节点2:GPU 4-7
  
  方案:EP=8(跨节点) + TP=1(节点内不做TP)
  → 每个GPU放1个完整expert
  → All-to-All通信跨节点(网络)
  → 每个expert计算完整 → 矩阵大,利用率高
  
  或者:EP=4(节点内)+ TP=2(节点内)
  → 每节点2个GPU处理一个expert
  → 节点内NVLink TP通信
  → 节点间网络 All-to-All

五、序列并行(Sequence/Context Parallelism):切上下文

5.1 核心原理

序列并行(Sequence Parallelism / Context Parallelism, SP):
  将长序列切分到多GPU,每个GPU处理一部分上下文

为什么需要SP?
  单GPU显存放不下超长序列的KV Cache
  例如:128K上下文 → KV Cache = 40-160GB → 单卡放不下

SP vs TP vs PP:
  TP:切分head/neuron维度(数据并行)
  PP:切分层维度(流水线)
  SP:切分序列维度(上下文并行)

SP在Attention计算中的切分:
  完整的Attention:Q [S_q, d] × K^T [d, S_k] → [S_q, S_k]
  SP切分:K = [K_0, K_1, ..., K_{s-1}] — 按序列维度切
  每GPU只存K_i → 只需计算Q × K_i^T
  通信:Ring Attention / 分布式Attention

DeepSeek V4/V3中使用的Context Parallelism:
  将128K序列切分到8个GPU → 每GPU处理16K
  KV Cache从160GB降到20GB/GPU
  通过Ring Attention在计算中传递K和V blocks

5.2 Ring Attention实现

class RingAttention(nn.Module):
    """
    Ring Attention(环形注意力)
    
    通过环形传递KV Block实现序列并行
    每个GPU只持有部分序列,在环上依次传递KV Block
    """
    def __init__(
        self,
        num_heads: int,
        head_dim: int,
        sp_size: int,
        sp_rank: int,
        local_seq_len: int,  # 本GPU持有的序列长度
    ):
        super().__init__()
        self.num_heads = num_heads
        self.head_dim = head_dim
        self.sp_size = sp_size
        self.sp_rank = sp_rank
        self.local_seq_len = local_seq_len
        
        # 环形通信组
        self.prev_rank = (sp_rank - 1) % sp_size
        self.next_rank = (sp_rank + 1) % sp_size
    
    def ring_attention_forward(
        self,
        q: torch.Tensor,     # [B, H, S_q, d]
        local_k: torch.Tensor,  # [B, H, S_local, d]
        local_v: torch.Tensor,  # [B, H, S_local, d]
        attention_mask=None,
    ):
        """
        Ring Attention前向
        
        每轮计算Q与本GPU的KV的attention得分
        然后传递KV到下一GPU,再算下一轮的得分
        累计所有轮的得分 → 完整attention
        """
        batch, num_heads, seq_q, _ = q.shape
        
        # 初始化输出
        output = torch.zeros_like(q)
        
        # Ring communication: 每轮传递KV block
        # 共sp_size轮,每轮处理一个block
        current_k = local_k
        current_v = local_v
        
        # 分块softmax统计量
        # 传统方式需要每轮保存score,这里用分段softmax方法
        max_score = torch.full((batch, num_heads, seq_q, 1), -float('inf'), device=q.device)
        exp_sum = torch.zeros(batch, num_heads, seq_q, 1, device=q.device)
        
        for step in range(self.sp_size):
            # 当前block的attention计算
            # score = Q × K^T / sqrt(d)
            # [B, H, S_q, S_local]
            attn_scores = torch.matmul(q, current_k.transpose(-2, -1))
            attn_scores = attn_scores / (self.head_dim ** 0.5)
            
            # 安全的softmax(分段计算)
            # safe softmax: exp(x - max)
            block_max = attn_scores.max(dim=-1, keepdim=True).values  # [B, H, S_q, 1]
            new_max = torch.maximum(max_score, block_max)
            
            # 调整之前的exp_sum到新的max
            exp_sum = exp_sum * torch.exp(max_score - new_max)
            
            # 当前block的exp值
            block_exp = torch.exp(attn_scores - new_max)  # [B, H, S_q, S_local]
            block_sum = block_exp.sum(dim=-1, keepdim=True)  # [B, H, S_q, 1]
            
            # 累加exp_sum
            exp_sum = exp_sum + block_sum
            max_score = new_max
            
            # 当前block对output的贡献
            block_output = torch.matmul(block_exp, current_v)  # [B, H, S_q, d]
            
            # 如果之前有output值,需要rescale
            if step > 0:
                output = output * torch.exp(max_score - block_max)
            
            output = output + block_output
            
            # 传递KV到下一GPU
            if step < self.sp_size - 1:
                # 发送自己的KV,接收下一GPU的KV
                # 这里简化实现
                send_k = current_k
                send_v = current_v
                
                # 实际需要:
                # dist.send(send_k, dst=self.next_rank)
                # recv_k = dist.recv(src=self.prev_rank)
                # current_k = recv_k
                # current_v同理
                pass
        
        # 最终softmax归一化
        output = output / exp_sum
        
        return output

5.3 SP的应用场景

SP的适用场景分析:

┌──────────────┬──────────┬──────────┬──────────┬──────────┐
│ 上下文长度    │ 单GPU   │ SP=4     │ SP=8     │ 推荐策略  │
│              │ 可行性   │          │          │          │
├──────────────┼──────────┼──────────┼──────────┼──────────┤
│ 4K           │ ✅       │ 不需要   │ 不需要   │ 单卡     │
│ 8K           │ ✅       │ 不需要   │ 不需要   │ 单卡     │
│ 32K          │ ✅       │ 可选     │ 可选     │ 单卡或SP │
│ 64K          │ ⚠️ 看模型 │ 推荐     │ 推荐     │ SP=4     │
│ 128K         │ ❌ 大部  │ 推荐     │ 推荐     │ SP=4-8   │
│ 256K         │ ❌       │ 推荐     │ 推荐     │ SP=8     │
│ 1M+          │ ❌       │ ⚠️ 部分   │ 推荐     │ SP=16+   │
└──────────────┴──────────┴──────────┴──────────┴──────────┘

生产实践:
  SP主要用于"超长上下文推理"
  典型配置:TP=8 + PP=4 + SP=4(3D并行 + SP)
  每GPU只处理 128K/4 = 32K 的KV Cache
  → KV Cache从160GB降到40GB/GPU

六、混合并行:3D/4D并行策略组合

6.1 3D并行(TP + PP + DP/EP)

2026年大模型推理的标准并行策略:3D并行

3D并行 = TP(张量平行,节点内)+ PP(流水线平行,跨节点)+ EP(专家并行,MoE)

LLaMA-70B推理的3D并行配置:
  GPUs=32(4节点 × 8卡)
  TP=8(节点内NVLink)
  PP=4(跨节点,4个节点)
  DP=1(推理不需要数据并行——因为每个请求不同)

每个维度解决什么问题:
  TP:切分权重矩阵 → 放下模型
  PP:切分层数 → 扩展多节点
  EP:切分expert → 处理MoE

┌──────────────────────────────────────────────────────────┐
│                                                           │
│  3D并行架构(4节点 × 8卡 = 32卡):                        │
│                                                           │
│  ┌─────────────────────────────────────────────────┐     │
│  │  PP=4 ——— 4个流水线阶段                           │     │
│  │                                                   │     │
│  │  Stage 0     Stage 1     Stage 2     Stage 3      │     │
│  │  (Node 0)    (Node 1)    (Node 2)    (Node 3)    │     │
│  │  ┌──────┐   ┌──────┐   ┌──────┐   ┌──────┐     │     │
│  │  │TP=8  │   │TP=8  │   │TP=8  │   │TP=8  │     │     │
│  │  │GPU 0-7│  │GPU 0-7│  │GPU 0-7│  │GPU 0-7│     │     │
│  │  │L0-19 │   │L20-39│   │L40-59│   │L60-79│     │     │
│  │  └──────┘   └──────┘   └──────┘   └──────┘     │     │
│  │       ↑ P2P ↑       ↑ P2P ↑       ↑ P2P ↑      │     │
│  │       (网络)         (网络)         (网络)        │     │
│  │                                                   │     │
│  └─────────────────────────────────────────────────┘     │
│                                                           │
│  通信层次:                                                │
│    TP通信:节点内NVLink — 900GB/s — AllReduce             │
│    PP通信:节点间网络 — 200GB/s (IB) — P2P send/recv      │
│                                                           │
└──────────────────────────────────────────────────────────┘

6.2 4D并行(3D + SP)

4D并行 = TP + PP + EP + SP

用于超大规模MoE + 超长上下文的组合

DeepSeek-V4推理的4D并行配置:
  GPUs=128(16节点 × 8卡)
  TP=4(节点内,2组×4)
  PP=4(跨4节点)
  EP=8(跨8节点,专家分布)
  SP=4(超长序列并行)

每个维度的显存分布:
  TP:权重按head切分 → 每GPU 1/4权重
  PP:按层切分 → 每GPU 1/4层
  EP:按expert切分 → 每GPU 1/8 expert
  SP:按序列切分 → 每GPU 1/4上下文

总GPU数 = TP × PP × EP(SP在序列维度复用)

4D并行的挑战:
  1. 通信拓扑复杂:TP的AllReduce + PP的P2P + EP的All-to-All + SP的Ring
  2. 调度:4个维度交织,需要全局协调
  3. 显存分配:需要精确计算每GPU的权重/缓存分配

6.3 并行策略组合选择

def recommend_parallel_strategy(
    model_params_b: float,      # 模型参数量(B)
    num_gpus: int,              # 可用GPU数
    gpu_memory_gb: float,       # 单GPU显存(GB)
    gpu_nvlink: bool,           # 是否有NVLink
    inter_node_bandwidth: float, # 跨节点带宽(GB/s)
    avg_seq_len: int,           # 平均序列长度
    is_moe: bool,               # 是否是MoE模型
    num_experts: int = None,    # MoE的expert数
):
    """
    推荐并行策略
    
    核心约束:权重 + KV Cache + 额外开销 ≤ 单GPU显存
    """
    # 权重显存(FP16)
    weights_gb = model_params_b * 2  # bytes
    
    # 最小并行度 = ceil(权重显存 / 单GPU可用显存)
    min_gpus = int(weights_gb / (gpu_memory_gb * 0.85))
    
    if num_gpus < min_gpus:
        return "ERR: GPU数量不足"
    
    if is_moe:
        # MoE模型 → EP优先
        return recommend_moe_parallel(
            num_gpus, gpu_nvlink, num_experts, avg_seq_len
        )
    
    if num_gpus <= 8:
        # 单节点
        if gpu_nvlink:
            return {"TP": num_gpus, "PP": 1, "SP": 1}
        else:
            return {"TP": 1, "PP": num_gpus, "SP": 1}
    
    if 8 < num_gpus <= 64:
        # 多节点,中等规模
        nodes = num_gpus // 8
        
        if avg_seq_len > 32768:
            return {"TP": 4, "PP": nodes, "SP": 2}
        else:
            return {"TP": 8, "PP": nodes, "SP": 1}
    
    # 大规模(64+ GPU)
    if avg_seq_len > 65536:
        return {"TP": 4, "PP": num_gpus // 16, "SP": 4}
    else:
        return {"TP": 8, "PP": num_gpus // 8, "SP": 1}


# 典型并行配置表
def example_parallel_configs():
    """
    典型模型的推荐并行配置(2026年):
    
    ┌─────────────────┬────────┬────────┬──────────────────────┐
    │ 模型             │ GPU数  │ 并行   │ 每GPU显存占用        │
    │                 │        │ 策略   │                      │
    ├─────────────────┼────────┼────────┼──────────────────────┤
    │ LLaMA-3.1-8B    │ 1      │ 单卡   │ 16GB(权重)+8GB(KV)   │
    │ LLaMA-3.1-70B   │ 4      │ TP=4   │ 35GB(权)+10GB(KV)    │
    │ LLaMA-3.1-70B   │ 8      │ TP=8   │ 17.5GB(权)+5GB(KV)   │
    │ LLaMA-3.1-405B  │ 16     │ TP=8+PP=2 │ 25GB+5GB         │
    │ Qwen-2.5-72B    │ 4      │ TP=4   │ 36GB+10GB            │
    │ DeepSeek-V4     │ 64     │ 4D     │ 多维度组合            │
    │ Mixtral-8x22B   │ 8      │ EP=8   │ 35GB(专家)+7GB(Router)│
    │ Mixtral-8x22B   │ 16     │ EP=8+TP=2 │ 17.5GB+3.5GB      │
    │ GLM-5-130B      │ 8      │ TP=8   │ 32.5GB+7GB           │
    │ GPT-5.5         │ 64+    │ 定制   │ 取决于配置            │
    └─────────────────┴────────┴────────┴──────────────────────┘
    """
    pass

七、各并行策略对比与选型指南

7.1 全面对比表

五大并行策略全面对比:

┌─────────────────┬────────┬────────┬────────┬────────┬────────┐
│ 特性             │ TP      │ PP      │ EP      │ SP      │ DP      │
│                 │ 张量    │ 流水线  │ 专家    │ 序列    │ 数据    │
├─────────────────┼────────┼────────┼────────┼────────┼────────┤
│ 切分维度         │ 权重   │ 层      │ 专家   │ 序列   │ 数据   │
│ 通信模式         │ AllRd  │ P2P    │ A2A    │ Ring   │ AllRd  │
│ 通信量           │ 高     │ 低     │ 中     │ 中     │ 高     │
│ 带宽需求         │ NVLink │ PCIe   │ PCIe   │ NVLink │ NVLink │
│ 推理延迟影响      │ 小     │ 大     │ 小     │ 中     │ N/A    │
│ Prefill加速       │ ✅     │ ⚠️     │ ✅     │ ✅     │ ❌     │
│ Decode加速       │ ✅     │ ❌     │ ✅     │ ⚠️     │ ❌     │
│ 显存节省          │ ✅     │ ✅     │ ✅     │ ✅     │ ❌     │
│ 负载均衡          │ ✅     │ ⚠️     │ ⚠️     │ ✅     │ ✅     │
│ 最大并行度        │ head数 │ 层数   │ 专家数 │ 序列长 │ 无限   │
│ 跨节点能力        │ ❌     │ ✅     │ ✅     │ ❌     │ ✅     │
│ 实现复杂度        │ 中     │ 高     │ 高     │ 极高   │ 低     │
└─────────────────┴────────┴────────┴────────┴────────┴────────┘

7.2 选型决策树

推理场景并行策略决策树:

你的模型多大?
│
├─ < 40B → 单卡H100就够了
│  └─ 直接单卡推理(最简单+最低延迟)
│
├─ 40B~200B(如70B、130B)
│  ├─ 单节点(≤8卡)?
│  │  ├─ 有NVLink → TP=GPU数(最佳)
│  │  └─ 无NVLink → TP=4 + PP=2
│  └─ 多节点 → TP=8(节点内)+ PP=节点数
│
├─ 200B~1T(如DeepSeek-V4)
│  ├─ MoE模型 → EP优先 + TP辅助
│  └─ Dense模型 → TP+PP组合
│
└─ > 1T
    └─ 需要定制并行方案(4D并行)

额外考虑:
  长上下文(>64K) → 加SP
  长输出(>2K) → 减小PP(PP增加decode延迟)
  延迟敏感 → 只用TP,不用PP
  吞吐优先 → 加大PP和micro-batch

八、生产部署最佳实践

8.1 vLLM多GPU配置

# ===== vLLM多GPU推理 =====

from vllm import LLM, SamplingParams

# ===== TP(张量并行) =====
# 最简单、效果最好的多GPU方案

llm_tp = LLM(
    model="meta-llama/Llama-3.1-70B-Instruct",
    tensor_parallel_size=4,     # TP=4 → 使用4张GPU
    # vLLM内部自动处理:权重切分 + AllReduce通信
    # 不需要手动实现TP逻辑
    
    # vLLM的TP配置要点:
    # - TP大小必须是2^N(2,4,8)
    # - TP大小 ≤ 单节点的GPU数
    # - 跨节点TP不可用(vLLM限制)
    
    max_model_len=65536,
    gpu_memory_utilization=0.90,
)

# ===== PP(流水线并行) =====
# vLLM 0.6.0+ 支持PP

llm_pp = LLM(
    model="meta-llama/Llama-3.1-70B-Instruct",
    tensor_parallel_size=4,     # 节点内TP
    pipeline_parallel_size=2,   # 跨节点PP
    # 总GPU = TP × PP = 4 × 2 = 8张
)

# ===== 混合并行 =====
# TP + PP组合

llm_3d = LLM(
    model="meta-llama/Llama-3.1-405B",
    tensor_parallel_size=8,     # 节点内8卡的NVLink AllReduce
    pipeline_parallel_size=4,   # 跨4个节点
    # 总GPU = 8 × 4 = 32张
    
    # 流水线参数
    num_scheduler_steps=8,      # Micro-batch大小
    # 越大 → 流水线越满 → 效率越高
    # 但需要更多并发请求
    
    max_num_seqs=64,            # 最大并发请求
    max_num_batched_tokens=65536,
)

# ===== TP下的KV Cache管理 =====
# vLLM在TP下自动管理:
# - KV Cache按TP维度切分(每GPU只存自己的head)
# - 不需要手动管理
# - 但注意:长序列下所有GPU的KV Cache总和需 ≤ 总显存

8.2 TensorRT-LLM并行配置

# TensorRT-LLM的多GPU配置
# 需要在build engine时确定,无法运行时更改

# 1. 构建引擎
def build_trt_llm_engine(model_dir, tp_size, pp_size):
    """
    构建TensorRT-LLM引擎
    TP和PP在构建时确定
    """
    from tensorrt_llm.builder import Builder
    
    builder = Builder()
    
    # TP配置
    builder.tensor_parallel = tp_size
    builder.pipeline_parallel = pp_size
    
    # KV Cache配置
    builder.max_num_tokens = 8192
    builder.max_batch_size = 64
    builder.max_input_len = 4096
    builder.max_output_len = 1024
    
    # 构建
    engine = builder.build_serial(model_dir)
    return engine

# 2. 运行时加载
def run_trt_llm_inference(engine_dir, tp_size):
    """
    运行TensorRT-LLM推理
    
    注意:TensorRT-LLM的TP在build时确定
    运行时不能更改
    """
    import tensorrt_llm
    
    # 初始化分布式
    tensorrt_llm.mpi_initialize()
    
    # 加载引擎
    runtime = tensorrt_llm.Runtime()
    
    # 指定运行的GPU
    runtime.set_device(tp_size)  # 使用tp_size张GPU
    
    # 推理
    session = runtime.create_session(engine_dir)
    outputs = session.infer(inputs)
    
    return outputs

8.3 并行推理常见问题排查

Q: TP推理时显存不足(即使在多GPU上)?
A: 检查:
  1. TP大小是否足够(每GPU的权重 = 总权重/TP)
  2. KV Cache是否也按TP切分
  3. gpu_memory_utilization是否适当
  4. 是否同时启用了其他并行(PP/EP会占用额外显存)
  
  公式:每GPU显存 = 权重/TP + KV_Cache/TP + 额外开销

Q: TP推理变慢(相比单卡没有加速)?
A: 原因:通信开销占比太大
  检查:
  1. 是否使用NVLink(PCIe的AllReduce很慢)
  2. batch是否太小(batch=1时通信占比大)
  3. TP是否太大(TP=16时碎片化严重)
  
  建议:
  - batch < 4 → TP=2或TP=4(不要TP=8)
  - batch > 32 → TP=8(通信被分摊)

Q: PP推理延迟比单卡还高?
A: 正常现象!PP在推理中通常不降低延迟。
  原因:PP把模型串行化了 → 需要串行通过所有阶段
  解决方法:
  1. 增加micro-batch数(填满流水线)
  2. 用TP代替PP(单节点内)
  3. 减小PP_size(PP=2而不是=4)

Q: 跨节点并行通信慢?
A: 检查:
  1. 网络带宽(InfiniBand vs 以太网)
  2. NCCL配置(NCCL_IB_DISABLE=0)
  3. 是否是跨机架/跨交换机
  4. 使用PP(通信量小)而非TP(通信量大)
  
  建议:跨节点用PP或EP,节点内用TP

8.4 性能调优清单

并行推理性能调优 checklist:

□ 1. 确定最小GPU数
   公式:ceil(模型权重显存 / (单GPU显存 × 0.85))
   70B (140GB) / (80GB × 0.85) = 2.06 → 至少3张H100

□ 2. TP大小选择
   单节点内:TP=GPU数(有NVLink)
   多节点:TP=8(节点内),PP=节点数
   TP大小必须是2的幂

□ 3. PP配置
   PP ≥ 2时 → 流水线气泡会增加延迟
   需要同时增大max_num_seqs(填满流水线)
   推荐:PP=2时至少4个并发请求

□ 4. Micro-batch大小(vLLM: num_scheduler_steps)
   PP+TP场景下,增大此值可以提高效率
   PP=2 → num_scheduler_steps=4
   PP=4 → num_scheduler_steps=8

□ 5. 验正通信
   nvidia-smi topo -m → 确认GPU拓扑
   NCCL测试:nccl-tests all_reduce_perf
   目标:AllReduce带宽 > 90% NVLink理论值

□ 6. 端到端测试
   用1条请求测试:确认延迟在可接受范围
   逐步增加并发 → 直到吞吐量达到峰值
   监控P50/P99 TPOT是否满足SLO

□ 7. 常见指标(70B, TP=4, H100×4):
   单请求P50 TPOT: 25-35ms
   吞吐量: 2000-4000 tok/s(取决于并发)
   P50 TTFT: 200-400ms(取决于输入长度)

九、面试高频问答

9.1 基础理解

Q1: 张量并行(TP)和流水线并行(PP)的根本区别?
A: 切分维度和通信模式不同:
  TP按权重矩阵的维度切分 —— 每层都需要AllReduce通信
  PP按层数切分 —— 只在层间做一次P2P通信
  
  类比:
  TP = 把人切成两半分给两个人,两人同时站起来(通信合并)
  PP = 两个人一人负责一半路程,接力跑(串行传递)

Q2: 为什么推理中PP会增加延迟?
A: 因为推理不需要反向传播,PP变成"纯串行"。
  训练时PP可以重叠前向和反向(1F1B调度),隐藏气泡。
  推理只有前向,所以每个请求必须串行通过所有PP阶段。
  结果是:PP_size × 单GPU时间 → PP=8 → 延迟增加8倍。

Q3: MoE模型为什么天然适合EP而不是TP?
A: MoE模型的expert是独立的FFN单元。
  TP把一个expert切到多GPU → 每GPU只算部分 → 还需要AllReduce
  EP把不同的expert放到不同GPU → 每个GPU算完整expert → 只需All-to-All路由

  简单说:TP增加了一块工作的复杂度,EP只是分了工作。

9.2 原理深度

Q4: TP的通信量为什么不随TP大小变化?
A: 因为TP的AllReduce需要汇总全部数据。
  每GPU先算1/tp,然后AllReduce合并所有GPU的结果。
  AllReduce的通信量 = 2 × 数据量 × (tp-1)/tp ≈ 2 × 数据量。
  所以tp增大 → 每个GPU通信量≈不变 → 只是并行度提高了。

Q5: PP推理中micro-batch怎么工作?
A: 类似CPU的指令流水线:
  - 没有micro-batch:请求A走完所有PP阶段后,再开始请求B
  - 有micro-batch:请求A在Stage1时,可以同时开始请求B在Stage0
  
  需要至少PP_size个并发请求才能填满流水线:
  PP=4 → 需要4个请求同时在流水线中
  第一个请求的延迟 = 4×单步时间(没变!)
  但后续请求的延迟 ≈ 单步时间(流水线填满后)
  所以PP在吞吐量上有效,在单请求延迟上无效。

Q6: EP的负载均衡为什么是挑战?
A: 因为MoE的Router可能把大多数token路由到少数expert。
  例如:50%的token路由到expert_0,5%到expert_7
  → expert_0的GPU负载是expert_7的10倍
  → 所有token都要等expert_0处理完 → 延迟受最慢的GPU限制
  
  解决方案:
  1. Router训练时加负载均衡loss
  2. 推理时用auxiliary loss约束路由分布
  3. 动态expert复制(把热点expert复制到多GPU)

9.3 生产与高级

Q7: TP=8 vs PP=8 在推理场景哪个更好?
A: 几乎总是TP更好(单节点内)。
  原因:TP=8时每步延迟 ≈ 单卡延迟(因为并行计算)
       PP=8时每步延迟 ≈ 8×单卡延迟(因为串行)
  
  但TP需要在NVLink互联的同一个节点内
  跨节点时只能用PP(PCIe/网络带宽不够TP的AllReduce)
  
  选型:单节点 → TP;多节点 → TP(节点内)+PP(跨节点)

Q8: 3D并行的通信瓶颈在哪里?
A: 三种通信的瓶颈不同:
  TP AllReduce:带宽瓶颈(NVLink 900GB/s够用)
  PP P2P:延迟瓶颈(网络延迟>计算延迟)
  EP All-to-All:负载均衡瓶颈(热点expert)
  
  通常PP是最大的瓶颈(网络延迟),其次是TP(带宽),最后是EP。

Q9: 大规模推理(64+GPU)的并行策略建议?
A: 推荐TP+PP+SP组合:
  - TP=4或8(节点内,利用NVLink)
  - PP=节点数(跨节点,通信少)
  - SP=2或4(超长上下文的额外维度)
  
  对于MoE模型:加EP
  不推荐:纯TP跨节点(带宽不够)
  不推荐:纯PP单节点(未利用NVLink)

Q10: 2026年模型并行的发展方向?
A: 三个趋势:
  1. 推理专用的并行策略(与训练解耦,如Splitwise的prefill/decode分离)
  2. PD分离(Prefill和Decode用不同并行配置)
  3. 存算分离(权重存CPU/SSD,只放KV Cache在GPU)

总结:模型并行是大模型推理"越大越需要"的技术。TP是最通用的策略(节点内,NVLink必需),PP用于跨节点扩展(但增加延迟),EP是MoE模型的最优解(通信少),SP用于超长上下文。实际部署中,3D并行(TP+PP+SP)是2026年70B+模型的标准配置。选型核心原则:能TP就不PP,有MoE就用EP,超长文本加SP。

下期预告:推理与部署篇11——服务化部署:从FastAPI到Triton Inference Server的生产级推理架构

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐