772025 年 8 月 29 日
88
99本文将循序渐进地介绍构成现代高吞吐量大语言模型推理系统的所有核心组件和高级特性。
10- 特别是将深入剖析 vLLM [[ 1 ]] ( https://www.aleksagordic. com/blog/ vllm#ref-1 ) 的工作原理。
10+ 特别是将深入剖析 [ vLLM ] ( https://github. com/vllm-project/vllm ) 的工作原理。
1111
1212本文是系列文章的第一篇。本文采用倒金字塔方法,从宏观入手,然后逐层深入细节,
1313以便你能在不被琐碎细节淹没的情况下,对整个系统形成精确的高层次心智模型。
1616
1717本文结构分为五个部分:
1818
19- 1 . [ 大语言模型引擎和引擎核心] ( https://www.aleksagordic.com/blog/vllm#cpt1 ) :vLLM 基础知识(调度、分页注意力、连续批处理等)
20- 2 . [ 高级特性] ( https://www.aleksagordic.com/blog/vllm#cpt2 ) :分块预填充、前缀缓存、引导解码与投机解码、P/D 分离
21- 3 . [ 扩容] ( https://www.aleksagordic.com/blog/vllm#cpt3 ) :从单 GPU 到多 GPU
22- 4 . [ 分层部署] ( https://www.aleksagordic.com/blog/vllm#cpt4 ) :分布式/并发 Web 框架
23- 5 . [ 基准测试与自动调优] ( https://www.aleksagordic.com/blog/vllm#cpt5 ) :测量延迟和吞吐量
19+ 1 . [ 大语言模型引擎和引擎核心] ( #_1 ) :vLLM 基础知识(调度、分页注意力、连续批处理等)
20+ 2 . [ 高级特性] ( #_5 ) :分块预填充、前缀缓存、引导解码与投机解码、P/D 分离
21+ 3 . [ 扩容] ( #uniprocexecutor-multiprocexecutor ) :从单 GPU 到多 GPU
22+ 4 . [ 分层部署] ( #vllm_1 ) :分布式/并发 Web 框架
23+ 5 . [ 基准测试与自动调优] ( #vs ) :测量延迟和吞吐量
2424
2525!!! note
2626
@@ -67,7 +67,7 @@ if __name__ == "__main__":
6767- 离线的(没有 Web/分布式系统的框架)
6868- 同步的(所有执行发生在单个阻塞进程中)
6969- 单 GPU(没有数据/模型/流水线/专家并行;DP/TP/PP/EP = 1)
70- - 使用标准 Transformer [[ 2 ]] ( https://www.aleksagordic.com/blog/vllm#ref-2 ) (支持 Jamba 等混合模型需要更复杂的混合 KV-cache 内存分配器)
70+ - 使用 [ 标准 Transformer] ( https://arxiv.org/abs/1706.03762 ) (支持 Jamba 等混合模型需要更复杂的混合 KV-cache 内存分配器)
7171
7272从这里开始,我们将逐步构建一个在线、异步、多 GPU、多节点的推理系统——但仍然部署标准的 Transformer。
7373
@@ -78,7 +78,7 @@ if __name__ == "__main__":
7878
7979让我们从分析构造函数开始。
8080
81- ## 大语言模型引擎构造函数
81+ ### 大语言模型引擎构造函数
8282
8383引擎的主要组件包括:
8484
@@ -99,7 +99,7 @@ if __name__ == "__main__":
9999
100100 1 . 策略设置:可以是 ** FCFS** (先到先服务)或 ** priority** (优先级高的请求优先服务)
101101 2 . ` waiting ` 和 ` running ` 队列
102- 3 . KV-cache 管理器:分页注意力的核心 [[ 3 ]] ( https://www.aleksagordic.com/blog/vllm#ref-3 )
102+ 3 . KV-cache 管理器:[ 分页注意力的核心 ] ( https://arxiv.org/abs/2309.06180 )
103103
104104KV-cache 管理器维护一个 ` free_block_queue ` 。这是所有可用 KV-cache 块形成的池(通常有几十万块,具体取决于显存大小和块大小)。在分页注意力期间,这些块作为索引结构,将 Token 映射到其计算的各个 KV-cache 块上。
105105
@@ -111,7 +111,7 @@ KV-cache 管理器维护一个 `free_block_queue`。这是所有可用 KV-cache
111111
112112!!! tip
113113
114- 标准 Transformer 层(非 MLA [[4]] (https://www.aleksagordic.com/blog/vllm#ref-4 ))的块大小计算公式为:
114+ 标准 Transformer 层([ 非 MLA] (https://arxiv.org/abs/2405.04434 ))的块大小计算公式为:
115115
116116 2 (key/value) * `block_size`(默认=16) * `num_kv_heads` * `head_size` * `dtype_num_bytes`(例如 bf16 为 2)
117117
@@ -135,7 +135,7 @@ KV-cache 管理器维护一个 `free_block_queue`。这是所有可用 KV-cache
135135
1361363 . 初始化 KV-cache:
137137
138- - 获取每层的 KV-cache 规格。历史上这总是 ` FullAttentionSpec ` (同质 Transformer),但对于混合模型(滑动窗口、Transformer/SSM 类 Jamba)会更复杂(参见 Jenga [[ 5 ]] ( https://www.aleksagordic.com/blog/vllm#ref-5 ) )
138+ - 获取每层的 KV-cache 规格。历史上这总是 ` FullAttentionSpec ` (同质 Transformer),但对于混合模型(滑动窗口、Transformer/SSM 类 Jamba)会更复杂(参见 [ Jenga ] ( https://arxiv.org/abs/2503.18292 ) )
139139 - 执行一次虚拟/分析前向计算并获取 GPU 内存快照,以计算可用显存中能容纳多少 KV-cache 块
140140 - 分配、调整形状并绑定 KV-cache 张量到注意力层
141141 - 准备注意力元数据(例如将后端设置为 FlashAttention),以供前向计算时内核使用
@@ -145,7 +145,7 @@ KV-cache 管理器维护一个 `free_block_queue`。这是所有可用 KV-cache
145145
146146现在我们已经初始化了引擎,让我们继续看 ` generate ` 函数。
147147
148- ## ` generate ` 函数
148+ ### ` generate ` 函数
149149
150150第一步是验证并将请求送入引擎。对于每个提示词,我们:
151151
@@ -154,7 +154,7 @@ KV-cache 管理器维护一个 `free_block_queue`。这是所有可用 KV-cache
1541543 . 将这些信息打包进 ` EngineCoreRequest ` ,添加优先级、采样参数和其他元数据
1551554 . 将请求传入引擎核心,它会将请求包装为 ` Request ` 对象并将状态设置为 ` WAITING ` 。然后该请求被加入调度器的 ` waiting ` 队列(如果是先来先服务(FCFS),则追加;如果是按优先级,则使用堆插入(heap-push)。)
156156
157- 此时,引擎已被喂入数据,执行可以开始。在同步引擎示例中,这些初始提示词是唯一处理的请求——没有机制在运行中注入新请求。相比之下,异步引擎支持此功能(即 ** 连续批处理 ** [[ 6 ]] ( https://www.aleksagordic.com/blog/vllm#ref-6 ) ):每步结束后,会同时考虑新旧请求。
157+ 此时,引擎已被喂入数据,执行可以开始。在同步引擎示例中,这些初始提示词是唯一处理的请求——没有机制在运行中注入新请求。相比之下,异步引擎支持此特性(即 [ 连续批处理 ] ( https://www.usenix.org/conference/osdi22/presentation/yu ) ):每步结束后,会同时考虑新旧请求。
158158
159159!!! tip
160160
@@ -185,7 +185,7 @@ KV-cache 管理器维护一个 `free_block_queue`。这是所有可用 KV-cache
185185
186186接下来,我们将更详细地探讨调度。
187187
188- ## 调度器
188+ ### 调度器
189189
190190推理引擎主要处理两类工作负载:
191191
@@ -225,7 +225,7 @@ V1 调度器可以在同一步中混合处理两类请求,这得益于更智
225225
226226现在,我们可以进行前向计算了!
227227
228- ## 执行前向计算
228+ ### 执行前向计算
229229
230230我们调用模型执行器的 ` execute_model ` ,它委托给 ` Worker ` ,再由 ` Worker ` 委托给 ` model_runner ` 。
231231
@@ -250,9 +250,9 @@ V1 调度器可以在同一步中混合处理两类请求,这得益于更智
250250图 4. 前向计算:连续批处理与分页注意力
251251</div >
252252
253- ## 高级功能 — 扩展核心引擎逻辑
253+ ## 高级特性 — 扩展核心引擎逻辑
254254
255- 在基础引擎流程建立之后,我们现在可以看看高级功能 。
255+ 在基础引擎流程建立之后,我们现在可以看看高级特性 。
256256
257257我们已经讨论了抢占、分页注意力和连续批处理。
258258
@@ -264,7 +264,7 @@ V1 调度器可以在同一步中混合处理两类请求,这得益于更智
2642644 . 投机解码
2652655 . P/D 分离(预填充/解码)
266266
267- ## 分块预填充
267+ ### 分块预填充
268268
269269分块预填充是处理长提示词的一种技术,它通过将预填充步骤拆分为更小的块来执行。若不使用此方法,可能会出现单个非常长的请求独占一次引擎步骤,从而阻止其他预填充请求运行。这会延迟所有其他请求并增加它们的延迟。
270270
@@ -278,7 +278,7 @@ V1 调度器可以在同一步中混合处理两类请求,这得益于更智
278278
279279在 vLLM V1 中,可以通过将 ` long_prefill_token_threshold ` 设置为正整数来启用分块预填充。(技术上,即使未设置该值,如果提示词长度超过 Token 预算,也会截断并执行分块预填充。)
280280
281- ## 前缀缓存
281+ ### 前缀缓存
282282
283283为了说明前缀缓存的工作原理,我们可以对原始代码示例进行一些调整:
284284
@@ -363,7 +363,7 @@ if __name__ == "__main__":
363363
364364前缀缓存默认启用。若要禁用:` enable_prefix_caching = False ` 。
365365
366- ## 引导解码(有限状态机)
366+ ### 引导解码
367367
368368引导解码是一种技术,在每个解码步骤中,logits 会受到基于语法的有限状态机约束。这确保了只有符合语法的 Token 才能被采样。
369369
@@ -403,7 +403,7 @@ if __name__ == "__main__":
403403在 vLLM 中的实现方式:
404404
4054051 . 在大语言模型引擎构建时,创建一个 ` StructuredOutputManager ` ;它可以访问分词器,并维护 ` _grammar_bitmask ` 张量。
406- 2 . 添加请求时,其状态被设置为 ` WAITING_FOR_FSM ` ,并由 ` grammar_init ` 选择后端编译器(例如 ` xgrammar ` [[ 7 ]] ( https://www.aleksagordic.com/blog/vllm#ref-7 ) ;注意后端为第三方代码)。
406+ 2 . 添加请求时,其状态被设置为 ` WAITING_FOR_FSM ` ,并由 ` grammar_init ` 选择后端编译器(例如 [ XGrammar ] ( https://arxiv.org/abs/2411.15100 ) ;注意后端为第三方代码)。
4074073 . 该请求的语法会异步编译。
4084084 . 在调度阶段,如果异步编译完成,状态切换为 ` WAITING ` ,并将 ` request_id ` 添加到 ` structured_output_request_ids ` ;否则,它被放入 ` skipped_waiting_requests ` ,在下一步引擎循环中重试。
4094095 . 调度循环结束后(仍在调度阶段),如果有 FSM 请求,` StructuredOutputManager ` 会请求后端准备/更新 ` _grammar_bitmask ` 。
@@ -426,13 +426,13 @@ if __name__ == "__main__":
426426图 6. 玩具示例
427427</div >
428428
429- 可以通过传入所需的 ` guided_decoding ` 配置在 vLLM 中启用此功能 。
429+ 可以通过传入所需的 ` guided_decoding ` 配置在 vLLM 中启用此特性 。
430430
431- ## 投机解码
431+ ### 投机解码
432432
433433在自回归生成中,每生成一个新 Token 都需要对大语言模型执行一次前向计算。这非常昂贵——每一步都要重新加载并应用所有模型权重,仅为了计算一个 Token!(假设批次大小 = 1,一般为 ` B ` )
434434
435- 投机解码 [[ 8 ]] ( https://www.aleksagordic.com/blog/vllm#ref-8 ) 通过引入一个较小的草稿模型来加速。草稿模型廉价地提出 ` k ` 个 Token 候选。但我们最终并不希望从小模型中采样——它只是用来猜测候选续写。大模型仍然决定哪些 Token 有效。
435+ [ 投机解码 ] ( https://arxiv.org/abs/2302.01318 ) 通过引入一个较小的草稿模型来加速。草稿模型廉价地提出 ` k ` 个 Token 候选。但我们最终并不希望从小模型中采样——它只是用来猜测候选续写。大模型仍然决定哪些 Token 有效。
436436
437437步骤如下:
438438
@@ -455,7 +455,7 @@ if __name__ == "__main__":
455455
456456 推荐查看 [gpt-fast](https://github.com/meta-pytorch/gpt-fast) 了解简单实现,以及 [原论文](https://arxiv.org/abs/2302.01318) 获取数学细节及与全模型采样等价的证明。
457457
458- vLLM V1 不支持使用 LLM 草稿模型方法,而是实现了更快但准确性略低的候选方案:n-gram、EAGLE [[ 9 ]] ( https://www.aleksagordic.com/blog/vllm#ref-9 ) 和 Medusa [[ 10 ]] ( https://www.aleksagordic.com/blog/vllm#ref-10 ) 。
458+ vLLM V1 不支持使用 LLM 草稿模型方法,而是实现了更快但准确性略低的候选方案:n-gram、[ EAGLE ] ( https://arxiv.org/abs/2401.15077 ) 和 [ Medusa ] ( https://arxiv.org/abs/2401.10774 ) 。
459459
460460各方案简述:
461461
@@ -520,7 +520,7 @@ if __name__ == "__main__":
520520
521521![ Verify stage & rejection sampling stage] ( https://www.aleksagordic.com/blog/vllm/specdec_pt2.png )
522522
523- ## P/D 分离
523+ ### P/D 分离
524524
525525上文提到了 P/D 分离的动机。
526526
@@ -610,7 +610,7 @@ if __name__ == "__main__":
610610
611611!!! note
612612
613- 我还尝试过 `LMCache` [[11]] (https://www.aleksagordic. com/blog/vllm#ref-11 ),这是最快的生产就绪 Connector(使用 NVIDIA 的 NIXL 作为后端),但它仍处于前沿状态,我遇到了一些 bug。由于其复杂性大多存在于外部仓库中,因此 `SharedStorageConnector` 更适合作为讲解示例。
613+ 我还尝试过 [ `LMCache`] (https://github. com/LMCache/LMCache ),这是最快的生产就绪 Connector(使用 NVIDIA 的 NIXL 作为后端),但它仍处于前沿状态,我遇到了一些 bug。由于其复杂性大多存在于外部仓库中,因此 `SharedStorageConnector` 更适合作为讲解示例。
614614
615615在 vLLM 中的步骤如下:
616616
@@ -660,7 +660,7 @@ if __name__ == "__main__":
660660 - 节点内带宽远高于节点间带宽,这也是为什么通常优先选择张量并行(TP)而非流水线并行(PP)。(同时,PP 传输的数据量也少于 TP。)
661661 - 我不讨论 expert parallelism (EP),因为我们关注的是标准 Transformer 而非 MoE,也不讨论 sequence parallelism,因为 TP 和 PP 在实践中最常用。
662662
663- 在这个阶段,我们需要多个 GPU 进程(Worker)以及一个协调层来管理它们。这正是 ` MultiProcExecutor ` 提供的功能 。
663+ 在这个阶段,我们需要多个 GPU 进程(Worker)以及一个协调层来管理它们。这正是 ` MultiProcExecutor ` 提供的特性 。
664664
665665![ MultiProcExecutor] ( https://www.aleksagordic.com/blog/vllm/multiprocexecutor.png )
666666
@@ -738,7 +738,7 @@ vllm serve <model-name>
738738
739739vLLM 中的实现方式:
740740
741- ## 在 headless 服务器节点
741+ ### 在 headless 服务器节点
742742
743743在 headless 节点上,` CoreEngineProcManager ` 启动 2 个进程(根据 ` --data-parallel-size-local ` ),每个进程运行 ` EngineCoreProc.run_engine_core ` 。每个函数会创建一个 ` DPEngineCoreProc ` (引擎核心),然后进入其忙循环。
744744
@@ -780,7 +780,7 @@ TL;DR:最终我们有 4 个子进程(每个 DP 副本一个),每个子
780780
781781接下来,我们来看第二部分:API 服务器节点会发生什么?
782782
783- ## 在 API 服务器节点
783+ ### 在 API 服务器节点
784784
785785我们实例化一个 ` AsyncLLM ` 对象(LLM 引擎的 asyncio 包装器)。内部会创建一个 ` DPLBAsyncMPClient ` (数据并行、负载均衡、异步、多进程客户端)。
786786
@@ -868,7 +868,7 @@ curl -X POST http://localhost:8000/v1/completions -H "Content-Type: application/
868868在解释为什么延迟与吞吐量相互竞争之前,我们先定义几个常见的推理指标:
869869
870870| 指标 | 定义 |
871- | : ----------------------------------- | :-------------------------------------------------------- --- |
871+ | --- | --- |
872872| ` TTFT ` (time to first token) | 从请求提交到接收到第一个输出 Token 的时间 |
873873| ` ITL ` (inter-token latency) | 两个连续 Token 之间的时间(例如,从 Token i-1 到 Token i) |
874874| ` TPOT ` (time per output token) | 单个请求中所有输出 Token 的平均 ITL |
@@ -906,7 +906,7 @@ curl -X POST http://localhost:8000/v1/completions -H "Content-Type: application/
906906
907907 更严格的分析需要考虑 kernel 自动调优:随着 `B` 增大,运行时可能为该形状切换到更高效的 kernel,从而改变实际性能 `P_kernel`。步骤延迟为 `t = FLOPs_step / P_kernel`,其中 `FLOPs_step` 为该步的计算量。可以看到,当 `P_kernel` 达到 `P_peak` 时,每步更多的计算量会直接导致延迟增加。
908908
909- ## 如何在 vLLM 中进行基准测试
909+ ### 如何在 vLLM 中进行基准测试
910910
911911vLLM 提供了一个 CLI 命令 ` vllm bench {serve,latency,throughput} ` ,它封装了 ` vllm/benchmarks/{server,latency,throughput}.py ` 脚本。
912912
@@ -934,30 +934,24 @@ vllm bench latency
934934
935935## 尾声
936936
937- 我们从基础引擎核心(` UniprocExecutor ` )开始,加入了如推测解码(speculative decoding)和前缀缓存(prefix caching)等高级特性 ,接着扩展到 ` MultiProcExecutor ` (TP/PP > 1),最终实现水平扩展,将所有组件封装到异步引擎和分布式服务栈中——最后展示了如何衡量系统性能。
937+ 我们从基础引擎核心(` UniprocExecutor ` )开始,加入了如投机解码和前缀缓存等高级特性 ,接着扩展到 ` MultiProcExecutor ` (TP/PP > 1),最终实现水平扩展,将所有组件封装到异步引擎和分布式服务栈中——最后展示了如何衡量系统性能。
938938
939939vLLM 还包含一些我未详细展开的专门处理,例如:
940940
941941- ** 多样化硬件后端:** TPU、AWS Neuron(Trainium/Inferentia)等
942942- ** 架构/技术:** ` MLA ` 、` MoE ` 、编码器/解码器(如 Whisper)、池化/嵌入式模型、` EPLB ` 、` m-RoPE ` 、` LoRA ` 、` ALiBi ` 、无注意力变体、滑动窗口注意力、多模态 LLM、状态空间模型(如 Mamba/Mamba-2、Jamba)
943943- ** TP/PP/SP**
944944- ** 混合 KV-cache 逻辑** (Jenga)、更复杂的采样方法如束式采样等
945- - ** 实验性功能 :** 异步调度
945+ - ** 实验性特性 :** 异步调度
946946
947- 好的一点是,这些大多数功能与上文描述的核心流程是正交的 ——几乎可以把它们当作“插件”来理解(当然实际中有部分耦合)。
947+ 好的一点是,这些大多数特性与上文描述的核心流程是正交的 ——几乎可以把它们当作“插件”来理解(当然实际中有部分耦合)。
948948
949- 我热爱理解系统。话虽如此,在这个高度概览中,细节有所损失。在后续文章中,我会聚焦具体子系统 ,深入探讨细节。
949+ 我热爱理解系统。话虽如此,在这个高度概览中,细节有所损失。在后续文章中,我会聚焦具体的子系统 ,深入探讨细节。
950950
951951!!! tip "💡联系我:"
952952
953953 如果你在本文中发现任何错误,请随时联系我——可以通过 [X](https://x.com/gordic_aleksa) 或 [LinkedIn](https://www.linkedin.com/in/aleksagordic/) 给我留言,也可以通过 [匿名反馈](https://docs.google.com/forms/d/1z1fEirrN2xtGxAsJvptpM7yV4ByT5SF25S-XiMPrXNA/edit) 提交。
954954
955- ## 致谢
956-
957- 衷心感谢 [ Hyperstack] ( https://www.hyperstack.cloud/ ) 在过去一年中提供 H100 GPU 供我进行实验!
958-
959- 感谢 [ Nick Hill] ( https://www.linkedin.com/in/nickhillprofile/ ) (vLLM 核心贡献者,RedHat)、[ Mark Saroufim] ( https://x.com/marksaroufim ) (PyTorch)、[ Kyle Krannen] ( https://www.linkedin.com/in/kyle-kranen/ ) (NVIDIA, Dynamo)以及 [ Ashish Vaswani] ( https://www.linkedin.com/in/ashish-vaswani-99892181/ ) 在博客预发布版本中提供反馈!
960-
961955## 参考文献
962956
9639571 . [ vLLM] ( https://github.com/vllm-project/vllm )
0 commit comments