vLLM 各核心模块原理深度解析

 

vLLM 各核心模块原理深度解析

请求调度与批处理核心机制:Continuous Batching

Continue Batching连续批处理,也被称为Iteration-Level Scheduling,是vLLM实现高吞吐量的核心调度机制之一。

传统的静态批处理 (Static Batching) 模式下,一组请求被组合成一个批次,然后一起送入GPU进行处理。这个批次中的所有请求必须全部完成其生成任务后,整个批次的结果才能返回,GPU才能开始处理下一个批次。这种方式的主要问题在于:

  • GPU空闲浪费: LLM的生成长度通常是动态的,且每个请求的输入和输出长度可能差异很大。在一个批次中,较短的请求会先完成生成,但它们占用的GPU资源(如KV缓存槽位)必须等到批次中最长的请求也完成后才能释放。这导致GPU在等待期间处于部分空闲状态,降低了利用率。
  • 高平均延迟: 先完成的请求不得不等待后完成的请求,增加了平均响应延迟和首token延迟(TTFT)。

Continuous Batching 原理

Continuous Batching通过在更细的粒度上进行调度来解决上述问题:(Anyscale Blog – Continuous Batching LLM Inference)

  • 迭代级调度: 调度决策不再是整个请求完成后,而是在每个迭代步骤(即模型生成一个或少数几个token)之后进行。

  • 动态加入与移除:

    • 当批次中的某个请求完成了其token生成(例如达到max_tokens或遇到停止符)时,它会立即从当前运行的批次中移除,其占用的资源(主要是KV缓存的物理块)也随之释放。
    • 与此同时,如果GPU资源允许(例如,有足够的空闲KV缓存块和计算能力),调度器会从等待队列中选择新的请求加入到当前运行的批次中,立即开始处理。

在vLLM中,Continuous Batching主要由Scheduler组件负责实现:

  • 请求队列管理:Scheduler维护多个请求队列:

    • Waiting Queue: 存放新到达、等待处理的请求。
    • Running Queue: 存放当前正在GPU上运行(即参与当前批次token生成的)的请求。
    • Swapped Queue: 当GPU显存不足以容纳所有活跃请求的KV缓存时,一部分优先级较低的请求的KV缓存状态可能被“交换”出去(在vLLM中通常是丢弃并后续重算其prompt,称为RECOMPUTE模式,而非真正意义上的磁盘交换SWAP),这些请求会进入Swapped Queue。
  • 调度策略:

    • FCFS (First-Come, First-Served): 基本调度原则是先到先服务
    • 资源约束: 调度器在选择请求组成批次时,必须考虑GPU的KV缓存容量(通过PagedAttention管理)和最大批处理Token数(max_num_batched_tokens)。
    • 抢占 (Preemption): 如果当前运行批次生成新token后,所需KV缓存超出现有容量,调度器可能需要抢占一个或多个运行中的请求(通常是优先级较低或已运行较长时间的),将其移至Swapped Queue,以释放空间保证更高优先级或新请求的运行。vLLM V1版本中,默认的抢占模式是RECOMPUTE,即被抢占请求的KV缓存不保留,后续恢复时需重新计算prompt部分的KV缓存,这在V1架构下通常比实际的内存交换更高效。(vLLM Docs – Preemption)
    • Prefill与Decode的平衡: 调度器还需要平衡处理新请求的prompt(Prefill阶段,计算密集)和为已处理prompt生成后续token(Decode阶段,访存密集)。vLLM V1的调度器通过统一处理prompt token和generation token,并结合Chunked Prefill,使得调度更加灵活高效。

KV 缓存管理与内存优化技术:PagedAttention

PagedAttention是vLLM在内存管理方面的核心创新,它借鉴了操作系统中虚拟内存分页管理的技术,旨在解决传统LLM推理中KV缓存管理效率低下的问题。

  • 显存占用巨大: KV Cache的大小与序列长度、批处理大小、模型隐藏层维度、注意力头数等成正比。对于长序列或大模型,KV Cache可能消耗数十GB的显存。

  • 内存碎片化: 传统方法通常为每个请求预先分配一块连续的、足够大的显存区域来存储其KV Cache。

    • 内部碎片: 由于请求的实际生成长度往往小于预分配的最大长度,导致预留空间内有大量未被使用的部分,造成内部浪费。
    • 外部碎片: 频繁地分配和释放大小不一的连续内存块,容易导致物理显存中出现许多小块的、不连续的空闲空间,虽然总量可能足够,但无法满足新的较大连续内存请求,造成外部浪费。
  • 内存共享困难: 在一些高级采样策略中,如并行采样(从同一prompt生成多个不同输出)或beam search,多个候选序列共享了相同的prompt部分的KV Cache。在传统的连续内存分配下,高效且安全地实现这种共享非常复杂,往往需要复制数据或引入复杂的管理逻辑。

PagedAttention 原理

PagedAttention通过以下方式解决上述挑战:

  • KV缓存分页: 将整个GPU显存中用于KV Cache的区域划分为许多固定大小的物理块(Physical Blocks),类似于操作系统中的物理页帧。
  • 逻辑块与物理块映射: 对于每个输入序列(request sequence),其KV Cache在逻辑上被视为一连串的逻辑块(Logical Blocks)。这些逻辑块并不需要在物理显存中连续存储。相反,通过一个“页表”(在vLLM中称为Block Table)的数据结构,将每个逻辑块动态地映射到一个物理块上。
  • 按需分配: 当序列生成新的token时,如果当前的逻辑块已满,PagedAttention会向内存管理器(BlockSpaceManager)申请新的物理块,并更新Block Table,将新的逻辑块指向这个物理块。这种方式实现了KV Cache的按需分配,“用多少,分配多少”。

PagedAttention带来的优势:

  • 消除内部碎片: 由于物理块是按需分配给逻辑块的,只有序列的最后一个逻辑块可能存在部分未利用空间,极大地减少了内部浪费。官方宣称内存浪费(主要来自最后一个块)通常低于4%。

  • 消除外部碎片: 物理块大小固定且统一管理,使得内存分配和回收更加高效,几乎消除了外部碎片问题。

  • 高效内存共享: 这是PagedAttention的一大亮点。

    • 并行采样/Beam Search: 当多个生成序列共享一个共同的prompt时,它们的Block Table可以将对应于prompt部分的逻辑块都指向相同的物理块集合。这意味着prompt的KV Cache在物理上只存储一份,被所有共享它的序列共同引用。
    • Copy-on-Write (COW): 当某个共享物理块中的数据需要被某个序列修改时(虽然KV Cache在生成阶段通常是追加式的,但在更复杂的场景或未来扩展中可能涉及修改),COW机制可以确保为该序列复制一份新的物理块进行修改,而不影响其他共享该块的序列。这保证了数据的一致性和隔离性。

模型并行执行引擎:张量并行、流水线并行与专家并行

张量并行 (Tensor Parallelism, TP)

  • 原理: 张量并行是一种层内并行技术。它将模型中单个大层(通常是权重矩阵,如全连接层或注意力机制中的查询、键、值投影矩阵)的计算分割到多个GPU上。例如,一个权重矩阵可以按行或按列切分,每个GPU只存储和处理切分后的一部分。在前向或后向传播过程中,每个GPU独立计算其负责的部分,然后通过高效的集合通信操作(如AllReduce用于合并部分和,或AllGather用于收集所有部分)来组合结果,得到与在单个GPU上计算等效的完整输出。

  • vLLM实现: vLLM通常采用与Megatron-LM类似的张量并行策略。它会自动识别模型中可以进行张量并行的层(如Linear层、自注意力层中的QKV投影和输出投影等),并根据用户指定的--tensor-parallel-size(即TP度)对这些层的权重进行切分和加载。

  • 通信开销: TP需要在同一层内的GPU之间进行频繁通信。因此,GPU之间的高带宽互连(如NVIDIA NVLink)对于TP的性能至关重要。跨节点TP的通信开销通常非常大,一般不推荐。

  • 适用场景:

    • 当单个模型的某些层(尤其是其权重参数)过大,无法完整放入单个GPU的显存时,TP是必要的。
    • 即使模型层可以放入单个GPU,使用TP也可以通过利用多个GPU的计算核心来加速该层的计算,从而可能提高整体吞吐量或降低延迟(但需注意通信开销的平衡)。

流水线并行 (Pipeline Parallelism, PP)

  • 原理: 流水线并行是一种层间并行技术。它将整个模型的不同层(或层组,称为“阶段”stages)顺序分配到不同的GPU或计算节点上。输入数据(通常以微批次micro-batch的形式)像在工厂流水线上一样,依次通过这些阶段进行处理。例如,GPU 0处理模型的1-10层,GPU 1处理11-20层,以此类推。为了提高效率和减少GPU空闲时间(即“流水线气泡”),可以同时在流水线的不同阶段处理不同的微批次。

  • vLLM实现: vLLM允许用户通过--pipeline-parallel-size指定流水线的阶段数。它负责将模型的层切分并分配到各个流水线阶段,并管理微批次在各阶段间的调度和数据(激活值)传递。

  • 通信开销: PP需要在相邻流水线阶段的GPU之间传递激活值。如果阶段分布在不同节点上,则涉及网络通信。

  • 适用场景:

    • 当整个模型非常深(层数极多),即使使用了TP,仍然无法在可接受数量的GPU上完整容纳时,PP可以将模型进一步分布到更多GPU或节点上。
    • 对于某些通信带宽受限但计算单元充足的场景,PP可能是比大规模TP更有效的扩展方式。

专家并行 (Expert Parallelism, EP) – (针对MoE模型)

  • 原理: 混合专家(MoE)模型(如Mixtral, DeepSeek-MoE)包含多个“专家”子网络(通常是MLP层)。在MoE层中,一个门控网络(Gating Network)会为每个输入token动态选择一个或少数几个(top-k)最合适的专家来处理它。专家并行就是将这些不同的专家网络分布到不同的GPU上。

  • vLLM实现: 当vLLM加载一个MoE模型并启用专家并行(通常通过--enable-expert-parallel或自动检测,并结合--tensor-parallel-size来确定专家分布的GPU数量)时,它会确保每个token的计算请求被路由到持有相应专家的GPU上。

    • 一个常见的做法是,如果TP度为N,则N个专家可以分别部署在N个GPU上。如果专家数量远大于TP度,则可能每个GPU承载多个专家,或者专家本身也进行TP切分。

(vLLM Blog – vLLM V1 and MoE support)

  • 通信开销: MoE层需要高效的AlltoAll集合通信操作,以便将不同token的激活值分发给持有对应专家的GPU,并在专家计算完成后收集结果。这在多GPU环境下对通信带宽要求较高。
  • 适用场景: 专门用于大规模MoE模型,以有效利用其稀疏激活的特性,在保持巨大参数总量的同时控制实际计算量。

vLLM中的分布式管理

  • Ray的使用: vLLM利用Ray框架来实现其分布式运行时的管理。Ray提供了创建和管理分布式Actor(在vLLM中对应Worker进程)、跨进程/跨节点通信以及集群协调等能力。当tensor_parallel_sizepipeline_parallel_size大于1时,vLLM会启动Ray Actors来执行分布式的模型加载和推理。
  • Worker概念: 在vLLM的分布式设置中,每个GPU通常会有一个对应的Worker进程(Ray Actor)。每个Worker负责加载和执行分配给它的模型部分(例如,TP切片或PP阶段)。LLMEngine作为中心协调者,将计算任务分派给这些Workers,并收集它们的结果。(vLLM Docs – Worker Overview)

通过灵活组合这些并行策略,vLLM能够有效地将LLM的推理负载分散到多个计算资源上,从而支持更大规模模型的部署并提升服务性能。选择哪种并行策略或组合,以及具体的并行度,需要根据模型大小、硬件配置(GPU数量、显存、互连带宽)和性能目标(吞吐量vs延迟)进行权衡。

核心引擎 (LLMEngine)与Worker

官方提供了两种 vLLM 的调用方法:Offline Batched Inference(同步,离线批处理)和 API Server For Online Serving(异步,在线批处理)

Offline Batched Inference,写脚本直接调用 LLMEngine,往往用在数据集评测、离线大批量算结果、benchmark。特点:调用是同步的,一次性给一批 prompt,等所有都跑完再拿结果。

API Server For Online Serving,起一个 HTTP / OpenAI-API 风格的服务;服务端内部用 AsyncLLMEngine 来承接来自很多客户端的请求;AsyncLLMEngine 再驱动底层的 LLMEngine 做连续批处理。特点:请求是异步/流式的,随时有人来问问题、随时有人结束,靠 continuous batching 来提高吞吐。

vLLM的两种调用方式与LLMEngine的关系如下(from vLLM团队2023 first meetup PPT

image-20251210151119463

LLMEngine 和 AsyncLLMEngine

class LLMEngine:
    def __init__(self, args, scheduler, executor, tokenizer, ...):
        self.scheduler = scheduler            # 调度器,负责决定哪些请求在本步运行
        self.model_executor = executor        # 模型执行器,负责调用模型前向推理
        self.seq_groups = {}                  # 当前活跃的请求(SequenceGroup 列表)
        self.block_manager = BlockSpaceManager() # KV cache 分配管理器
模块 作用
Scheduler 决定哪些请求参与本步推理(prefill / decode / chunked prefill)
ModelExecutor 将 Scheduler 安排的 batch 输入送入 GPU,执行推理(PagedAttention 内核)
BlockManager 管理 KV Cache 分页分配(PagedAttention 支撑结构)
Tokenizer 对输入 prompt 编码 / 解码输出 token
SequenceGroup 每个用户请求的上下文(prompt + output + 状态 + 参数)

LLMEngine的核心函数是LLMEngine.step(),这个函数是vLLM调度执行的最小单位,每次只生成“一个或若干 token”,但可以批量执行多个请求。流程如下:

  • 调用Scheduler进行调度,它会决定这一轮要跑多少请求,每个请求跑多少token
  • 构建 batch 输入张量,确定好哪些请求参与后,Engine 将这些请求的输入 token 拼起来(使用 PagedAttention 支撑的索引表)
  • 调用 ModelExecutor 执行推理,准备好输入 tensor(input_ids、position_ids、attn_mask、block_table 等),调用 PyTorch 模型的 forward,返回 logits。这一步是真正触发 GPU 计算的地方
  • 后处理:采样 + 状态更新
  • 返回结果

一句话总结一下 LLMEngine 的逻辑:它会调用 Scheduler 对seq_group(其中seq_group是在schedule中类似于context的一个东西,用于保存prompt、outputs等内容)按照调度的顺序,交由Model Executor执行。

class AsyncLLMEngine:
    def __init__(self, engine_configs):
        self._engine = LLMEngine(engine_configs)
        self._request_queue = asyncio.Queue()  # 用户新请求队列
        self._result_streams = {}               # 每个请求的结果流
        self._background_task = asyncio.create_task(self._run_engine_loop())

对于AsyncLLMEngine,后台循环 _run_engine_loop() 是它的灵魂。

async def _run_engine_loop(self):
    while True:
        # 等待或收集新请求
        self._gather_new_requests()
        
        # 调用底层 LLMEngine 执行一次 step()
        outputs = self._engine.step()

        # 把每个 seq_group 的新 token 分发到对应的 async stream
        self._dispatch_results(outputs)

        await asyncio.sleep(0)  # 让出事件循环

  1. 不断循环:这是一个“持续调度循环”,每次执行一次 step;

  2. 每一步执行逻辑:

    • 检查是否有新请求;
    • 调用 LLMEngine.step();
    • 收到结果后立即分发(例如通过 asyncio.Queue、Future、WebSocket 等传给客户端);
  3. 实现连续批处理 + 实时推流

主要的异步逻辑是在AsyncLLMEngine中实现的,它给LLMEngine加了一些异步的逻辑,具体的调度执行逻辑还是在LLMEngine中实现的。

img

LLMEngine 与 Scheduler

scheduler最主要的两个函数就是Scheduler.schedule()add_seq_group(),前者是负责调度任务队列中的seq_group, 从 waiting / running 等队列里挑选任务,组成当前这一步的 batch。后者是把新的请求添加到waiting队列里,供schedule调度

 

img

Worker

  • 角色:Worker是实际执行模型计算的单元。在多GPU部署中,通常每个GPU会有一个专属的Worker进程(作为Ray Actor运行)。

  • 主要职责:

    • 加载模型分片: 在分布式设置下(如Tensor Parallelism或Pipeline Parallelism),每个Worker只加载模型的一部分权重。
    • 执行模型前向传播: 根据LLMEngine(通过Scheduler)传递过来的指令(包括要处理的序列、输入数据、KV缓存块信息),在其负责的GPU上执行模型的一个或多个层的前向计算。
    • 与PagedAttention Kernel交互:Worker内部的模型层(特别是Attention层)会调用vLLM优化的CUDA Kernel,这些Kernel根据PagedAttention的块表信息从GPU显存中高效地存取KV数据。
    • 管理本地KV缓存: 每个Worker管理其GPU上的物理KV块。
    • 返回计算结果: 将计算得到的logits(或其他中间结果,如在流水线并行中)返回给LLMEngine或流水线的下一个阶段。

(vLLM Docs – Worker Overview)

  • LLMEngine的交互:Worker是被动执行者。它等待LLMEngine通过RPC(通常由Ray管理)发送过来的计算任务,并在完成后返回结果。

工作流程

请求生命周期(简化版)

  1. 请求到达: 用户通过API(如OpenAI兼容服务器)或Python SDK提交一个文本生成请求,包含prompt和采样参数。
  2. 请求入队: 请求被API层(如AsyncLLMEngine)接收,封装成一个或多个SequenceGroup对象,并被添加到Scheduler的等待队列 (waiting_queue)。
  3. 调度决策:LLMEngine周期性地调用其step()方法。在step()内部,Scheduler会执行其调度逻辑(Continuous Batching):
  • 检查是否有已完成的请求可以从运行队列 (running_queue) 中移除。
  • 根据当前GPU资源(主要是空闲KV块数量)和调度策略,从waiting_queue(可能还有swapped_queue)中选择一批SequenceGroup加入到running_queue
  • 为这些被选中的序列动态分配或映射PagedAttention所需的物理KV块。

(李乾坤的博客 – 大模型推理服务框架vLLM (调度流程部分))

  1. 模型执行:LLMEngine将选定批次的序列数据(包括token ID、位置信息、KV块映射表等元数据)发送给Worker(s)
  • Prefill阶段: 如果是新请求或被抢占后重算的请求,Worker(s)会完整处理其prompt部分,计算并填充相应的KV缓存。
  • Decode阶段: 对于已完成Prefill的序列,Worker(s)会基于已有的KV缓存和上一个生成的token,计算出下一个token的logits。

 

  1. 结果收集与采样:LLMEngineWorker(s)收集logits,然后根据请求指定的SamplingParams(如temperature, top_p, top_k等)进行采样,得到新生成的token(s)。
  2. 状态更新与迭代:
  • 新生成的token被追加到对应的Sequence对象中。
  • 如果序列达到了max_tokens限制、生成了停止符(stop_sequences),或发生错误,则该序列(或整个SequenceGroup)被标记为完成。
  • 如果序列未完成,它会留在running_queue中,等待下一轮的Decode步骤。

 

  1. 结果返回: 生成的token或最终完成的文本通过API层流式地或一次性地返回给用户。

这个流程不断循环,使得vLLM能够高效地并发处理大量请求,同时通过PagedAttention和Continuous Batching等机制最大化GPU资源的利用率。详细的架构和交互可以参考 知乎专栏 – vLLM(二)架构概览 中提供的架构图和工作流描述(尽管链接可能无法访问,但其标题表明了内容方向)。

博客内容均系原创,未经允许严禁转载!
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇