# PR #31738 完整报告

- 仓库：`sgl-project/sglang`
- 标题：Fix stop boundaries for grammar-constrained speculative decoding
- 合并时间：2026-07-21 08:55
- 原文链接：http://prhub.com.cn/sgl-project/sglang/pull/31738

---

# 执行摘要

- 一句话：修复 grammar 约束推测解码时停止边界错误
- 推荐动作：建议精读。本 PR 虽小，但揭示了在多重停止条件下正确确定完成边界的通用设计问题，且 review 中关于 Future 守卫的讨论体现了对生命周期不变量的深刻理解，值得参考。

# 功能与动机

在 grammar 约束的推测解码中，grammar 终止判断先于 stop token 检查，导致 grammar 终止时直接以最后一个 token 作为 finish 边界，忽略了更早出现的 stop token。例如，当输出序列为 [STOP, EOS] 且 grammar 终止时，错误地将 EOS 作为 matched token，finished_len 未设置，导致输出包含 EOS。

# 实现拆解

1. **调整检查顺序**：在 `python/sglang/srt/managers/schedule_batch.py` 的 `update_finish_state` 方法中，将 grammar 终止检查从 stop 检查之前移动到 stop 字符串和 stop token 检查之后。
2. **新增完整测试文件**：创建 `test/registered/unit/managers/test_grammar_stop_speculative.py`，包含两个测试用例：
 - `test_requested_stop_token_wins_over_trailing_eos`：请求级 stop_token_ids 场景
 - `test_tokenizer_stop_token_wins_over_trailing_eos`：tokenizer 级 additional_stop_token_ids 场景
 两个测试均构造 4 个 token 的输出序列 [11, 13, STOP, EOS]，验证 grammar 终止时 finished_reason.matched 为 STOP、finished_len 为 3、output_ids_through_stop 排除 EOS。

关键文件：
- `python/sglang/srt/managers/schedule_batch.py`（模块 调度器；类别 source；类型 core-logic；符号 update_finish_state）: 核心逻辑变更：调整 grammar 终止检查在 update_finish_state 中的执行顺序，从 stop 检查之前移到之后，仅 4 行新增、5 行删除。
- `test/registered/unit/managers/test_grammar_stop_speculative.py`（模块 测试；类别 test；类型 test-coverage；符号 _FakeTokenizer, _TerminatedGrammar, is_terminated, _make_req）: 新增完整测试文件，覆盖请求级 stop_token_ids 和 tokenizer 级 additional_stop_token_ids 两种场景，验证修复的正确性。

关键符号：update_finish_state

## 关键源码片段

### `python/sglang/srt/managers/schedule_batch.py`

核心逻辑变更：调整 grammar 终止检查在 update_finish_state 中的执行顺序，从 stop 检查之前移到之后，仅 4 行新增、5 行删除。

```python
# python/sglang/srt/managers/schedule_batch.py (head)

def update_finish_state(self, new_accepted_len: int = 1):
    if self.finished():
        return
    if self.to_finish:
        self.finished_reason = self.to_finish
        self.to_finish = None
        return
    # 1. 先检查 max_new_tokens 长度限制
    if len(self.output_ids) >= self.sampling_params.max_new_tokens:
        self.finished_reason = FINISH_LENGTH(
            length=self.sampling_params.max_new_tokens
        )
        self.finished_len = self.sampling_params.max_new_tokens
        return

    # 2. 获取本次新接受的 token 列表
    new_accepted_tokens = self.output_ids[-new_accepted_len:]

    # 3. 检查 vocab 越界 /NaN
    if self._check_vocab_boundary_finish(new_accepted_tokens):
        return

    # 4. 停止字符串检查（优先于 token 检查，防止推测解码多 token 时漏匹配）
    if self._check_str_based_finish(new_accepted_len):
        return

    # 5. token 级别的停止检查（stop_token_ids / eos_token_id）——原 bug：grammar 终止在此前返回，
    # 导致 STOP token 被跳过而 EOS 被错误匹配
    if self._check_token_based_finish(new_accepted_tokens):
        return

    # 6. 最后检查 grammar 是否终止（仅当没有其他停止条件时使用）
    if self.grammar is not None and self.grammar.is_terminated():
        self.finished_reason = FINISH_MATCHED_TOKEN(matched=self.output_ids[-1])
        return

```

# 评论区精华

gemini-code-assist[bot] 建议在 grammar 终止检查中添加 Future 类型守卫，因为 `self.grammar` 类型可能为 `Future[BaseGrammarObject]`。作者 windscope 回应：虽然类型包含 Future，但有 grammar 的请求会先由 `GrammarManager.grammar_queue` 管理，`get_ready_grammar_requests()` 会在请求进入调度器前将 Future 替换为实际结果；失败的请求在 `update_finish_state` 开始处已返回；添加 Future 守卫会掩盖生命周期不变量违例，可能导致 grammar 终止实际生效时继续生成。最终未采纳该建议。

- 新增 Future 守卫以避免 is_terminated 调用异常 (correctness): 作者反驳：有 grammar 的请求会先由 GrammarManager 管理，Future 在进入调度器前已被解析；添加守卫会掩盖生命周期错误。未采纳。

# 风险与影响

- 风险：变更仅调整了 `update_finish_state` 中两个条件判断的顺序，逻辑等价性有测试保障。风险较低。但若其他代码路径依赖旧的评估顺序（例如 grammar 终止后立即停止生成以跳过 stop 检查），则可能产生行为变化。目前代码库中 `update_finish_state` 是唯一调用点，且 stop 检查内部会正确处理 grammar 终止后的边界，因此回归风险可控。
- 影响：直接影响 grammar 约束的推测解码功能（如 Eagle、DFlash 等）。修复前，下游应用可能收到包含 EOS 的多余 token；修复后，stop token 优先被识别，输出序列正确截断。不影响非 grammar 场景或非推测解码场景。对已有用户是向前兼容的 bugfix。
- 风险标记：核心路径变更 , 缺少 Future 守卫但设计合理

# 关联脉络

- PR #31787 Fix dropped Inkling reasoning at stream end: 同为 speculative decoding 场景下的边界条件修复，涉及 finish_state 和 reasoning 解析
- PR #31677 [Spec] Extract DFlash compact draft-cache rebuild helpers: 涉及 speculative decoding 模块，DFlash 同样可能受 grammar 停止边界问题影响