Skip to content

第 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 的计算公式为:

O=softmax(QKTd)V

其中 Q,K,VRN×dN 为序列长度,d 为头维度。

性能瓶颈在 内存带宽(Memory Bound) 而非计算量。GPU 的计算单元(Compute)速度极快,但 HBM 的读写带宽(Bandwidth)有限。

标准实现的硬件执行流如下:

  1. MatMul QK:从 HBM 读取 Q,K → 计算 S=QKT写回 HBMN×N 矩阵,高昂 IO)
  2. Softmax:从 HBM 读取整个 S → 计算 P=softmax(S)(需行最大值和行和)→ 写回 HBMN×N 矩阵,高昂 IO)
  3. MatMul PV:从 HBM 读取 P,V → 计算 O=PV → 写回 HBM

中间矩阵 SP 的大小均为 O(N2),远超 GPU 片上 SRAM 容量(通常仅几百 KB/SM),因此必须反复读写 HBM,产生巨量 IO 开销。

Flash Attention 的两大创新

Flash Attention 的贡献本质上是两个经典技巧的结合:

  1. Tiled Attention(分块矩阵乘法):将 Q,K,V 切成能放入 SRAM 的小块,在片上完成所有计算,坚决不将 N×N 中间矩阵写回 HBM。
  2. Online(Streaming)Softmax:以流式方式维护运行最大值和运行和,无需看到全部 scores 即可完成归一化。其数值稳定性基于:softmax(x)=softmax(xmax(x))

算法详解

符号定义:将输入矩阵划分为块:

  • 行块 Q1,,QTr,每块 Br×d
  • 列块 K1,,KTcV1,,VTc,每块 Bc×d

对每个查询块 Qi,在 SRAM 中维护以下状态变量:

  • OiRBr×d:累积的未归一化输出
  • miRBr:当前行的局部最大值(数值稳定性)
  • iRBr:指数和/分母(Running Sum)

初始化(外层循环,对每个 Qi):

mi(0)=,i(0)=0,Oi(0)=0

j 步迭代(内层循环,处理第 j 个键值块 Kj,Vj):

  1. 计算分块 Attention Score:
Sij=QiKjTdRBr×Bc
  1. 计算当前块的局部最大值:
m~ij=rowmax(Sij)
  1. 更新全局最大值:
mi(j)=max(mi(j1),m~ij)
  1. 计算重缩放因子(Online Softmax 核心——最大值变了,旧的累加结果需要修正):
αi(j)=exp(mi(j1)mi(j))
  1. 计算当前块的非归一化概率:
Pij=exp(Sijmi(j))
  1. 更新分母(旧分母乘缩放因子 + 当前块指数和):
i(j)=i(j1)αi(j)+rowsum(Pij)
  1. 更新输出(旧输出同样重缩放):
Oi(j)=diag(αi(j))Oi(j1)+PijVj

最终归一化(内层循环结束后,写回 HBM 前):

Oi=diag(i(Tc))1Oi(Tc)

核心直觉:分子和分母分开维护。Oi 始终存储未归一化的加权和(eSijVj),i 存储归一化常数(eSij)。循环中二者不断被重缩放以对齐新的全局最大值,但始终保持"未除"状态,最后一步统一归一化。

Python 模拟实现

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 默认计算 N×N 注意力矩阵,若需可视化注意力权重,须强制使用 eager 实现:

python
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 版本有严格要求。常见安装方案:

bash
# 方案 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-isolation

glibc 兼容问题ImportError: .../libc.so.6: version GLIBC_2.32 not found 意味着预编译 wheel 是在较新系统上构建的。使用 --no-binary 在本地编译,编译器会针对当前 glibc 版本链接,可规避此问题。可通过 ldd --version 检查当前 glibc 版本。

验证安装:

python
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.x0.8.x 系列稳定组合
2.7+0.9.0+pip install vllm==0.9.x 会自动升级 torch 到 2.7+

对于需要 Flash Attention 2.7.4.post1 的场景,可能需要先降级 PyTorch:

bash
pip install torch==2.6.0 --index-url https://download.pytorch.org/whl/cu126
pip install flash_attn==2.7.4.post1

15.3 硬件感知优化

GPU 内存层次结构与硬件感知优化策略,包括 RoPE 算子融合。

GPU 内存层次

Flash Attention 的设计核心是硬件感知(Hardware-Aware)——充分利用 GPU 的内存层次结构:

存储层次带宽容量特征
寄存器(Register)极高极小每线程私有
L1 / 共享内存(SRAM)极高~192 KB/SMSM 内线程共享,是 Flash Attention 的"主战场"
L2 缓存数 MB整卡共享
HBM(高带宽显存)较低40~80 GB容量大,但读写延迟高

Flash Attention 的核心策略:

  • 最大化 SRAM 利用:将 Qi,Kj,Vj 的小块保留在 SRAM 中完成全部计算
  • 最小化 HBM IO:整个过程 HBM 读写次数从 O(N2) 降至 O(N)

关键洞察: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 库的主要方式):

python
# 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),即可实现不受序列长度限制的流式推理:

  • 固定保留前 k 个 sink token 的 KV Cache(k 通常很小,如 4)
  • 滑动窗口保留最近 w 个 token 的 KV Cache
  • KV Cache 总量恒定为 k+w,不随序列长度增长

这避免了 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 训练 rolloutGRPO/PPO 训练中的采样引擎verl 框架中 rollout.name = "vllm"

基础服务启动

bash
vllm serve Qwen/Qwen2.5-1.5B-Instruct

生产级配置示例:

bash
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_waitingnum_requests_running
  • http://127.0.0.1:8000/v1/chat/completions — OpenAI 兼容接口

数据并行部署:

bash
vllm serve $MODEL --data-parallel-size 2

SGLang

SGLang(Structure Generation Language)是另一个高性能推理引擎,强调结构化生成(JSON Schema、工具调用)的效率。在 verl 训练框架中与 vLLM 并列支持:

python
# 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 大小:

Sizetoken=2×L×Nkv×Dhead×Pbyte

其中:2 表示 K 和 V 两个矩阵,L 为层数,Nkv 为 KV 头数(GQA 中通常远小于 Q 头数),Dhead 为单头维度,Pbyte 为精度字节数。

以 Qwen2.5-7B 为例(L=28,Nkv=4,Dhead=128,BF16 即 Pbyte=2):

2×28×4×128×2=57,344 bytes56 KB/token

实际日志验证:

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:

KV Cache=(总显存×gpu_memory_utilization)模型权重激活值峰值CUDA Graph
┌──────────────────────────────────────────────────────┐
│  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 tokens

Profiling 细节:vLLM 构造 max_num_seqs 个虚拟请求,总 token 数精确等于 max_num_batched_tokens,跑一次真实前向传播,通过 torch.cuda.max_memory_allocated() 捕获峰值显存。这代表了系统可能面临的最坏情况。

核心参数详解

参数含义默认值影响
--gpu-memory-utilizationvLLM 可用显存比例(模型权重 + 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 拖垮其他请求的延迟:

total_tokens=(prefill_chunk_tokens)+num_decode_seqsmax_num_batched_tokens

场景举例

场景 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_seqsmax_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)。

bash
# 关闭 CUDA Graph(显存紧张时的临时方案,或调试用)
vllm serve ... --enforce-eager

单机多卡部署多服务

在 8 卡机器上同时部署两个独立服务,每个服务使用 4 张 GPU 做 Data Parallel:

bash
# 服务 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,激活 FP16Ampere (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 量化,其配置中刻意排除了部分层以维护精度:

json
{
  "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)3.2×109×2 bytes~6.4 GB
单 token KV Cache2×36×4×128×2 bytes~72 KB
激活值峰值8192×50 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):

python
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):

python
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 等核心推理指标的定义与百分位延迟监控。

核心指标

指标全称含义
TTFTTime to First Token从请求发出到收到第一个 token 的时间(Prefill 延迟)
TPOTTime Per Output TokenDecode 阶段每生成一个 token 的平均时间
TTETime to End (E2E latency)完整响应的端到端延迟
Throughput吞吐量每秒处理的 token 数(tokens/s)或请求数(QPS)

基本关系

TTETTFT+output_tokens/Tdecode

百分位延迟

生产环境中,平均值不足以衡量服务质量,需使用百分位指标:

指标含义用途
P50(中位数)50% 的请求延迟不超过此值反映"典型"体验
P9090% 的请求延迟不超过此值反映大多数用户体验
P9999% 的请求延迟不超过此值反映长尾问题,是 SLA 的关键

把一批请求的时延从小到大排序,位于第 50/90/99 百分位的值即为 P50/P90/P99。

vLLM 指标监控

通过 /metrics 端点(Prometheus 格式)实时监控关键指标:

指标含义调优参考
num_requests_waiting排队中的请求数持续增长说明吞吐不足
num_requests_running正在执行的请求数接近 max_num_seqs 说明并发已满
gpu_cache_usage_percGPU KV Cache 使用率较低时可增大 max_num_seqsmax_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)——线程完成顺序不确定,加法结果因 (a+b)+ca+(b+c) 而不同。

但更精确的分析表明:在典型 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.

实验验证

python
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 分块策略
矩阵乘法yi=kAikBkjbatch 变化导致 tile/Split-K 策略变化
AttentionSoftmax 分母 jexp(scorej)序列维归约顺序随 batch 变化

解决方案

  1. 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
  2. 禁用多进程调度(vLLM V1):

    bash
    VLLM_ENABLE_V1_MULTIPROCESSING=0 vllm serve ...
  3. 固定全局 seed:vLLM V1 的 seed 参数默认为 0,即使 temperature > 0 也能保证每次 run 结果一致

  4. 硬件级支持(最优方案):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 TTEmax-model-len, enforce-eager
吞吐量(Throughput)QPS、tokens/smax-num-batched-tokens, max-num-seqs
成本(Cost)GPU 时数、电费gpu-memory-utilization, 量化方案
可用性(Availability)服务 SLA多实例、健康检查

确定性推理配置

对于需要可复现结果的场景(评测、RL 训练):

bash
# 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 模型:

bash
# 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。

请求示例:

bash
# 开启 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 配置

bash
# --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,在显存有限时保留更长的上下文历史。

生产级部署模板

bash
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 推理引擎的核心技术栈:

  1. Flash Attention 通过分块(Tiling)+ Online Softmax,将标准 Attention 的 HBM IO 从 O(N2) 降至 O(N),核心直觉是"分子分母分开维护,最后统一归一化"。

  2. 硬件感知设计 是 Flash Attention 的灵魂——分块大小精确匹配 SM 的 SRAM 容量,RoPE 等操作可进一步融合进 Attention Kernel 以消除冗余 HBM 访问。

  3. vLLM 以 PagedAttention 为核心,借鉴 OS 页式内存管理实现动态 KV Cache 分配,配合 Continuous Batching、Prefix Caching、Chunked Prefill、CUDA Graph 等特性构建完整推理引擎。

  4. 显存分配公式 KV Cache=(总显存×利用率)权重激活峰值 是调优的核心依据。max-model-lenmax-num-seqsmax-num-batched-tokens 三个参数从不同维度约束显存使用。

  5. 量化(INT4/FP8)大幅降低显存占用,但需区分"权重量化"(存储压缩)和"浮点计算格式"(计算加速)两个概念。

  6. 推理非确定性 的根因不是并发原子加,而是 batch size 影响了 GEMM kernel 的 reduction 策略。在 RL 训练中需要 Batch-Invariant 操作确保采样器和训练器数值一致。

  7. Attention Sink 现象为长序列流式推理(StreamingLLM)提供了理论基础——保留 sink tokens + 滑动窗口即可实现恒定 KV Cache 的无限长度推理。


延伸阅读