第 15 章:推理引擎与部署
大语言模型(LLM)从训练到实际服务之间,横亘着一道工程鸿沟:模型参数动辄数十亿乃至千亿,而用户期望毫秒级响应。本章从底层算子(Flash Attention)出发,沿"算子 → 引擎 → 系统 → 部署"的路径,系统拆解现代 LLM 推理引擎的核心技术栈。
本章讨论的推理优化技术大多围绕注意力计算展开,相关基础(Scaled Dot-Product Attention、Multi-Head / Multi-Query / Grouped-Query Attention)见 §4。
15.1 Flash Attention 原理与实现
标准 Attention 的内存瓶颈与 Flash Attention 的两大创新:Tiling 分块与 Online Softmax。
标准 Attention 的瓶颈
标准 Scaled Dot-Product Attention 的计算公式为:
其中
性能瓶颈在 内存带宽(Memory Bound) 而非计算量。GPU 的计算单元(Compute)速度极快,但 HBM 的读写带宽(Bandwidth)有限。
标准实现的硬件执行流如下:
- MatMul QK:从 HBM 读取
→ 计算 → 写回 HBM( 矩阵,高昂 IO) - Softmax:从 HBM 读取整个
→ 计算 (需行最大值和行和)→ 写回 HBM( 矩阵,高昂 IO) - MatMul PV:从 HBM 读取
→ 计算 → 写回 HBM
中间矩阵
Flash Attention 的两大创新
Flash Attention 的贡献本质上是两个经典技巧的结合:
- Tiled Attention(分块矩阵乘法):将
切成能放入 SRAM 的小块,在片上完成所有计算,坚决不将 中间矩阵写回 HBM。 - Online(Streaming)Softmax:以流式方式维护运行最大值和运行和,无需看到全部 scores 即可完成归一化。其数值稳定性基于:
算法详解
符号定义:将输入矩阵划分为块:
- 行块
,每块 - 列块
和 ,每块
对每个查询块
:累积的未归一化输出 :当前行的局部最大值(数值稳定性) :指数和/分母(Running Sum)
初始化(外层循环,对每个
第
- 计算分块 Attention Score:
- 计算当前块的局部最大值:
- 更新全局最大值:
- 计算重缩放因子(Online Softmax 核心——最大值变了,旧的累加结果需要修正):
- 计算当前块的非归一化概率:
- 更新分母(旧分母乘缩放因子 + 当前块指数和):
- 更新输出(旧输出同样重缩放):
最终归一化(内层循环结束后,写回 HBM 前):
核心直觉:分子和分母分开维护。
始终存储未归一化的加权和( ), 存储归一化常数( )。循环中二者不断被重缩放以对齐新的全局最大值,但始终保持"未除"状态,最后一步统一归一化。
Python 模拟实现
import torch
import math
def standard_attention(Q, K, V):
"""标准 Attention,存在 N×N 中间矩阵的显存瓶颈"""
d_k = Q.size(-1)
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k)
probs = torch.softmax(scores, dim=-1)
return torch.matmul(probs, V)
def minimal_flash_attention_simulation(Q, K, V, block_size_M=64, block_size_N=64):
"""
Flash Attention 逻辑模拟(Tiled + Online Softmax)
不生成完整 N×N 矩阵,分块计算并动态 Rescale
"""
N, d = Q.shape
O = torch.zeros_like(Q)
scale = 1.0 / math.sqrt(d)
for i in range(0, N, block_size_M):
Q_i = Q[i : i + block_size_M]
O_i = torch.zeros_like(Q_i)
m_i = torch.ones(block_size_M, 1) * -torch.inf
l_i = torch.zeros(block_size_M, 1)
for j in range(0, N, block_size_N):
K_j = K[j : j + block_size_N]
V_j = V[j : j + block_size_N]
S_ij = torch.matmul(Q_i, K_j.T) * scale
# Online Softmax 核心
m_ij_block_max = torch.max(S_ij, dim=-1, keepdim=True)[0]
m_i_new = torch.maximum(m_i, m_ij_block_max)
P_ij = torch.exp(S_ij - m_i_new)
# 重缩放旧状态 + 累加新块贡献
l_i = l_i * torch.exp(m_i - m_i_new) + P_ij.sum(-1, keepdim=True)
O_i = O_i * torch.exp(m_i - m_i_new) + P_ij @ V_j
m_i = m_i_new
O[i : i + block_size_M] = O_i / l_i
return O
# 验证等价性
torch.manual_seed(42)
N, d = 128, 64
Q, K, V = torch.randn(N, d), torch.randn(N, d), torch.randn(N, d)
out_std = standard_attention(Q, K, V)
out_flash = minimal_flash_attention_simulation(Q, K, V, block_size_M=32, block_size_N=32)
diff = (out_std - out_flash).abs().max()
print(f"Max Difference: {diff.item()}") # ~2.98e-07
assert torch.allclose(out_std, out_flash, atol=1e-5)运行输出 Max Difference: 2.98e-07,数值上完全等价。
SDPA 与 Flash Attention 的关系
PyTorch(Transformers v4.36+)内置了 SDPA(Scaled Dot Product Attention),是 Flash Attention 的原生实现。SDPA 默认不计算 eager 实现:
model = GPT2LMHeadModel.from_pretrained(model_name, attn_implementation="eager")即使指定
output_attentions=True,在 SDPA 路径下也可能返回占位符(None),这并非 bug 而是设计取舍。
15.2 Flash Attention 在 vLLM 中的应用
Flash Attention 的安装、版本兼容性与 vLLM 集成实践。
安装 Flash Attention
Flash Attention 对 CUDA 版本和系统 glibc 版本有严格要求。常见安装方案:
# 方案 1:直接安装预编译 wheel(最快)
pip install flash-attn==2.8.1
# 方案 2:解决 GLIBC 版本不足(如 glibc < 2.32)
# 预编译 wheel 可能依赖较新 glibc,本地编译会针对当前系统链接
pip install flash-attn==2.8.1 --no-binary flash-attn \
--no-build-isolation --no-cache-dir
# 方案 3:源码编译(需要 ninja)
pip install packaging ninja
MAX_JOBS=4 pip install flash-attn --no-build-isolationglibc 兼容问题:
ImportError: .../libc.so.6: version GLIBC_2.32 not found意味着预编译 wheel 是在较新系统上构建的。使用--no-binary在本地编译,编译器会针对当前 glibc 版本链接,可规避此问题。可通过ldd --version检查当前 glibc 版本。
验证安装:
import torch, flash_attn
print(f"Flash Attention: {flash_attn.__version__}, CUDA: {torch.version.cuda}")
from flash_attn import flash_attn_func
q = torch.randn(1, 10, 32, 128, device='cuda', dtype=torch.float16)
k = torch.randn(1, 10, 32, 128, device='cuda', dtype=torch.float16)
v = torch.randn(1, 10, 32, 128, device='cuda', dtype=torch.float16)
out = flash_attn_func(q, k, v)
print("Flash Attention test passed!")vLLM 与 PyTorch 版本对应
| PyTorch 版本 | 推荐 vLLM 版本 | 说明 |
|---|---|---|
| 2.6.x | 0.8.x 系列 | 稳定组合 |
| 2.7+ | 0.9.0+ | pip install vllm==0.9.x 会自动升级 torch 到 2.7+ |
对于需要 Flash Attention 2.7.4.post1 的场景,可能需要先降级 PyTorch:
pip install torch==2.6.0 --index-url https://download.pytorch.org/whl/cu126
pip install flash_attn==2.7.4.post115.3 硬件感知优化
GPU 内存层次结构与硬件感知优化策略,包括 RoPE 算子融合。
GPU 内存层次
Flash Attention 的设计核心是硬件感知(Hardware-Aware)——充分利用 GPU 的内存层次结构:
| 存储层次 | 带宽 | 容量 | 特征 |
|---|---|---|---|
| 寄存器(Register) | 极高 | 极小 | 每线程私有 |
| L1 / 共享内存(SRAM) | 极高 | ~192 KB/SM | SM 内线程共享,是 Flash Attention 的"主战场" |
| L2 缓存 | 高 | 数 MB | 整卡共享 |
| HBM(高带宽显存) | 较低 | 40~80 GB | 容量大,但读写延迟高 |
Flash Attention 的核心策略:
- 最大化 SRAM 利用:将
的小块保留在 SRAM 中完成全部计算 - 最小化 HBM IO:整个过程 HBM 读写次数从
降至
关键洞察:Streaming Multiprocessor (SM) 是 GPU 的基本计算单元,每个 SM 拥有独立的 SRAM(共享内存)。Flash Attention 的分块大小需要精确匹配 SM 的 SRAM 容量,这正是"硬件感知"的含义。
RoPE 与 Flash Attention 的融合
位置编码(RoPE)与 Flash Attention 的协作有两种模式(关于 RoPE 的数学基础见 §5,多模态场景下的 M-RoPE 扩展见 §7):
标准流程——显式分离(PyTorch 原生 SDPA 和 Dao-AILab flash-attention 库的主要方式):
# 1. 从 HBM 读取 Q, K → SM 计算旋转 → 写回 HBM
q_rotated = apply_rope(q, cos, sin)
k_rotated = apply_rope(k, cos, sin)
# 2. 从 HBM 读取 Q', K' → SM 计算 Attention → 输出
output = flash_attn_func(q_rotated, k_rotated, v)进阶流程——算子融合(vLLM、TensorRT-LLM 等推理引擎追求极致速度的做法):
Kernel 从 HBM 读取原始 Q, K
→ 在 SRAM(寄存器)中直接对 Q, K 应用旋转
→ 进行 Q · K^T 计算
→ 最终输出写回 HBM融合后省去了 RoPE 的中间 HBM 读写,进一步降低内存带宽压力。在推理场景中,这一优化对 TTFT(首 token 延迟)有显著改善。
15.4 Attention Sink
LLM 注意力权重向序列开头聚集的现象,及其在 StreamingLLM 流式推理中的应用。
Attention Sink 现象指 LLM 在处理长序列时,注意力权重集中("沉")到序列开头的少数几个 token(典型如 BOS token)上。这些 token 承载了大量注意力权重,但未必包含实际的语义信息——它们充当了 Softmax 归一化的"汇聚点"。
直觉理解:Softmax 要求注意力权重之和为 1。当模型对某些 key 没有强偏好时,剩余的注意力分数需要有地方"倒",BOS 等固定位置的 token 就成了默认的"垃圾桶"。
应用——StreamingLLM:基于此现象,StreamingLLM 提出只保留"sink tokens"(序列开头的少数 token)和一个滑动窗口(最近的 token),即可实现不受序列长度限制的流式推理:
- 固定保留前
个 sink token 的 KV Cache( 通常很小,如 4) - 滑动窗口保留最近
个 token 的 KV Cache - KV Cache 总量恒定为
,不随序列长度增长
这避免了 KV Cache 无限膨胀的问题,使得 LLM 能够处理任意长度的输入流。
15.5 vLLM 与 SGLang 推理引擎对比
vLLM 与 SGLang 两大推理引擎的定位、核心创新与使用场景对比。
vLLM
vLLM(由 UC Berkeley LMSYS 开发)是目前最广泛使用的开源 LLM 推理引擎。其核心创新是 PagedAttention——受操作系统虚拟内存管理启发,将 KV Cache 以"页(Block)"为单位动态分配。
两大使用场景:
| 场景 | 说明 | 典型入口 |
|---|---|---|
| 推理/部署 | offline batch generation、online API 服务 | vllm serve ... 或 LLM.generate() |
| RL 训练 rollout | GRPO/PPO 训练中的采样引擎 | verl 框架中 rollout.name = "vllm" |
基础服务启动
vllm serve Qwen/Qwen2.5-1.5B-Instruct生产级配置示例:
vllm serve Qwen/Qwen2.5-7B-Instruct \
--max-model-len 8192 \
--host 0.0.0.0 --port 8000 \
--served-model-name qwen2.5-7b \
--gpu-memory-utilization 0.85 \
--enable-prefix-caching \
--enable-auto-tool-choice \
--tool-call-parser hermes \
--api-key "abc123"API 端点(FastAPI 自动生成):
http://127.0.0.1:8000/docs— Swagger 文档http://127.0.0.1:8000/metrics— Prometheus 指标(num_requests_waiting、num_requests_running)http://127.0.0.1:8000/v1/chat/completions— OpenAI 兼容接口
数据并行部署:
vllm serve $MODEL --data-parallel-size 2SGLang
SGLang(Structure Generation Language)是另一个高性能推理引擎,强调结构化生成(JSON Schema、工具调用)的效率。在 verl 训练框架中与 vLLM 并列支持:
# verl 配置(二选一)
rollout.name = "vllm" # 或 "sglang"vLLM 生态更广泛、社区更大;SGLang 在结构化输出和特定调度策略上有独到优势。两者均在快速演进中。
15.6 vLLM 高级特性与性能优化
PagedAttention 分页 KV Cache、Chunked Prefill、CUDA Graph 等 vLLM 核心机制深度解析。
PagedAttention:分页 KV Cache
传统 LLM 推理中,每条序列的 KV Cache 需要占据一块连续的显存。这就像在没有虚拟内存的旧电脑上存文件——必须找到一块完整的连续空间,否则即使零碎空间加起来够用,也会 OOM。
vLLM 借鉴操作系统的页式内存管理:
- 逻辑上连续,物理上离散:KV Cache 被切成固定大小的 Block(默认
block_size=16) - Block Table(页表):维护"逻辑第几个 Block → 物理显存地址"的映射
- Attention Kernel 通过 Block Table 正确访问任意分散的 KV 数据
具体示例(block_size=4):
用户输入 "The sky is blue"
→ 分配 Block #7:[The, sky, is, blue]
模型生成 "and"(Block #7 已满)
→ 分配空闲 Block #2:[and, _, _, _]
物理上 Block #7 和 #2 可能相隔很远
但 Block Table 记录了逻辑连续关系
Attention Kernel 透明地跨 Block 访问优势:
- 消除内存碎片:不再需要连续大块显存
- 跨请求共享:多个请求可共享相同的 KV Block(Prefix Caching 的基础)
- 动态按需分配:不必提前预留最大长度的显存
KV Cache 大小计算
单个 token 的 KV Cache 大小:
其中:
以 Qwen2.5-7B 为例(
实际日志验证:
Available KV cache memory: 5.54 GiB → 5.54 × 1024³ / 57344 ≈ 103,734 tokens
INFO ... num_gpu_blocks is: 6482
6482 blocks × 16 tokens/block = 103,712 tokens ✓显存分配全景
vLLM 启动时进行一次 Memory Profiling(用 dummy input 跑一次前向,测量峰值激活值),然后将剩余显存全部分配给 KV Cache Pool:
┌──────────────────────────────────────────────────────┐
│ 1. 模型权重(固定) │ 14.25 GB (7B BF16)
├──────────────────────────────────────────────────────┤
│ 2. 激活值峰值(由 Profiling 测定) │ 0.28 GB
│ Prefill Peak ∝ max_num_batched_tokens │
│ Decode Peak ∝ max_num_seqs │
├──────────────────────────────────────────────────────┤
│ 3. CUDA Graph(可选) │ 0.17 GB
├──────────────────────────────────────────────────────┤
│ 4. KV Cache Pool(全部剩余显存) │ 5.54 GB
│ = Total × utilization - (1) - (2) - (3) │
└──────────────────────────────────────────────────────┘实际日志示例(24 GB 显卡,gpu-memory-utilization=0.85):
Free memory on device (23.2/23.65 GiB)
Desired GPU memory utilization: 0.85 → 20.1 GiB budget
Model weights: 14.25 GiB
Peak activation: 0.28 GiB (Memory profiling takes 5.92 seconds)
CUDAGraph: 0.17 GiB
KV Cache: 5.54 GiB → 103,712 tokensProfiling 细节:vLLM 构造
max_num_seqs个虚拟请求,总 token 数精确等于max_num_batched_tokens,跑一次真实前向传播,通过torch.cuda.max_memory_allocated()捕获峰值显存。这代表了系统可能面临的最坏情况。
核心参数详解
| 参数 | 含义 | 默认值 | 影响 |
|---|---|---|---|
--gpu-memory-utilization | vLLM 可用显存比例(模型权重 + KV Cache,不含 CUDA Graph) | 0.9 | 直接决定 KV Cache 总量 |
--max-model-len | 单条请求最大总 token 数(prompt + 输出) | 模型 max_position_embeddings | 影响最大并发数 |
--max-num-seqs | 最大并发请求数(调度上限) | 256 | 影响 Decode 激活值峰值 |
--max-num-batched-tokens | 每次前向最大 token 数(跨所有请求) | 2048 | 影响 Prefill 激活值峰值,也是 Chunked Prefill 的分块大小 |
--enable-prefix-caching | 跨请求共享公共前缀的 KV | 关 | 对有公共 system prompt 的场景显著提升吞吐 |
--enforce-eager | 禁用 CUDA Graph,使用 PyTorch eager 模式 | 关 | 省显存(不捕获图),启动更快,牺牲 Decode 速度 |
max-model-len 与并发的关系——这是一个物理约束,实际并发 = min(逻辑约束 max-num-seqs,物理约束 KV Cache 总量/每请求 token 数):
max-model-len 32768:KV 98,960 tokens → 98,960/32,768 ≈ 3 个并发
max-model-len 8192:KV 98,960 tokens → 98,960/8,192 ≈ 12 个并发Prefix Caching vs. KV Cache 的区分:
- KV Cache:单个请求内部自回归生成时缓存已计算的 K/V(用于 Decode 阶段)
- Prefix Caching:跨请求共享相同前缀(如 system prompt)的 KV Cache(用于 Prefill 阶段)
两阶段(Prefill/Decode)与 Chunked Prefill
LLM 推理分为两个性质截然不同的阶段:
| 阶段 | 操作 | 计算特征 | token 数/步 |
|---|---|---|---|
| Prefill | 处理 Prompt,一次性计算所有输入 token 的 KV | 计算密集(Compute Bound) | 数百~数万 |
| Decode | 自回归生成,每步新增 1 个 token | 内存密集(Memory Bound) | 1 × 并发数 |
Chunked Prefill(默认开启):当 Prompt 过长时,将其切成多个 chunk 分批灌入 KV Cache,与 Decode 混合调度,避免一个超长 Prompt 独占整个 batch 拖垮其他请求的延迟:
场景举例:
场景 A:只有一条 10k token 的长 Prompt,无 Decode
max_num_batched_tokens = 8192
第 1 步:chunk = min(10000, 8192) = 8192,还剩 1808
第 2 步:chunk = 1808,prefill 完成
场景 B:256 个 Decode 并发 + 新到一条长 Prompt
decode_tokens = 256(每条 Decode 序列贡献 1 token)
prefill_budget = 8192 - 256 = 7936
本步 prefill chunk = min(prompt_remaining, 7936)verl 调优建议:如果 GPU Cache Utilization 较低,增大
max_num_seqs或max_num_batched_tokens可扩大 Decode 阶段的有效 batch size。推荐max_num_batched_tokens > 2048以获得更高吞吐。
CUDA Graph
CUDA Graph 解决 Decode 阶段的 CPU Launch Overhead:
在 Decode 阶段,每个序列每步只生成 1 个 token,GPU Kernel 执行时间极短,可能比 CPU 下发该 Kernel 的指令时间还短,导致 GPU 大量空闲等待 CPU。
CUDA Graph 将一系列 CUDA 操作预先"录制"成图,之后每次只需一条指令触发整个图的执行。代价是需要额外显存存储图(日志中的 Capturing CUDA graphs ... took 0.17 GiB)。
# 关闭 CUDA Graph(显存紧张时的临时方案,或调试用)
vllm serve ... --enforce-eager单机多卡部署多服务
在 8 卡机器上同时部署两个独立服务,每个服务使用 4 张 GPU 做 Data Parallel:
# 服务 A(GPU 0-3)
CUDA_VISIBLE_DEVICES=0,1,2,3 \
MASTER_ADDR=127.0.0.1 MASTER_PORT=29581 \
nohup vllm serve Qwen/Qwen2.5-7B-Instruct \
--data-parallel-size 4 \
--max-model-len 10240 \
--host 0.0.0.0 --port 8000 \
--api-key "abc123" \
--served-model-name qwen7b-a > vllmA.log 2>&1 &
# 服务 B(GPU 4-7)
CUDA_VISIBLE_DEVICES=4,5,6,7 \
MASTER_ADDR=127.0.0.1 MASTER_PORT=29582 \
nohup vllm serve Qwen/Qwen2.5-7B-Instruct \
--data-parallel-size 4 \
--max-model-len 10240 \
--host 0.0.0.0 --port 8001 \
--api-key "abc123" \
--served-model-name qwen7b-b > vllmB.log 2>&1 &关键:两个服务必须使用不同的
MASTER_PORT以避免分布式通信冲突。
15.7 vLLM 量化推理
权重量化(INT4)与浮点计算格式(FP8)的区分,以及量化配置的工程实践。
权重量化 vs. 浮点计算格式
两个常被混淆的概念:
| 类型 | 代表 | 本质 | 用途 |
|---|---|---|---|
| 整数权重量化 | INT4 (W4A16) | 4-bit 有符号两补码,按块 scale | 压缩权重存储,降低显存 |
| 浮点计算格式 | FP8 (E4M3/E5M2) | 8-bit 浮点格式 | 加速矩阵乘法计算 |
- INT4:权重以 INT4 存储,推理时反量化为 FP16 参与计算。RTX 4090(Ada Lovelace)的 4 代 Tensor Core 支持 FP8;vLLM 的 INT4 W4A16 要求 NVIDIA GPU compute capability > 8.0(Ampere 及更新架构)。
- FP8:E4M3(1 sign + 4 exp + 3 mantissa)用于前向计算,E5M2 用于更大动态范围场景。RTX 4090 有原生 FP8 Tensor Engine。
量化格式对比
| 格式 | 说明 | 硬件要求 |
|---|---|---|
| W4A16(INT4 量化) | 权重 INT4,激活 FP16 | Ampere (A100) 及更新 GPU |
| FP8 (E4M3/E5M2) | 8-bit 浮点,兼顾精度与速度 | Ada Lovelace (RTX 4090)、Hopper (H100) |
| GPTQ / AWQ | 常见 INT4 后训练量化方案 | 主流 GPU |
量化配置示例——Kimi-K2.5
Kimi-K2.5 采用 native INT4 量化,其配置中刻意排除了部分层以维护精度:
{
"num_bits": 4,
"format": "pack-quantized",
"ignore": [
"lm_head",
"re:.*self_attn.*",
"re:.*shared_experts.*",
"re:.*mlp\\.(gate|up|gate_up|down)_proj.*"
]
}设计考量:注意力层(
self_attn)和部分 MLP 层(gate/up/down projection)被排除在量化之外(保持 FP16),因为这些层对精度更为敏感。lm_head(最终输出层)同样保留全精度。
显存节省估算
以 Qwen2.5-VL-3B 为例的显存分析:
| 组件 | 计算 | 大小 |
|---|---|---|
| 模型权重(BF16) | ~6.4 GB | |
| 单 token KV Cache | ~72 KB | |
| 激活值峰值 | ~0.4 GB |
对于 120B 参数的大模型:BF16 需要 240 GB,INT4 量化后仅 60 GB,降低 75%,使其可在 2~4 张 80 GB GPU 上运行。
15.8 异步 vLLM
离线批量推理与异步服务的差异,Continuous Batching 的调度原理。
离线推理 vs. 异步服务
离线批量推理(Offline)——注重吞吐量(Throughput):
from vllm import LLM, SamplingParams
llm = LLM(model="Qwen/Qwen2.5-7B-Instruct")
outputs = llm.generate(prompts, SamplingParams(temperature=0))
# generate 是同步阻塞的:所有 prompts 处理完才返回本质上是"输入数据一次性全部就绪"的 Continuous Batching,适合数据集处理、评测等场景。
异步服务(AsyncLLMEngine)——注重延迟(Latency):
from vllm import AsyncLLMEngine, AsyncEngineArgs
from vllm.sampling_params import SamplingParams
engine = AsyncLLMEngine.from_engine_args(AsyncEngineArgs(model=...))
async def generate():
async for output in engine.generate(request_id, prompt, sampling_params):
yield output # 流式返回每个 token请求随时到达,引擎立即安排进入下一个 Iteration,用户最快速度看到第一个字。API 服务必须使用 Async 模式。
Continuous Batching
Continuous Batching 是 vLLM 的调度核心,解决了传统静态 Batching 的 Straggler(落后者)问题:
静态 Batching:
凑齐一批 → 一起处理 → 等最长序列完成 → 整批返回
问题:短请求被长请求拖累,GPU 资源浪费
Continuous Batching:
每个 iteration 后,已完成的 seq 退出,新 seq 立即加入
→ 没有固定"批次"概念,GPU 始终满载
→ 短请求快速完成并释放槽位Offline 模式并非不用 Continuous Batching——vLLM 的离线推理本质上也是 Continuous Batching,只是请求全部预先提交。区别在于 Async 模式允许请求运行时动态到达。
15.9 推理指标
TTFT、TPOT、TTE 等核心推理指标的定义与百分位延迟监控。
核心指标
| 指标 | 全称 | 含义 |
|---|---|---|
| TTFT | Time to First Token | 从请求发出到收到第一个 token 的时间(Prefill 延迟) |
| TPOT | Time Per Output Token | Decode 阶段每生成一个 token 的平均时间 |
| TTE | Time to End (E2E latency) | 完整响应的端到端延迟 |
| Throughput | 吞吐量 | 每秒处理的 token 数(tokens/s)或请求数(QPS) |
基本关系:
百分位延迟
生产环境中,平均值不足以衡量服务质量,需使用百分位指标:
| 指标 | 含义 | 用途 |
|---|---|---|
| P50(中位数) | 50% 的请求延迟不超过此值 | 反映"典型"体验 |
| P90 | 90% 的请求延迟不超过此值 | 反映大多数用户体验 |
| P99 | 99% 的请求延迟不超过此值 | 反映长尾问题,是 SLA 的关键 |
把一批请求的时延从小到大排序,位于第 50/90/99 百分位的值即为 P50/P90/P99。
vLLM 指标监控
通过 /metrics 端点(Prometheus 格式)实时监控关键指标:
| 指标 | 含义 | 调优参考 |
|---|---|---|
num_requests_waiting | 排队中的请求数 | 持续增长说明吞吐不足 |
num_requests_running | 正在执行的请求数 | 接近 max_num_seqs 说明并发已满 |
gpu_cache_usage_perc | GPU KV Cache 使用率 | 较低时可增大 max_num_seqs 或 max_num_batched_tokens |
调优公式:
max_num_batched_tokens ≈ max_concurrency × avg_tokens_per_seq如果 GPU Cache Utilization 持续偏低,说明 KV Cache 池有大量空闲——增大 batch 参数可提升有效吞吐。
15.10 LLM 推理中的非确定性
推理非确定性的真正根因不是原子加,而是 batch size 影响 GEMM kernel 的 reduction 策略。
传统认知与更精确的分析
传统认知认为 GPU 浮点运算的非确定性来自并发原子加(atomic add)——线程完成顺序不确定,加法结果因
但更精确的分析表明:在典型 LLM 前向传播中,通常不存在单个 atomic add。真正的根因是 batch size 影响了 CUDA Kernel 的 reduction 策略。
The requirement for batch invariance is that the reduction order for each element must be fixed regardless of the batch-size of the kernel.
实验验证
import torch
torch.set_default_device('cuda')
B, D = 2048, 4096
a = torch.linspace(-1000, 1000, B*D).reshape(B, D)
b = torch.linspace(-1000, 1000, D*D).reshape(D, D)
# 方法 1:矩阵-向量乘法(batch=1)
out1 = torch.mm(a[:1], b)
# 方法 2:矩阵-矩阵乘法后取第一行(batch=2048)
out2 = torch.mm(a, b)[:1]
print((out1 - out2).abs().max()) # 输出约 1152.0同一个元素的计算,batch size 不同,结果差异高达 1152! 原因是 GPU GEMM kernel 在不同 batch size 下会选择不同的瓦片(tile)配置和 reduction 顺序(如 Split-K、Stream-K 策略),导致浮点累加顺序不同。
需要关注 Batch Invariance 的三个算子
在 LLM 前向传播中,只有涉及 reduction(沿某个维度求和/求均值)的操作才可能引入 batch 相关的非确定性:
| 算子 | Reduction 操作 | 敏感原因 |
|---|---|---|
| RMSNorm | 对向量求平方和(行内归约) | batch size 影响 kernel 分块策略 |
| 矩阵乘法 | batch 变化导致 tile/Split-K 策略变化 | |
| Attention | Softmax 分母 | 序列维归约顺序随 batch 变化 |
解决方案
Batch-Invariant 矩阵乘法:固定 kernel 配置(禁用 Split-K/Stream-K、固定瓦片大小和指令族),确保 reduction 顺序与 batch size 无关
python# 使用 batch_invariant_ops 库 # Standard PyTorch: Difference 4.19, Non-deterministic # Batch-Invariant Mode: Difference 0.0, Deterministic禁用多进程调度(vLLM V1):
bashVLLM_ENABLE_V1_MULTIPROCESSING=0 vllm serve ...固定全局 seed:vLLM V1 的 seed 参数默认为 0,即使
temperature > 0也能保证每次 run 结果一致硬件级支持(最优方案):NVIDIA H100/H200/B100/B200(compute capability >= 9.0)原生支持 Batch Invariance——输出不随 batch size 变化
为什么 RL 训练需要关注确定性
在 RLHF/RLVR 训练中,这不仅仅是学术问题:
- 推理引擎(rollout)采样时的 log prob 与训练器计算的 log prob 存在数值差异
- vLLM 的 Continuous Batching 导致同一请求可能独自处理,也可能与其他请求拼 batch,batch size 随时变化
- 数值差异 → KL 散度计算偏差 → 理论上的 on-policy 算法变成事实上的 off-policy → 训练不稳定甚至崩溃
通过 Batch-Invariant 操作,可以确保采样器和训练器之间的数值比特级一致,实现真正的 on-policy 训练。
15.11 私有化部署实践
从确定性推理到 Reasoning 模型部署,生产级 vLLM 服务的配置与调优。
部署的核心指标权衡
| 指标 | 说明 | 对应参数 |
|---|---|---|
| 延迟(Latency) | TTFT、P50/P90/P99 TTE | max-model-len, enforce-eager |
| 吞吐量(Throughput) | QPS、tokens/s | max-num-batched-tokens, max-num-seqs |
| 成本(Cost) | GPU 时数、电费 | gpu-memory-utilization, 量化方案 |
| 可用性(Availability) | 服务 SLA | 多实例、健康检查 |
确定性推理配置
对于需要可复现结果的场景(评测、RL 训练):
# 1. 禁用多进程(确保调度确定性)
VLLM_ENABLE_V1_MULTIPROCESSING=0
# 2. seed 参数控制随机状态
# vLLM V1 默认 seed=0,即使 temperature>0 也保证每次 run 一致
# 3. 终极方案:Batch Invariance(需 H100/H200/B100/B200)vLLM 确定性推理文档:https://docs.vllm.ai/en/latest/usage/reproducibility/
Reasoning 模型部署
对 Qwen3 等 thinking 模型:
# vLLM >= 0.10.0:只需 --reasoning-parser 即开启 reasoning
vllm serve Qwen/Qwen3-14B --reasoning-parser qwen3
# vLLM 0.9.x:需同时指定
vllm serve Qwen/Qwen3-14B --enable-reasoning --reasoning-parser qwen3注意:指定
--reasoning-parser后,流式输出会在完整输出<think></think>之后才开始。不指定则无法区分 reasoning content 和一般 content。
请求示例:
# 开启 thinking
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "qwen3-14b",
"messages": [{"role": "user", "content": "证明 7543+3542 的计算过程"}],
"max_tokens": 512}'
# 关闭 thinking
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "qwen3-14b",
"messages": [{"role": "user", "content": "2+2 等于几?"}],
"max_tokens": 64,
"chat_template_kwargs": {"enable_thinking": false}}'
# 软开关(在消息内容中指定)
{"role": "user", "content": "/think 详细证明计算过程"}
{"role": "user", "content": "/no_think 仅给出结果"}Chat Template 配置
# --chat-template-content-format 控制 content 字段格式
# "string":传统纯文本
# {"role": "user", "content": "Hello world"}
# "openai":多模态格式
# {"role": "user", "content": [{"type": "text", "text": "Hello"}, {"type": "image_url", ...}]}
# "auto":vLLM 自动检测(默认,推荐)验证模板渲染:https://huggingface.co/spaces/huggingfacejs/chat-template-playground
KV Cache 扩展
LMCache 是 vLLM 的 KV Cache 扩展方案,支持将 KV Cache 溢出到 CPU 内存乃至 SSD,在显存有限时保留更长的上下文历史。
生产级部署模板
vllm serve Qwen/Qwen2.5-7B-Instruct \
--host 0.0.0.0 --port 8000 \
--max-model-len 8192 \
--gpu-memory-utilization 0.90 \
--enable-prefix-caching \
--max-num-batched-tokens 16384 \
--api-key "your-api-key" \
--served-model-name "production-llm"调优日志关键字段:
export VLLM_LOGGING_LEVEL=DEBUG— 开启详细日志GPU KV cache usage— 监控运行时 KV Cache 占用prefix cache hit rate— 开启 Prefix Caching 后的命中率
本章小结
本章从底层算子到生产部署,系统梳理了 LLM 推理引擎的核心技术栈:
Flash Attention 通过分块(Tiling)+ Online Softmax,将标准 Attention 的 HBM IO 从
降至 ,核心直觉是"分子分母分开维护,最后统一归一化"。 硬件感知设计 是 Flash Attention 的灵魂——分块大小精确匹配 SM 的 SRAM 容量,RoPE 等操作可进一步融合进 Attention Kernel 以消除冗余 HBM 访问。
vLLM 以 PagedAttention 为核心,借鉴 OS 页式内存管理实现动态 KV Cache 分配,配合 Continuous Batching、Prefix Caching、Chunked Prefill、CUDA Graph 等特性构建完整推理引擎。
显存分配公式
是调优的核心依据。 max-model-len、max-num-seqs、max-num-batched-tokens三个参数从不同维度约束显存使用。量化(INT4/FP8)大幅降低显存占用,但需区分"权重量化"(存储压缩)和"浮点计算格式"(计算加速)两个概念。
推理非确定性 的根因不是并发原子加,而是 batch size 影响了 GEMM kernel 的 reduction 策略。在 RL 训练中需要 Batch-Invariant 操作确保采样器和训练器数值一致。
Attention Sink 现象为长序列流式推理(StreamingLLM)提供了理论基础——保留 sink tokens + 滑动窗口即可实现恒定 KV Cache 的无限长度推理。
延伸阅读
- Flash Attention 原始论文:Dao et al., "FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness"
- Flash Attention 2:Dao, "FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning"
- Flash Attention 3 (vLLM Kernel):https://huggingface.co/kernels-community/vllm-flash-attn3
- vLLM 官方文档:https://docs.vllm.ai
- vLLM 性能优化指南:https://docs.vllm.ai/en/latest/configuration/optimization/
- vLLM 确定性推理:https://docs.vllm.ai/en/latest/usage/reproducibility/
- PagedAttention 论文:Kwon et al., "Efficient Memory Management for Large Language Model Serving with PagedAttention"
- Batch Invariance 研究:https://thinkingmachines.ai/blog/defeating-nondeterminism-in-llm-inference/
- Batch Invariant Ops 库:https://github.com/thinking-machines-lab/batch_invariant_ops
- LLM 推理原理:https://arpitbhayani.me/blogs/how-llm-inference-works
- Prompt Caching 原理:https://sankalp.bearblog.dev/how-prompt-caching-works/
- vLLM 显存估算:https://medium.com/@kimdoil1211/how-much-gpu-memory-do-you-really-need-for-efficient-llm-serving-4d26d5b8b95b
- RTX 4090 vLLM Benchmark:https://www.databasemart.com/blog/vllm-gpu-benchmark-rtx4090