Distributed Training & Inference Deployment
LLM 大规模训练
与部署
分布式训练并行 · 推理服务化 · 量化压缩

六大章节 | 训练并行 + 推理工程 + 部署服务化

DDP · FSDP · Tensor/Pipeline · ZeRO · vLLM · 量化 · Gateway

系统与工程
目 录
第一章   数据并行:DP / DDP / FSDP
1.1   引言:为什么需要并行
1.2   DataParallel 的局限
1.3   DistributedDataParallel 与 AllReduce
1.3.1   Ring-AllReduce 的通信代价
1.3.2   NCCL 后端与拓扑感知
1.4   梯度桶与通信计算重叠
1.5   FSDP:全分片
1.5.1   显存对比
1.5.2   实现要点
1.5.3   包装单元 (wrap unit) 选择
1.6   梯度累积与有效批大小
1.7   决策树
第二章   模型并行:Tensor 与 Pipeline
2.1   张量并行的基本切分(Megatron-LM)
2.2   Column-Parallel 与 Row-Parallel
2.2.1   列并行
2.2.2   行并行
2.2.3   组合:先列后行
2.3   Attention 与 MLP 的并行化
2.3.1   TP 通信代价对比例算例
2.4   流水线并行 GPipe / 1F1B
2.5   流水线气泡与 Interleaved 调度
2.6   序列并行与 Context Parallel
第三章   ZeRO 与 3D 并行
3.1   显存占用拆解
3.1.1   激活显存的估算
3.2   ZeRO Stage 1 / 2 / 3
3.2.1   实践选型
3.3   ZeRO-Offload 与 ZeRO-Infinity
3.4   3D 并行的组合与拓扑
3.5   DeepSpeed vs Megatron-LM
3.6   训练 GPU 数估算公式
3.6.1   MFU 实测与诊断
第四章   推理服务架构与批处理
4.1   推理工作流:Prefill 与 Decode
4.1.1   Roofline 分析
4.2   PagedAttention 与 KV Cache 分页
4.2.1   Prefix Cache 与 RadixAttention
4.3   连续批处理
4.3.1   准入控制与显存压力
4.4   投机解码 (Speculative / EAGLE)
4.4.1   接受率与加速比关系
4.5   Prefill / Decode 分离部署
4.6   主流框架对比
4.6.1   性能对比基准
第五章   量化与模型压缩
5.1   量化基础
5.1.1   比特位与精度权衡
5.2   GPTQ:基于二阶信息
5.3   AWQ:激活感知
5.4   SmoothQuant
5.4.1   三方法对比
5.5   KV Cache 量化
5.6   蒸馏、剪枝与稀疏
5.6.4   压缩决策清单
第六章   生产部署与服务化
6.1   关键指标 TTFT/TPOT
6.2   SLO 设计
6.3   弹性扩缩容
6.4   LLM Gateway 与多模型路由
6.4.1   智能路由的决策算法
6.5   监控、灰度发布与回滚
6.5.1   质量监控的挑战
6.6   成本估算与优化
6.6.1   部署方案成本对比
6.7   生产清单
第 一 章
数据并行:DP / DDP / FSDP

1.1 引言:为什么需要并行

训练一个语言模型本质上是用大量样本估计参数。单卡训练大模型的瓶颈是两件事——显存放不下、吞吐跑不完。当模型参数量超过单 GPU 显存上限(即便上了梯度检查点和混合精度也放不下),或单卡训练完所需时间无法接受(动辄几个月),必须把工作切到多卡甚至多机。

分布式训练的并行策略主要分三类:

三者可以正交组合,构成 3D 并行:$N_{\text{GPU}} = \text{DP} \times \text{TP} \times \text{PP}$。本章先讲扩展性最好、最易上手、最常用的数据并行,串起三代实现:PyTorch DataParallel(DP,单机多卡,已过时)、DistributedDataParallel(DDP,事实标准)、FullyShardedDataParallel(FSDP,ZeRO-3 的 PyTorch 原生版本)。

[ Architecture Diagram ]
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"]

1.2 DataParallel 的局限

PyTorch 早期的 nn.DataParallel 将一个 batch 切分到多卡前向,主卡(rank 0)负责聚合损失、反传梯度、广播参数。它的核心问题有四个:

结论:生产中不要再用 DP,统一用 DDP。PyTorch 官方文档也明确不推荐。

1.3 DistributedDataParallel 与 AllReduce

DDP 是 多进程架构——每张 GPU 对应一个独立 Python 进程,每个进程持有完整模型副本,绕开 GIL。前向各算各的,反向时通过 AllReduce 把所有进程的梯度求和并平均,保证参数更新一致。

1.3.1 Ring-AllReduce 的通信代价

朴素 AllReduce 让所有节点把数据发给一个聚合节点再广播回来,总通信量 $2(N-1)Pb$,且聚合节点带宽成瓶颈。Ring-AllReduce(Baidu, 2017)把所有 GPU 排成环,分两阶段:

  1. ReduceScatter:把数组切成 $N$ 份,每个 GPU 负责一份的归约。沿环走 $N-1$ 步,每步发送 $P/N$ 大小的数据。
  2. AllGather:每个 GPU 把自己持有的归约结果传给后续邻居,再走 $N-1$ 步。

总通信量:

$$\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-AllReduceNCCL 的 Double Binary Tree 把延迟降到 $O(\log N)$。

[ Architecture Diagram ]
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

1.3.2 NCCL 后端与拓扑感知

实际通信由 NVIDIA NCCL 库执行。NCCL 在初始化时自动探测节点内 NVLink/NVSwitch、节点间 InfiniBand/RoCE 拓扑,构造最优环:

排查通信问题最有用的环境变量:NCCL_DEBUG=INFONCCL_TOPO_DUMP_FILE=topo.xmlNCCL_IB_DISABLE=1(关 IB 用 TCP 兜底)。

1.4 梯度桶与通信计算重叠

朴素 DDP 每个参数的梯度都触发一次小 AllReduce,开销巨大(NCCL 启动延迟约 10 μs/调用)。PyTorch 用 gradient bucket(默认 25 MB)合并相邻反向算出的梯度,凑满一桶后一次性 AllReduce。这一桶 AllReduce 在后台 NCCL stream 上进行,与上游层的反向计算并行执行,理想情况下完全隐藏通信时间。

[ Architecture Diagram ]
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 进一步优化通信
)

关键调优点:

1.5 FSDP:参数、梯度、优化器状态全分片

DDP 把参数完整复制到每张卡,显存浪费严重。FSDP(Fully Sharded Data Parallel,对应 DeepSpeed 的 ZeRO-3)把参数梯度优化器状态都按 GPU 分片,只在前向/反向需要某层时临时 AllGather 整层参数。

1.5.1 显存对比

模型参数量 $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$× 显存。在显存受限时是绝对值得的。

1.5.2 实现要点

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 四档:

1.5.3 包装单元 (wrap unit) 选择

FSDP 把模型拆成若干 wrap unit,每个 unit 内的参数一起 AllGather/ReduceScatter。Unit 越细:

实践默认:每个 Transformer 层一个 unit(用 transformer_auto_wrap_policy)。对于 70B+ 模型,每层参数大约 200 MB~500 MB,平衡得最好。

1.6 梯度累积与有效批大小

当单步显存放不下目标 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 默认就是这种模式。

常见陷阱

1.7 决策树

[ Architecture Diagram ]
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 与 Pipeline

2.1 张量并行的基本切分(Megatron-LM)

当单层参数大到放不进单卡时,必须做层内切分,即张量并行(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 让单层永远不需要在单卡上完整存在,从根本上解决"一层太大"的问题。

2.2 Column-Parallel 与 Row-Parallel

线性层 $Y = X A$,矩阵 $A \in \mathbb{R}^{d_{\text{in}} \times d_{\text{out}}}$ 有两种切法。理解这两种切法的对偶性是 Megatron 设计的核心。

[ Architecture Diagram ]
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

2.2.1 列并行(Column-Parallel)

把 $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$ 的梯度。

[ Architecture Diagram ]
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

2.2.2 行并行(Row-Parallel)

把 $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。

2.2.3 组合:先列后行

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 论文的精髓所在:切分方向的选择消除了中间通信,把通信代价压到最低。

2.3 Attention 与 MLP 的并行化

多头注意力天然适合按 head 维度切:把 $h$ 个 head 平均分到 $t$ 张卡,每卡负责 $h/t$ 个头。$Q, K, V$ 的投影矩阵列并行切,每个头独立算 attention,输出投影行并行汇总。同样只在子层结尾一次 AllReduce。

[ Architecture Diagram ]
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 拖死。

2.3.1 TP 通信代价对比例算例

以 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 数是铁律。

2.4 流水线并行 GPipe / 1F1B

当层数远超 TP 切分能力时,把不同放到不同 GPU 上,构成流水线并行(Pipeline Parallelism, PP)。挑战是:原始的串行训练会让 GPU 大部分时间在等上游/下游,利用率极低。

2.4.1 GPipe(朴素流水线)

把一个 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%。

[ Architecture Diagram ]
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

2.4.2 1F1B(PipeDream / Megatron)

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。

[ Architecture Diagram ]
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 的激活内存"]

2.5 流水线气泡与 Interleaved 调度

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。

2.6 序列并行(Sequence Parallelism)

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 的现代版本默认开启。

2.6.1 Context Parallel (CP) 与超长上下文

序列并行进一步极端化就是 Context Parallel:把整条序列沿 $s$ 切到多卡,每张卡只持有 $s/c$ token 的激活。挑战在 attention:softmax 需要看到完整 QK,跨卡需要 AllGather 整段 K, V。Ring Attention (Liu et al., 2023) 用环形交换 KV 块,把 attention 做成流水线,理论上序列长度只受总 HBM 限制。这是训练 100K-1M context 模型的关键技术之一。

第 三 章
ZeRO 与 3D 并行

3.1 显存占用拆解

分析任何并行策略,第一步要算清楚单卡显存账。混合精度训练(Adam,主流)下,参数量 $P$ 个的模型,每个参数占用:

精度字节/参数用途
FP16 / BF16 参数fp162前向反向计算
FP16 梯度fp162反向产物
FP32 主权重fp324Adam 用于更新(避免精度漂移)
Adam $m$(一阶动量)fp324EMA(gradient)
Adam $v$(二阶动量)fp324EMA(gradient²)
合计16

所以 7B 模型纯训练态显存约 $7 \times 10^9 \times 16 = 112 \text{ GB}$,再加激活、临时 buffer,单 80GB H100 也放不下。

3.1.1 激活显存的估算

前向中所有层中间结果保留下来给反向用,这部分通常被忽视但实际占比惊人。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% 额外开销。

[ Architecture Diagram ]
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

3.2 ZeRO Stage 1 / 2 / 3

ZeRO(Rajbhandari et al., 2020)的核心思想:DDP 让每张卡持有相同的 16P 字节是冗余的,可以按 GPU 数 $N$ 分片。三个 stage 分别分片不同部分:

[ Architecture Diagram ]
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$× 显存。

3.2.1 实践选型

3.3 ZeRO-Offload 与 ZeRO-Infinity

当 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 集群的场景。

[ Architecture Diagram ]
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
}

3.4 3D 并行的组合与拓扑

三种并行可以正交组合:$N_{\text{GPU}} = \text{DP} \times \text{TP} \times \text{PP}$。一个典型 1024 卡 训练 175B 模型的配置:

[ Architecture Diagram ]
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

选型经验法则:

3.5 DeepSpeed vs Megatron-LM

维度DeepSpeedMegatron-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.6 训练 GPU 数估算公式

训练总计算量(前向+反向 ≈ 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{ 天}$$

3.6.1 MFU 实测与诊断

MFU 是衡量训练栈效率的核心指标。常见 MFU 范围:

场景典型 MFU瓶颈
纯 DDP,小模型,单机50-60%kernel 启动开销
FSDP,70B 单机多卡40-50%AllGather 通信
3D 并行 175B,千卡30-40%PP 气泡 + 跨节点通信
MoE 多机训练20-30%All-to-All 路由
调优良好的 H100 集群 SOTA50%已接近理论值

诊断 MFU 偏低的方法:

这个公式是 BD/采购对话的硬通货——记住它,能让你在和销售/老板对账时少花冤枉钱。

第 四 章
推理服务架构与批处理

4.1 推理工作流:Prefill 与 Decode

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 读取

[ Architecture Diagram ]
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

4.1.1 Roofline 分析:为什么 decode 是 memory-bound

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(投机解码的根本动机)。

4.2 PagedAttention 与 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\%$$

[ Architecture Diagram ]
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

关键收益:

4.2.1 Prefix Cache 与 RadixAttention

更进一步:把所有请求共享的 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 场景的关键优化。

4.3 连续批处理(Continuous Batching)

静态批处理(static batching)等所有序列生成完再返回——长序列拖死整批,GPU 利用率惨。连续批处理(也叫 in-flight batching,Yu et al., 2022 / Orca)让每一步 decode 都可以替换已完成的序列:

[ Architecture Diagram ]
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

实际工程要点:

4.3.1 准入控制与显存压力

调度器需要根据 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,调度器进入抢占模式

  1. 优先抢占最近抵达、未生成多少 token 的序列(沉没成本小)
  2. 抢占方式:方案 A 把 KV 拷贝到 CPU(再调度时拷回,~200ms 延迟);方案 B 直接丢弃从头重算(重算成本可能比拷贝低)
  3. vLLM 默认用方案 B;TGI 用方案 A

4.4 投机解码(Speculative Decoding / EAGLE)

核心观察:decode 是 memory-bound,每步从 HBM 读完整模型权重生成 1 token,计算单元闲置。若能用小 draft 模型预测出 $k$ 个候选 token,再让大模型一次 forward 同时验证这 $k$ 个,就可以一次步进出多个 token,分摊 HBM 读取。

[ Architecture Diagram ]
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×。

新一代方案:

4.4.1 接受率与加速比关系

接受率 $\alpha$$k=3$ 加速$k=5$ 加速典型场景
0.51.75×1.94×跨任务、跨域
0.72.17×2.85×同分布微调 draft
0.82.36×3.36×EAGLE/Medusa 良好训练
0.92.71×4.10×同模型不同采样温度

4.5 Prefill / Decode 分离部署

把 prefill 和 decode 调度到不同的 GPU 池,因为两阶段的最优配置不同:

阶段瓶颈最优 TP最优 batch典型卡型
Prefillcompute高(TP=4-8)小(甚至 1)H100/H200(强算力)
Decodememory bandwidth低(TP=1-2)大(128+)L40S/A10(性价比 HBM)
[ Architecture Diagram ]
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%。

4.6 主流框架对比

框架厂商核心特性适用场景
vLLMUC BerkeleyPagedAttention,开源生态最完整开源模型自托管,研究/创业
TGIHugging FaceHF 模型零配置接入HF 模型快速 demo
SGLangUC Berkeley / xAIRadixAttention(prefix tree 共享),结构化输出多 turn、共享 prompt 场景
TensorRT-LLMNVIDIA编译优化最深,与 Triton 集成纯 NVIDIA 栈、极致性能
LMDeployInternLMTurboMind 内核,AWQ 量化优秀低显存部署、中文模型

2024-2025 的快速参考:研究/原型 选 vLLM;生产高吞吐 + NVIDIA 栈选 TensorRT-LLM;多 turn 重共享 prompt 选 SGLang。

4.6.1 性能对比基准(Llama-3-70B, H100×8)

框架throughput (tok/s)P99 TTFT (ms)P99 TPOT (ms)显存利用率
vLLM 0.628008204292%
TGI 2.424009504888%
SGLang31007404094%
TRT-LLM36006803590%

注:实测受 prompt 分布、并发数、batch 上限影响很大,仅供量级参考。

第 五 章
量化与模型压缩

5.1 量化基础:对称 / 非对称 / 静态 / 动态

量化的目的是用低精度整数表示原本的 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 后激活那样偏向一侧的分布。

[ Architecture Diagram ]
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):

5.1.1 比特位与精度的权衡

位宽每参数字节典型 perplexity 涨幅 (Llama-2-7B)HBM 需求用途
BF162baseline14 GB训练 / 服务端
FP8 (E4M3)1~0.057 GBH100 服务端
INT81~0.1-0.37 GB所有 GPU
INT4 (GPTQ)0.5~0.53.5 GB消费 GPU
INT4 (AWQ)0.5~0.33.5 GB低显存推理
INT3 (NF4 type)~0.4~1.02.8 GBQLoRA 微调存储
INT20.25~3-51.75 GB实验性,未稳定

5.2 GPTQ:基于二阶信息的逐层量化

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$$

[ Architecture Diagram ]
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 可推。

5.3 AWQ:激活感知权重量化

AWQ (Lin et al., 2023) 的关键观察:权重 outlier 不重要,激活 outlier 才重要。某些通道(约 1%)的激活幅度极大,量化其对应的权重时引入的误差被激活放大。

[ Architecture Diagram ]
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。典型场景

5.4 SmoothQuant:激活迁移到权重

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 服务端推理的事实标准。

5.4.1 GPTQ / AWQ / SmoothQuant 对比

方法权重激活目标硬件校准代价典型应用
GPTQINT4 / INT3FP16 / BF16消费 GPU中(需 Hessian)开源模型本地推理
AWQINT4FP16消费 GPU低(仅统计)chat 模型边缘部署
SmoothQuantINT8INT8服务端 (Hopper 用 FP8)高吞吐推理
FP8 PTQFP8FP8H100/H200极低(无 zero)服务端 SOTA

5.5 KV Cache 量化(INT8 / FP8)

长上下文场景下 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)。

[ Architecture Diagram ]
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) 必须做"]

实现要点:

5.6 蒸馏、剪枝与结构化稀疏

5.6.1 蒸馏

知识蒸馏:让小学生模型 $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 系列(精心蒸馏的小模型)。

5.6.2 剪枝

[ Architecture Diagram ]
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/>非结构化只省显存不省算"]

5.6.3 量化-蒸馏-剪枝组合

生产流水线:预训练 → 剪枝(结构化)→ 蒸馏恢复 → INT4 量化 → 部署。Llama-3-8B 经此组合可压到 2.5 GB,跑在手机端。代表项目:

5.6.4 压缩决策清单

  1. 显存够吗? 不够 → 先量化(INT8 不损失,INT4 损失 0.3-0.5 PPL 可接受)
  2. 是 Hopper / Ada? 是 → FP8 优先(无 zero-point,硬件原生)
  3. 长上下文? 是 → KV Cache 也量化(INT8 必须,INT4 谨慎)
  4. 端侧 / 边缘? 是 → 走 GGUF/MLX 栈,INT4 + 蒸馏
  5. 追求极致小? 训一个 1-3B 蒸馏小模型,比量化大模型经济
第 六 章
生产部署与服务化

6.1 关键指标:TTFT / TPOT / Throughput

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真实可用容量
[ Architecture Diagram ]
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

用户体验阈值经验值:

6.2 SLO 设计与吞吐-延迟权衡

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$$

[ Architecture Diagram ]
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"。

常见反模式:

6.3 弹性扩缩容与冷启动

LLM 服务扩缩容比一般 web 服务难得多,原因:

[ Architecture Diagram ]
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

常用模式:

6.4 LLM Gateway 与多模型路由

真实业务通常同时跑十几个模型(不同尺寸、不同微调版本、不同语言),需要一层网关统一接入。核心能力:

[ Architecture Diagram ]
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。

6.4.1 智能路由的决策算法

路由决策本质是一个分类问题:给定 prompt,选择哪个模型。简单到复杂:

  1. 规则路由:按 prompt 长度、关键词、用户 tier 直接分流。覆盖 80% 场景,0 训练成本。
  2. 分类器路由:训一个轻量分类器(fine-tuned bert / 小模型)预测"哪个模型最适合"。LangChain 的 Router 类即如此。
  3. Cascading 路由:先用小模型答,若 confidence 低再升级到大模型。FrugalGPT (Chen et al., 2023) 的核心思想。可省 50%+ 成本。
  4. RL/Bandit 路由:根据历史 reward 在线学习最优路由,适合多模型 A/B。

6.5 监控、灰度发布与回滚

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 评分
[ Architecture Diagram ]
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

灰度发布的工程模式:

  1. 影子流量(shadow):新模型只读取请求、不返回,对比输出
  2. 按租户灰度:1% 用户 → 5% → 20% → 50% → 100%
  3. 关键样本回放:维护一组 golden prompt(含 jailbreak、边界 case),每次发布前自动跑
  4. 自动回滚触发器:业务错误率、refusal 率、P95 延迟任一指标恶化超 X% 即自动回滚

6.5.1 质量监控的挑战

LLM 输出难以用确定指标评判,常见做法:

6.6 成本估算与优化

以 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 从高到低):

  1. continuous batching:相对静态 batch 提升 5-10×(基础设施级,必须做)
  2. INT8/FP8 量化:throughput 1.8~2.5×,几乎无质量损失
  3. Prefix Cache / Prompt Cache:长系统 prompt 跨请求共享,节省 prefill 算力(chatbot/agent 场景 30-50%)
  4. 投机解码:decode 阶段 2-3×
  5. Prefill/Decode 分离:长 prompt 业务可省 30-50% 成本
  6. 小模型路由:80% 简单请求路给 8B,整体均摊成本下降一个量级
  7. 蒸馏一个专用小模型:业务收敛后训自家小模型,比开源大模型 + prompt 便宜 10×
  8. Spot/Preemptible 节点:非关键流量用 spot,省 60-70%

6.6.1 不同部署方案的成本对比

方案硬件1M tokens 成本典型业务
OpenAI GPT-4o API第三方$5 (input) + $15 (output)原型 / 低量
Claude 3.5 Sonnet API第三方$3 + $15原型 / 长上下文
自托管 Llama-3-70B BF164× H100~$1.5中量、隐私敏感
自托管 Llama-3-70B FP82× H100~$0.5高量服务端
自托管 8B + 智能路由1× L40S~$0.05简单业务大量调用
自托管 1.5B 蒸馏小模型1× T4~$0.01专用场景大量调用

2024-2025 的趋势:从"上 GPT-4"过渡到"自家蒸馏小模型 + 路由到大模型 fallback"。一个好的 LLM 系统工程师的核心价值,是把每 1000 token 的成本压到业务能承受的水平。

6.7 生产清单

上线前的 checklist(来自实际事故经验):

  1. ☐ TTFT/TPOT P95/P99 已设 SLO 并有 alert
  2. ☐ KV Cache 监控 + 抢占重计算率有 dashboard
  3. ☐ max_tokens 上限按业务设置(防止 runaway generation)
  4. ☐ Prompt 注入防护(user input 与 system prompt 严格分隔)
  5. ☐ 速率限制按 token+请求双维度
  6. ☐ Golden prompts 回归测试集
  7. ☐ 灰度发布流程演练
  8. ☐ 一键回滚到上版本
  9. ☐ 权重 checkpoint 三副本(区域+对象存储+本地 NVMe)
  10. ☐ Cost dashboard 按租户/业务拆分
  11. ☐ 异常 prompt(jailbreak、PII、敏感词)采样审计
  12. ☐ 日志含 trace_id、模型版本、采样参数