六大章节 | 训练并行策略 + 推理工程实践
DDP · FSDP · Tensor/Pipeline 并行 · ZeRO · vLLM · 量化 · 服务化
单卡训练大模型的瓶颈是显存与吞吐。当模型参数量超过单 GPU 显存上限,或单卡训练时间无法接受时,必须把工作切到多卡甚至多机。并行策略主要分三类:数据并行(不同 GPU 看不同样本,参数副本相同)、模型并行(不同 GPU 持有模型不同部分)、流水线并行(不同 GPU 负责不同层)。三者可以正交组合,构成所谓 3D 并行。
数据并行是最常用、最简单、扩展性最好的策略,本章系统讲清三代实现:PyTorch DataParallel(DP,单机多卡,已过时)、DistributedDataParallel(DDP,事实标准)、FullyShardedDataParallel(FSDP,ZeRO-3 的 PyTorch 原生版本)。
PyTorch 早期的 nn.DataParallel 将一个 batch 切分到多卡前向,主卡(rank 0)负责聚合损失、反传梯度、广播参数。它的核心问题有四个:
结论:生产中不要再用 DP,统一用 DDP。
DDP 是 多进程架构,每张 GPU 对应一个独立进程,每个进程持有完整模型副本,绕开 GIL。前向各算各的,反向时通过 AllReduce 把所有进程的梯度求和并平均,保证参数更新一致。
设总参数量 $P$、GPU 数 $N$、每个 float 占 $b$ 字节,Ring-AllReduce 的总通信量约为:
$$\text{Comm}_{\text{AllReduce}} \approx 2 \cdot \frac{N-1}{N} \cdot P \cdot b \approx 2 P b \quad (N \to \infty)$$
因为 AllReduce = ReduceScatter + AllGather,两阶段各传 $\tfrac{N-1}{N} P b$。一个关键事实:通信量与 GPU 数 $N$ 无关,所以 Ring-AllReduce 在带宽足够时是线性扩展的。但这只是 带宽 维度的好消息——延迟 仍随 $N$ 线性增长,因为环路一圈要走 $2(N-1)$ 步。
朴素 DDP 每个参数的梯度都触发一次小 AllReduce,开销巨大。PyTorch 用 gradient bucket(默认 25 MB)合并相邻反向算出的梯度,凑满一桶后一次性 AllReduce。这一桶 AllReduce 在后台 NCCL stream 上进行,与上游层的反向计算并行执行,理想情况下完全隐藏通信时间。
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, # 关闭可加速;开启会增加一次反向遍历
)
关键调优点:
DDP 把参数完整复制到每张卡,显存浪费严重。FSDP(Fully Sharded Data Parallel,对应 DeepSpeed 的 ZeRO-3)把参数、梯度、优化器状态都按 GPU 分片,只在前向/反向需要某层时临时 AllGather 整层参数。
显存对比,模型参数量 $P$,混合精度训练(FP16 参数+FP32 优化器主权重):
| 策略 | 参数 | 梯度 | 优化器状态 | 合计(fp16/fp32 Adam) |
|---|---|---|---|---|
| DDP | $2P$ (fp16) | $2P$ (fp16) | $12P$ (master+m+v, fp32) | $16P$ 字节/卡 |
| FSDP / ZeRO-3 | $2P / N$ | $2P / N$ | $12P / N$ | $16P / N$ 字节/卡 |
FSDP 的代价是多了两次通信:前向前 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 队列深度,省显存
)
FSDP 的 ShardingStrategy 三档:
FULL_SHARD(ZeRO-3):参数+梯度+优化器都分片;SHARD_GRAD_OP(ZeRO-2):只分片梯度和优化器,参数仍完整复制;NO_SHARD:等同 DDP。当单步显存放不下目标 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):
with model.no_sync() if (step + 1) % K != 0 else nullcontext():
loss = model(batch) / K # 缩放避免梯度爆炸
loss.backward() # 累加到 .grad
if (step + 1) % K == 0:
optimizer.step()
optimizer.zero_grad()
关键技巧 model.no_sync():在累积期间禁用 DDP/FSDP 的梯度同步,只在最后一次反向才触发 AllReduce/ReduceScatter,可以省 $K-1$ 次通信。Hugging Face Trainer 默认就是这种模式。
当单层参数大到放不进单卡时,必须做层内切分,即张量并行(Tensor Parallelism, TP)。最经典的方案来自 NVIDIA Megatron-LM (Shoeybi et al., 2019),针对 Transformer 的两个核心子层——MLP 和多头注意力——给出了精心设计的切分方式。
设 TP 度为 $t$(即把一层在 $t$ 张卡上切),切分准则是:同一前向中只在子层结尾做一次 AllReduce,把中间激活的切分代价压到最低。
线性层 $Y = X A$,矩阵 $A \in \mathbb{R}^{d_{\text{in}} \times d_{\text{out}}}$ 有两种切法:
把 $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]$ 的拼接。前向无通信,但输出是切分的。
把 $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}$$
多头注意力天然适合按 head 维度切:把 $h$ 个 head 平均分到 $t$ 张卡,每卡负责 $h/t$ 个头。$Q, K, V$ 的投影矩阵列并行切,每个头独立算 attention,输出投影行并行汇总。同样只在子层结尾一次 AllReduce。
反向时同样需要一次 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 切分能力时,把不同层放到不同 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%。
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。
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 上得权衡)。
graph LR
subgraph "1F1B Pipeline (p=4 stages, m=8 microbatches)"
direction LR
S1["Stage 1
F1 F2 F3 F4 B1 F5 B2 F6 B3 ..."]
S2["Stage 2
... F1 F2 F3 B1 F4 B2 ..."]
S3["Stage 3
... ... F1 F2 B1 F3 ..."]
S4["Stage 4
... ... ... F1 B1 F2 ..."]
S1 --> S2 --> S3 --> S4
end
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 的现代版本默认开启。
分析任何并行策略,第一步要算清楚单卡显存账。混合精度训练(Adam,主流)下,参数量 $P$ 个的模型,每个参数占用:
| 项 | 精度 | 字节/参数 |
|---|---|---|
| FP16 / BF16 参数 | fp16 | 2 |
| FP16 梯度 | fp16 | 2 |
| FP32 主权重(Adam 更新用) | fp32 | 4 |
| Adam $m$(一阶动量) | fp32 | 4 |
| Adam $v$(二阶动量) | fp32 | 4 |
| 合计 | 16 | |
所以 7B 模型纯训练态显存约 $7 \times 10^9 \times 16 = 112 \text{ GB}$,再加激活、临时 buffer,单 80GB H100 也放不下。
激活显存的估算(前向中所有层中间结果):
$$M_{\text{act}} \approx s \cdot b \cdot L \cdot (34 h + 5 a s) \text{ bytes}$$
其中 $s$ 序列长、$b$ batch、$L$ 层数、$h$ 隐藏维、$a$ 注意力头数(Korthikanti 2022 公式)。激活检查点(gradient checkpointing)可以把这项降到 $\sqrt{L}$ 量级,代价是反向多算 33%。
ZeRO(Rajbhandari et al., 2020)的核心思想:DDP 让每张卡持有相同的 16P 字节是冗余的,可以按 GPU 数 $N$ 分片。三个 stage 分别分片不同部分:
| 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$(仅参数)。
ZeRO-Infinity(Rajbhandari et al., 2021)更进一步,把参数也卸载到 CPU 甚至 NVMe SSD,配合 PCIe/NVMe 带宽预取。可以在 8 张 V100 上训练万亿参数模型,但训练吞吐显著下降(IO 成为瓶颈),主要用在无 H100 集群的场景。
{
"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 模型的配置:
选型经验法则:
| 维度 | 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,取两者之长。
训练总计算量(前向+反向 ≈ 3 倍前向):
$$C = 6 \cdot P \cdot D \quad \text{FLOPs}$$
$P$ 参数量,$D$ 训练 token 数。一张 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{ 天}$$
这个公式是 BD/采购对话的硬通货,记住。
LLM 推理可清晰地分为两个阶段,二者的计算特性完全不同:
这个不对称是后续所有调度优化的根因:prefill 想攒大 batch 跑满算力,decode 想最小化 KV Cache 读取。
传统实现给每个序列预分配一块连续 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\%$$
关键收益:
静态批处理(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)。核心观察:decode 是 memory-bound,每步从 HBM 读完整模型权重生成 1 token,计算单元闲置。若能用小 draft 模型预测出 $k$ 个候选 token,再让大模型一次 forward 同时验证这 $k$ 个,就可以一次步进出多个 token,分摊 HBM 读取。
Leviathan et al. (2022) 证明:只要 draft 模型的接受率 $\alpha$ 不为零,每步期望生成 token 数
$$\mathbb{E}[\text{tokens per step}] = \frac{1 - \alpha^{k+1}}{1 - \alpha}$$
且输出分布与原模型严格等价(拒绝采样保证)。典型加速 2~3×。
新一代方案:
把 prefill 和 decode 调度到不同的 GPU 池,因为两阶段的最优配置不同:
| 阶段 | 瓶颈 | 最优 TP | 最优 batch | 典型卡型 |
|---|---|---|---|---|
| Prefill | compute | 高(TP=4-8) | 小(甚至 1) | H100/H200(强算力) |
| Decode | memory bandwidth | 低(TP=1-2) | 大(128+) | L40S/A10(性价比 HBM) |
代表系统:DistServe(Zhong et al., 2024)、Mooncake(Moonshot 2024)、SplitWise(Patel et al., 2024)。代价是 KV Cache 必须从 prefill GPU 转给 decode GPU(RDMA 传输),约 100 μs/MB,大模型场景值得。
| 框架 | 厂商 | 核心特性 | 适用场景 |
|---|---|---|---|
| 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。
量化的目的是用低精度整数表示原本的 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 后激活那样偏向一侧的分布。
按 scale 来源又分:
粒度(granularity):
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$$
关键工程优化:使用 Cholesky 分解一次性算出 $H^{-1}$ 的上三角,避免数值不稳定;group_size=128 配合 act-order,可以把 4-bit 量化 perplexity 损失压到 0.1 以内。Llama-2-70B GPTQ 4-bit 显存从 140 GB 降到 35 GB,单 A100 可推。
AWQ (Lin et al., 2023) 的关键观察:权重 outlier 不重要,激活 outlier 才重要。某些通道(约 1%)的激活幅度极大,量化其对应的权重时引入的误差被激活放大。解决方案:在量化前给这些通道的权重乘一个缩放 $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 服务端推理的事实标准。
长上下文场景下 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)。
实现要点:
知识蒸馏:让小学生模型 $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 问题。
生产流水线:预训练 → 剪枝(结构化)→ 蒸馏恢复 → INT4 量化 → 部署。Llama-3-8B 经此组合可压到 2.5 GB,跑在手机端。代表项目:Llama.cpp 生态的 Q4_K_M 量化、Apple MLX 框架。
LLM 服务的指标体系:
| 指标 | 含义 | 影响 |
|---|---|---|
| TTFT (Time To First Token) | 从请求到达至吐出第一个 token 的时间 | 用户感知"系统是否还活着",prefill 决定 |
| TPOT (Time Per Output Token) | 之后每个 token 的平均生成时间 | 用户感知"打字流畅度",decode 决定 |
| ITL (Inter-Token Latency) | 相邻 token 之间的间隔(含波动) | 用户感知"卡顿",体现尾延迟 |
| Throughput | 单位时间处理的 token / 请求数 | 成本与并发能力 |
| Goodput | 满足 SLO 的 throughput | 真实可用容量 |
用户体验阈值经验值:TTFT < 1 秒 体感即时,TPOT 对应阅读速度 > 30 token/s 即流畅(中文每 token 约 1-2 字)。Code / agent 类应用对 TTFT 更敏感。
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$$
每张 GPU 找一个最大 batch 使得 P95 TTFT 仍满足 SLO,然后通过加机器扩并发。一个实用的断点估算:当 KV Cache 占据全部可用显存的 90% 时,加 batch 不再提升 throughput(开始抢占重计算),此时即"满 batch"。
常见反模式:
LLM 服务扩缩容比一般 web 服务难得多,原因:
常用模式:
真实业务通常同时跑十几个模型(不同尺寸、不同微调版本、不同语言),需要一层网关统一接入。核心能力:
代表系统:LiteLLM、Portkey、Anyscale Endpoint。自建可基于 Envoy + 自定义 filter。
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 评分 |
灰度发布的工程模式:
以 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 从高到低):
2024-2025 的趋势:从"上 GPT-4"过渡到"自家蒸馏小模型 + 路由到大模型 fallback"。一个好的 LLM 系统工程师的核心价值,是把每 1000 token 的成本压到业务能承受的水平。