Prhub

#32412 Use native batched llguidance mask generation

原始 PR 作者 merrymercy 合并时间 2026-07-26 07:36 文件变更 5 提交数 2 评论 4 代码增减 +335 / -15

执行摘要

使用 llguidance 原生批量掩码填充,替换逐行调用,提升语法解码性能。

PR 描述中明确指出目标是 'batch regular and speculative llguidance mask generation through a shared executor' 并 'reuse compiled matchers',动机是性能优化——通过合并多个掩码填充请求为一次原生内核调用,消除每请求的 Python 调用开销,并利用 GPU 并行性加速掩码生成。之前的实现中,每个活跃的文法对象都在循环中单独调用 fill_vocab_mask,这导致了大量的 Python 函数调用和多次小规模内核启动,缺乏并行度。

值得精读,尤其是 fill_vocab_mask_batched 的设计模式——在基类提供默认回退、子类重写为原生批量调用,是性能优化的典型做法。对于后端开发人员,了解 register_vocab_mask_bufferinitialize_vocab_mask_buffer 有助于实现自己的固定缓冲区优化。测试用例 test_batched_matches_serial 是验证批处理正确性的好范例。

讨论亮点

本 PR 未产生技术性 review 讨论。作者自我批准(approved),仅有由 gemini-code-assist[bot] 发出的生命周期提示和 CI 重跑命令。从 issue 评论可以看出,CI 测试一度失败,但作者通过 /rerun-test 命令重新运行后全部通过(44 passed, 3 subtests passed),确认了功能的正确性。

实现拆解

  1. 引入批量填充抽象接口:在 python/sglang/srt/constrained/base_grammar_backend.py 中定义 GrammarRow NamedTuple 和 BaseGrammarObject.fill_vocab_mask_batched 静态方法,默认实现为对列表中每个条目调用 entry.grammar.fill_vocab_mask(vocab_mask, entry.row),为不支持批量操作的后端提供回退。同时添加 reset_vocab_mask 抽象方法和 BaseGrammarBackend.initialize_vocab_mask_buffer 方法,为可重用掩码缓冲区提供入口。
  2. 实现 llguidance 的批量填充内核:在 python/sglang/srt/constrained/llguidance_backend.py 中新增模块级函数 fill_token_bitmask_batchedfill_token_bitmask_with_draft_tokens,分别对应常规解码和推测解码的 draft-chain 掩码填充。它们通过 @cache 装饰的 _get_or_init_mask_executor() 获取全局共享的 LLExecutor 实例,将输入列表转换为 (matcher, row)(matcher, base_row, draft_tokens) 元组后调用 fill_next_token_bitmask_parfill_next_token_bitmask_par_with_draft_tokensGuidanceGrammar 类重写 fill_vocab_mask_batched 委托给 fill_token_bitmask_batched,并新增 reset_vocab_maskinitialize_vocab_mask_buffer 方法。此外,GuidanceGrammar.__init__ 接受可选的 ll_matcher 参数以支持编译后匹配器的重用。
  3. 集成到采样批处理流程:在 python/sglang/srt/sampling/sampling_batch_info.pyupdate_regex_vocab_mask 方法中,用列表推导式收集所有活跃文法的 GrammarRow 条目,然后调用 first_grammar.fill_vocab_mask_batched(entries, vocab_mask) 统一填充,替换原有的 for i, grammar in enumerate(self.grammars): grammar.fill_vocab_mask(vocab_mask, i) 循环。
  4. 配套测试:新增 test/registered/unit/constrained/test_llguidance_batched_mask.py,包含批量填充与串行填充的比特位一致性测试、已完成行保持全允许状态的测试、不支持的条目使用串行回退测试,以及固定掩码缓冲区初始化测试。修改 test/registered/unit/sampling/test_sampling_batch_info.py,用 side_effect 模拟批量填充行为以验证新调用路径。
文件 模块 状态 重要度
python/sglang/srt/constrained/llguidance_backend.py 约束后端 modified 8.57
python/sglang/srt/constrained/base_grammar_backend.py 约束后端 modified 8.26
python/sglang/srt/sampling/sampling_batch_info.py 采样批信息 modified 6.31
test/registered/unit/constrained/test_llguidance_batched_mask.py 测试 added 7.49
test/registered/unit/sampling/test_sampling_batch_info.py 测试 modified 6.08

关键符号

fill_token_bitmask_batched fill_token_bitmask_with_draft_tokens GrammarRow GrammarDraftRow _get_or_init_mask_executor BaseGrammarObject.fill_vocab_mask_batched BaseGrammarObject.reset_vocab_mask BaseGrammarBackend.initialize_vocab_mask_buffer register_vocab_mask_buffer GuidanceGrammar.__init__ GuidanceGrammar.fill_vocab_mask_batched GuidanceGrammar.reset_vocab_mask GuidanceGrammar.initialize_vocab_mask_buffer

关键源码片段

python/sglang/srt/constrained/llguidance_backend.py core-logic

核心变更文件,添加了批量填充函数 `fill_token_bitmask_batched`、`fill_token_bitmask_with_draft_tokens`、共享执行器 `_get_or_init_mask_executor`、`GrammarDraftRow` 类型,并修改 `GuidanceGrammar` 以支持批量填充、重置和固定缓冲区初始化。

# llguidance_backend.py 新增模块级批量填充函数
# 使用 llguidance 原生批处理内核,通过共享 LLExecutor 并发填充多个文法掩码@cache
def _get_or_init_mask_executor() -> LLExecutor:
    """返回缓存中的全局共享 LLExecutor 实例,避免重复创建。"""
    return LLExecutor()def fill_token_bitmask_batched(
    entries: List[GrammarRow],
    vocab_mask: torch.Tensor,
) -> None:
    """批量填充常规解码的词汇掩码行。"""
    if not entries:
        return
    # 将 GrammarRow (row, grammar) 列表转换为 (matcher, row) 元组
    matchers = [(e.grammar.ll_matcher, e.row) for e in entries]
    # 调用 llguidance 的并行填充函数,matchers 列表长度即 batch 大小
    fill_next_token_bitmask_par(_get_or_init_mask_executor(), matchers, vocab_mask)def fill_token_bitmask_with_draft_tokens(
    entries: List[GrammarDraftRow],
    vocab_mask: torch.Tensor,
) -> None:
    """批量填充推测解码的 draft-chain 掩码。    每个 GrammarDraftRow 包含基行号、文法和建议 tokens 列表。
    内核会推进匹配器直到遇到非法 token,然后回滚恢复状态。
    """
    if not entries:
        return
    matchers = [(e.grammar.ll_matcher, e.base_row, e.draft_tokens) for e in entries]
    fill_next_token_bitmask_par_with_draft_tokens(
        _get_or_init_mask_executor(), matchers, vocab_mask
    )
python/sglang/srt/constrained/base_grammar_backend.py architecture

定义了批量填充的抽象接口 `fill_vocab_mask_batched`、`GrammarRow` 类型、`reset_vocab_mask`、`initialize_vocab_mask_buffer` 以及全局注册函数 `register_vocab_mask_buffer`,是统一不同后端的核心抽象层。

# base_grammar_backend.py 中新增的批量填充基类方法与数据结构class GrammarRow(NamedTuple):
    """语法与目标行索引,用于批量掩码填充。"""
    row: int
    grammar: "BaseGrammarObject"class BaseGrammarObject:
    # ... 现有方法 ...
​
    @staticmethod
    def fill_vocab_mask_batched(
        entries: List[GrammarRow], vocab_mask: torch.Tensor
    ) -> None:
        """批量填充指定行的掩码,未指定行保持原值。        默认实现逐条调用 fill_vocab_mask,子类可覆盖为原生批量调用。
        """
        for entry in entries:
            entry.grammar.fill_vocab_mask(vocab_mask, entry.row)class BaseGrammarBackend:
    # ... 现有方法 ...
​
    def initialize_vocab_mask_buffer(
        self,
        name: str,
        vocab_size: int,
        max_rows: int,
        device,
    ) -> Optional[torch.Tensor]:
        """子类可覆盖以分配固定容量的可重用掩码缓冲区,默认返回 None。"""
        return None# 全局函数:注册固定容量的掩码缓冲区,检查形状 / 类型 / 设备一致性
def register_vocab_mask_buffer(
    name: str, vocab_mask: torch.Tensor, max_rows: int
) -> torch.Tensor:
    if max_rows <= 0:
        raise ValueError(...)
    buffers = get_resources().buffers
    existing = buffers.get(name)
    if existing is not None:
        if existing.shape != vocab_mask.shape or existing.dtype != vocab_mask.dtype or existing.device != vocab_mask.device:
            raise RuntimeError(...)
        return existing # 如果已有相同缓冲区,直接返回现有引用
    buffers[name] = vocab_mask
    return vocab_mask
python/sglang/srt/sampling/sampling_batch_info.py core-logic

调度核心路径 `update_regex_vocab_mask` 被修改为使用批量填充,直接影响每个推理步的词汇掩码生成。

# sampling_batch_info.py 中 update_regex_vocab_mask 的修改后核心部分def update_regex_vocab_mask(self):
    # ... 前面的代码 ...
​
    # 收集所有活跃语法行(finished / terminated / None 自动跳过)
    entries = [
        GrammarRow(row=row, grammar=grammar)
        for row, grammar in enumerate(self.grammars)
        if grammar and not grammar.finished and not grammar.is_terminated()
    ]
    # 批量填充:将整个列表传给第一个文法的批量填充方法
    first_grammar.fill_vocab_mask_batched(entries, vocab_mask)
​
    # 移动掩码到设备(如果有需要)
    vocab_mask = first_grammar.move_vocab_mask(vocab_mask, self.device)
    # ...

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  • 回退兼容性base_grammar_backend.pyfill_vocab_mask_batched 的默认实现逐行调用 fill_vocab_mask,因此未覆盖该方法的任何后端依然能正常运作,但无法获得性能提升。
  • 共享 LLExecutor 实例的线程安全_get_or_init_mask_executor 使用 @cache 缓存,返回同一个 LLExecutor 实例。llguidance 的 LLExecutor 是否线程安全需关注,但当前 SGLang 的调度模型使得同一时刻只有一个推理步在执行,风险较低。
  • 掩码缓冲区注册冲突register_vocab_mask_buffer 会检查已有缓冲区形状/类型/设备是否一致,不一致时抛出 RuntimeError。这在多后端或热更新场景下可能引发中断,但属于防御性设计。
  • 性能影响:没有提供 benchmark 对比数据,无法量化收益。但对于大规模并发语法解码场景,减少 Python 循环和内核启动次数预期有明显提升。
  • 用户影响:无感知;对于使用 llguidance 后端的用户,语法约束解码在多 batch 或推测解码场景下可能明显加速;API 完全向后兼容。
  • 系统影响:修改了核心采样流程中的掩码填充逻辑,从逐行改为批量,可能影响 SamplingBatchInfo 的运行时行为;新增了固定容量掩码缓冲区,减少了内存分配次数。
  • 团队影响:代码结构更清晰,将批量掩码抽象提升到基类,便于其他后端实现类似的优化。新引入的 GrammarRow 和数据流约定需要团队成员了解。
核心路径变更 缺少性能基准 回退路径保证

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论