Prhub

#52384 [Rust Frontend][gRPC] Preserve skip_special_tokens decoding option

原始 PR 作者 biswapanda 合并时间 2026-08-15 16:54 文件变更 2 提交数 2 评论 7 代码增减 +5 / -1

执行摘要

gRPC Generate 新增 skip_special_tokens 解码选项,对齐 Python API

PR body 明确要求与 vLLM 的 Python serving API 对齐:/v1/chat/completions、/v1/completions、/v1/responses、/inference/v1/generate 均默认 skip_special_tokens = true,但 gRPC Generate 请求未暴露该选项,导致调用方无法为 reasoning / tool parser 保留 tokenizer 定义的 special markers。protobuf 字段设计为 optional,使服务端能区分省略与显式 false。

值得快速精读:5 行核心改动演示了 proto optional 字段如何承载三层语义(缺省/true/false)、以及与 Python 默认值对齐的做法。关注点:字段归属的 review 迭代(StoppingCriteria 到 ResponseOptions)与测试取舍,可作为小改动 PR 的范本。

讨论亮点

核心讨论围绕字段归属与测试取舍展开:

  • connorcarpenter15 指出 skip_special_tokens 不应放在 StoppingCriteria,而应放在 ResponseOptions(原话:"This should probably be in ResponseOptions, not StoppingCriteria."),作者 biswapanda 回复 "good point, fixed it." 并完成移动。
  • 同一评审者认为作者新增的两个转换单测不是严格必要的("I don't think these tests are strictly necessary."),作者接受并移除。
  • 合并者 njhill 最终批准并致谢("Thanks @biswapanda")。

实现拆解

  1. 协议扩展rust/proto/inference.proto):在 ResponseOptions 消息中新增 optional bool skip_special_tokens = 8,注释标明省略时默认 true。选择 optional 而非普通 bool,是为了保留“未提供”与“显式 false”两种语义,与 Python 默认 true 对齐。早期版本把字段放在 StoppingCriteria,review 后移入 ResponseOptions,因为它属于输出解码行为而非停止条件。
  2. 转换映射rust/src/server/src/grpc/convert.rs):在 to_text_request 中,TextDecodeOptionsskip_special_tokens 从硬编码 true 改为 response.and_then(|options| options.skip_special_tokens).unwrap_or(true)and_then 消费 Option<ResponseOptions>,再取内部 optional 字段,unwrap_or(true) 保证缺省时沿用 Python 默认值。
  3. 测试与验证:作者最初在 StoppingCriteria 中新增字段并附带两个单测(缺省 true / 显式 false 保留),review 后字段移入 ResponseOptions,单测按评审意见移除;最终通过 cargo fmt --check 与既有 grpc::convert::tests 13 个用例。由于 proto 变更无新增直接测试,回归保护依赖转换层既有用例。
文件 模块 状态 重要度
rust/src/server/src/grpc/convert.rs 请求转换 modified 5.07
rust/proto/inference.proto 协议定义 modified 2.14

关键符号

to_text_request

关键源码片段

rust/src/server/src/grpc/convert.rs core-logic

将硬编码的 skip_special_tokens: true 改为从 ResponseOptions 读取并用 unwrap_or(true) 保持 Python 默认值,是功能生效的核心转换逻辑。

// to_text_request 内部:将 gRPC 的 ResponseOptions 映射为 v1 引擎的 TextDecodeOptions。
// 关键点:skip_special_tokens 用 optional 字段区分“未提供”和“显式 false”。
let response = req.response.as_ref();let decode_options = TextDecodeOptions {
    // 与 Python 各端点默认值保持一致:请求未携带该字段时默认跳过 special tokens;
    // 调用方显式传入 Some(false) 时则保留 special tokens(部分 reasoning / tool parser 依赖)。
    skip_special_tokens: response
        .and_then(|options| options.skip_special_tokens)
        .unwrap_or(true),
    include_stop_str_in_output: stopping.is_some_and(|s| s.include_stop_strings),
    stop_strings: stopping.map(|s| &s.stop_strings).filter(|ss| !ss.is_empty()).cloned(),
    min_tokens: stopping.map_or(0, |s| s.min_new_tokens),
};

评论区精华

skip_special_tokens 字段放置位置 设计

connorcarpenter15 在 inference.proto 上评论:"This should probably be in ResponseOptions, not StoppingCriteria.",指出作者最初把字段放在 StoppingCriteria 语义不对。

结论:作者接受并改为放在 ResponseOptions,与 Python 端点的字段语义对齐。 · 已解决

新增转换单测是否必要 测试

connorcarpenter15 在 convert.rs 上评论:"I don't think these tests are strictly necessary.",针对作者新增的 absent_skip_special_tokens_defaults_to_true 与 explicit_false_skip_special_tokens_is_preserved 两个测试。

结论:作者回复 "good point, fixed it." 并移除了这两个测试,最终合并版本未包含直接单测。 · 已解决

风险与影响

协议契约变化:ResponseOptions 是公开 gRPC 接口,新增 optional 字段不影响旧客户端(缺省行为不变);但新客户端显式设置 false 后,输出文本可能包含 special tokens,依赖输出的下游解析逻辑需感知此行为变化。覆盖不足:最终版本没有针对该映射的直接单测,回归保护依赖既有 13 个转换测试。兼容性:Rust 侧 TextDecodeOptions 语义与 Python 保持一致,不会改变采样与 EOS 行为,风险面被 PR body 明确限定在解码文本。

影响面集中在 rust/proto 与 rust/server 两个目录,面向使用原生 Generate gRPC 端点的用户;行为与 Python 各端点对齐后,跨语言调用方获得一致解码语义。对既有部署,不在请求中携带该字段则完全无感知,属于低风险前向兼容变更。对团队而言,该 PR 继续完善 gRPC 前端与 v1 引擎的转换层,为后续 Rust 前端扩展提供基线。

proto API 契约扩展 新逻辑无直接单测覆盖 显式 false 时输出文本可能含 special tokens

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论