Prhub

#33328 [Fix] Treat an empty grammar constraint as unset in SamplingParams

原始 PR 作者 hnyls2002 合并时间 2026-08-03 12:45 文件变更 4 提交数 1 评论 3 代码增减 +32 / -1

执行摘要

空字符串语法约束归一化为 None,修复 UnboundLocalError 崩溃

PR body 明确指出:An empty grammar constraint (e.g. structural_tag='' from /generate) passes the is not None entry check in GrammarManager.process_req_with_grammar but matches no branch in the selection chain, so the key lookup runs with nothing assigned and raises UnboundLocalError。修复思路是把空字符串折叠为 None,并让选择链与入口条件保持一致。

值得快速阅读。PR 很小但展示了两个值得借鉴的点:一是把“空值归一化”放在数据类构造入口,而不是散落到下游判断;二是选择链的分支条件必须与入口条件严格一致,避免 falsy 值与 is not None 之间的缝隙。可作为 API 参数边界处理的范例。

讨论亮点

该 PR 没有实质性的 review 讨论,页面评论只有两条:Gemini Code Assist 机器人的下线声明,以及作者发起 /rerun-test 后得到的 CI 结果。整个修复思路清晰,无开放性争议。

实现拆解

  1. 参数归一化(python/sglang/srt/sampling/sampling_params.py):在 SamplingParams.__post_init__ 中新增 4 行,用 self.xxx = self.xxx or Nonejson_schemaregexebnfstructural_tag 四个字段的空字符串等 falsy 值统一视为未设置。这样下游所有基于 is not None 的判断都能得到一致行为。
  2. 选择链对齐(python/sglang/srt/constrained/grammar_manager.py)GrammarManager.process_req_with_grammarstructural_tag 分支从真值判断改为 is not None,与其他三个分支一致,从源头消除“入口能进、选择链不匹配”的缝隙。
  3. 回归测试:test/registered/unit/sampling/test_sampling_params.py 新增 test_empty_grammar_constraint_becomes_none,用 subTest 验证四个字段的空字符串都会变成 None;test/registered/unit/constrained/test_grammar_manager.py 新增 test_falsy_structural_tag_still_resolves_a_key,模拟 structural_tag='' 请求,断言仍能走通语法 key 解析流程并得到 ('structural_tag', '') 的 key。
  4. CI 验证:作者触发 /rerun-test 重跑两组单测,ubuntu-latest 上 2 个测试全部通过,无配置或部署配套改动。
文件 模块 状态 重要度
python/sglang/srt/sampling/sampling_params.py 采样参数 modified 5.19
python/sglang/srt/constrained/grammar_manager.py 语法管理 modified 4.49
test/registered/unit/sampling/test_sampling_params.py 采样测试 modified 4.67
test/registered/unit/constrained/test_grammar_manager.py 语法测试 modified 4.8

关键符号

SamplingParams.__post_init__ GrammarManager.process_req_with_grammar test_empty_grammar_constraint_becomes_none test_falsy_structural_tag_still_resolves_a_key

关键源码片段

python/sglang/srt/sampling/sampling_params.py core-logic

核心修复点:在 __post_init__ 中将四个语法字段的空字符串归一化为 None,统一了空值语义。

        # 空字符串语法约束表示未设置,而不是约束为空。
        # 之前 '' 能通过下游的 is not None 检查,
        # 但选择链上没有分支匹配,导致 key 未定义并抛出 UnboundLocalError。
        self.json_schema = self.json_schema or None
        self.regex = self.regex or None
        self.ebnf = self.ebnf or None
        self.structural_tag = self.structural_tag or None
python/sglang/srt/constrained/grammar_manager.py core-logic

选择链与入口条件对齐:structural_tag 分支改为 is not None,修复 falsy-but-set 值漏分支的问题。

        # 入口条件:只要任一语法字段非 None,就进入语法处理流程。
        if (
            req.sampling_params.json_schema is not None
            or req.sampling_params.regex is not None
            or req.sampling_params.ebnf is not None
            or req.sampling_params.structural_tag is not None
        ):
            if self.grammar_backend is None:
                # 无后端时直接中止该请求
                req.set_finish_with_abort(
                    'Grammar-based generation (json_schema, regex, ebnf, structural_tag) '
                    'is not supported when the server is launched with --grammar-backend none'
                )
            else:
                # 选择链与入口条件保持一致:structural_tag 也使用 is not None,
                # 避免 falsy-but-set 值(如空字符串)漏掉所有分支。
                if req.sampling_params.json_schema is not None:
                    key = ('json', req.sampling_params.json_schema)
                elif req.sampling_params.regex is not None:
                    key = ('regex', req.sampling_params.regex)
                elif req.sampling_params.ebnf is not None:
                    key = ('ebnf', req.sampling_params.ebnf)
                elif req.sampling_params.structural_tag is not None:
                    key = ('structural_tag', req.sampling_params.structural_tag)

评论区精华

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

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

风险与影响

行为变化:原本显式传空字符串意图“禁用语法约束”的调用方,现在获得与不传完全一致的语义(无约束),更符合直觉;但若外部系统曾依赖空字符串作为“有值”标记,需要留意。影响面:改动位于 SamplingParams.__init__,影响所有经过采样参数构造的请求路径(/generate、OpenAI 兼容接口等),但仅作用于四个语法字段,不涉及采样超参。兼容性:字段类型仍为 str/None,不会破坏序列化。测试覆盖:新增的两条单测只覆盖空字符串,未覆盖空列表或空白字符串等 falsy 变体,不过这些在现有 API 中通常不会出现。

修复了 /generate 传空字符串语法字段时请求崩溃的问题,所有使用语法约束的请求路径更健壮。对用户:空字符串不再视为非法输入或崩溃来源,行为与不传一致。对系统:无性能影响。对团队:维护约定更明确——空字符串等于未设置,且入口条件与选择链必须保持一致。

空字符串语义变化 公共请求路径 仅单元测试覆盖

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论