惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LINUX DO - 最新话题
MyScale Blog
MyScale Blog
月光博客
月光博客
S
SegmentFault 最新的问题
C
CERT Recently Published Vulnerability Notes
P
Proofpoint News Feed
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
人人都是产品经理
人人都是产品经理
K
Kaspersky official blog
Forbes - Security
Forbes - Security
宝玉的分享
宝玉的分享
爱范儿
爱范儿
V
Visual Studio Blog
博客园 - 聂微东
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
N
News and Events Feed by Topic
阮一峰的网络日志
阮一峰的网络日志
V
V2EX
The Cloudflare Blog
Attack and Defense Labs
Attack and Defense Labs
美团技术团队
L
LangChain Blog
NISL@THU
NISL@THU
IT之家
IT之家
T
Tor Project blog
云风的 BLOG
云风的 BLOG
Security Latest
Security Latest
Apple Machine Learning Research
Apple Machine Learning Research
Cisco Talos Blog
Cisco Talos Blog
I
InfoQ
Help Net Security
Help Net Security
Engineering at Meta
Engineering at Meta
Know Your Adversary
Know Your Adversary
I
Intezer
Recent Commits to openclaw:main
Recent Commits to openclaw:main
TaoSecurity Blog
TaoSecurity Blog
P
Palo Alto Networks Blog
GbyAI
GbyAI
Last Week in AI
Last Week in AI
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - Franky
L
Lohrmann on Cybersecurity
The Register - Security
The Register - Security
W
WeLiveSecurity
Recorded Future
Recorded Future
大猫的无限游戏
大猫的无限游戏
AWS News Blog
AWS News Blog
G
GRAHAM CLULEY

土法炼钢兴趣小组的算法知识备份

国密算法与国密 TLS 系列索引 【系统架构设计】架构质量属性:不只是"高可用高性能" 【系统架构设计百科】告警策略:如何避免"狼来了" 【系统架构设计】CQRS:读写分离的架构哲学 【系统架构设计】空间架构:极端扩展场景的解法 【系统架构设计】微服务架构深度审视:优势、代价与适用边界 【系统架构设计】扩展性原理:水平、垂直与对角扩展 【系统架构设计】无状态设计:扩展的第一步也是最难的一步 【系统架构设计】缓存架构:从本地到分布式的多级缓存体系 【系统架构设计】管道与过滤器:Unix 哲学的架构表达 【系统架构设计】复杂性管理:架构的核心战场 【系统架构设计】消息队列架构:异步解耦的设计与陷阱 【系统架构设计】CDN 架构:全球加速的设计原理 【系统架构设计】连接池设计:被忽视的性能杀手 【系统架构设计】弹性设计模式:熔断器、舱壁与超时 【系统架构设计】高可用设计模式:冗余、故障转移与仲裁 【系统架构设计】容量规划:从拍脑袋到数据驱动 【系统架构设计】数据库扩展:分库分表的工程实践与替代方案 【系统架构设计】SLO 工程:可靠性的量化管理 【系统架构设计】性能建模:用数学思维分析系统瓶颈 【系统架构设计】混沌工程:主动验证系统的韧性 【系统架构设计】零拷贝与内存映射:数据搬运的极致优化 【系统架构设计】线程模型:从 thread-per-request 到协程 【系统架构设计】容灾架构:多活与灾备设计 【系统架构设计】数据库性能模式:索引、查询与连接管理 【系统架构设计】数据建模:从关系范式到文档模型的真实权衡 【系统架构设计】吞吐量优化:批处理、流水线与并发模型 【系统架构设计】流处理架构:从批处理到实时的范式迁移 【系统架构设计】搜索引擎架构:倒排索引之上的系统设计 【系统架构设计】时序数据架构:监控与 IoT 的存储设计 【系统架构设计】数据迁移与版本化:在线不停机的数据演进 【系统架构设计】数据湖与数据仓库:分析架构的演进路线 【系统架构设计】API 网关设计:入口层的职责边界 【系统架构设计】应用层数据一致性模式:在正确性与性能之间走钢丝 【系统架构设计】多模数据库选型:Polyglot Persistence 的工程实践 【系统架构设计】服务发现与注册:动态拓扑的基础设施 【系统架构设计】配置管理架构:从配置文件到配置中心 【系统架构设计】全链路压测:大规模系统的性能验证 【系统架构设计】幂等性设计:分布式环境下的安全重试 【系统架构设计】契约测试与 Schema 演进:服务间的信任协议 【系统架构设计】长连接与推送架构:WebSocket、SSE 与 MQTT 【系统架构设计】延迟分析:从 P50 到 P999 的全链路追踪 【系统架构设计百科】DDD 战术模式:聚合、实体与值对象 【系统架构设计百科】防腐层与开放主机服务:系统集成的 DDD 方案 【系统架构设计百科】领域事件与事件风暴:从业务到架构的桥梁 【系统架构设计百科】CQRS + Event Sourcing 完整实战:从领域建模到部署 【系统架构设计百科】DDD 与微服务:用领域模型划分服务边界 【系统架构设计】DDD 战略设计:限界上下文与上下文映射 【系统架构设计百科】认证架构:从 Session 到 JWT 到 OIDC 【系统架构设计】API 设计哲学:REST vs GraphQL vs gRPC 的真实权衡 排序算法专题:从 TimSort 到并行排序 【密码学百科】国密算法体系:SM2/SM3/SM4/SM9 全景解读 【密码学百科】承诺方案:Pedersen 承诺、向量承诺与多项式承诺 【密码学百科】不经意传输与隐私信息检索:OT、OT 扩展与 PIR 【密码学百科】门限密码学:门限签名、门限解密与分布式密钥生成 完美哈希:从理论到 gperf 实践 【密码学百科】安全多方计算:从 Yao 的混淆电路到实用 MPC 【密码学百科】同态加密:从 Paillier 到全同态加密(FHE) 【密码学百科】零知识证明系统:zk-SNARKs、zk-STARKs 与 Bulletproofs 【密码学百科】概率论与密码分析:生日攻击、差分分析与线性分析 【密码学百科】计算复杂性与归约:密码安全性证明的基石 【密码学百科】秘密共享:Shamir 方案、VSS 与安全多方计算入口 【密码学百科】椭圆曲线代数:Weierstrass 方程、点群运算与曲线选择 【密码学百科】离散对数与配对密码学:从 DLP 到 BLS 签名 【密码学百科】格密码数学基础:SVP、LWE 与格基约化 【密码学百科】抽象代数:群、环、域的密码学视角 【密码学百科】有限域算术:GF(2^n) 运算与在 AES/ECC 中的应用 【密码学百科】数论进阶:二次剩余、椭圆曲线上的 Weil 配对 【密码学百科】密码学简史:从凯撒密码到量子时代 【密码学百科】威胁模型与安全目标:CIA 三要素之外 【密码学百科】Kerckhoffs 原则与现代密码设计哲学 【密码学百科】随机性:密码学的基石 【密码学百科】信息论入门:熵、完美保密与 Shannon 定理 【密码学百科】分组密码原理:Feistel 网络与 SPN 结构 【密码学百科】AES 逐步拆解:SubBytes 到 MixColumns 的数学 【密码学百科】分组密码工作模式全览:ECB/CBC/CTR/OFB/CFB 【密码学百科】流密码:RC4 的兴衰与 ChaCha20 的崛起 【密码学百科】密码学哈希函数:MD5→SHA-2→SHA-3 的进化之路 【密码学百科】MAC 与 HMAC:消息认证的正确姿势 【密码学百科】认证加密(AEAD):GCM、ChaCha20-Poly1305 与 OCB 【密码学百科】密钥派生函数:HKDF、PBKDF2、Argon2 与密码存储 【密码学百科】公钥密码的数论基础:模运算、群、原根 【密码学百科】RSA 从原理到攻击:教科书 RSA 为什么不安全 【密码学百科】Diffie-Hellman 密钥交换与离散对数问题 【密码学百科】椭圆曲线密码学(ECC):从几何直觉到点群运算 【密码学百科】数字签名:ECDSA、EdDSA 与 Schnorr 签名 【密码学百科】现代密钥交换:X25519、ECDHE 与前向保密 【密码学百科】混合加密与 KEM/DEM 范式:ECIES 与 HPKE 【密码学百科】填充方案:PKCS#1 v1.5、OAEP 与 PSS 【密码学百科】TLS 协议全解析:从握手到 0-RTT 【密码学百科】PKI 与数字证书:信任链的构建与崩塌 【密码学百科】密码认证协议:从 SRP 到 OPAQUE 【密码学百科】零知识证明入门:如何证明你知道而不泄露 【密码学百科】安全信道构造:Noise 协议框架与 Signal 协议 【密码学百科】密钥管理工程:HSM、KMS 与密钥生命周期 【密码学百科】侧信道攻击:从时序攻击到功耗分析 【密码学百科】密码学实现陷阱:三层漏洞分类、审计工具链与系统性预防 密码敏捷性:如何设计可升级的密码系统 【密码学百科】OpenSSL/BoringSSL 架构剖析:ENGINE、Provider 与 FIPS 模块 排序基准测试:用数据说话
【大模型基础设施工程】12:PagedAttention 与 Continuous Batching
2026-04-22 · via 土法炼钢兴趣小组的算法知识备份

上一篇《推理引擎基础》把 Prefill / Decode、KV cache、吞吐/延迟三大指标梳理了一遍。但如果仅靠这些基本盘,LLM 服务大概只能跑出 HuggingFace Transformers 级别的吞吐——面对真实业务几百上千 QPS 的请求洪流,GPU 利用率往往只有 20–30%,显存则一半以上在”等未来可能用到”。

2022 年底 Orca 提出 iteration-level scheduling(迭代级调度)、2023 年 vLLM 把它和 PagedAttention 合并为一套通用引擎,一举把 LLaMA-13B 的吞吐做到 HF Transformers 的 14–24 倍、TGI 的 2.2–3.5 倍。从那以后,“连续批处理 + 分页 KV 缓存”几乎成为所有生产级推理引擎(vLLM、SGLang、TensorRT-LLM、TGI、Mindie、LMDeploy、RTP-LLM……)的默认骨架。

本篇把这两项技术、以及围绕它们演化出的 Chunked PrefillPrefix CachingPriority SchedulingvLLM v1 架构 讲透,并给出可直接上线的调参手册。

一、传统推理为什么慢

1.1 静态批处理的三宗罪

静态批处理(static batching)是 TensorFlow Serving / TorchServe 时代的标配:凑齐 N 条请求 → 一起送进模型 → 一起返回。对 CV 的图像分类这很合理——每条请求的计算量几乎一样。但 LLM 不是。

罪一:等最长。一个 batch 里最短的请求生成 20 token、最长的 2000 token,整个 batch 得跑 2000 步。先完成的那几条请求的 GPU slot 在剩下 1980 步里完全浪费。

罪二:按最长预留显存。HuggingFace generate() 习惯用 max_new_tokens 预先 allocate KV cache。假设 batch=32、max_seq_len=4096、hidden=5120、num_layers=40,光 KV cache 就是 32 × 4096 × 40 × 5120 × 2 × 2B ≈ 107 GiB。即便平均实际长度只有 800 token,60–80% 的显存被空置。A100 80G 当场 OOM。

罪三:Prefill 与 Decode 混批效率低。Prefill 是 compute-bound(每个 token 一次 matmul),Decode 是 memory-bound(每步一次 KV 读)。硬塞在同一个 step 里,要么 decode 等 prefill,要么 prefill 被拆成小块喂给 decode kernel,两头不讨好。

1.2 一个具体的数字

拿 LLaMA-7B / A100-80G 举例:

方案 batch 平均 GPU util 吞吐(tok/s)
HF generate() batch=1 1 8% 40
HF 静态 batch=16 16 35% 600
vLLM continuous batching 动态 ~64 85%+ 5500+

吞吐差距近 10×,核心不是 kernel 快了,而是空闲时间被消灭了

1.3 核心洞察

LLM 推理的 decode 阶段,每一步只前进 1 个 token——这正是”换人”的最佳切点。既然每一步都要重新跑一次 attention,那就每一步都重新决定”谁上、谁下、谁插队”。

这就是 Continuous Batching 的立足点。

1.4 还有哪些”隐性损耗”

除了以上三宗罪,传统推理还有几类不那么明显但同样致命的损耗:

  • Tokenizer 阻塞主循环:Python 侧 tokenizer 如果和 forward 同线程,长 prompt 的 tokenize 本身就可能吃掉几十毫秒;
  • Sampling 在 CPU:top-k、top-p、重复惩罚等后处理如果回到 CPU 做,每步都有 GPU→CPU→GPU 的往返;
  • Request 到达的泊松性:真实流量不是均匀的,静态 batch 凑不齐就死等;
  • 权重加载冷启动:HF from_pretrained 在主循环里做,首次请求 TTFT 爆炸;
  • CUDA graph 缺失:每步 forward 都走 eager 模式、反复 launch kernel,overhead 在小 batch 下很明显;
  • Attention mask 的 padding:静态 batch 下 pad 到 max_len,然后让 attention 忽略 pad 位——本质是白算了很多 FLOPs。

现代引擎(vLLM / SGLang / TRT-LLM)这些点基本都处理好了:tokenizer 独立线程池、sampler 完全在 GPU、异步请求队列、权重预加载和 warmup、CUDA graph 捕获常见 shape、PagedAttention 天然无 padding。但自己手搓推理时,这些坑一个都跑不掉。

2.1 Orca 的 iteration-level scheduling

2022 OSDI 的 Orca 论文(Yu et al., SNU + FriendliAI)提出:调度粒度从 request 降到 iteration

传统调度:

Request A: [====================] 2000 steps
Request B: [===]                  30 steps   ← 完成后 slot 空转 1970 步
Request C: [=======]              500 steps  ← 完成后 slot 空转 1500 步

迭代级调度:

step 1:  [A, B, C, D]
step 31: B 完成 → [A, C, D, E(新来)]
step 501:C 完成 → [A, D, E, F(新来)]
...

每跑完一步 forward,调度器检查: 1. 哪些 sequence 产生了 EOS / 达到 max_tokens → 立即下架,返回结果给客户端; 2. 队列里有没有新请求 → 立即上架(先做 prefill,再和大家一起 decode); 3. 剩余显存够不够容纳下一步的 KV。

2.2 Prefill 与 Decode 的混批难题

新请求插队需要先 prefill,其 token 数可能是几百上千;而老请求的 decode 每条只要 1 个 token。把 prefill 和 decode 强行拼到一个 batch 里,shape 不齐、kernel 不友好。

Orca 的做法是 selective batching:让 attention 算子按 sequence 各自算,其他算子(Linear、LayerNorm)按 flatten 的 token 维度算。这样不同长度的 token 可以共享 matmul,attention 各算各的。

vLLM 更进一步,用 PagedAttention kernel——我们下一节讲。

2.4 SVG:Continuous Batching 时序图

下图对比了静态 batch(上)和 continuous batch(下)的 GPU 占用模式。静态 batch 的右侧大片空白是”等最长”的浪费,continuous batch 则被新请求连续填满。

SVG:Continuous Batching 时序图

2.5 实现细节:内存屏障与调度周期

调度每步都要做: 1. 收集已到达的新请求 → 放入 waiting queue; 2. 检查上一步完成情况 → 把 EOS/max_tokens 的 seq 从 running 里移除; 3. 尝试从 waiting 里拉请求到 running(需要足够的 KV block); 4. 构造本步的 batched tensor(token ids、position ids、block_tables、slot_mapping); 5. 调用 forward; 6. sampling、更新 KV、检查停止条件。

其中 3–6 要非常快——如果这段逻辑比 GPU forward 还慢,GPU 就会空转。这就是为什么 vLLM v0 的 Python 调度被重写成 v1 的 C++ 调度:当 batch 很大、请求翻动频繁时,Python 侧每步几毫秒的 overhead 积累起来就能把 GPU util 打到 60%。

2.6 为什么只有 LLM 这么搞

Transformer 的 auto-regressive decode 天然可中断、可恢复:每一步输入都是”过去的 KV + 当前 token”,没有跨 step 的中间激活需要保留(相对应地,训练就不行——要留所有 activation 做反向)。这让 iteration-level 调度的切换成本几乎为零。

CV 模型(CNN、ViT 分类)、传统 Transformer encoder 做不了 iteration 级调度,因为它们是一次性前向、没有”自我延续”的结构。这也是为什么 continuous batching 基本只在 LLM 推理世界流行。

三、PagedAttention:给 KV Cache 做虚拟内存

Continuous Batching 把”谁在跑”的问题解决了,但”跑起来之后 KV cache 怎么放”还是个大问题:每条请求长度不同、还在增长,物理内存必须连续——这和操作系统在 1960 年代面对的内存碎片是同一类问题。

3.1 传统做法:预分配 max_seq_len

为每条 sequence 预先 malloc 一段连续显存:

GPU DRAM:
[Seq A: reserved 4096 tokens | Seq B: reserved 4096 | Seq C: ... | ... ]
       实际用 230                实际用 88              实际用 12

浪费率: - 内部碎片:reserved - used(预留但没用到) - 外部碎片:一条 seq 下架后释放出的窟窿,放不下后来 4096 的新请求

vLLM 论文实测 KV 显存实际利用率只有 20–40%

3.2 PagedAttention 的操作系统类比

OS 虚拟内存 PagedAttention
进程 Sequence
虚拟地址 逻辑 block index
物理页(4KB) KV block(默认 16 token)
页表 Block table
缺页中断 分配新 block
swap in/out 显存/内存/磁盘三级迁移
COW(写时复制) parallel sample / beam search 共享 block

核心思想:KV cache 不再要求物理连续。把每层、每个 head 的 KV 切成固定大小的 block,逻辑 block 到物理 block 通过 block table 查表。

3.3 数据结构

Sequence A(20 token):
  block_table = [17, 42, ?]  # block size=16,前 2 个 block 已满,第 3 个还在填
  逻辑地址 token_id=18 → block 1 内 offset 2 → 物理 block 42 offset 2

Sequence B(33 token):
  block_table = [7, 23, 5, ?]

Physical memory pool(每格 16 token 的 KV):
  [ blk0 | blk1 | ... | blk5(SeqB) | ... | blk7(SeqB) | ... | blk17(SeqA) | ... ]

每次 decode 时: 1. 模型计算出新 token 的 K/V; 2. 查当前 seq 的最后一个 block 有没有空位 → 有则追加;没有则从空闲池 alloc 一个物理 block; 3. Attention kernel 根据 block_table 跳转读取历史 KV。

3.4 SVG:逻辑/物理 block 映射

SVG:逻辑/物理 block 映射

3.5 天然支持的三类高级场景

Parallel sampling(同 prompt 生成 N 条):prompt 部分的 block 所有 sample 共享(引用计数 += N),只在某个 sample 开始分叉时 COW。省一大截 prefill 计算与 KV 显存。

Beam search:beam 候选间也用 COW 共享公共前缀。

Prefix sharing:多个请求共享系统 prompt、few-shot demo 的 block——这就是 §5 要讲的 Prefix Caching。

3.6 PagedAttention Kernel

核心是把普通 FlashAttention 的 “K[b, h, :T, d]” 连续访问,改成 “按 block_table 跳转访问”。vLLM 基于 FlashAttention 做了 paged 版本:

// 伪代码
for (int block_idx = 0; block_idx < num_blocks; block_idx++) {
    int phys_block = block_table[seq_id][block_idx];
    // 从物理 block 地址读 K / V 分块,参与 softmax(QK^T) V
    load_kv_block(kv_cache + phys_block * block_stride, ...);
    online_softmax_accumulate(...);
}

对 kernel 侧来说,唯一的新增开销是每个 block 多一次间接跳转(L2 命中良好)。实测 paged attention 比 FlashAttention 原生慢不到 5%,却换来了几倍的显存利用率和吞吐——完胜

3.7 block_size 的取舍

默认 16 token/block。可选 8、16、32(早期也支持 64,后期多数引擎收敛到 16)。

block_size 优点 缺点
8 内部碎片更小,KV 利用率 > 95% 指针开销大、L2 未命中率略高、短 seq 无差异
16(默认) 平衡点 ——
32 kernel 读取连续性更好 短请求内部碎片变大

一般别改。除非在做极端短 prompt(<32 token)且 throughput 敏感的场景,可以试 8。

3.8 COW 与引用计数

PagedAttention 对 parallel sample / beam search 的支持,本质是一套简化版的引用计数 + COW(Copy-On-Write)

初始:prompt 有 3 个 block(0,1,2),ref_count 都 = 1
parallel_sample(n=4):
    给 4 个 sample 各自的 block_table 都填 [0, 1, 2]
    ref_count 变成 [4, 4, 4]

sample_0 开始 decode:
    最后一个 block 2 的 ref_count > 1,不能原地追加
    → alloc 新 block 7,把 block 2 的内容复制过去,写入新 token
    → sample_0 的 block_table 变成 [0, 1, 7]
    → block 2 的 ref_count -= 1 → 3

sample_1, sample_2, sample_3 类似各自分叉

最终:blocks 0, 1 永远共享;block 2 在第一次 decode 时被各自 COW

在 beam search 中(beam=4,每步保留 top-4 候选),前缀的显存和计算开销几乎只付一份。对 n=4 的 parallel sampling,KV 显存从 4× prompt 降到 ~1× prompt,这是 PagedAttention 在实际产品(比如代码补全 “给我 4 个候选”)中隐形省钱的关键。

3.9 Swap 的 IO 代价

前面提过抢占时可以 swap 到 CPU。一个 block ~3.5 MiB;PCIe 4.0 x16 理论 32 GB/s、实测 ~25 GB/s。一条 8K seq = 512 blocks = 1.75 GiB,swap out 约 70 ms。对比重算 8K prefill(~200 ms on A100),swap 通常更快——但前提是 PCIe 没有和其他流量打架。多卡 TP 时,PCIe 上还有权重加载、NVLink 之外的梯度同步等,swap 的实际延迟会更高。

3.10 业界的其他”分页”变体

PagedAttention 并非唯一的 KV 管理方案。一些变体:

  • Block-sparse KV(DeepSpeed-FastGen):block 粒度 + 稀疏访问模式;
  • Ring KV(Ring Attention):长上下文下 KV 按环拓扑在多卡间分片,和 PagedAttention 正交;
  • Token-level KV pool(早期 FasterTransformer):不分块,直接 token 连续存储,抢占代价大;
  • Tile-based KV(TensorRT-LLM):和 PagedAttention 概念近似,tile size 可配。

在工程选型上,只要是现代引擎,底层都是”某种形式的分块 + 间接映射”,差别主要在 block 大小、元数据布局、和 attention kernel 的耦合度。PagedAttention 胜在开源、通用、生态完整

四、Chunked Prefill:消灭 TTFT 抖动

4.1 问题

老的 vLLM 调度策略:prefill 优先。只要队列里有新请求,就先做完 prefill 再继续 decode。一条 4K token 的 prefill 可能要跑 30–100 ms,这期间所有正在 decode 的请求都被冻住——尾延迟(P99 TPOT)爆炸

但如果decode 优先,新请求可能迟迟得不到响应,TTFT(首 token 时延)飙高

4.2 Chunked Prefill 的做法

Sarathi 论文(Microsoft)提出:把长 prefill 切成小块,每块与正在 decode 的请求混在一个 batch 里

配置一个 max_num_batched_tokens(例如 2048): - 本 step 先塞满所有 decode 请求(每条 1 token,比如 64 条 decode); - 剩下的预算(2048 − 64 = 1984 tokens)分给 prefill:可以是某条新请求 prefill 的一个 chunk,也可以是多条新请求各切一小段; - 下一 step 继续。

step t:  [64 × decode] + [A 的 prefill chunk: 1984 tokens]
step t+1:[64 × decode] + [A 的 prefill chunk: 1984 tokens]
step t+2:[64 × decode] + [A 剩余 128 + B 的 prefill 1856]  ← A prefill 完成,开始 decode
step t+3:[65 × decode] + [B 的 prefill chunk ...]

效果: - 每 step 总 token 数固定 ≈ max_num_batched_tokens延迟抖动小; - decode 不再被整段 prefill 阻塞,TPOT 稳定; - 代价是单条请求的 prefill 拖长(但对用户而言 TTFT 仍可控,因为第一块 chunk 出来就能继续)。

vLLM 0.6+ 默认开启 chunked prefill;SGLang、TensorRT-LLM 也都内置了等价机制(TRT-LLM 称之为 “in-flight batching + chunked context”)。

4.3 Chunk 大小的选择

max_num_batched_tokens 本质是”每步 token 预算”。一些经验值:

GPU 模型 建议 max_num_batched_tokens
A10 / L4 (24 GB) 7B FP16 1024–2048
A100-40G 7B / 13B 2048–4096
A100-80G 13B / 34B 4096–8192
H100-80G 70B TP=4 8192–16384

太小 → prefill 切得太碎,单条请求的 TTFT 被拖长;太大 → 每步 forward 耗时长,decode 的 TPOT 尾延迟变差。推荐做法:压测画 latency-throughput 曲线,选 P99 TPOT 在 SLA 内的最大值。

4.4 和 Continuous Batching 的关系

Continuous Batching 解决”谁在 batch 里”,Chunked Prefill 解决”每个 step 塞多少 token”。两者正交、配合使用。

4.5 Prefill-Decode Disaggregation(分离部署)

2024 年后出现一种更激进的方案:把 prefill 和 decode 放到不同的 GPU / 节点

  • Prefill 节点:compute-bound,堆 FLOPS,batch 小、step 间隔长;
  • Decode 节点:memory-bound,堆显存带宽、多 batch,step 间隔短;
  • 中间通过 KV cache 传输(NVLink / RDMA)把 prefill 出的 KV 推给 decode 节点。

代表作:DistServe(2024)、Mooncake(月之暗面,2024)。Mooncake 在 Kimi 生产上落地,把 TTFT 和 TPOT 的互相干扰彻底拆开。代价是架构复杂度爆炸,要做 KV cache 路由、跨机传输、容错。规模 > 千卡的服务值得做,中小规模 chunked prefill 就够。

五、Prefix Caching:省下重复的 Prefill

5.1 动机

真实业务里大量请求共享前缀:

  • 系统 prompt"你是一个专业客服助手,回答要简洁……"(~500 token),每个请求都带;
  • Few-shot 示例"输入:A 输出:B / 输入:C 输出:D / 输入:E 输出:..."(~2000 token);
  • 长文档 RAG:同一份文档被反复问不同问题;
  • 多轮对话:第 N 轮请求 = 第 N-1 轮 prompt + N-1 轮回复 + 新问题。

这些前缀的 KV cache 算一次就能复用,没必要每次都 prefill。

5.2 vLLM 的 Prefix Caching(Hash)

vLLM 按 block(16 token)对前缀做哈希:hash(block_content + hash(prev_block))。两条请求共享同样的前缀 block → 哈希表命中 → block_table 直接指向同一个物理 block(引用计数 +1)。

  • 命中的 block 跳过 prefill 计算
  • 只有前缀首次 mismatch 的 block 之后才开始新的 KV 计算;
  • LRU 淘汰:当 KV 池满,引用数为 0 的 block 按 LRU 释放。

命令行打开:

vllm serve Qwen/Qwen2.5-7B-Instruct --enable-prefix-caching

5.3 SGLang 的 RadixAttention

SGLang(Zheng et al., 2024)把前缀共享做到了极致:Radix Tree(基数树)管理 token 序列

  • 所有请求共用一棵 tree;
  • 新请求到来时,沿着 tree 走最长公共前缀 → 这部分 KV 直接复用;
  • 走到第一个 mismatch 节点 → 从该节点分叉;
  • LRU 回收时按 tree 叶子优先淘汰。

相比 vLLM 的 block 级 hash: - 命中粒度是 token 级(更准); - 对 Agent、ReAct、树搜索这类前缀分叉高的工作负载优化明显(命中率能到 80–95%); - SGLang 还内置结构化输出(JSON schema / grammar),对 tool use 场景很舒服。

5.4 命中率对数据的影响

一组参考数据(Qwen2.5-7B、A100、系统 prompt 800 tokens):

场景 前缀命中率 TTFT 吞吐(req/s)
关闭 prefix cache 0% 220 ms 42
vLLM hash prefix cache 92% 38 ms 115
SGLang RadixAttention 96% 28 ms 130

TTFT 降一个数量级、吞吐翻 2–3 倍。生产环境强烈建议开启

5.5 注意事项

  • 隐私:prefix cache 跨请求共享 KV,如果前缀里有用户敏感数据,要按 tenant 隔离 cache(vLLM --enable-prefix-caching 全局共享,需自行在网关层做 tenant key);
  • 一致性:模型权重变更后务必清缓存;模型升级、量化精度切换都需要 flush;
  • 显存预算:prefix cache 占住的 block 也算在 KV 池里,命中率高时等于”白赚”,命中率低时会挤压 in-flight 请求的空间;
  • Tokenizer 一致性:跨服务复用 cache 时(比如多副本、多引擎共享远端 KV),tokenizer 必须逐字节一致,否则哈希对不上;
  • 观察 Evict 频率:若 prefix_cache_evict_rate 长期偏高,说明 KV 池装不下热前缀集合,要么加显存要么缩前缀;
  • Prompt 顺序:即便内容一样,顺序不同的 prompt(如 few-shot 位置变化)也 miss。如果能控制上游,尽量固化前缀顺序。

5.6 多级 KV 缓存

生产系统里,prefix cache 通常不只放 GPU 显存。业界正在形成三级结构

L1: GPU HBM      —— 高速、容量小(几十 GiB)
L2: CPU DRAM     —— 中速(~20 GB/s PCIe)、容量中(百 GiB 级)
L3: NVMe / 分布式 —— 低速(~5 GB/s)、容量大(TB 级,跨机共享)
  • Mooncake 把 L2/L3 做成集群级 KV pool,所有推理节点共享;
  • SGLang --enable-hierarchical-cache 支持 CPU offload;
  • vLLM 社区也在做 kv_connector 插件对接外部 KV 存储(例如 LMCache 项目)。

对长上下文、系统 prompt 超长(8K+)、RAG 文档复用密集的场景,多级 KV 几乎是”必配”。

六、Priority Scheduling:当显存不够

6.1 抢占(Preemption)

假设当前有 100 条 decode、KV 占满 90%。突然来一条超长 prompt 新请求,prefill 需要 20 个新 block,但池里只剩 5 个。怎么办?

vLLM 的两种抢占策略:

Recompute:挑一条优先级最低(或最近到达)的 seq,释放它的所有 block、把它踢回等待队列。后续再调度时从头 prefill。 - 优点:简单、无 IO; - 缺点:重算代价 = 之前的 prefill + 已生成 decode 步数的 prefill 等价计算。

Swap:把被抢占 seq 的 KV block 换出到 CPU 内存,通过 PCIe 拷走。恢复时再 swap in。 - 优点:保留已有计算; - 缺点:PCIe 带宽有限(~30 GB/s),大 batch 时 swap 开销可能比 recompute 还大。

经验法则:seq 短、显存压力偶发 → swap;seq 长、显存压力持续 → recompute。vLLM 默认 recompute。

6.3 优先级调度的几个维度

真实系统里,“谁先跑”通常要看:

  • SLA tier:付费用户 / 免费用户 / 内部测试;
  • Deadline:流式场景里,某条请求已经等了 2 秒,再不响应就超时;
  • Job type:同步 API(低延迟敏感)vs 离线 batch(只看吞吐);
  • LoRA adapter:多 LoRA 服务时,已加载的 LoRA 的请求优先,减少 LoRA 切换开销;
  • 公平性:同一 tier 内用 weighted fair queueing 防止大请求饿死小请求。

6.4 Fairness vs Throughput

FIFO 调度简单但不公平(长请求饿死短请求);优先队列(按 deadline / SLA / user tier)更复杂。vLLM 社区近年在做 prioritypreempt_modelora_priority 等字段。生产系统通常在网关层做 user-level rate limiting 和优先级分类,再下发给推理引擎。相关内容第 22 篇”大模型网关”会详细展开。

七、vLLM v0 → v1:引擎重写

7.1 v0 的痛点

vLLM v0(2023–2024)架构: - Python 主循环:调度器用 Python 写,每 step 都要过一次 Python → C++ → CUDA → C++ → Python; - Block manager 开销:Python 侧管理 block_table,每 step 有可观的 CPU overhead; - CPU 阻塞 GPU:batch 数大、请求变更频繁时,CPU 调度成为瓶颈,GPU 反而空转; - 功能补丁多:prefix cache、chunked prefill、LoRA、spec decode 都以补丁形式叠加,代码路径复杂。

7.2 v1 的重构(2025)

vLLM v1(官方在 2024 年底开始 RFC,2025 年逐步 GA)做了几件事: - 调度器从 Python 搬到 C++(或用更高效的 Rust/C++ 引擎层); - 零 CPU 阻塞:调度与 forward 在不同线程 pipeline,GPU 饱和度提升; - 统一的 request lifecycle:prefill / decode / preempt / prefix-hit 走同一条路径; - 原生多模态:Vision encoder 与 LLM decoder 共享调度; - 更好的 PP/TP 支持:多卡推理的调度同步开销大幅下降。

官方数据:vLLM v1 在 LLaMA-3-70B / 8× H100 上,吞吐相对 v0 再 +30–100%,P99 延迟降低 30–50%

启用方式(0.6.x+):

VLLM_USE_V1=1 vllm serve meta-llama/Llama-3.1-8B-Instruct

到 0.7.x 已默认启用,并逐步移除 v0。

7.3 v1 的新能力

除了性能,v1 还带来几个重要能力:

  • Prefix caching 内建:v0 下 prefix cache 是可选插件,v1 下是默认路径的一部分;
  • Spec decoding 一等公民:draft model、Medusa、MTP 都统一到调度器(第 15 篇详细展开);
  • Structured output:JSON schema / regex 约束原生支持(追赶 SGLang);
  • Multi-LoRA:同时加载多个 LoRA adapter,按请求路由;
  • Disaggregated prefill:v1 的 kv_connector 接口为分离部署铺路。

7.4 升级建议

  • 生产老服务(vLLM 0.4/0.5):先小流量灰度 v1,重点验证吞吐、TTFT、P99 TPOT、prefix 命中率、LoRA 路由
  • 新服务直接上 v1;
  • 某些自定义 kernel / 自研 sampler 在 v1 可能还没 port,临时可 VLLM_USE_V1=0 回落;
  • 多模态(Vision-LM)v1 支持明显更好,有需求建议直接 v1。

7.5 Python 性能的边界

顺带一提:Python 并非全无优势。Python 调度的好处是生态和灵活性——插件、钩子、自定义采样器、快速迭代。v1 把热路径搬到 C++,但暴露的 Python API 和插件机制仍在。这是”快路径低语言、慢路径高语言”的经典工程取舍。

对自研团队的启示:先用 Python 把功能做对,再用 profiling 识别真正的 overhead 热点做下沉。vLLM 的演化路径是非常好的参考案例。

八、工程实操:vLLM 上线手册

8.1 起一个 OpenAI 兼容服务器

pip install vllm

vllm serve Qwen/Qwen2.5-7B-Instruct \
  --host 0.0.0.0 --port 8000 \
  --tensor-parallel-size 1 \
  --gpu-memory-utilization 0.9 \
  --max-model-len 8192 \
  --max-num-batched-tokens 4096 \
  --max-num-seqs 256 \
  --enable-prefix-caching \
  --enable-chunked-prefill

启动后 http://localhost:8000/v1/chat/completions 即为 OpenAI 格式接口,可直接对接 LangChain、LlamaIndex、OpenAI SDK。

8.2 关键参数解读

参数 含义 建议
--gpu-memory-utilization KV 池占显存比例 0.85–0.92;留 5–10% 给 activation 和 overhead
--max-model-len 支持的最大 context 按业务需要;越大 KV 池能塞的请求越少
--max-num-batched-tokens 单 step 最多处理 token 数(含 prefill + decode) 2048–8192;越大吞吐越高、延迟波动越大
--max-num-seqs 同时在 batch 里的 seq 上限 128–512;和 KV 池大小一起定
--tensor-parallel-size TP 切分数 单机多卡;多机要加 --pipeline-parallel-size
--block-size KV block 大小 16(默认)
--enable-prefix-caching 前缀复用 开(除非多租户隔离要求严格)
--enable-chunked-prefill 分块 prefill 开(vLLM 0.6+ 默认)
--swap-space CPU swap 空间(GiB) 4–16;承担抢占换出
--kv-cache-dtype KV 精度 auto / fp8(SM89+,显存翻倍)

8.3 KV 显存测算

启动时 vLLM 会打印类似:

# GPU blocks: 2048, # CPU blocks: 512

每个 GPU block = block_size × 2 × num_layers × num_kv_heads × head_dim × dtype_bytes

例:Qwen2.5-7B(32 层、28 KV head、128 dim、FP16、block_size=16) 16 × 2 × 32 × 28 × 128 × 2 = 3.5 MiB / block

A100-80G(扣掉权重 14 GiB、activation ~2 GiB,剩 ~60 GiB 给 KV)→ 约 17000 blocks。同时在线 ≈ 17000 × 16 / 平均 seq_len。若平均 1K token → ~270 条并发

8.4 压测脚本

# bench.py —— 用 vLLM 自带或 wrk/k6 都行,这里用 asyncio + openai SDK
import asyncio, time, random
from openai import AsyncOpenAI

client = AsyncOpenAI(base_url="http://localhost:8000/v1", api_key="x")

SYSTEM = "你是一个简洁的中文客服助手。" * 20  # ~300 token 系统 prompt,测 prefix cache

async def one_req(i):
    t0 = time.time()
    r = await client.chat.completions.create(
        model="Qwen/Qwen2.5-7B-Instruct",
        messages=[
            {"role": "system", "content": SYSTEM},
            {"role": "user", "content": f"帮我写一段关于主题 {i} 的 100 字介绍。"},
        ],
        max_tokens=200,
    )
    return time.time() - t0, r.usage.completion_tokens

async def main(n=200, concurrency=64):
    sem = asyncio.Semaphore(concurrency)
    async def go(i):
        async with sem:
            return await one_req(i)
    t0 = time.time()
    results = await asyncio.gather(*[go(i) for i in range(n)])
    dur = time.time() - t0
    tokens = sum(x[1] for x in results)
    print(f"QPS={n/dur:.1f}, tok/s={tokens/dur:.0f}, avg_latency={sum(x[0] for x in results)/n:.2f}s")

asyncio.run(main())

对比开/关 --enable-prefix-caching,实测 QPS 能差 2–3 倍。

8.5 直接用 LLM 离线推理

from vllm import LLM, SamplingParams

llm = LLM(
    model="Qwen/Qwen2.5-7B-Instruct",
    gpu_memory_utilization=0.9,
    max_model_len=4096,
    enable_prefix_caching=True,
)

sp = SamplingParams(temperature=0.7, max_tokens=256)

prompts = [
    "用 Python 写一个快速排序。",
    "用 Python 写一个归并排序。",
    "用 Python 写一个堆排序。",
] * 100  # 300 条请求

outputs = llm.generate(prompts, sp)
for o in outputs[:2]:
    print(o.outputs[0].text[:120], "...")

vLLM 会自动做 continuous batching——300 条请求并不会按顺序跑,而是同时在引擎里”流动”。

8.6 Prefix Caching 效果对比

import time
from vllm import LLM, SamplingParams

SYS = "You are a helpful assistant.\n" + "示例:Q: 1+1 A: 2\n" * 100  # 长系统 prompt

def bench(enable_cache):
    llm = LLM(model="Qwen/Qwen2.5-7B-Instruct",
              enable_prefix_caching=enable_cache,
              gpu_memory_utilization=0.85)
    sp = SamplingParams(max_tokens=64)
    prompts = [SYS + f"Q: 求 {i}*{i} 等于多少?A:" for i in range(200)]
    # 预热
    llm.generate(prompts[:4], sp)
    t0 = time.time()
    llm.generate(prompts, sp)
    return time.time() - t0

print("no  cache:", bench(False), "s")
print("with cache:", bench(True),  "s")
# 典型结果:no=42s / with=11s,约 4× 加速

8.7 关键监控指标

上线后要盯的指标(vLLM 暴露 Prometheus /metrics 端点):

指标 含义 健康区间
vllm:num_requests_running 当前 in-flight 请求数 接近 max_num_seqs → batch 打满,好
vllm:num_requests_waiting 排队请求 持续 > 0 → 容量不足
vllm:num_requests_swapped 被 swap/抢占的 > 0 → 显存压力,降 GMU 或减 max_seqs
vllm:gpu_cache_usage_perc KV 池占用率 70–90% 健康;>95% 易抢占;<50% 资源浪费
vllm:cpu_cache_usage_perc CPU swap 池占用 越低越好
vllm:prefix_cache_hit_rate 前缀命中率 业务相关;客服/RAG 应 > 80%
vllm:time_to_first_token_seconds TTFT P95 < 500 ms(交互场景)
vllm:time_per_output_token_seconds TPOT P95 < 50 ms
vllm:e2e_request_latency_seconds 端到端延迟 业务 SLA
vllm:prompt_tokens_total / generation_tokens_total 累计 token 算成本

配合 Grafana dashboard(社区有现成的)实时观察。TTFT 尖峰通常对应 prefill 阻塞;TPOT 尖峰对应 batch 内有长 seq 抢计算;swap 频繁对应 KV 池压力。

8.8 常见调参决策树

服务刚上线、还没压测?
  ├─ 先用默认:GMU=0.9, max_num_seqs=256, max_num_batched_tokens=2048
  └─ 开 prefix caching + chunked prefill

发现 TTFT 高?
  ├─ max_num_waiting 持续 > 0 → 容量不足,加卡 or 降 max_model_len
  ├─ 长 prompt 多 → 调大 max_num_batched_tokens、看 chunked prefill 是否开
  └─ 前缀固定 → 开 prefix cache

发现 TPOT 尖峰?
  ├─ 查 prefill/decode 混批比例,缩小 prefill chunk
  └─ 看是否有超长 seq 在 decode,考虑 max_model_len 裁剪

发现 OOM / 频繁 swap?
  ├─ 降 GMU 到 0.85
  ├─ 降 max_num_seqs
  ├─ 开 fp8 KV cache(kv_cache_dtype=fp8)
  └─ 量化模型(AWQ/GPTQ,第 14 篇)

吞吐上不去、GPU util 低?
  ├─ max_num_seqs 偏小 → 调大
  ├─ max_num_batched_tokens 偏小 → 调大
  └─ CPU 成瓶颈 → 切 v1

8.9 多机多卡部署

TP(张量并行)单机内用 NVLink 跑,延迟低;PP(流水并行)跨机,延迟高但能装更大模型。vLLM 支持:

# 单机 8 卡 TP=8,跑 70B
vllm serve meta-llama/Llama-3.1-70B-Instruct \
  --tensor-parallel-size 8 \
  --gpu-memory-utilization 0.92

# 2 机 × 8 卡,TP=8 + PP=2,跑 405B
# 用 Ray 做 worker 编排
ray start --head                       # node0
ray start --address=<node0-ip>:6379    # node1
vllm serve meta-llama/Llama-3.1-405B-Instruct \
  --tensor-parallel-size 8 \
  --pipeline-parallel-size 2

要点: - 跨机 PP 对延迟有可感的增加(每层多一跳),对吞吐影响小; - 建议跨机走 InfiniBand / RoCE,参见第 4 篇《互联与网络》; - 调度与 KV 管理在 PP 下复杂度上升,v1 对这一块做了专门优化。

8.10 灰度与回滚

生产环境换引擎、升版本、改参数,请遵循:

  1. 先离线 benchmark:用业务采样的真实 prompt 分布,对比新旧配置的 QPS、TTFT、TPOT、准确性;
  2. 小流量金丝雀:1–5% 流量、观察 24h,重点看 P99 尾延迟、OOM 次数、prefix 命中率、生成质量(抽样人审或自动打分);
  3. 逐步放量:10% → 30% → 50% → 100%,每一档观察至少一个完整的流量周期;
  4. 准备回滚:保留老版本的镜像、配置、warmup 脚本,出问题 5 分钟内切回;
  5. 观测大屏对比:新旧版本关键指标同屏,便于即时判断。

特别注意:vLLM 不同 minor 版本(例如 0.6 → 0.7)之间,默认参数、内部行为可能有变化(比如 v1 默认开、prefix cache 默认开等),必须重测,不能直接信赖旧配置。

九、性能与对手

9.1 吞吐对比(LLaMA-2-13B / A100-80G,来自 vLLM 论文与社区复现)

引擎 吞吐(req/s) 相对 HF
HuggingFace Transformers 0.3 1.0×
DeepSpeed-MII 0.9
FasterTransformer 1.5
TGI v0.9 2.1
vLLM v0 4.7 16×
vLLM v1 6.8 23×
SGLang(prefix 命中高) 7.5 25×
TensorRT-LLM(手调 kernel) 8.0 27×

注:吞吐绝对值会随模型、请求分布、上下文长度剧烈变化。相对排序比绝对数字更有参考价值。

9.1.1 不同请求分布下的差异

同一台 A100-80G、同一个 7B 模型,不同请求分布吞吐能差数倍:

分布 特点 vLLM 吞吐(req/s)
短 in / 短 out(Q&A,128/128) 大 batch、decode 为主 160
中 in / 中 out(聊天,512/512) 均衡 85
长 in / 短 out(摘要,4K/128) prefill 为主 35
短 in / 长 out(创作,128/2K) decode 为主、长尾 45
长 in / 长 out(RAG+长答,4K/1K) 双头压力 18

所以做容量规划时,必须用业务真实分布压测,拍脑袋选”10× HF”这种通用数字会严重偏离。

9.2 对手画像

SGLang(UC Berkeley / LMSYS): - RadixAttention → 前缀命中率一流; - 结构化输出 / 语法约束 / DSL(LMQL 风格); - Agent、tool call、ReAct 场景首选; - 近年也补齐了 TP/PP/量化,差距在缩小。

TensorRT-LLM(NVIDIA): - In-flight batching ≈ continuous batching; - Paged KV cache + 手工优化的 CUDA kernel(MHA/GQA 特化、FP8); - 极限吞吐最强,但编译流程复杂(要 build engine)、灵活性差; - 多卡 / H100 / Blackwell 上优势明显。

TGI(HuggingFace): - 生态好、易上手,但性能近年被 vLLM 拉开; - 2024 之后重构为 TGI v3,吸收了 paged / chunked 思路。

LMDeploy / RTP-LLM / Mindie(国内厂): - 原理一致,都用 continuous batching + paged KV,各有侧重(Mindie 针对昇腾 NPU、LMDeploy 量化完善、RTP-LLM 蚂蚁生产验证)。

9.3 不同工作负载的引擎偏好

不同业务特征对引擎的敏感度不同,一张速查表:

负载特征 首选引擎 原因
短 prompt + 短生成(问答、分类) vLLM continuous batching 稳
长系统 prompt + 频繁重复(客服、RAG) SGLang RadixAttention 命中率高
Agent / 多轮 / 树搜索 SGLang radix + 结构化输出
超长上下文(128K+) SGLang / vLLM v1 + 分级 KV KV 容量敏感
极限 QPS / 稳定流量 TensorRT-LLM kernel 最优
国产 NPU(昇腾 / 寒武纪) Mindie / 厂商 SDK 硬件适配
多 LoRA 服务 vLLM v1 / LMDeploy 原生 multi-LoRA
FP8 / INT4 量化重度 LMDeploy / TensorRT-LLM 量化实现成熟

下一篇《vLLM / SGLang / TensorRT-LLM / TGI 对比》会做更全面横评。

十、小结与实操清单

10.1 心智模型一句话

Continuous Batching 让 GPU 永不空转;PagedAttention 让显存永不浪费。两者加起来,LLM 推理服务才真的”现代化”了。

10.2 从 OS 回看 LLM 推理

如果把 PagedAttention + Continuous Batching + Prefix Caching + 抢占调度串起来看,会发现它就是一个针对 LLM 工作负载裁剪过的操作系统

操作系统概念 LLM 推理对应
进程 Sequence / Request
线程调度(CFS / priority) Continuous Batching + priority
虚拟内存 / 分页 PagedAttention
Page Cache / TLB Prefix Cache / block table
Swap in/out KV swap (GPU ↔︎ CPU ↔︎ NVMe)
COW fork Parallel sampling / beam share
NUMA TP/PP 多卡拓扑
Scheduler tunables GMU / max_num_seqs / chunk size

理解这层类比,对读 vLLM / SGLang 源码、设计自研引擎、排查线上诡异现象都极有帮助。许多生产问题(抖动、OOM、卡顿)在 OS 语境里都是经典问题的变体。

10.3 工程要点

  1. 生产推理必须用 continuous batching 的引擎(vLLM / SGLang / TRT-LLM / TGI v3),别自己用 HF generate() 硬撑;
  2. 开 prefix caching——系统 prompt、few-shot、RAG 前缀、多轮对话都吃这个红利;
  3. 开 chunked prefill——降低 TTFT 和 TPOT 的抖动;
  4. gpu_memory_utilization 设 0.85–0.92,别贪 0.95(会频繁抢占);
  5. max_num_batched_tokens 是吞吐—延迟的旋钮,按 SLA 试;
  6. 多租户系统要在网关层做优先级和隔离,不要指望引擎内部;
  7. 显存紧张时优先考虑 FP8 / INT8 KV cache、再考虑减 batch;
  8. 切到 vLLM v1 通常免费送 30% 吞吐;
  9. 对于 Agent / 树搜索 / 结构化输出,认真评估 SGLang
  10. 对于极限吞吐 + 稳定负载 + NVIDIA 全栈,评估 TensorRT-LLM

10.4 常见坑

  • 前缀命中率低于预期:检查 tokenizer 是否一致、系统 prompt 是否每次都完全相同(一个空格差异就会 miss);
  • TTFT 忽高忽低:开 chunked prefill、调小 max_num_batched_tokens 里留给 prefill 的预算;
  • P99 TPOT 尖峰:通常是大 prefill 挤压了 decode,同上;
  • OOM:降 gpu_memory_utilization、降 max_model_len、开 KV FP8;
  • v1 兼容性:某些插件(特定 LoRA、老版 spec decode)在 v1 上还没 GA,必要时回落 v0。

下一站:横向对比四大推理引擎,并把选型决策树画出来。

10.5 一句话对每一节的总结

章节 核心
一 传统低效 等最长、预留 max、prefill/decode 混批差
二 Continuous Batching 调度粒度从 request 降到 iteration
三 PagedAttention KV cache 虚拟内存化,零外部碎片
四 Chunked Prefill 长 prefill 切块混入 decode,抖动小
五 Prefix Caching 共享前缀跳过 prefill,TTFT 降一个数量级
六 Priority Scheduling 抢占 = swap or recompute,短用 swap 长用 recompute
七 vLLM v1 调度器从 Python 搬到 C++,吞吐 +30–100%
八 实操 开 prefix cache + chunked prefill + GMU 0.9
九 对手 SGLang Agent 首选,TRT-LLM 吞吐王,LMDeploy/Mindie 国产

10.6 延伸阅读路线图

  • 想深入 kernel 层优化(paged attention 如何和 FlashAttention-2/3 融合)→ 第 3 篇《CUDA 生态》+ 原论文;
  • 想做推理引擎选型→ 第 13 篇;
  • 想进一步省显存、上速度→ 第 14 篇(量化)、第 15 篇(推测解码);
  • 想搞长上下文 128K/1M→ 第 16 篇;
  • 想搭生产级服务(网关、多副本、金丝雀、可观测)→ 第 21–23 篇。

10.7 思考题

  1. 若业务 90% 请求前缀都是同一套 20KB 的系统 prompt,你会怎么分配 gpu_memory_utilizationmax_num_seqs?为什么?
  2. A100-80G、LLaMA-13B FP16 权重 26 GiB,KV 给 50 GiB,block_size=16,算一下理论最大并发请求数(假设平均 context 2K token)。
  3. 什么情况下 recompute 优于 swap?反之呢?PCIe 4.0 → 5.0 会改变答案吗?
  4. SGLang 的 RadixAttention 比 vLLM 的 hash prefix cache 好在哪里?在什么场景下这个差异可忽略?
  5. 如果你要做一个只服务付费 API的推理网关,从 continuous batching 和优先级调度出发,你会在网关层、引擎层分别做什么?

10.8 FAQ

Q:开 prefix caching 会影响生成结果的确定性吗? A:不会。KV 命中等价于”这段 token 已经算过了”,和重新算的结果在数学上一致(忽略浮点误差级别的差异)。生成结果由 sampling 和 seed 决定。

Q:chunked prefill 会让单条请求的 prefill 变慢吗? A:会一点。一条 prefill 被切成 N 块,每块会和 decode 共享 step,单条 prefill 的 wall time 通常增加 10–30%。但整体 QPS 上升、尾延迟下降,多数业务可以接受。

Q:PagedAttention kernel 自己写难吗? A:能写,但不建议。vLLM / FlashInfer / xFormers 都提供了优化版本,覆盖 MHA/MQA/GQA、FP16/BF16/FP8、sliding window、ALiBi、RoPE 变体等组合。自己写很容易在边界(block 不满、跨 block、起始位置对齐)出 bug。

Q:分离部署(P/D disaggregation)和 chunked prefill,选哪个? A:中小规模先 chunked prefill(一条命令的事);百卡/千卡集群、流量大且稳定、对 TTFT 和 TPOT 都有硬 SLA 才值得上分离部署。分离部署要额外处理 KV 路由、跨机传输、容错,不是免费午餐。

Q:v1 现在稳吗?生产能用吗? A:vLLM 0.7+ 默认 v1,主流模型(LLaMA、Qwen、Mistral、DeepSeek、GLM)都稳定。冷门模型、自定义 kernel、老版 spec decoding 建议先回归测试。2025 年起新项目直接 v1。

Q:KV cache 能压缩吗? A:能。FP8 KV(显存减半、精度几乎无损)、INT4 KV(显存 1/4、需校准)、H2O/StreamingLLM(只保留关键 token 的 KV)都在产线用。细节见第 14 篇量化和第 16 篇长上下文。

Q:prefix cache 命中率怎么提升? A:(1) 固化系统 prompt,别每次拼接动态时间戳;(2) few-shot 顺序固定;(3) 多轮对话把历史放在 prompt 前缀;(4) RAG 的 retrieved chunks 按稳定顺序拼接;(5) 考虑 SGLang RadixAttention 做更细粒度复用。

参考资料

  1. Yu et al., Orca: A Distributed Serving System for Transformer-Based Generative Models, OSDI 2022.
  2. Kwon et al., Efficient Memory Management for Large Language Model Serving with PagedAttention, SOSP 2023.(vLLM 原论文)
  3. Agrawal et al., SARATHI: Efficient LLM Inference by Piggybacking Decodes with Chunked Prefills, 2023.
  4. Zheng et al., SGLang: Efficient Execution of Structured Language Model Programs, 2024.(RadixAttention)
  5. vLLM 官方文档与 v1 RFC:https://docs.vllm.ai/https://github.com/vllm-project/vllm/issues/8779
  6. NVIDIA TensorRT-LLM 文档:https://nvidia.github.io/TensorRT-LLM/
  7. HuggingFace TGI:https://github.com/huggingface/text-generation-inference
  8. Dao et al., FlashAttention-2, 2023.
  9. Zhong et al., DistServe: Disaggregating Prefill and Decoding for Goodput-Optimized Large Language Model Serving, OSDI 2024.
  10. Moonshot AI, Mooncake: A KVCache-Centric Disaggregated Architecture for LLM Serving, 2024.
  11. vLLM v1 blog: https://blog.vllm.ai/2025/01/27/v1-alpha-release.html
  12. LMCache:跨节点 KV 共享 https://github.com/LMCache/LMCache
  13. 内部数据与社区复现:vLLM Discord、SGLang GitHub issues(版本差异较大,落地前建议自测)。

上一篇推理引擎基础 下一篇vLLM / SGLang / TensorRT-LLM / TGI 对比

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-22 · architecture / ai-infra

大模型基础设施工程

面向中国工程团队的大模型基础设施系列。从 GPU/CUDA/互联、训练框架与 3D 并行、vLLM/SGLang 推理引擎、量化与推测解码、RAG/Agent 到服务化、网关、可观测性与安全合规,覆盖 LLMOps 全链路。