vLLM 推理内核深度解析
第18章 设计哲学与架构智慧
第18章 设计哲学与架构智慧
“Simplicity is the ultimate sophistication.” — Leonardo da Vinci
本章要点
- 回顾 vLLM 前 17 章,提炼贯穿整个代码库的 12 条设计原则
- 理解 V0 → V1 重构背后的架构判断:什么时候该重写、如何降低重写风险
- 认识”借用已有领域的智慧”这一跨领域思维的实操路径
- 以 Linux 内核、Nginx、Kubernetes、Kafka、Redis 为对照,理解这些原则为什么能跨领域迁移,但不把类比当作证据本身
- 拿到一套可迁移的工具箱:当你开始设计下一个高性能系统时,这些原则直接可用
18.1 跨领域借鉴:操作系统思维
vLLM 最核心的创新——PagedAttention——不是凭空发明的。它是把操作系统领域 62 年前的虚拟内存思想迁移到 GPU 显存管理中。这不是修辞——论文原文的 Related Work 第一段就明确致敬了 1961 年的 Atlas 计算机。
更有趣的是,这种”跨领域搬运”在 vLLM 中反复出现:
| vLLM 里的机制 | 操作系统里的对应 | 章节 |
|---|---|---|
| 分页 KV Cache | 虚拟内存分页 | 第 4 章 |
| Block Table | 页表 | 第 4-5 章 |
| Free queue + 缓存驱逐顺序 | 页面置换算法的队列思想 | 第 5 章 |
| 引用计数 + 满块共享 | 文件系统 inode、Rust Arc | 第 5 章 |
| 连续批处理 | 进程调度的时间片 + 抢占 | 第 3 章 |
| 前缀缓存哈希链 | Git 的内容寻址、文件系统的哈希树 | 第 10 章 |
| SchedulerStats 指标 | Linux proc / sys 文件 | 第 3 章 |
| collective 调用语义 | MPI 的集合通信 | 第 6 章 |
| 共享内存数据通道 | POSIX shm 的思路 | 第 6 章 |
这不是说 vLLM 和操作系统一一等价,而是说成熟领域(操作系统、数据库、编译器、分布式系统)积累了大量经过验证的不变量。把这些不变量搬到新场景时,仍然要重新验证边界:GPU 显存不是 DRAM,KV block 不是 4KB page,prefix cache 也不是 Linux page cache。
跨领域搬运的三步法
从 PagedAttention 这个成功案例里能提炼出一套复用方法:
- 识别你的本领域问题的本质结构
- 例:KV Cache 的本质是”按请求生长、按请求释放、大小不定”的动态数据
- 去掉”GPU”、“KV”、“注意力”这些领域词后,它看起来像什么?
- 在相邻领域里找结构对应的成熟解
- 例:同样是”按进程生长、大小不定的动态数据”——操作系统进程内存
- 问自己:那个领域花了多少年、走过多少错路才得到今天的方案?
- 把对应方案的不变量抽出来,套到本领域
- 例:虚拟内存的核心不变量是”逻辑连续、物理离散、中间加一张翻译表”
- 把这个不变量实例化成 Block Table + Block Pool,就是 PagedAttention
这三步做一次你可能觉得是”灵光一闪”;做三次你会发现是一种可训练的能力。
其他领域的类似成功案例:
- Nginx 的 event-driven 架构:借用 Linux
epoll/kqueue的思想,让一个进程处理几万连接 - Kubernetes 的 declarative API:借用 SQL 和 make 的”声明期望状态”思想,把运维变成代码
- Kafka 的分区日志:借用数据库 WAL 的思想,把消息队列建在仅追加的有序日志之上
- Rust 的所有权系统:借用线性类型理论、区域推断、RAII,把垃圾回收放进编译期
vLLM 给我们的启发:遇到新领域的难题,第一反应不应该是”我怎么发明”,而是”谁已经解决过结构上同样的问题”。
18.2 统一抽象:让一类问题消失
V1 最深刻的变革不是多进程化,而是统一 Token 调度——调度器不再区分 Prefill 和 Decode,只看”这一拍给这个请求分配多少个 Token”。
这个看起来”简化”的抽象带来了一连串连锁效应:
graph TB
U["统一 {req_id: num_tokens} 抽象"]
U --> C1["Chunked Prefill<br/>自然支持<br/>(不需要特殊 branch)"]
U --> C2["Prefill + Decode 混合批<br/>天然兼容<br/>(都是一个 batch 里 N 个 token)"]
U --> C3["Spec Decode<br/>只是一次产多 token<br/>(N=4 而不是 N=1)"]
U --> C4["调度路径<br/>更少区分<br/>(减少分支地狱)"]
U --> C5["Worker 不再需要<br/>多种 execute_model 形态"]
U --> C6["单元测试更容易<br/>(输出是纯函数 dict)"]
style U fill:#3b82f6,color:#fff,stroke:none
style C1 fill:#10b981,color:#fff,stroke:none
style C2 fill:#10b981,color:#fff,stroke:none
style C3 fill:#10b981,color:#fff,stroke:none
style C4 fill:#10b981,color:#fff,stroke:none
style C5 fill:#10b981,color:#fff,stroke:none
style C6 fill:#10b981,color:#fff,stroke:none
好的抽象不是让一件事变容易,而是让一类问题消失。V0 的 Chunked Prefill 是一个独立 feature,需要专门代码路径;V1 里它根本不是 feature——它是统一调度的”副产品”。
类似的思想在其他系统里俯拾皆是:
- Linux 的”一切皆文件”——文件、设备、socket、管道、进程,都是文件描述符;一套
read/write/selectAPI 处理所有 I/O,消除了”专门的 I/O API”这一整类复杂性 - Kubernetes 的 Controller-Reconciler 模式——Deployment、StatefulSet、Job、CronJob、HPA、VPA、PVC 全部用同一套”期望态 → 观察态 → 差值 → 执行”的循环实现
- React 的”一切皆组件”——按钮、列表、页面、App、路由、全局状态都是组件,消除了”页面 vs 组件”、“容器 vs 展示”的早期区分
- Unix pipe 的”一切皆文本流”——
grep | awk | sort | uniq这种组合正是因为各命令都只处理文本流
vLLM 给我们的启发:当你在一个系统里看到 if X then ... elif Y then ... else ... 的长分支,问自己:“是不是有一种更高维度的抽象能让 X、Y、Z 都变成同一件事?” 这是架构演进的一个最高 ROI 动作。
18.3 为主路径优化,谨慎接纳边缘功能
V1 有一系列”收窄核心路径”的决策:
- 砍掉
SequenceGroup(为 beam search 设计的组抽象)—— beam search 在生产里几乎不用 - 砍掉 SWAP 式抢占 —— 只保留 RECOMPUTE 式
- 砍掉 KV Cache 多种分配策略的切换 —— 只保留 Paged 一种
- 砍掉一些 V0 里”开关式”的 cross-attention 优化
这不是偷懒。这是有意识的取舍。
在 LLM 推理的常见在线服务里,greedy / top-p / top-k 采样、paged KV、recompute 式恢复往往覆盖主路径。beam search、复杂 swap 恢复、非常规 attention 仍然有价值,但它们不一定应该进入最热的引擎内核。
长尾功能的复杂度代价会由所有维护者承担:代码维护者、调试者、性能优化者、新贡献者、以及未来读这段代码的人。这个税需要被显式评估。
V1 的做法更接近:把主路径做到清楚,把长尾路径放在明确边界外。不是所有长尾都要删除,而是进入核心前必须说明它值得让所有人承担复杂度。
对照:
- Go 语言:砍掉泛型(V1 时代)、砍掉异常、砍掉继承。简化了语言,但偶尔让你写出略啰嗦的代码。Go 团队接受了这个代价,因为它让新手两天上手
- Redis 的单线程模型:牺牲多核扩展能力(可以 cluster 补回),换来内部数据结构全部无锁——bug 量级减少
- SQLite 的”不做事务日志归档”:牺牲点 crash-safety 余地,换来单文件部署的零运维体验
vLLM 给我们的启发:砍功能比加功能更需要判断力。当你添加一个”万一有人要用呢”的可选 feature,要想清楚长尾代价,以及它是否能留在引擎外部。
18.4 关注点分离:进程、模块、数据契约的三重分离
V1 的多进程架构(API Server / EngineCore / Worker 分属不同进程)不只是为了”绕过 GIL”——更深的意义是关注点分离:
- API Server:只关心 HTTP / gRPC、tokenize、response 流式化
- EngineCore:只关心调度、KV 资源、请求状态机
- Worker:只关心 GPU 前向、sample、tensor 并行
每个组件都能独立优化、独立扩展、独立失败。而且:
graph LR
subgraph "数据契约(进程间传递的类型)"
D1["ClientRequest<br/>API 层接收"]
D2["EngineCoreRequest<br/>跨进程最小化"]
D3["SchedulerOutput<br/>Scheduler → Worker"]
D4["ModelRunnerOutput<br/>Worker → Scheduler"]
D5["EngineCoreOutput<br/>EngineCore → API"]
end
D1 --> D2
D2 --> D3
D3 --> D4
D4 --> D5
style D2 fill:#8b5cf6,color:#fff,stroke:none
style D3 fill:#f59e0b,color:#fff,stroke:none
style D4 fill:#f59e0b,color:#fff,stroke:none
每一个跨进程边界都用明确的 DTO(Data Transfer Object)定义。这些 DTO 的特征:
- 边界清楚——API 层对象、EngineCoreRequest、SchedulerOutput、ModelRunnerOutput 分属不同阶段,不把整个上游对象透传到底
- 字段最小化——只带下游真正需要的字段,其他都不传(见 ch02 里 EngineCoreRequest vs Request 的对照)
- 可跨进程传输——V1 的本地 IPC 路径要求这些对象能被序列化;第 6 章已经校正过,MessageQueue 数据路径使用共享内存,控制对象走 Python 序列化,而不是把
msgpack当成事实 - 演进可控——字段增减要考虑 API Server、EngineCore、Worker 三层的边界影响
这种”数据契约最小化”让 vLLM 能把变化压在局部。换一个 API Server,理想情况下 EngineCore 不应该被迫理解 HTTP 细节;换一个 Executor,Scheduler 不应该被迫知道底层是 MultiProc 还是 Ray。现实里边界不可能完美,但契约越窄,重构成本越可控。
对照:
- Linux 内核:syscall 表是内核和用户态的数据契约;已经稳定了 30 年
- Kafka:broker 和 consumer 的有线协议兼容几十个 minor 版本
- Kubernetes API:CRD 按版本分层(v1alpha1 / v1beta1 / v1),只在 v1 稳定版承诺兼容
vLLM 给我们的启发:当你设计一个有多个组件的系统,先设计它们之间的数据契约,然后每个组件独立实现。不要让进程 A 的内部状态结构暴露到进程 B——那是未来所有重构灾难的起点。
18.5 可插拔设计:稳定接口是一切生态的前提
vLLM 在多个维度采用可插拔设计,每一处都有稳定的接口契约:
graph TD
subgraph "vLLM 的可插拔接口矩阵"
EX["Executor 接口<br/>UniProc / Multiproc / Ray"]
ML["ModelLoader 接口<br/>Default / Runai / Tensorizer / BnB"]
QC["QuantizationConfig 接口<br/>GPTQ / AWQ / FP8 / BnB / Marlin / ..."]
MR["ModelRunner 接口<br/>GPU / TPU / CPU / Neuron"]
AB["AttentionBackend 接口<br/>FlashAttn / FlashInfer / Triton / Pallas / MLA"]
SD["SpecDecode Proposer 接口<br/>Draft model / EAGLE / MLP / NGram"]
LM["LogitsProcessor 接口<br/>结构化输出 / XGrammar / outlines"]
end
Dev["社区贡献者"] -->|"实现接口<br/>提交 PR"| EX & ML & QC & MR & AB & SD & LM
style Dev fill:#3b82f6,color:#fff,stroke:none
本书前面已经看过几个具体例子:模型加载器有多种实现,量化通过 QuantizationConfig 接入,AttentionBackend 可以在 FlashAttention、FlashInfer、Triton、Pallas、MLA 等路径之间切换,Executor 也有 UniProc、MultiProc、Ray 等形态。共同点是:核心引擎尽量面向稳定接口,而不是面向某个具体实现。
可插拔的关键不是”有接口”——几乎所有项目都有接口——而是”接口边界稳定”。接口太薄,插件会被迫绕回内部状态;接口太厚,核心每次改动都要拖着所有实现一起改。vLLM 的工程价值就在于不断把这条线往合适的位置推:AttentionBackend 管物理 layout 和 kernel 调用,Scheduler 仍然只管 block id 和 token 数。
对照:
- Linux VFS:通过稳定的
file_operations结构体,支持了上百种文件系统 - POSIX:让 C 程序在 Linux / macOS / BSD / Solaris 之间几乎无缝迁移
- JVM:class 文件格式 30 年没破坏性变化,让 Java 生态积累到今天
- WebGPU / WebGL:稳定的 API 让前端图形生态可以跨浏览器工作
vLLM 给我们的启发:当你有一个大家都想插手的系统(“我也想加一个我的 XXX 支持!”),先把接口契约冻结,再开放实现。接口越稳定,生态越繁荣。
18.6 性能优化的分层心智
vLLM 的优化不是”碰到哪里慢就优化哪里”的乱炖,而是有一个清晰的层次结构:
graph TB
L1["第一层:算法与数据结构<br/>PagedAttention, Prefix Cache, Spec Decode"]
L2["第二层:系统架构<br/>多进程 + 统一调度 + Continuous Batching"]
L3["第三层:工程优化<br/>CUDA Graph, shared memory, 持久化 InputBatch"]
L4["第四层:微观优化<br/>numpy buffer, fast path, 少分配对象"]
L1 --> L2
L2 --> L3
L3 --> L4
style L1 fill:#ef4444,color:#fff,stroke:none
style L2 fill:#f59e0b,color:#fff,stroke:none
style L3 fill:#3b82f6,color:#fff,stroke:none
style L4 fill:#10b981,color:#fff,stroke:none
层次规则:
- 先对齐第一层 —— 如果数据结构选错了,后面三层怎么优化都是在劣势上挣扎。比如没有 paged KV,再精细调 kernel launch 也很难解决碎片和预留浪费。
- 然后是第二层 —— 架构决定了天花板。V1 把 API、EngineCore、Worker 拆开,才有空间分别优化请求处理、调度和 GPU 执行。
- 第三层给”最后一公里” —— CUDA Graph、shared memory、持久化 InputBatch 这些优化都建立在前两层已经合理的前提上。
- 最后才是第四层 —— 少分配对象、少转换格式、给热路径加 fast path 都有价值,但不要本末倒置。
新人常犯的错误是跳过前三层直接盯第四层——花两天把某个热函数改用 C 扩展重写,结果发现上面的 Python 调度器多了 10 倍 GC 成本,整体反而变慢。
对照:
- 编译器优化也分层:算法选择 → IR 选择 → 寄存器分配 → 指令调度 → 窥孔优化
- 数据库性能优化:schema 设计 > 索引 > 查询计划 > 缓存调优 > 连接池参数
- 前端性能:代码分包 > 请求合并 > 图片压缩 > 字节对齐
vLLM 给我们的启发:性能问题不要从最底层开始优化。先问”算法对吗”,再问”架构对吗”,最后才问”细节对吗”。
18.7 有状态 Worker:通信模型的思想变革
V0 → V1 的另一个影响深远的变化是 Worker 从无状态变成有状态(ch06 详述)。这个变化表面看是”为了省通信量”,深层其实是通信模型的范式转移。
无状态 Worker 的通信模型:
每步:Executor → Worker(全量状态)
Worker → Executor(全量结果)
有状态 Worker 的通信模型:
第一步:Executor → Worker(全量 bootstrap)
后续步:Executor → Worker(只传 diff)
Worker → Executor(只传新增 token)
这和编程世界里两种著名范式的对立完全吻合:
- 无状态 = 函数式思维:每次调用都是纯函数,没有副作用,重复调用结果相同
- 有状态 = 对象式思维:组件有内部状态,调用是”指令”,依赖状态演化
两种范式都有经典代表:
| 无状态(函数式) | 有状态(对象式) |
|---|---|
| HTTP 1.x 请求 | WebSocket 连接 |
| RESTful API | gRPC streaming |
| Lambda functions | Actor model (Erlang, Ray, Akka) |
| MapReduce | Spark 的 RDD 持久化 |
| Linux syscalls | Windows 的 DeviceIoControl |
V0 的”纯函数”Worker 模型更好测试、更好调试,但在高频 RPC 场景下会反复传递大量状态。V1 切到”有状态”后,通信内容更接近 diff,代价是必须处理一致性。这个代价在”同机 IPC + fail-fast”的约束下更可控;一旦扩展到跨机、弱网络或需要局部恢复,判断就要重新做。
vLLM 给我们的启发:函数式 vs 对象式不是信仰之争。选哪个要看通信频率、状态大小、一致性要求。高频 + 大状态 + 同机 = 有状态(actor / streaming);低频 + 小状态 + 跨机 = 无状态(REST / function)。
18.8 fail-fast 哲学:不要尝试半恢复
V1 在多处选择 fail-fast 而不是复杂恢复:
- Worker 崩溃 → 整个 EngineCore 退出,等 K8s 重建
- 启动时 OOM → 直接退出,不尝试降采样
- ZMQ 消息格式错乱 → 抛异常
- Scheduler 状态不一致 → assert 失败而不是尝试修复
这背后的哲学是:“半恢复”的状态几乎总比”完全退出”糟糕。
为什么?
- 半恢复的状态空间巨大:一个 Worker 复活后,它的 KV 池、CUDA Graph、TP rendezvous 哪一层没对齐,都会把错误留到后续请求里
- 错误会累积:第一次半恢复漏掉的状态 bug,在后面会成倍放大
- 用户体验反而更差:半恢复期间的请求看起来像”卡住”,用户重试一次;而 fail-fast 下用户几秒内就切到别的副本
- SRE 心智模型简单:fail-fast 的实例要么正常、要么死;没有”僵尸”状态
对照:
- Erlang 的 “Let it crash”:发明 fail-fast 一代的哲学源头。进程挂了就挂了,监督树重启
- Kubernetes 的健康检查:不健康直接 kill + 重建,不自愈
- Go 的 panic:不恢复到不一致状态的默认行为
- WAL + fsync + crash-recovery:数据库也不尝试”部分保留正在写的事务”,要么整个事务 commit、要么整个回滚
vLLM 给我们的启发:当你在写一个”如果失败怎么办”的分支,先问自己——“我复活的这个状态,对吗?” 如果不能百分百确定对,选 fail-fast + 外部重建。
18.9 勇于重写:V0 → V1 的技术自省
V1 不是 V0 的渐进改良,而是一次核心路径重写。EngineCore、调度器、KV Cache 管理器、Worker、Executor 都被重新组织。这种重写的风险很高:旧接口、旧 feature、旧性能假设都会同时受到冲击。
重写的底气来自五个要件:
- 清晰的目标——V1 的每个改动对应一个 V0 中已知的痛点(Scheduler 的分支地狱、Worker 的冗余通信、LLMEngine 的 God Object)
- 渐进过渡——V1 先作为
VLLM_USE_V1=1opt-in 发布,稳定几个月后才成为默认 - 向后兼容——V0 代码保留一段时间,V1 不支持的 feature 能 fallback 到 V0
- 基准数据——重写前后要用吞吐、延迟、稳定性、回归测试一起验证收益,不是”感觉更好”
- 组织共识——核心维护者群就 V1 方向达成一致才开始
对照:
- Python 2 → Python 3:花了 12 年,痛苦无比;但最终社区都迁移过去了
- Vue 2 → Vue 3:渐进式采用,Composition API 是 opt-in
- Angular 1 → Angular 2:一次性重写而非兼容,但中间花了几年补生态
- Rust 2015 → 2018 → 2021 Edition:借助”Edition”机制让旧代码继续跑、新代码享受新 feature
vLLM 给我们的启发:不要害怕重写,但要先确认目标、迁移路径、兼容策略、数据证据和组织共识。缺少任何一个,重写都可能变成长期分叉。
18.10 数据驱动:用基准代替直觉
性能章节最容易犯的错,是把一次实验里的数字写成永久真理。模型、batch、硬件、CUDA 版本、attention backend、prompt/output 分布一变,数字就可能改变。因此本章不保留”某优化固定提升几倍”这类说法,只保留工程原则:性能决策必须能被复现。
一份有用的 benchmark 至少要写清楚:
- 模型、dtype、KV cache dtype、max_model_len、并行配置。
- 输入分布:prompt 长度、output 长度、并发、是否流式。
- 后端:attention backend、executor、是否 CUDA Graph、是否 prefix cache。
- 指标:吞吐、TTFT、TPOT、p95/p99、显存峰值,而不只是一条平均值。
- 复现方式:脚本、commit、环境和失败阈值。
生产工程师特别容易犯的一个错:凭”我觉得”优化。结果有时候不但没提升,还引入 bug。更稳妥的团队规则应该是:
没有可复现基准和回归阈值的性能 PR,不应该进入热路径。
对照:
- Linux 内核:scheduler 改动要能在标准负载上解释收益和退化
- PostgreSQL:每个 optimizer 变化都要过
pgbench+ tpc-h - React:新 reconciler 发布前跑数百个 benchmark case
vLLM 给我们的启发:你的团队应该有一个能持续运行的 benchmark suite。不需要一开始很复杂,覆盖三到五个核心场景就够。每个性能相关 PR 都挂一条可复现数字,久了你就会拥有一个”真实的”系统性能观。
18.11 可观测性不是锦上添花:SchedulerStats 作为样板
V1 的 SchedulerStats 体现了一个重要选择:调度器不是只产出 token,它也产出解释系统状态的指标。
- 每一 step 都能把调度状态转成统计信息
- 指标命名、聚合方式、暴露到监控系统的映射需要稳定
- 生产调优时 “看哪个指标” 应该是 Runbook 里的第一步
这和”先做功能、最后再加监控”完全相反。后者的指标往往五花八门、测不到关键路径、prod 出问题时无效。更好的做法是从组件设计阶段就问:这个模块如果变慢、变满、卡住、回退,我靠什么指标判断?
对照:
- Kafka:broker 暴露大量 JMX 指标,命名和语义需要长期稳定
- PostgreSQL:
pg_stat_*视图家族,从 pg_stat_activity 到 pg_stat_bgwriter - Kubernetes:kubelet / apiserver / etcd 全部暴露
/metricsPrometheus 端点
vLLM 给我们的启发:你下次设计一个新组件,第一个要画的不是 “功能流程图”,而是 “指标输出列表”。你监控不到的东西,等于不存在。
18.12 架构原则速查表
把全书提炼成一张表放这儿,面对未来系统设计决策时可以查:
| 原则 | vLLM 里的体现 | 应用建议 | 章节 |
|---|---|---|---|
| 跨领域借鉴 | 虚拟内存 → PagedAttention | 遇到难题先想:谁已经解过结构上同样的问题? | 4, 5 |
| 统一抽象 | Prefill/Decode → 统一 Token 调度 | 看到长 if/elif 链,想想能不能升维消除 | 3, 11 |
| 为主路径优化 | 收窄热路径上的长尾功能 | 添加功能前问”能否留在核心外部?“ | 9 |
| 关注点分离 | API / EngineCore / Worker 三进程拆分 | 先设计数据契约,再各自实现 | 2, 6 |
| 稳定接口契约 | Executor / Attention / QuantConfig 可插拔 | 接口稳定后再开放实现;破坏变更要有迁移路径 | 6, 7, 13 |
| 分层优化心智 | 算法 → 架构 → 工程 → 微优化 | 按层次投入精力,跳层优化是自找麻烦 | 4, 8 |
| 有状态 vs 无状态 | V1 改有状态 Worker,减少重复传输 | 高频/大状态更适合有状态,低频/小状态更适合无状态 | 6 |
| fail-fast 哲学 | Worker 崩 → 整个引擎退出 | 复活状态难验证时,选”退出 + 外部重建” | 2, 6 |
| 勇于重写 | V0 → V1 核心路径重组 | 满足”目标清晰 + 渐进 + 兼容 + 基准 + 共识”才写 | 全书 |
| 数据驱动 | 性能 PR 需要可复现基准 | 建立 CI benchmark,用数字说话 | 全书 |
| 可观测性是核心功能 | SchedulerStats 解释调度状态 | 先设计指标再写代码 | 3, 6 |
| 预分配胜过动态分配 | KV 池启动一次性分配,运行时无 cudaMalloc | 热路径拒绝动态分配;对象池 / 预分配数组 | 4, 5 |
18.13 vLLM 教给我们的
把全书浓缩成一句话:
高性能系统的核心不是”快”,而是”不浪费”。
- PagedAttention 不浪费显存
- Continuous Batching 不浪费 GPU 周期
- 前缀缓存不浪费重复计算
- 有状态 Worker 不浪费通信带宽
- CUDA Graph 不浪费 kernel 启动
- 投机解码不浪费 memory-bound 的 decode 时间
- 持久化输入缓冲不浪费每步重复构造成本
每一个优化的本质都是识别出一种浪费,然后消除它。
这也是为什么理解 vLLM 对所有系统开发者都有价值——不是因为每个人都要写 LLM 推理引擎,而是因为 “找到浪费并消除它” 是一切高性能系统的通用方法论。
推理引擎是 AI 基础设施里最”隐形”的一层。用户看到的是流畅的对话;开发者看到的是简洁的 API。但在这背后,是 PagedAttention 的虚拟内存思想,是调度器在显存与延迟之间的取舍,是 Worker 在 GPU 上不断复用状态。是 KV blocks 被分配、共享、驱逐;是合适的路径被 CUDA Graph 捕获;是进程间通信尽量少搬重复状态。
希望这本书让你看到了管道里的风景。
看到风景的人,才能建造更好的管道。
vLLM 的 V1 会被 V2 取代,API 会演进,但本书讨论的跨领域借鉴、统一抽象、关注点分离、fail-fast、分层优化这些原则在下一代推理引擎里仍然会反复出现。