六大章节 | 训练并行 + 推理工程 + 部署服务化
DDP · FSDP · Tensor/Pipeline · ZeRO · vLLM · 量化 · Gateway
训练一个语言模型本质上是用大量样本估计参数。单卡训练大模型的瓶颈是两件事——显存放不下、吞吐跑不完。当模型参数量超过单 GPU 显存上限(即便上了梯度检查点和混合精度也放不下),或单卡训练完所需时间无法接受(动辄几个月),必须把工作切到多卡甚至多机。
分布式训练的并行策略主要分三类:
三者可以正交组合,构成 3D 并行:$N_{\text{GPU}} = \text{DP} \times \text{TP} \times \text{PP}$。本章先讲扩展性最好、最易上手、最常用的数据并行,串起三代实现:PyTorch DataParallel(DP,单机多卡,已过时)、DistributedDataParallel(DDP,事实标准)、FullyShardedDataParallel(FSDP,ZeRO-3 的 PyTorch 原生版本)。
flowchart LR
subgraph Strategies["三种并行策略"]
direction TB
DP["Data Parallel<br/>同一参数副本<br/>不同 batch slice"]
TP["Tensor Parallel<br/>同一层切到多卡<br/>层内通信密集"]
PP["Pipeline Parallel<br/>不同层在不同卡<br/>流水线气泡"]
end
DP -.正交.- TP
TP -.正交.- PP
DP -.正交.- PP
Strategies --> D3["3D 并行<br/>N_GPU = DP x TP x PP"]
PyTorch 早期的 nn.DataParallel 将一个 batch 切分到多卡前向,主卡(rank 0)负责聚合损失、反传梯度、广播参数。它的核心问题有四个:
结论:生产中不要再用 DP,统一用 DDP。PyTorch 官方文档也明确不推荐。
DDP 是 多进程架构——每张 GPU 对应一个独立 Python 进程,每个进程持有完整模型副本,绕开 GIL。前向各算各的,反向时通过 AllReduce 把所有进程的梯度求和并平均,保证参数更新一致。
朴素 AllReduce 让所有节点把数据发给一个聚合节点再广播回来,总通信量 $2(N-1)Pb$,且聚合节点带宽成瓶颈。Ring-AllReduce(Baidu, 2017)把所有 GPU 排成环,分两阶段:
总通信量:
$$\text{Comm}_{\text{AllReduce}} = 2 \cdot \frac{N-1}{N} \cdot P \cdot b \approx 2 P b \quad (N \to \infty)$$
关键事实:通信量与 GPU 数 $N$ 无关,所以 Ring-AllReduce 在带宽足够时是线性扩展的。但这只是带宽维度的好消息——延迟仍随 $N$ 线性增长,环路一圈要走 $2(N-1)$ 步。所以 Ring 在小集群(< 16 GPU)效率最高;大集群常用 Tree-AllReduce 或 NCCL 的 Double Binary Tree 把延迟降到 $O(\log N)$。
flowchart LR
subgraph Ring["Ring-AllReduce on 4 GPUs"]
direction LR
G0["GPU0<br/>shard a"]
G1["GPU1<br/>shard b"]
G2["GPU2<br/>shard c"]
G3["GPU3<br/>shard d"]
G0 -->|step 1| G1
G1 -->|step 2| G2
G2 -->|step 3| G3
G3 -->|step 4| G0
end
note["每步只发 P/N 数据<br/>2(N-1) 步完成 ReduceScatter+AllGather<br/>总通信约 2Pb,与 N 无关"]
Ring -.- note
实际通信由 NVIDIA NCCL 库执行。NCCL 在初始化时自动探测节点内 NVLink/NVSwitch、节点间 InfiniBand/RoCE 拓扑,构造最优环:
排查通信问题最有用的环境变量:NCCL_DEBUG=INFO、NCCL_TOPO_DUMP_FILE=topo.xml、NCCL_IB_DISABLE=1(关 IB 用 TCP 兜底)。
朴素 DDP 每个参数的梯度都触发一次小 AllReduce,开销巨大(NCCL 启动延迟约 10 μs/调用)。PyTorch 用 gradient bucket(默认 25 MB)合并相邻反向算出的梯度,凑满一桶后一次性 AllReduce。这一桶 AllReduce 在后台 NCCL stream 上进行,与上游层的反向计算并行执行,理想情况下完全隐藏通信时间。
sequenceDiagram
participant L as Layers (bwd order)
participant B as Bucket
participant N as NCCL stream
L->>B: dL/dW_n (last layer)
L->>B: dL/dW_(n-1)
Note over B: 桶 1 凑满 25 MB
B->>N: AllReduce(bucket 1)
L->>B: dL/dW_(n-2)
L->>B: dL/dW_(n-3)
Note over L,N: 计算与通信重叠
B->>N: AllReduce(bucket 2)
L->>B: dL/dW_1 (first layer)
B->>N: AllReduce(last bucket)
import torch
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
dist.init_process_group(backend="nccl")
torch.cuda.set_device(local_rank)
model = MyModel().to(local_rank)
model = DDP(
model,
device_ids=[local_rank],
bucket_cap_mb=25, # 桶大小
gradient_as_bucket_view=True, # 梯度直接用 bucket 视图,省一次拷贝
find_unused_parameters=False, # 关闭可加速;开启会增加一次反向遍历
static_graph=True, # 训练图不变时启用,可让 DDP 进一步优化通信
)
关键调优点:
DDP 把参数完整复制到每张卡,显存浪费严重。FSDP(Fully Sharded Data Parallel,对应 DeepSpeed 的 ZeRO-3)把参数、梯度、优化器状态都按 GPU 分片,只在前向/反向需要某层时临时 AllGather 整层参数。
模型参数量 $P$,混合精度训练(FP16 / BF16 计算 + FP32 主权重 + Adam $m,v$):
| 策略 | 参数 | 梯度 | 优化器状态 | 合计 |
|---|---|---|---|---|
| DDP | $2P$ (fp16) | $2P$ (fp16) | $12P$ (master+m+v, fp32) | $16P$ 字节/卡 |
| FSDP / ZeRO-3 | $2P / N$ | $2P / N$ | $12P / N$ | $16P / N$ 字节/卡 |
代价:多了两次通信。FSDP 一次前向需要 AllGather 整层参数(一次),反向需要 AllGather 一次 + ReduceScatter 梯度一次。总通信量约 $3Pb$(DDP 是 $2Pb$)。即 1.5× 通信换 $N$× 显存。在显存受限时是绝对值得的。
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
from torch.distributed.fsdp import MixedPrecision, ShardingStrategy
from torch.distributed.fsdp.wrap import transformer_auto_wrap_policy
from functools import partial
mp_policy = MixedPrecision(
param_dtype=torch.bfloat16, # 参数与计算用 bf16
reduce_dtype=torch.float32, # 梯度规约用 fp32 防溢出
buffer_dtype=torch.bfloat16,
)
wrap_policy = partial(
transformer_auto_wrap_policy,
transformer_layer_cls={TransformerBlock}, # 每个 Transformer 层作为一个分片单元
)
model = FSDP(
model,
sharding_strategy=ShardingStrategy.FULL_SHARD, # ZeRO-3
mixed_precision=mp_policy,
auto_wrap_policy=wrap_policy,
device_id=local_rank,
limit_all_gathers=True, # 限制 AllGather 队列深度,省显存
use_orig_params=True, # 必开,否则 optimizer 看到的不是原参数对象
backward_prefetch=BackwardPrefetch.BACKWARD_PRE, # 反向前预取下一层参数
)
FSDP 的 ShardingStrategy 四档:
FULL_SHARD(ZeRO-3):参数+梯度+优化器都分片;最省显存,通信最重SHARD_GRAD_OP(ZeRO-2):只分片梯度和优化器,参数仍完整复制;通信量与 DDP 同NO_SHARD:等同 DDPHYBRID_SHARD(最新):节点内 FULL_SHARD,节点间 NO_SHARD —— NVLink 上做参数分片,节点间走 AllReduce。是 H100 集群的实用甜点。FSDP 把模型拆成若干 wrap unit,每个 unit 内的参数一起 AllGather/ReduceScatter。Unit 越细:
实践默认:每个 Transformer 层一个 unit(用 transformer_auto_wrap_policy)。对于 70B+ 模型,每层参数大约 200 MB~500 MB,平衡得最好。
当单步显存放不下目标 batch size,可以用梯度累积把一个大 batch 拆成 $K$ 个 micro-batch,每个 micro-batch 反向出来的梯度累加在 .grad 上但不立即 optimizer.step(),累加 $K$ 次后再统一更新:
$$\text{Effective Batch} = \text{micro\_batch} \times \text{DP\_world\_size} \times \text{grad\_accum\_steps}$$
for step, batch in enumerate(loader):
is_last_micro = (step + 1) % K == 0
sync_ctx = nullcontext() if is_last_micro else model.no_sync()
with sync_ctx:
loss = model(batch) / K # 缩放避免梯度爆炸
loss.backward() # 累加到 .grad
if is_last_micro:
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
optimizer.zero_grad(set_to_none=True)
关键技巧 model.no_sync():在累积期间禁用 DDP/FSDP 的梯度同步,只在最后一次反向才触发 AllReduce/ReduceScatter,可以省 $K-1$ 次通信。Hugging Face Trainer 默认就是这种模式。
常见陷阱:
loss / K:累加后梯度幅度是单步的 $K$ 倍,学习率等效变大 $K$ 倍。除以 $K$ 即可。step() 之前、累加完成之后调用,不要在每个 micro-batch 调。flowchart TD
Start[选择数据并行策略] --> Q1{模型放得下单卡?}
Q1 -->|放得下| Q2{多机训练?}
Q1 -->|放不下| Q3{显存差多少?}
Q2 -->|单机| DDP1[DDP 即可]
Q2 -->|多机| DDP2[DDP + NCCL InfiniBand]
Q3 -->|差 2-4x| FSDP2[FSDP SHARD_GRAD_OP<br/>ZeRO-2]
Q3 -->|差 4x以上| FSDP3[FSDP FULL_SHARD<br/>ZeRO-3]
Q3 -->|远超 N倍| OFF[ZeRO-Offload<br/>+ HYBRID_SHARD]
DDP1 --> Done[训练 OK]
DDP2 --> Done
FSDP2 --> Done
FSDP3 --> Done
OFF --> Done
当单层参数大到放不进单卡时,必须做层内切分,即张量并行(Tensor Parallelism, TP)。最经典的方案来自 NVIDIA Megatron-LM (Shoeybi et al., 2019),针对 Transformer 的两个核心子层——MLP 和多头注意力——给出了精心设计的切分方式。
设 TP 度为 $t$(即把一层在 $t$ 张卡上切),切分准则是:同一前向中只在子层结尾做一次 AllReduce,把中间激活的切分代价压到最低。
为什么数据并行不够?考虑 175B 模型,混合精度训练每卡至少需要 $16P = 2.8$ TB 显存(按之前推导)。即使 FSDP 全分片到 1024 卡,每卡仍要 2.7 GB 参数 + 同等梯度+优化器,但单层的最大矩阵(GPT-3 是 $12288 \times 12288 \times 4 = 600$ MB 一层)放进单卡时,AllGather 整层参数的瞬时显存峰值就吃不消。TP 让单层永远不需要在单卡上完整存在,从根本上解决"一层太大"的问题。
线性层 $Y = X A$,矩阵 $A \in \mathbb{R}^{d_{\text{in}} \times d_{\text{out}}}$ 有两种切法。理解这两种切法的对偶性是 Megatron 设计的核心。
flowchart TB
subgraph Col["Column-Parallel: Y = X A, A 按列切"]
direction LR
X1["X (batch, d_in)
完整复制到每张卡"]
X1 --> M1["GPU0: X · A_1
= Y_1 (d_out/t)"]
X1 --> M2["GPU1: X · A_2
= Y_2 (d_out/t)"]
X1 --> M3["GPU2: X · A_3
= Y_3 (d_out/t)"]
M1 --> Out1["输出 Y 是切分的
无通信 ✓"]
M2 --> Out1
M3 --> Out1
end
把 $A$ 按列切:$A = [A_1, A_2, \ldots, A_t]$,每张卡持有 $A_i \in \mathbb{R}^{d_{\text{in}} \times d_{\text{out}}/t}$。每张卡独立计算 $Y_i = X A_i$,得到 $Y = [Y_1, \ldots, Y_t]$ 的拼接。前向无通信,但输出是切分的。
反向中需要 AllReduce 输入梯度 $\partial L / \partial X = \sum_i (\partial L / \partial Y_i) A_i^\top$,因为每张卡都贡献了对 $X$ 的梯度。
flowchart TB
subgraph Row["Row-Parallel: Y = X A, A 按行切,X 也切"]
direction LR
X1["X 已切分 = [X_1, X_2, X_3]"]
A1["A 按行切 = [A_1; A_2; A_3]"]
X1 --> M1["GPU0: X_1 · A_1 = Y_partial_1"]
X1 --> M2["GPU1: X_2 · A_2 = Y_partial_2"]
X1 --> M3["GPU2: X_3 · A_3 = Y_partial_3"]
M1 --> AR[AllReduce
Y = sum Y_partial_i]
M2 --> AR
M3 --> AR
AR --> Y[Y 完整输出]
end
把 $A$ 按行切:$A = [A_1; A_2; \ldots; A_t]$,输入也必须切分 $X = [X_1, \ldots, X_t]$。每张卡算 $Y_i = X_i A_i$,最后AllReduce 求和得到完整 $Y = \sum_i X_i A_i$。前向需要一次 AllReduce。
Megatron 的关键 trick:MLP 的第一个线性层用列并行(输出切分),紧接的 GeLU 是逐元素的可以直接在切分上算,第二个线性层用行并行(输入是切分的,正好对接)。整个 MLP 只在最后一次 AllReduce:
$$X \xrightarrow{\text{Col}} [Y_1, \ldots, Y_t] \xrightarrow{\text{GeLU}} [Z_1, \ldots, Z_t] \xrightarrow{\text{Row}} \sum_i Z_i B_i \xrightarrow{\text{AllReduce}} \text{Output}$$
反向同样只一次 AllReduce(梯度对应)。这是 Megatron 论文的精髓所在:切分方向的选择消除了中间通信,把通信代价压到最低。
多头注意力天然适合按 head 维度切:把 $h$ 个 head 平均分到 $t$ 张卡,每卡负责 $h/t$ 个头。$Q, K, V$ 的投影矩阵列并行切,每个头独立算 attention,输出投影行并行汇总。同样只在子层结尾一次 AllReduce。
flowchart LR
subgraph TransformerLayer["TP=2 下的 Transformer 层(一次前向)"]
direction TB
X["Input X
完整复制"]
subgraph Attn["Multi-Head Attention"]
direction LR
QKV0["GPU0: Q,K,V
head 0..h/2"]
QKV1["GPU1: Q,K,V
head h/2..h"]
A0["GPU0: softmax(QK)V
per-head"]
A1["GPU1: softmax(QK)V
per-head"]
OUT_AR1["AllReduce after
output projection"]
QKV0 --> A0 --> OUT_AR1
QKV1 --> A1 --> OUT_AR1
end
subgraph MLP["FFN / MLP"]
direction LR
FC1_0["GPU0: W1 col-split"]
FC1_1["GPU1: W1 col-split"]
G0["GeLU"]
G1["GeLU"]
FC2_0["GPU0: W2 row-split"]
FC2_1["GPU1: W2 row-split"]
OUT_AR2["AllReduce"]
FC1_0 --> G0 --> FC2_0 --> OUT_AR2
FC1_1 --> G1 --> FC2_1 --> OUT_AR2
end
X --> Attn --> MLP
end
反向时同样需要一次 AllReduce(梯度对应),所以一层 Transformer 前+反共 4 次 AllReduce(MLP 一次正一次反,Attention 一次正一次反)。设隐藏维 $h$、序列长 $s$、micro-batch 大小 $b$,单次 AllReduce 通信量 $\sim 2bsh$(输出激活),总通信量约:
$$\text{Comm}_{\text{TP,1 layer}} \approx 4 \cdot \frac{t-1}{t} \cdot 2bsh \cdot \text{bytes}$$
关键:TP 通信量与激活成正比,与参数量无关。这与 DDP 的 AllReduce(与参数成正比)正交,互补。TP 适合放在节点内(高速 NVLink),跨节点会被 InfiniBand 拖死。
以 LLaMA-3 70B 为例:$h=8192$,$s=4096$(训练 ctx),$b=2$ micro-batch,bf16(2 bytes),TP=8:
$$\text{Per layer Comm} \approx 4 \cdot \frac{7}{8} \cdot 2 \cdot 2 \cdot 4096 \cdot 8192 \cdot 2 \approx 920 \text{ MB / 层}$$
80 层共约 73 GB / step / 卡。NVLink4 单向 600 GB/s,理论传输时间 73/600 ≈ 122 ms,与单层计算时间相近,所以 TP=8 在 H100 NVLink 域内仍然可扩展。一旦跨节点(IB NDR 50 GB/s),传输时间涨到 1.5 s 量级,不可接受。TP 度严格不超过节点内 GPU 数是铁律。
当层数远超 TP 切分能力时,把不同层放到不同 GPU 上,构成流水线并行(Pipeline Parallelism, PP)。挑战是:原始的串行训练会让 GPU 大部分时间在等上游/下游,利用率极低。
把一个 mini-batch 拆成 $m$ 个 micro-batch,前向阶段连续把 $m$ 个 micro-batch 灌入流水线,等所有前向完成再统一反向。设 PP 度 $p$(即流水线深度)、单 stage 时间 $t$:
$$T_{\text{GPipe}} = (m + p - 1) \cdot t \cdot 2 \quad\text{(前向+反向)}$$
气泡(GPU 空闲)比例:
$$\text{Bubble Fraction} = \frac{p - 1}{m + p - 1}$$
典型实践 $m = 4p \sim 8p$,气泡率约 11%~20%。
gantt
title GPipe Schedule (p=4 stages, m=4 microbatches)
dateFormat X
axisFormat %s
section Stage 1
F1 :0, 1
F2 :1, 2
F3 :2, 3
F4 :3, 4
idle :crit, 4, 8
B4 :8, 9
B3 :9, 10
B2 :10, 11
B1 :11, 12
section Stage 2
idle :crit, 0, 1
F1 :1, 2
F2 :2, 3
F3 :3, 4
F4 :4, 5
idle :crit, 5, 7
B4 :7, 8
B3 :8, 9
B2 :9, 10
B1 :10, 11
section Stage 3
idle :crit, 0, 2
F1 :2, 3
F2 :3, 4
F3 :4, 5
F4 :5, 6
idle :crit, 6, 6
B4 :6, 7
B3 :7, 8
B2 :8, 9
B1 :9, 10
section Stage 4
idle :crit, 0, 3
F1 :3, 4
F2 :4, 5
F3 :5, 6
F4 :6, 7
B4 :7, 8
B3 :8, 9
B2 :9, 10
B1 :10, 11
GPipe 的问题是反向阶段所有 stage 的前向激活必须存满,显存占用 $O(m)$。1F1B 调度让每个 stage 在前向后立刻反向(One-Forward-One-Backward),最多只需要保留 $p$ 个 micro-batch 的激活:
$$\text{Activation Mem} = O(p) \cdot \text{per micro-batch}$$
气泡率与 GPipe 相同,但显存大幅降低。Megatron-LM 默认就是 1F1B。
flowchart LR
subgraph S1["Stage 1 (1F1B)"]
direction LR
F1_1["F1"] --> F1_2["F2"] --> F1_3["F3"] --> F1_4["F4"] --> B1_1["B1"] --> F1_5["F5"] --> B1_2["B2"] --> F1_6["F6"] --> B1_3["B3"] --> F1_7["F7"]
end
subgraph S4["Stage 4 (1F1B)"]
direction LR
idle["idle"] --> F4_1["F1"] --> B4_1["B1"] --> F4_2["F2"] --> B4_2["B2"] --> F4_3["F3"] --> B4_3["B3"] --> F4_4["F4"] --> B4_4["B4"]
end
note["注意: 一旦 stage 4 完成第 1 个 F+B
就可以释放该 micro-batch 的激活内存"]
Interleaved 1F1B(Megatron, Narayanan et al., 2021)把每张 GPU 的层数再分成 $v$ 个虚拟 stage,让 GPU 在多个虚拟 stage 间交替执行。气泡率降至:
$$\text{Bubble} = \frac{1}{v} \cdot \frac{p - 1}{m + p - 1}$$
$v = 2$ 即可把气泡减半。代价是通信次数从 $p$ 增至 $p \cdot v$,需要更低延迟的链路(NVLink/NVSwitch 上很值,跨节点 InfiniBand 上得权衡)。
对比表:
| 调度 | 气泡率 | 显存(per stage) | 通信次数 | 实现复杂度 |
|---|---|---|---|---|
| GPipe | $(p-1)/(m+p-1)$ | $O(m)$ 激活 | $2p$ | 简单 |
| 1F1B | $(p-1)/(m+p-1)$ | $O(p)$ 激活 | $2p$ | 中等 |
| Interleaved 1F1B | $\frac{1}{v}\cdot$ 上式 | $O(p \cdot v)$ | $2p \cdot v$ | 高 |
| Zero Bubble (ZB) | $\approx 0$ | $O(p)$ | 较 1F1B 多 | 很高 |
Zero Bubble Pipeline(Qi et al., 2024)把反向拆成"对激活的反向(B)"和"对权重的反向(W)"两步独立调度,可以把气泡进一步压到接近 0,是目前 SOTA。
TP 把激活按特征维 $h$ 切了,但 LayerNorm 和 Dropout 仍持有完整激活(因为 LN 涉及 $h$ 维统计,不能在 $h$ 切的副本上算)。激活显存占比可达 25%。序列并行(Korthikanti et al., 2022)把这些层的激活沿序列维 $s$ 切,与 TP 的特征切互补,整层激活全切:
$$\text{Act Mem per GPU} = \text{Layer Acts} \cdot \frac{1}{t} \quad \text{(seq + tensor)}$$
代价是 TP 的 AllReduce 换成 ReduceScatter+AllGather(通信量相同),不增加通信量但需要额外同步点。Megatron-LM 的现代版本默认开启。
序列并行进一步极端化就是 Context Parallel:把整条序列沿 $s$ 切到多卡,每张卡只持有 $s/c$ token 的激活。挑战在 attention:softmax 需要看到完整 QK,跨卡需要 AllGather 整段 K, V。Ring Attention (Liu et al., 2023) 用环形交换 KV 块,把 attention 做成流水线,理论上序列长度只受总 HBM 限制。这是训练 100K-1M context 模型的关键技术之一。
分析任何并行策略,第一步要算清楚单卡显存账。混合精度训练(Adam,主流)下,参数量 $P$ 个的模型,每个参数占用:
| 项 | 精度 | 字节/参数 | 用途 |
|---|---|---|---|
| FP16 / BF16 参数 | fp16 | 2 | 前向反向计算 |
| FP16 梯度 | fp16 | 2 | 反向产物 |
| FP32 主权重 | fp32 | 4 | Adam 用于更新(避免精度漂移) |
| Adam $m$(一阶动量) | fp32 | 4 | EMA(gradient) |
| Adam $v$(二阶动量) | fp32 | 4 | EMA(gradient²) |
| 合计 | 16 | ||
所以 7B 模型纯训练态显存约 $7 \times 10^9 \times 16 = 112 \text{ GB}$,再加激活、临时 buffer,单 80GB H100 也放不下。
前向中所有层中间结果保留下来给反向用,这部分通常被忽视但实际占比惊人。Korthikanti 等 (2022) 给出闭式:
$$M_{\text{act}} \approx s \cdot b \cdot L \cdot (34 h + 5 \cdot a \cdot s) \text{ bytes}$$
其中 $s$ 序列长、$b$ batch、$L$ 层数、$h$ 隐藏维、$a$ 注意力头数。$5as$ 那一项随序列平方增长,长上下文场景激活内存爆炸。
对策:梯度检查点(activation checkpointing)。只保留少数层的激活("checkpoint"),其它层反向时重新前向算一次。代价是反向多算约 33%。配 selective recomputation(只对计算便宜的层重算,比如 attention 的 softmax 和 dropout),实际可压到 5% 额外开销。
flowchart TB
subgraph Memory["7B 模型单卡显存占用 (b=2, s=4096)"]
direction TB
Param["参数 14 GB
梯度 14 GB"]
Optim["Adam 主权重+m+v 84 GB"]
Act["激活 约 50 GB
不含 checkpoint"]
TmpBuf["NCCL buffer+kernel 5 GB"]
Total["合计 约 167 GB
需 ZeRO-3 + checkpoint 才能装 80GB H100"]
Param --> Total
Optim --> Total
Act --> Total
TmpBuf --> Total
end
ZeRO(Rajbhandari et al., 2020)的核心思想:DDP 让每张卡持有相同的 16P 字节是冗余的,可以按 GPU 数 $N$ 分片。三个 stage 分别分片不同部分:
flowchart LR
subgraph S0["DDP / ZeRO-0"]
direction TB
D0_P["参数 2P × N"]
D0_G["梯度 2P × N"]
D0_O["优化器 12P × N"]
end
subgraph S1["ZeRO-1: 切优化器"]
direction TB
D1_P["参数 2P × N"]
D1_G["梯度 2P × N"]
D1_O["优化器 12P / N × N"]
end
subgraph S2["ZeRO-2: 切优化器 + 梯度"]
direction TB
D2_P["参数 2P × N"]
D2_G["梯度 2P / N × N"]
D2_O["优化器 12P / N × N"]
end
subgraph S3["ZeRO-3 (FSDP): 全分片"]
direction TB
D3_P["参数 2P / N × N"]
D3_G["梯度 2P / N × N"]
D3_O["优化器 12P / N × N"]
end
S0 --> S1 --> S2 --> S3
| Stage | 分片对象 | 单卡显存(混合精度 Adam) | 额外通信 |
|---|---|---|---|
| 0 (DDP) | 无 | $16P$ | $2P$ AllReduce |
| 1 | 优化器状态($m, v$, master weights) | $4P + 12P/N$ | $2P$ |
| 2 | + 梯度 | $2P + 14P/N$ | $2P$ |
| 3 | + 参数(FSDP) | $16P/N$ | $3P$(AllGather+ReduceScatter+AllGather) |
Stage 1/2 的关键好处是通信量不增加(仍是 $2P$,只是把 AllReduce 改成 ReduceScatter),但显存大幅下降。Stage 3 需要额外的 AllGather 来重组参数做前向,所以通信量从 $2P$ 涨到 $3P$,1.5× 代价换 $N$× 显存。
当 GPU 还是放不下,ZeRO-Offload(Ren et al., 2021)把优化器状态和梯度卸载到 CPU 内存,CPU 做 Adam 更新(CPU 慢但够用,因为 step 频率低)。GPU 显存进一步降到 $\sim 2P$(仅参数)。代价是 PCIe 传梯度成为瓶颈。
ZeRO-Infinity(Rajbhandari et al., 2021)更进一步,把参数也卸载到 CPU 甚至 NVMe SSD,配合 PCIe/NVMe 带宽预取。可以在 8 张 V100 上训练万亿参数模型,但训练吞吐显著下降(IO 成为瓶颈),主要用在无 H100 集群的场景。
flowchart TB
subgraph Tier["ZeRO-Infinity 显存分层 (单 GPU 视角)"]
direction LR
GPU["GPU HBM
当前活跃层参数
当前激活
当前梯度"]
CPU["CPU RAM
优化器状态
不活跃层参数"]
NVM["NVMe SSD
冷参数
checkpoint"]
GPU <-->|PCIe 64 GB/s 双向| CPU
CPU <-->|NVMe 7 GB/s 顺序| NVM
end
{
"zero_optimization": {
"stage": 3,
"offload_optimizer": {"device": "cpu", "pin_memory": true},
"offload_param": {"device": "cpu", "pin_memory": true},
"overlap_comm": true,
"contiguous_gradients": true,
"reduce_bucket_size": 1e8,
"stage3_prefetch_bucket_size": 5e7,
"stage3_param_persistence_threshold": 1e6
},
"bf16": {"enabled": true},
"gradient_accumulation_steps": 8,
"train_micro_batch_size_per_gpu": 1
}
三种并行可以正交组合:$N_{\text{GPU}} = \text{DP} \times \text{TP} \times \text{PP}$。一个典型 1024 卡 训练 175B 模型的配置:
flowchart TB
subgraph Cluster["1024 GPU = DP=8 × TP=8 × PP=16"]
direction TB
subgraph PP_Group["PP 维度: 16 个 stage,跨 16 节点"]
direction LR
ST1["Stage 1
Layers 1-6"]
ST2["Stage 2
Layers 7-12"]
STDOT["..."]
ST16["Stage 16
Layers 91-96"]
ST1 --> ST2 --> STDOT --> ST16
end
subgraph TP_Group["每 stage 内: TP=8 张卡,节点内 NVLink"]
direction LR
T1["GPU0..GPU7"]
end
subgraph DP_Group["DP=8 副本: 8 个完整流水线"]
direction LR
DP1["副本 1"]
DP2["副本 2"]
DPDOT["..."]
DP8["副本 8"]
end
PP_Group -.- TP_Group
TP_Group -.- DP_Group
end
选型经验法则:
| 维度 | DeepSpeed | Megatron-LM |
|---|---|---|
| 主要创新 | ZeRO 系列(显存分片) | 张量并行 + 1F1B 流水线 |
| 易用性 | JSON 配置,Hugging Face Trainer 集成 | 需要改模型代码(替换为并行版 layer) |
| 极限规模 | 用 Offload 可推到万亿 | 需要 3D 组合,需手工调拓扑 |
| 典型场景 | 10B 以下、显存受限 | 100B+、需 TP/PP |
实践中常见的 Megatron-DeepSpeed 组合:用 Megatron 的 TP+PP,用 DeepSpeed 的 ZeRO-1 做 DP,取两者之长。这也是 BLOOM、OPT、LLaMA-2 训练用的栈。
训练总计算量(前向+反向 ≈ 3 倍前向,对所有参数):
$$C = 6 \cdot P \cdot D \quad \text{FLOPs}$$
$P$ 参数量,$D$ 训练 token 数(DeepMind Chinchilla 法则建议 $D \approx 20P$)。一张 H100 的 BF16 算力 989 TFLOPS,实际利用率(MFU, Model FLOPs Utilization)通常 30%~50%。训练时间:
$$T_{\text{days}} = \frac{6 P D}{N \cdot \text{MFU} \cdot 989 \times 10^{12} \cdot 86400}$$
例:70B 模型,2T token,MFU = 45%,512 张 H100:
$$T = \frac{6 \times 7 \times 10^{10} \times 2 \times 10^{12}}{512 \times 0.45 \times 989 \times 10^{12} \times 86400} \approx 42 \text{ 天}$$
MFU 是衡量训练栈效率的核心指标。常见 MFU 范围:
| 场景 | 典型 MFU | 瓶颈 |
|---|---|---|
| 纯 DDP,小模型,单机 | 50-60% | kernel 启动开销 |
| FSDP,70B 单机多卡 | 40-50% | AllGather 通信 |
| 3D 并行 175B,千卡 | 30-40% | PP 气泡 + 跨节点通信 |
| MoE 多机训练 | 20-30% | All-to-All 路由 |
| 调优良好的 H100 集群 SOTA | 50% | 已接近理论值 |
诊断 MFU 偏低的方法:
这个公式是 BD/采购对话的硬通货——记住它,能让你在和销售/老板对账时少花冤枉钱。
LLM 推理可清晰地分为两个阶段,二者的计算特性完全不同:
| 阶段 | 计算量 | 访存量 | 算术强度 | 瓶颈 |
|---|---|---|---|---|
| Prefill | $2 L_p \cdot P$ FLOPs | $P \cdot b$ 字节 (一次读完) | $\sim L_p$ | compute-bound |
| Decode | $2 P$ FLOPs / token | $P \cdot b$ 字节 / token | $\sim 2$ | memory-bound (HBM 带宽) |
这个不对称是后续所有调度优化的根因:prefill 想攒大 batch 跑满算力,decode 想最小化 KV Cache 读取。
sequenceDiagram
participant U as User
participant S as Server
participant K as KV Cache
U->>S: prompt (L_p tokens)
Note over S: Prefill: 一次 forward,并行算 L_p 个位置
S->>K: 写入 L_p × 2 × L × h 的 KV
S-->>U: 第一个生成的 token (TTFT)
loop Decode: 自回归生成 N 个 token
Note over S: 一次 forward,1 个 token
读取整个 KV Cache
S->>K: 追加 1 个 KV 条目
S-->>U: 下一个 token (TPOT)
end
H100 BF16 算力 989 TFLOPS,HBM3 带宽 3.35 TB/s。Roofline 的临界算术强度:
$$\text{Ridge Point} = \frac{989 \times 10^{12}}{3.35 \times 10^{12}} \approx 295 \text{ FLOPs / byte}$$
Prefill 算术强度 $\approx L_p$(>> 295 当 $L_p$ 大),算力受限。Decode 算术强度 $\approx 2$ FLOPs/byte(远小于 295),带宽受限。Decode 阶段 GPU 算力闲置 99%+,唯一的优化方向是减少 HBM 读取或一次读取产出更多 token(投机解码的根本动机)。
传统实现给每个序列预分配一块连续 KV Cache(按最大长度 $S_{\max}$),导致严重的内部碎片(实际生成长度往往远短于 $S_{\max}$)和外部碎片(不同序列长度差异大)。vLLM (Kwon et al., 2023) 提出 PagedAttention,把 KV Cache 切成固定大小的 block(典型 16 token),用一张页表(block table)映射逻辑块到物理块,效仿操作系统虚拟内存:
$$\text{Mem Utilization}_{\text{naive}} \approx 30\%, \quad \text{Mem Utilization}_{\text{paged}} \approx 96\%$$
flowchart LR
subgraph LogView["逻辑视角: 序列 A B C 各自的 KV"]
A_log["Seq A 逻辑块 0,1,2"]
B_log["Seq B 逻辑块 0,1"]
C_log["Seq C 逻辑块 0,1,2,3"]
end
subgraph Page["页表 block_tables[seq_id]"]
A_pt["A: [#5, #2, #8]"]
B_pt["B: [#3, #1]"]
C_pt["C: [#7, #4, #6, #0]"]
end
subgraph Phys["物理 KV 池 (固定大小 block)"]
direction LR
P0["#0"] --- P1["#1"] --- P2["#2"] --- P3["#3"]
P4["#4"] --- P5["#5"] --- P6["#6"] --- P7["#7"]
P8["#8"]
end
LogView --> Page --> Phys
关键收益:
更进一步:把所有请求共享的 prompt 前缀的 KV 持久缓存。SGLang 的 RadixAttention(Zheng et al., 2024)用 radix tree 索引 prompt,新请求来时查找最长公共前缀,对应 KV 直接复用:
$$\text{Prefill Cost Saved} = \frac{L_{\text{shared}}}{L_p}$$
典型场景:
实测可省 50-90% 的 prefill 算力,是长对话/agent 场景的关键优化。
静态批处理(static batching)等所有序列生成完再返回——长序列拖死整批,GPU 利用率惨。连续批处理(也叫 in-flight batching,Yu et al., 2022 / Orca)让每一步 decode 都可以替换已完成的序列:
sequenceDiagram
participant S as Scheduler
participant Q as Pending Queue
participant E as Execution Batch
loop Each iteration
S->>Q: pull new requests
S->>E: evict completed sequences
S->>E: add new prefill / continue decode
E->>E: one forward step
E->>S: emit tokens, mark finished
end
实际工程要点:
max_num_batched_tokens)。调度器需要根据 KV Cache 剩余容量动态决策。一个序列长度 $s$ 的 KV Cache 占用:
$$\text{KV mem per seq} = 2 \cdot L \cdot s \cdot h \cdot b_{kv} \text{ bytes}$$
$b_{kv}$ 是 KV 元素的字节数(fp16=2, fp8=1, int4=0.5)。当总占用 $\geq 90\%$ HBM,调度器进入抢占模式:
核心观察:decode 是 memory-bound,每步从 HBM 读完整模型权重生成 1 token,计算单元闲置。若能用小 draft 模型预测出 $k$ 个候选 token,再让大模型一次 forward 同时验证这 $k$ 个,就可以一次步进出多个 token,分摊 HBM 读取。
sequenceDiagram
participant U as User
participant D as Draft Model 小
participant T as Target Model 大
U->>T: prompt
T->>U: token_0
loop 每个 verification step
D->>D: 串行生成 k 个候选 t1..tk (便宜)
D->>T: 候选 [t1..tk]
T->>T: 一次 forward 同时算 k+1 个位置的 logits
T->>T: 拒绝采样比较 draft/target 分布
alt 接受所有 k 个
T->>U: 输出 t1..tk + 额外 1 个
Note over T: 一次 forward 推出 k+1 token
else 第 j 个被拒绝
T->>U: 输出 t1..t(j-1) + 用 target 重采样的 tj'
Note over T: 总输出 j 个
end
end
Leviathan et al. (2022) 证明:只要 draft 模型的接受率 $\alpha$ 不为零,每步期望生成 token 数
$$\mathbb{E}[\text{tokens per step}] = \frac{1 - \alpha^{k+1}}{1 - \alpha}$$
且输出分布与原模型严格等价(拒绝采样保证)。典型加速 2~3×。
新一代方案:
| 接受率 $\alpha$ | $k=3$ 加速 | $k=5$ 加速 | 典型场景 |
|---|---|---|---|
| 0.5 | 1.75× | 1.94× | 跨任务、跨域 |
| 0.7 | 2.17× | 2.85× | 同分布微调 draft |
| 0.8 | 2.36× | 3.36× | EAGLE/Medusa 良好训练 |
| 0.9 | 2.71× | 4.10× | 同模型不同采样温度 |
把 prefill 和 decode 调度到不同的 GPU 池,因为两阶段的最优配置不同:
| 阶段 | 瓶颈 | 最优 TP | 最优 batch | 典型卡型 |
|---|---|---|---|---|
| Prefill | compute | 高(TP=4-8) | 小(甚至 1) | H100/H200(强算力) |
| Decode | memory bandwidth | 低(TP=1-2) | 大(128+) | L40S/A10(性价比 HBM) |
flowchart LR
subgraph Client
U1[User Request]
end
subgraph Router
R[Disagg Router]
end
subgraph PrefillPool["Prefill 池: 少量高算力 GPU"]
P1[H100 #1]
P2[H100 #2]
end
subgraph DecodePool["Decode 池: 大量 HBM GPU"]
D1[L40S #1]
D2[L40S #2]
D3[L40S #3]
D4[L40S #4]
end
subgraph KVTrans["RDMA KV Transfer
~100 μs/MB"]
T[InfiniBand / NVLink]
end
U1 --> R
R -->|prompt| PrefillPool
PrefillPool -->|KV Cache| T
T -->|KV| DecodePool
DecodePool -->|tokens| U1
代表系统:DistServe(Zhong et al., 2024)、Mooncake(Moonshot 2024)、SplitWise(Patel et al., 2024)。代价是 KV Cache 必须从 prefill GPU 转给 decode GPU(RDMA 传输),约 100 μs/MB,大模型场景值得。
实测收益(Mooncake 论文):长 prompt(> 4K)场景吞吐提升 1.5-2.5×,TTFT 降低 30-50%。
| 框架 | 厂商 | 核心特性 | 适用场景 |
|---|---|---|---|
| vLLM | UC Berkeley | PagedAttention,开源生态最完整 | 开源模型自托管,研究/创业 |
| TGI | Hugging Face | HF 模型零配置接入 | HF 模型快速 demo |
| SGLang | UC Berkeley / xAI | RadixAttention(prefix tree 共享),结构化输出 | 多 turn、共享 prompt 场景 |
| TensorRT-LLM | NVIDIA | 编译优化最深,与 Triton 集成 | 纯 NVIDIA 栈、极致性能 |
| LMDeploy | InternLM | TurboMind 内核,AWQ 量化优秀 | 低显存部署、中文模型 |
2024-2025 的快速参考:研究/原型 选 vLLM;生产高吞吐 + NVIDIA 栈选 TensorRT-LLM;多 turn 重共享 prompt 选 SGLang。
| 框架 | throughput (tok/s) | P99 TTFT (ms) | P99 TPOT (ms) | 显存利用率 |
|---|---|---|---|---|
| vLLM 0.6 | 2800 | 820 | 42 | 92% |
| TGI 2.4 | 2400 | 950 | 48 | 88% |
| SGLang | 3100 | 740 | 40 | 94% |
| TRT-LLM | 3600 | 680 | 35 | 90% |
注:实测受 prompt 分布、并发数、batch 上限影响很大,仅供量级参考。
量化的目的是用低精度整数表示原本的 FP16/BF16 张量,省显存、加快矩阵乘(Tensor Core 对 INT8/INT4 吞吐是 FP16 的 2×/4×)。基本映射:
$$q = \text{round}\!\left(\frac{x}{s}\right) + z, \qquad x \approx s(q - z)$$
其中 $s$ 是 scale,$z$ 是 zero-point。$z=0$ 称对称量化,适合分布零中心化的权重;$z\neq 0$ 是非对称量化,适合像 ReLU 后激活那样偏向一侧的分布。
flowchart LR
subgraph Sym["对称量化 (zero-point = 0)"]
direction TB
S1["FP 区间 [-α, α]"]
S2["INT8 区间 [-127, 127]"]
S1 -->|s = α/127| S2
S3["适合: 权重
无 z 偏移开销"]
end
subgraph Asy["非对称量化 (zero-point ≠ 0)"]
direction TB
A1["FP 区间 [β_min, β_max]"]
A2["UINT8 区间 [0, 255]"]
A1 -->|s, z 同时拟合| A2
A3["适合: ReLU/SiLU 后激活
覆盖单侧分布"]
end
按 scale 来源又分:
粒度(granularity):
| 位宽 | 每参数字节 | 典型 perplexity 涨幅 (Llama-2-7B) | HBM 需求 | 用途 |
|---|---|---|---|---|
| BF16 | 2 | baseline | 14 GB | 训练 / 服务端 |
| FP8 (E4M3) | 1 | ~0.05 | 7 GB | H100 服务端 |
| INT8 | 1 | ~0.1-0.3 | 7 GB | 所有 GPU |
| INT4 (GPTQ) | 0.5 | ~0.5 | 3.5 GB | 消费 GPU |
| INT4 (AWQ) | 0.5 | ~0.3 | 3.5 GB | 低显存推理 |
| INT3 (NF4 type) | ~0.4 | ~1.0 | 2.8 GB | QLoRA 微调存储 |
| INT2 | 0.25 | ~3-5 | 1.75 GB | 实验性,未稳定 |
GPTQ (Frantar et al., 2022) 借鉴 Optimal Brain Surgeon 思想:把量化每一列权重视作一次"删除",用 Hessian 信息补偿剩余列。对线性层 $Y = XW$,目标是最小化输出误差:
$$\min_{\hat W} \| X W - X \hat W \|_F^2$$
$\hat W$ 是量化后权重。逐列贪心求解,量化第 $i$ 列时把误差按 $H^{-1}$($H = 2 X^\top X$)补偿到后续未量化列:
$$\delta W_{j} = -\frac{w_i - \text{quant}(w_i)}{[H^{-1}]_{ii}} \cdot [H^{-1}]_{ij}, \quad j > i$$
flowchart LR
A[收集 calibration data] --> B[算 H = 2X^T X 每层]
B --> C[Cholesky 分解 H]
C --> D[逐列 / 逐 group 贪心]
D --> E[量化第 i 列 w_i]
E --> F[算量化误差 Δ_i]
F --> G[按 H^-1 补偿剩余列权重]
G --> H{还有未量化列?}
H -->|是| E
H -->|否| I[输出 GPTQ 模型 + zero/scale]
关键工程优化:
Llama-2-70B GPTQ 4-bit 显存从 140 GB 降到 35 GB,单 A100 可推。
AWQ (Lin et al., 2023) 的关键观察:权重 outlier 不重要,激活 outlier 才重要。某些通道(约 1%)的激活幅度极大,量化其对应的权重时引入的误差被激活放大。
flowchart LR
subgraph Before["量化前的通道 (问题)"]
direction TB
X1["通道 1 激活 量级 5"]
X2["通道 2 激活 量级 5"]
Xk["通道 k 激活 量级 200 ← outlier"]
Xn["通道 n 激活 量级 6"]
end
subgraph After["AWQ 后 通过 scale 平衡"]
direction TB
XW1["通道 1: x/s_1 · W·s_1, 激活降, 权重升"]
XWk["通道 k: x/200 · W·200, 激活到平均水平"]
end
Before --> Scale[per-channel 缩放搜索 s_i = |x_i|^α] --> After
解决方案:在量化前给这些通道的权重乘一个缩放 $s_i$,对应激活除以 $s_i$,数学上等价,但缩放后的权重更易量化:
$$Y = (X \cdot \text{diag}(s)^{-1}) \cdot (\text{diag}(s) \cdot W) = X W$$
缩放因子 $s_i$ 通过对每通道激活幅值的统计搜索得到,典型 $s_i = (|\bar x_i|)^\alpha$,$\alpha$ 在 [0.5, 1.0] 网格搜索。
AWQ 对指令微调模型友好,量化损失常优于 GPTQ。典型场景:
SmoothQuant (Xiao et al., 2022) 同样针对激活 outlier 问题,但目标是W8A8(权重和激活都 INT8)。它在每个 LayerNorm 后插入一个对角缩放,把激活的难量化"迁移"给权重:
$$\hat X = X \cdot \text{diag}(s)^{-1}, \quad \hat W = \text{diag}(s) \cdot W$$
$s$ 同样按激活通道幅值搜索。代价是这些缩放可以折叠进前一个 LayerNorm 的 $\gamma$ 参数(数学等价),运行时零开销。这一性质让 SmoothQuant 成为 INT8 服务端推理的事实标准。
| 方法 | 权重 | 激活 | 目标硬件 | 校准代价 | 典型应用 |
|---|---|---|---|---|---|
| GPTQ | INT4 / INT3 | FP16 / BF16 | 消费 GPU | 中(需 Hessian) | 开源模型本地推理 |
| AWQ | INT4 | FP16 | 消费 GPU | 低(仅统计) | chat 模型边缘部署 |
| SmoothQuant | INT8 | INT8 | 服务端 (Hopper 用 FP8) | 低 | 高吞吐推理 |
| FP8 PTQ | FP8 | FP8 | H100/H200 | 极低(无 zero) | 服务端 SOTA |
长上下文场景下 KV Cache 显存占比超过权重。Llama-2-70B 在 32K 上下文、batch=8 时,KV Cache 显存:
$$\text{KV Mem} = 2 \cdot L \cdot s \cdot h \cdot 2 \cdot b = 2 \times 80 \times 32768 \times 8192 \times 2 \times 8 / 10^9 \approx 686 \text{ GB}$$
显然要量化。INT8 KV Cache(per-token + per-head scale)几乎零精度损失;INT4 需要更精心的 group 设计,长上下文可能掉 1-2 分。H100 起原生支持 FP8,是性价比最好的选择(Hopper 架构 FP8 算力比 BF16 快 2×,且无需 zero-point)。
flowchart LR
subgraph KVMem["32K ctx, batch=8, Llama-2-70B"]
direction TB
BF16K["BF16 KV: 686 GB"]
I8K["INT8 KV: 343 GB"]
FP8K["FP8 KV: 343 GB"]
I4K["INT4 KV: 172 GB"]
end
BF16K -->|2× 压缩| I8K
BF16K -->|2× 压缩 + FP 范围| FP8K
I8K -->|2× 压缩| I4K
note["短 ctx 不需要 KV 量化<br/>长 ctx (>= 16K) 必须做"]
实现要点:
知识蒸馏:让小学生模型 $S$ 模仿大教师 $T$ 的输出分布。最经典的 KD loss:
$$\mathcal{L} = \alpha \cdot \text{CE}(y, p_S) + (1 - \alpha) \cdot T^2 \cdot \text{KL}(p_T^{/T} \,\|\, p_S^{/T})$$
$T$ 是温度。LLM 蒸馏的现代变体(MiniLLM, GKD)改用反向 KL 或 on-policy 数据避免 mode-covering 问题。代表项目:DistilBERT、Llama-Pruner、Phi 系列(精心蒸馏的小模型)。
flowchart LR
subgraph Dense["原始权重 (dense)"]
direction LR
D[". · · · · · · · · · · · · · · ."]
end
subgraph Unstruct["非结构化稀疏: 任意位置 0"]
direction LR
U["0 · · 0 · 0 · · · 0 · · · · · 0"]
end
subgraph Sparse24["2:4 结构化: 每 4 个保留 2"]
direction LR
S2["0 · · 0 · · 0 0 · 0 · 0 0 · · 0"]
end
Dense --> Unstruct
Dense --> Sparse24
note["2:4 在 Ampere/Hopper Tensor Core 上<br/>有硬件加速,2× 算力<br/>非结构化只省显存不省算"]
生产流水线:预训练 → 剪枝(结构化)→ 蒸馏恢复 → INT4 量化 → 部署。Llama-3-8B 经此组合可压到 2.5 GB,跑在手机端。代表项目:
LLM 服务的指标体系比传统 web 服务复杂得多——一个请求是流式的,"延迟"不能用单一数字概括。
| 指标 | 含义 | 影响 |
|---|---|---|
| TTFT (Time To First Token) | 从请求到达至吐出第一个 token 的时间 | 用户感知"系统是否还活着",prefill 决定 |
| TPOT (Time Per Output Token) | 之后每个 token 的平均生成时间 | 用户感知"打字流畅度",decode 决定 |
| ITL (Inter-Token Latency) | 相邻 token 之间的间隔(含波动) | 用户感知"卡顿",体现尾延迟 |
| E2E Latency | 从请求到完整响应的总时间 | TTFT + N · TPOT |
| Throughput | 单位时间处理的 token / 请求数 | 成本与并发能力 |
| Goodput | 满足 SLO 的 throughput | 真实可用容量 |
gantt
title 单次请求的延迟分解
dateFormat X
axisFormat %s
section Phase
queue :crit, 0, 50
prefill TTFT:active, 50, 500
decode 1 : 500, 540
decode 2 : 540, 580
decode 3 : 580, 620
decode 4 : 620, 660
decode N-1 : 660, 1300
decode N : 1300, 1340
用户体验阈值经验值:
SLO(Service Level Objective)通常以 P95 / P99 表达,比如"P95 TTFT < 500 ms"。在固定硬件下,批大小 (batch size) 是延迟与吞吐的旋钮:
$$\text{Throughput} \uparrow, \quad \text{Latency} \uparrow \quad \text{as batch size} \uparrow$$
xychart-beta
title "吞吐 vs P95 延迟 (固定硬件)"
x-axis "Batch Size" [1, 2, 4, 8, 16, 32, 64, 128]
y-axis "归一化指标" 0 --> 1.2
line [0.1, 0.2, 0.35, 0.55, 0.75, 0.9, 0.98, 1.0]
line [0.05, 0.1, 0.2, 0.32, 0.45, 0.6, 0.85, 1.15]
(蓝线:吞吐归一化;红线:P95 latency 归一化。Batch 加大吞吐先快后饱和,延迟先慢后陡升,找拐点。)
每张 GPU 找一个最大 batch 使得 P95 TTFT 仍满足 SLO,然后通过加机器扩并发。一个实用的断点估算:当 KV Cache 占据全部可用显存的 90% 时,加 batch 不再提升 throughput(开始抢占重计算),此时即"满 batch"。
常见反模式:
LLM 服务扩缩容比一般 web 服务难得多,原因:
flowchart LR
subgraph Triggers["扩容触发器(任一条件)"]
T1["P95 TTFT > SLO 持续 60s"]
T2["等待队列深度 > 阈值"]
T3["KV Cache 占用 > 85%"]
T4["GPU 利用率 > 90%"]
end
subgraph Pool["Pod Pool"]
Active["Active Pods 8"]
Warm["Warm Pool 2
已加载权重,无流量"]
Cold["Cold (k8s ready 待创建)"]
end
Triggers --> Decision{需要更多容量?}
Decision -->|快速| Warm
Warm -->|提升流量| Active
Decision -->|慢速| Cold
Cold -->|加载权重 5-15min| Warm
常用模式:
真实业务通常同时跑十几个模型(不同尺寸、不同微调版本、不同语言),需要一层网关统一接入。核心能力:
flowchart TB
User[业务方] -->|OpenAI 兼容 API| GW[LLM Gateway]
GW --> Auth[认证 / 配额]
Auth --> Cache{Prompt 缓存命中?}
Cache -->|是| Resp[直接返回]
Cache -->|否| Route[模型路由]
Route -->|简单 Q| Small[7B 模型池]
Route -->|复杂 Q| Mid[34B 模型池]
Route -->|推理 Q| Big[70B 模型池]
Route -->|代码 Q| Code[CodeLlama 池]
Small --> Monitor[指标收集]
Mid --> Monitor
Big --> Monitor
Code --> Monitor
Monitor --> Resp
Resp --> User
代表系统:LiteLLM、Portkey、Anyscale Endpoint。自建可基于 Envoy + 自定义 filter。
路由决策本质是一个分类问题:给定 prompt,选择哪个模型。简单到复杂:
LLM 服务需要监控的指标远多于传统服务:
| 类别 | 指标 |
|---|---|
| 性能 | TTFT / TPOT / ITL(P50/P95/P99),throughput, goodput, GPU util, HBM util, KV Cache 占用 |
| 容量 | 每个 worker 的 batch size、active sequences、waiting queue depth |
| 错误 | 超时率、OOM、抢占重计算次数、连接重置 |
| 业务 | 平均回答 token 数、stop reason 分布、token 成本 |
| 质量 | refusal 率、JSON 格式合规率、A/B 模型 head-to-head 评分 |
flowchart LR
subgraph Stages["灰度发布的 5 个阶段"]
direction LR
S1[Shadow 流量 0%
仅 log 对比] -->|质量 OK| S2[Canary 1%]
S2 -->|24h 无回归| S3[Stage 5%]
S3 -->|72h 监控| S4[Ramp 25%]
S4 -->|7d| S5[Full 100%]
S2 -.业务/质量回归.-> RB[自动回滚]
S3 -.业务/质量回归.-> RB
S4 -.业务/质量回归.-> RB
end
灰度发布的工程模式:
LLM 输出难以用确定指标评判,常见做法:
以 H100(约 $4/小时云租)跑 Llama-3-70B BF16 为例:
$$\text{Cost per 1M tokens} = \frac{N_{\text{GPU}} \cdot \$/\text{hr}}{3600 \cdot \text{throughput}_{\text{tok/s}}} \cdot 10^6$$
4 张 H100 TP=4 部署,throughput ~3000 token/s(混合 prefill/decode),则:
$$\frac{4 \times 4}{3600 \times 3000} \times 10^6 \approx \$1.48 \text{ / 1M tokens}$$
降本的杠杆(按 ROI 从高到低):
| 方案 | 硬件 | 1M tokens 成本 | 典型业务 |
|---|---|---|---|
| OpenAI GPT-4o API | 第三方 | $5 (input) + $15 (output) | 原型 / 低量 |
| Claude 3.5 Sonnet API | 第三方 | $3 + $15 | 原型 / 长上下文 |
| 自托管 Llama-3-70B BF16 | 4× H100 | ~$1.5 | 中量、隐私敏感 |
| 自托管 Llama-3-70B FP8 | 2× H100 | ~$0.5 | 高量服务端 |
| 自托管 8B + 智能路由 | 1× L40S | ~$0.05 | 简单业务大量调用 |
| 自托管 1.5B 蒸馏小模型 | 1× T4 | ~$0.01 | 专用场景大量调用 |
2024-2025 的趋势:从"上 GPT-4"过渡到"自家蒸馏小模型 + 路由到大模型 fallback"。一个好的 LLM 系统工程师的核心价值,是把每 1000 token 的成本压到业务能承受的水平。
上线前的 checklist(来自实际事故经验):