vLLM 学习笔记|Guided Decoding (V1)
vLLM 学习笔记|Guided Decoding (V1)
一、引言
Guided Decoding,又叫 Structured Output,是大模型推理领域中非常重要的一个特性,主要用于引导大模型输出符合某种特定格式(如:SQL、Json)的结果,以便更好地将大模型落地到具体的应用场景中。
在我的上一篇文章中,简要地介绍了 Guided Decoding 的原理,并详细分析了 vLLM 中相关代码的实现(V0),文章链接如下:link。
自从 vLLM v0.8.x 之后,V1 Engine 将作为 vLLM 启动时的默认选项。关于 V1 Engine 的系统设计以及具体的优化点,我将会在之后逐步梳理并分享出来(如果有空的话)。而在本文中,我将针对 V1 Engine,分享 Structured Output 模块的整体设计与具体实现。
二、V1 Engine 整体架构
在介绍 Structured Output 模块的设计之前,让我们先来看下 vLLM V1 Engine 的整体架构。在 V1 中,vLLM 将不同类型的 CPU 密集型操作拆分到了两个相互独立的进程中,以便能够异步执行不同的 CPU 操作,减少了不同步骤之间相互等待的时间,因此能够更好地压榨硬件的计算性能。

Process 0主要负责请求的预处理(如:参数校验)、Tokenization 以及 Detokenization 等操作;Process 1主要负责请求的调度和推理执行等操作。
在优化前(V0),Process 0 和 Process 1 中的操作顺序执行,因此存在许多 CPU 空闲等待的时间,而在 V1 中则是并行执行上面两个进程中的操作,因此极大地提升了整体的推理效率。
三、V1 Engine 整体流程
接下来,让我们再看一下 V1 Engine 的整体推理流程:

高清图片链接:link,画图不易,走过路过欢迎点一个 Star!
LLMEngine属于Process 0,主要用于与推理引擎外部进行交互(如:接收用户请求、输出推理结果等)。vLLM 的离线推理引擎LLM和在线推理引擎AsyncLLM都是基于LLMEngine的封装;EngineCore属于Process 1,是推理引擎的核心,包含了请求调度、模型前向推理等关键实现。
3.1 LLMEngine 整体流程

- 当一个新的 prompt 输入到 vLLM 后,
LLMEngine会调用Processor的process_inputs()方法,对输入数据进行一些校验和预处理。其中,有一个步骤叫做_validate_structured_output(),会对 Structured Output 后端的设置进行检查(目前 V1 只支持 engine-level 的后端,即全局只能使用同一个后端,不允许针对不同的请求使用不动的后端进行处理),并判断输入内容是否能被转换成 Structured Output 后端能够识别的语法; LLMEngine调用step(),并通过get_output()方法从EngineCoreClient获取推理结果(EngineCoreClient是LLMEngine中专门用于与EngineCore进行通信的类);LLMEngine调用OutputProcessor中的process_outputs()方法,对推理结果进行后处理,比如:Detokenization、准备RequestOutput对象以及终止已经生成结束符的请求。
3.2 EngineCore 整体流程
当 LLMEngine 调用 make_client() 方法创建 EngineCoreClient 时,可以根据配置创建不同的 EngineCoreClient,用户可以选择继续使用 V0-style 的 Engine,也可以采用 V1-style(本文仅讨论 V1)。当使用 V1 Engine 时,会创建一个新的进程,运行 EngineCoreProc 中的 run_engine_core() 方法,并开启一个 busy loop。

run_busy_loop() 主要分为两步:
_process_input_queue():处理来自EngineCoreClient的请求。当有新的请求到来时,调用add_request()方法:(1)将EngineCoreRequest转换为Request;(2)异步编译 Structured Output Grammar;(3)将请求添加到Scheduler中。_process_engine_step():执行调度和推理。

注意:本文主要聚焦于 Structured Output 特性,前面只是大致介绍下 V1 Engine 的整体情况,更多具体的内容(如:调度流程、解码算法等)后面有空再单独写文章进行分享。
四、Structured Output 代码实现
了解了 V1 Engine 的整体架构和推理流程之后,下面我们将以 XGrammar 后端为例,梳理 Structured Output 具体的代码实现。

高清图片链接:link,画图不易,走过路过欢迎点一个 Star!
4.1 请求预处理
当一个新的请求从 EngineCoreClient 发送到 EngineCore 时,是以 EngineCoreRequest 的形式。EngineCore 会先通过 Request.from_engine_core_request() 方法将其转换为 Request 形式,在这一步中,会创建 StructuredOutputRequest 对象并将 SamplingParams 传递给它。而在 SamplingParams 中就包含了用户设置的 GuidedDecodingParams。

若 GuidedDecodingParams 为空,则代表不使用 Structured Output,因此会将 use_structured_output 设置为 False,反之则为 True。GuidedDecodingParams 的部分内容如下:
1 |
|
一个简单的使用示例(根据文字判断感情,并输出对应的选项)如下:
1 | # Guided decoding by Choice (list of possible options) |
4.2 异步编译 Grammar
StructuredOutputManager 是 EngineCore 中的一个组件,当 Request 转换完成后,会调用 StructuredOutputManager 中的 grammar_init() 方法。

grammar_init() 的主要步骤如下:
- 将 engine-level backend 设置到
StructuredOutputManager中; - 通过 backend 调用
compile_grammar()方法,并开始异步编译语法。编译完成后,会生成一个CompiledGrammar对象,然后利用该对象创建GrammarMatcher对象,最后将这些内容打包为一个XgrammarGrammar对象放到Request的StructuredOutputRequest属性中。

上述流程的部分代码如下:
1 | class StructuredOutputManager: |
注意:在 V1 Engine 中,每一个 Structured Output 后端都需要实现两个类——
StructuredOutputBackend和StructuredOutputGrammar,在这两个抽象类中定义了每个后端应该实现的接口。其中,StructuredOutputBackend是 engine-level 的 backend,而StructuredOutputGrammar是 request-level 的 backend,即前者是整个推理引擎共用的后端,而后者存在于Request对象中,只负责对自己所属的请求进行处理。
4.3 分配 bitmask 并更新 FSM 状态
当前置准备完成后,EngineCore 就会调用 step() 方法进行请求调度和模型推理。

step() 的主要步骤如下:
Scheduler调用schedule()方法对请求进行调度。其中,会调用StructuredOutputManager的grammar_bitmask()方法:- 调用 XGrammar 后端的
allocate_token_bitmask()方法,为当前 batch 中的所有请求准备下一个 token 的 bitmask(bitmask 的具体作用在上一篇文章中有做解释); - 针对每一个设置了 Structured Output 的请求,调用
GrammarMatcher中的fill_next_token_bitmask()方法为它们填充 bitmask。
- 调用 XGrammar 后端的
Scheduler调度完成后生成调度结果SchedulerOutput,并调用ModelExecutor开始执行推理(调用链路:ModelExecutor->Worker->ModelRunner)。其中,ModelRunner会调用apply_grammar_bitmask()方法将 bitmask 应用到模型输出的 logits 上,从而起到 Guided Decoding 的效果。该方法底层调用的是 XGrammar 后端中的apply_token_bitmask_inplace()方法。- 最后,
Scheduler根据之前生成的调度结果SchedulerOutput以及模型推理输出的结果ModelRunnerOutput更新状态并生成EngineCoreOutputs结果,返回给EngineCoreClient。其中,每一个use_structured_output = True的请求都会调用自己的XgrammarGrammar对象,调用GrammarMatcher中的accept_token()方法,使对应的 FSM 接受新生成的 token 并向前跳转到下一个状态。
上述流程中涉及的部分 XGrammar API 的说明如下:
1 | def allocate_token_bitmask(batch_size: int, |
到此为止,我们就介绍完了 vLLM 是怎么将一个 Structured Output 的请求转换为对应格式的输出的。
五、总结
目前,vLLM 的 V1 Engine 还在积极开发中(变动频繁),其 Structured Output 特性也还只支持 xgrammar 和 guidance 后端。本文中介绍的内容,仅代表写下这篇文章时代码的状态(v0.8.4),后续 Structured Output 模块如果还有比较大的调整,再考虑另写文章进行分享。
另外,目前我的工作就是全职参与 vLLM 社区的开发和维护,后续我还会持续分享更多关于 vLLM 最新进展的学习笔记,欢迎大家持续关注~