【推理与部署篇10】模型并行实战指南:张量并行、流水线并行与专家并行
·
【推理与部署篇10】模型并行实战指南:张量并行、流水线并行与专家并行
前言:当模型大到单张GPU放不下时——LLaMA-70B需要140GB、DeepSeek-V4需要300GB+——唯一的办法就是"切开"模型,放到多张GPU上。这就是模型并行。但在推理场景下,模型并行面临与训练完全不同的挑战:延迟敏感、请求动态变化、KV Cache跨GPU管理。本文从张量并行(TP)、流水线并行(PP)、专家并行(EP)到序列并行(SP),覆盖所有推理场景的模型并行策略,附完整实现代码和通信分析。
📋 目录
- 一、为什么需要模型并行?
- 二、张量并行(Tensor Parallelism):切矩阵,不减通信
- 三、流水线并行(Pipeline Parallelism):切层数,藏延迟
- 四、专家并行(Expert Parallelism):MoE模型的"自然切分"
- 五、序列并行(Sequence/Context Parallelism):切上下文
- 六、混合并行:3D/4D并行策略组合
- 七、各并行策略对比与选型指南
- 八、生产部署最佳实践
- 九、面试高频问答
一、为什么需要模型并行?
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的生产级推理架构
更多推荐



所有评论(0)