执行摘要
- 一句话:DSPARK 推测解码支持语法约束与 tool_choice=auto
- 推荐动作:值得精读的 PR。它展示了如何在 speculative decoding 的 verify 阶段无缝集成 grammar mask,以及如何通过 step1 (显式 grammar) 和 step2 (隐式 tool_choice) 分步解决实际生产问题。特别关注
fold_eligible 与 CUDA graph 交互的设计决策,以及 supports_grammar_overlap 方法的多态扩展。
功能与动机
DSPARK speculative decoding 之前对携带 grammar 约束的请求(如 response_format 、 json_schema 、 regex 、 ebnf )返回 HTTP 400 拒绝。实际上 DSPARK 的 target verify 路径已经具备应用 vocab mask 的能力,只是缺少 admission 守卫释放和 mask 应用。该 PR 是 #30096(DFLASH 语法约束)的配对补充,同时解决了生产环境中 tool_choice=auto 下标记泄露的问题。
实现拆解
- 解除 admission 封锁(
python/sglang/srt/speculative/dflash_utils.py ):validate_dflash_request 移除 spec_algorithm 参数和 spec_algorithm.supports_grammar_overlap() 的检查,不再拒绝 DSPARK 的 grammar 请求(DFLASH 仍拒绝)。同步更新调度器( python/sglang/srt/managers/scheduler.py )调用处,去掉 spec_algorithm 参数。
- verify 阶段应用 vocab mask(
python/sglang/srt/speculative/dspark_components/dspark_worker_v2.py ):在 _forward_decode 中,当 batch.has_grammar 时,构建 GrammarTree.from_linear_chain (DSPARK 为线性验证树),调用 build_grammar_vocab_mask 生成掩码,并通过 draft_input.grammar.apply_vocab_mask 应用到 next_token_logits 。同时修改 fold_eligible 条件,排除 batch.has_grammar 的 batch,强制走 eager path 使得 mask 生效。
- 扩展数据类(
python/sglang/srt/speculative/dflash_info_v2.py ):DFlashDraftInputV2 新增 grammar 字段(类型 BaseGrammarObject),用于在 verify 过程中记录 grammar 对象。
- 语法重叠能力声明(
python/sglang/srt/speculative/spec_info.py ):supports_grammar_overlap 从 is_dflash() 改为 is_dflash_family(),使 DSPARK 与 DFLASH 一样上报支持 grammar 重叠。
- 函数调用自动约束(第二个 commit,涉及 function_call_parser ):
should_constrain_auto 支持 DFLASH-family spec algorithm,通过 detector 暴露的 structural tag 触发 mask,解决 tool_choice=auto 下的标记泄露问题。
- 测试配套(
test/registered/core/test_basic_sanity_dspark.py ):添加 JSONConstrainedMixin 和 SpecGrammarKit 组合,以及一个被跳过的 test_grammar_logprob_count_matches_completion_tokens 因 admission 阶段拒绝 return_logprob 。同时存在 CPU 单元测试 test_dflash_validate_request.py 验证 admission 行为。
关键文件:
python/sglang/srt/speculative/dspark_components/dspark_worker_v2.py(模块 推测解码;类别 source;类型 core-logic;符号 forward_batch_generation, _forward_decode): 核心实现文件:在 _forward_decode 中添加 grammar mask 逻辑,修改 fold_eligible 排除 grammar batch,新增 grammar_barrier 参数。是整个功能的关键。
python/sglang/srt/speculative/dflash_utils.py(模块 推测解码;类别 source;类型 core-logic;符号 validate_dflash_request): 定义 admission 检验函数 validate_dflash_request,移除对 DSPARK 的 grammar 拒绝逻辑,使得 grammar 请求能被调度到 DSPARK worker。
test/registered/core/test_basic_sanity_dspark.py(模块 集成测试;类别 test;类型 test-coverage;符号 test_grammar_logprob_count_matches_completion_tokens): 集成测试:通过 mixin 组合 JSONConstrainedMixin 和 SpecGrammarKit 确保 DSPARK 能够处理 grammar 请求,并新增被跳过的 test_grammar_logprob_count_matches_completion_tokens 作为已知限制的标记。
关键符号:validate_dflash_request, forward_batch_generation, _forward_decode, supports_grammar_overlap, should_constrain_auto
关键源码片段
python/sglang/srt/speculative/dspark_components/dspark_worker_v2.py
核心实现文件:在 _forward_decode 中添加 grammar mask 逻辑,修改 fold_eligible 排除 grammar batch,新增 grammar_barrier 参数。是整个功能的关键。
# python/sglang/srt/speculative/dspark_components/dspark_worker_v2.py
# 摘要:_forward_decode 中新增 grammar mask 逻辑
class DSparkWorkerV2(BaseSpecWorker):
def _forward_decode(
self, batch: ScheduleBatch, on_publish, grammar_barrier=None
) -> GenerationBatchResult:
# ... 构建 verify_ids_2d, draft_input ...
# 构建 grammar tree(DSPARK 是线性链,每个位置只有一个子节点)
grammar_tree = (
GrammarTree.from_linear_chain(verify_ids_2d) if batch.has_grammar else None
)
# fold_eligible 原条件;现在增加 not batch.has_grammar
# 当有 grammar 时,fold 会跳过 CUDA graph epilogue,保证 mask 生效
fold_eligible = (
self._verify_executor.verify_epilogue is not None
and proposal.folded
and verify_logits_adjustments_are_noop(sampling_info)
and self._simulate_acc_len <= 0
and not batch.has_grammar # 新增:有 grammar 时强制 eager
)
with self._observers.segment(InfoSegment.TARGET_VERIFY):
# ... 运行 verify launch ...
logits_output = target_verify.logits_output
if batch.has_grammar:
# grammar barrier 非 None 时等待 host 侧 FSM 前进
if grammar_barrier is not None:
grammar_barrier()
# 构建 vocab mask,复用 EAGLE/NGRAM 路径的 build_grammar_vocab_mask
vocab_mask = build_grammar_vocab_mask(
reqs=batch.reqs,
verify_input=draft_input,
tree=grammar_tree,
sampling_info=sampling_info,
device=logits_output.next_token_logits.device,
)
if vocab_mask is not None:
draft_input.grammar.apply_vocab_mask(
logits=logits_output.next_token_logits, vocab_mask=vocab_mask
)
python/sglang/srt/speculative/dflash_utils.py
定义 admission 检验函数 validate_dflash_request,移除对 DSPARK 的 grammar 拒绝逻辑,使得 grammar 请求能被调度到 DSPARK worker。
# python/sglang/srt/speculative/dflash_utils.py
# 变更后 validate_dflash_request 签名和逻辑
def validate_dflash_request(req: Req, enable_overlap: bool) -> Optional[str]:
"""
检查 DFLASH-family 请求是否可接受。
现在 DSPARK 的 grammar 请求不被拒绝,因为 DSPARK worker 支持 mask。
"""
if req.return_logprob:
return "DFLASH speculative decoding does not support return_logprob yet."
if enable_overlap and req.return_hidden_states:
return "DFLASH speculative decoding does not support return_hidden_states yet."
# grammar 拒绝检查已移除,因为 DSPARK 支持;DFLASH 仍然不支持,
# 但 DFLASH 的拒绝由其他机制处理(仍然在 admission 出错时返回错误)。
return None
评论区精华
- tool_choice=auto 的补充(作者 shanemort1982):第一个 commit 只处理了显式 grammar 请求,但 tool_choice=auto 不携带 grammar 对象,导致生产环境工具调用出现 5-8 倍泄露。第二个 commit 通过 should_constrain_auto 扩展解决了此问题,并附上 7 小时生产验证零泄露的数据。
- CI 触发与测试通过(作者与合并者 hnyls2002 ):合并者通过 /rerun-test 命令执行了特定测试( test_basic_sanity_dspark.py 、 test_dflash_validate_request.py 、 test_dflash.py ),全部通过。
- 重构与冲突解决(hnyls2002 的后续提交):多次合并 main 分支解决冲突,尤其是 main 上 FunctionCallParser 已经重构为使用 get_auto_tool_call_structural_tag() ,需要适配。前作者(shanemort1982)的 Codex review 也指出需要跟踪最新 main 的变更。
- tool_choice=auto 的补充 (other): 合并者接受了此扩展,两个 commit 合并一起发布。
- CI 测试触发与通过 (testing): CI 测试通过,无新增失败。
- 与 main 分支的冲突解决 (other): 最终提交 7fd4eb2 中冲突解决,新逻辑适配了 main 的接口。
风险与影响
关联脉络
- PR #30096 [DFLASH] Support grammar-constrained decoding in speculative verify: 本 PR 是 #30096 的 DSPARK 配对实现,共享类似的验证时 mask 机制和 admission 调整。
- PR #30155 [xgrammar] vocab-mask on the multi-layer EAGLE verify path: 使用了相同的 generate_token_bitmask / apply_vocab_mask 设施。
参与讨论