
vLLM深度解析:高吞吐量大语言模型推理系统的内部架构
博客文章(Inside vLLM: Anatomy of a High-Throughput LLM Inference System)深度解析了vLLM的内部架构,我简单整理了一下
LLM引擎和引擎核心
LLM引擎是vLLM的基础构建块。单独而言,它已经能够实现高吞吐量推理——但仅限于离线设置。
使用以下离线推理代码片段作为示例:
from vllm import LLM, SamplingParams
prompts = [
"Hello, my name is",
"The president of the United States is",
]
sampling_params = SamplingParams(temperature=0.8, top_p=0.95)
def main():
llm = LLM(model="TinyLlama/TinyLlama-1.1B-Chat-v1.0")
outputs = llm.generate(prompts, sampling_params)
if __name__ == "__main__":
main()
LLM引擎构造函数
引擎的主要组件包括:
•vLLM配置:包含所有用于配置模型、缓存、并行性等的参数
•处理器:通过验证、分词和处理将原始输入转换为EngineCoreRequests
•引擎核心客户端:在我们的示例中使用InprocClient
•输出处理器:将原始EngineCoreOutputs转换为用户看到的RequestOutput
引擎核心本身由几个子组件组成:
•模型执行器:驱动模型的前向传播
•结构化输出管理器:用于引导解码
•调度器:决定哪些请求进入下一个引擎步骤,包含:
–策略设置(FCFS或优先级)
–等待和运行队列
–KV缓存管理器——分页注意力的核心
图片
KV缓存管理器维护一个free_block_queue——可用KV缓存块的池(通常有数十万个,取决于VRAM大小和块大小)。
标准transformer层的块大小计算如下:
2 * block_size (默认=16) * num_kv_heads * head_size * dtype_num_bytes (bf16为2)
在模型执行器构建期间,创建Worker对象,并执行三个关键程序:
1.初始化设备:
–分配CUDA设备并检查模型数据类型支持
–验证是否有足够的VRAM可用
–设置分布式设置
2.加载模型:
–实例化模型架构
–加载模型权重
–调用model.eval()
–可选:对模型调用torch.compile()
3.初始化KV缓存:
–获取每层KV缓存规范
–运行虚拟/分析前向传播并获取GPU内存快照
–分配、重塑并绑定KV缓存张量到注意力层
–准备注意力元数据
–捕获CUDA图以提高延迟
Generate函数
第一步是验证并向引擎提供请求。对于每个提示:
1.创建唯一的请求ID并记录到达时间
2.调用输入预处理器对提示进行分词
3.将此信息打包到EngineCoreRequest中
4.将请求传递到引擎核心,设置状态为WAITING
图片
接下来,只要有请求需要处理,引擎就会重复调用其step()函数。每个步骤有三个阶段:
1.调度:选择在此步骤中运行哪些请求(解码和/或预填充)
2.前向传播:运行模型并采样令牌
3.后处理:将采样的令牌ID附加到每个请求,去分词,并检查停止条件
停止条件包括:
•请求超过其长度限制
•采样的令牌是EOS ID
•采样的令牌匹配任何stop_token_ids
•输出中存在停止字符串
调度器
推理引擎处理两种主要的工作负载类型:
•预填充请求——对所有提示令牌的前向传播。这些通常是计算密集型的
•解码请求——仅对最近令牌的前向传播。所有较早的KV向量已经被缓存。这些是内存带宽受限的
V1调度器可以在同一步骤中混合两种类型的请求,这是更智能的设计选择。
调度器优先处理解码请求——即已经在运行队列中的请求。对于每个这样的请求:
1.计算要生成的新令牌数量
2.调用KV缓存管理器的allocate_slots函数
3.通过减去令牌数量来更新令牌预算
之后,它处理来自等待队列的预填充请求。
allocate_slots的功能:
1.计算块数量——确定必须分配多少个新的KV缓存块
2.检查可用性——如果管理器池中没有足够的块,则提前退出
3.分配块——通过KV缓存管理器的协调器从块池中获取前n个块
图片
运行前向传播
调用模型执行器的execute_model,主要步骤:
1.更新状态——从input_batch中修剪已完成的请求
2.准备输入——从CPU→GPU复制缓冲区;计算位置;构建slot_mapping
3.前向传播——使用自定义分页注意力内核运行模型。所有序列被扁平化并连接成一个长的"超级序列"
4.收集最后令牌状态——提取每个序列最终位置的隐藏状态并计算logits
5.采样——根据采样配置从计算的logits中采样令牌
前向传播步骤有两种执行模式:
•急切模式——启用急切执行时运行标准PyTorch前向传播
•"捕获"模式——当不强制急切时执行/重播预捕获的CUDA图
高级特性——扩展核心引擎逻辑
接下来我们深入了解:
分块预填充
分块预填充是通过将预填充步骤分成更小的块来处理长提示的技术。如果没有它,我们可能会遇到单个很长的请求独占一个引擎步骤的情况,从而阻止其他预填充请求运行。
例如,让每个块包含n(=8)个令牌。执行P的完整预填充将需要≥3个引擎步骤,只有在最后的分块预填充步骤中我们才会采样一个新令牌。
在vLLM V1中,通过将long_prefill_token_threshold设置为正整数来启用分块预填充。
图片
前缀缓存
前缀缓存避免重新计算多个提示在开头共享的令牌。
关键在于long_prefix:定义为任何长于KV缓存块(默认16个令牌)的前缀。
工作原理:
1.在第一次generate调用期间,引擎调用hash_request_tokens:
–将long_prefix + prompts[0]分成16令牌块
–对于每个完整块,计算哈希
–每个结果存储为包含哈希和其令牌ID的BlockHash对象
2.引擎调用find_longest_cache_hit检查这些哈希是否已存在于cached_block_hash_to_block中
3.在第二次具有相同前缀的generate调用中,find_longest_cache_hit找到所有n个块的匹配。引擎可以直接重用这些KV块
前缀缓存默认启用。要禁用它:enable_prefix_caching = False。
引导解码(FSM)
引导解码是一种技术,在每个解码步骤中,logits被基于语法的有限状态机约束。这确保只能采样语法允许的令牌。
示例代码:
from vllm.sampling_params import GuidedDecodingParams
guided_decoding_params = GuidedDecodingParams(choice=["Positive", "Negative"])
sampling_params = SamplingParams(guided_decoding=guided_decoding_params)
图片
vLLM中的工作原理:
1.在LLM引擎构建时,创建StructuredOutputManager
2.添加请求时,状态设置为WAITING_FOR_FSM,语法异步编译
3.在调度期间,如果异步编译完成,状态切换到WAITING
4.在前向传播产生logits后,使用位掩码将不允许的logits设置为-∞
5.采样下一个令牌后,通过accept_tokens推进请求的FSM
推测解码
在自回归生成中,每个新令牌都需要大型LM的前向传播。推测解码通过引入较小的草稿LM来加速这一过程。
步骤:
1.草稿:在小模型上运行并便宜地提议k个令牌
2.验证:在大模型上对上下文+k个草稿令牌运行一次
3.接受/拒绝
:从左到右遍历k个草稿令牌:
–如果大模型的概率≥草稿概率,接受它
–否则,以p_large(token)/p_draft(token)的概率接受它
–在第一次拒绝时停止,或接受所有k个草稿令牌
vLLM V1支持更快但不太准确的提议方案:n-gram、EAGLE和Medusa。
示例配置:
speculative_cnotallow={
"method": "ngram",
"prompt_lookup_max": 5,
"prompt_lookup_min": 3,
"num_speculative_tokens": 3,
}
图片
分离式P/D(预填充/解码)
预填充和解码具有非常不同的性能配置文件(计算密集型vs内存带宽受限),因此分离它们的执行是明智的设计。
在实践中,我们运行N个vLLM预填充实例和M个vLLM解码实例,根据实时请求混合自动缩放它们。预填充工作器将KV写入专用KV缓存服务;解码工作器从中读取。
图片
从UniprocExecutor到MultiProcExecutor
当模型权重不再适合单个GPU的VRAM时,第一个选择是使用张量并行性(例如TP=8)在同一节点的多个GPU上分片模型。如果模型仍然不适合,下一步是跨节点的流水线并行性。
MultiProcExecutor的工作原理:
1.初始化rpc_broadcast_mq消息队列
2.构造函数循环遍历world_size并为每个rank生成守护进程
3.每个工作器设置两个队列:
–rpc_broadcast_mq(与父进程共享)用于接收工作
–worker_response_mq用于发送响应
4.工作器进入忙循环,阻塞在rpc_broadcast_mq.dequeue上
5.运行时,MultiProcExecutor将请求入队到所有子工作器的rpc_broadcast_mq中
分布式系统服务vLLM
假设我们有两个H100节点,想要在它们上运行四个vLLM引擎。如果模型需要TP=4,我们可以这样配置节点。
在无头服务器节点上:
vllm serve <model-name>
--tensor-parallel-size 4
--data-parallel-size 4
--data-parallel-size-local 2
--data-parallel-start-rank 0
--data-parallel-address <master-ip>
--data-parallel-rpc-port 13345
--headless
vLLM中的工作原理:
在无头节点上,CoreEngineProcManager启动2个进程,每个运行EngineCoreProc.run_engine_core。每个函数创建一个DPEngineCoreProc,然后进入其忙循环。
在API服务器节点上,我们实例化一个AsyncLLM对象。内部创建一个DPLBAsyncMPClient。
完整的请求生命周期:
1.请求命中API服务器上的create_completion路由
2.函数异步分词提示,并准备元数据
3.调用AsyncLLM.generate,最终调用DPAsyncMPClient.add_request_async
4.根据DP协调器的状态进行负载均衡
5.ADD请求发送到选择的引擎的input_socket
6.在该引擎处:
–输入线程解除阻塞,从输入socket解码数据
–主线程在input_queue上解除阻塞,将请求添加到引擎
–输出线程在output_queue上解除阻塞并通过输出socket发送结果
7.这些结果触发AsyncLLM输出异步任务,将令牌传播回FastAPI的create_completion路由
基准测试和自动调优——延迟vs吞吐量
在最高级别上有两个竞争的指标:
•延迟——从提交请求到返回令牌的时间
•吞吐量——系统每秒可以生成/处理的令牌/请求数量
延迟和吞吐量竞争的本质变得清晰:随着批大小B↓趋近于1,ITL下降;随着B↑趋近于无穷大,ITL上升,但吞吐量提高。
屋顶线模型有助于理解:在饱和批B_sat以下,步骤时间由HBM带宽主导;超出B_sat,内核变为计算受限,步骤时间大致与B成正比。
如何在vLLM中进行基准测试
vLLM提供vllm bench {serve,latency,throughput} CLI:
•latency:使用短输入(默认32令牌)和小批次(默认8)采样128个输出令牌
•throughput:一次性提交固定的提示集(默认:1000个ShareGPT样本)
•serve:启动vLLM服务器并通过从泊松分布采样请求到达间隔时间来模拟真实世界工作负载
本文转载自AI帝国,作者:无影寺
