Prhub

#44620 [Bugfix][Rust Frontend] Fix UTF-8 char-boundary panic in incremental detokenizer

原始 PR 作者 Sunt-ing 合并时间 2026-06-05 15:36 文件变更 1 提交数 1 评论 2 代码增减 +25 / -0

执行摘要

修复 Rust 前端增量解码器 UTF-8 边界崩溃

当流式输出包含多字节字符(如中文、emoji)且请求携带 stop string 时,Rust 前端的增量解码器 next_chunk 会因切片落在字符中间而 panic,导致整个 vllm-rs 进程崩溃,所有并发连接被丢弃。Python 前端无此问题。

建议立即合并,此修复解决了一个严重的生产环境稳定性问题,代码改动小且风险低。

讨论亮点

BugenZhao 审核通过,并称赞 "This is a good catch! Thanks for the fix." 未发现争议或未解决问题。

实现拆解

  1. 定位问题:在 rust/src/tokenizer/src/incremental.rsnext_chunk 方法中,cutoff 计算为 cumulative_output.len().saturating_sub(min_bytes_to_buffer),未使用 floor_char_boundary 对齐到 UTF-8 字符边界。
  2. 修复方法:在计算 cutoff 后添加 self.cumulative_output.floor_char_boundary(cutoff),确保切片位置始终是一个字符边界,与 push_tokenflush 方法保持一致。
  3. 新增回归测试:添加 next_chunk_cutoff_respects_char_boundary 测试,使用 hold-back 为 2 字节的多字节字符串("你好A")进行流式解码,验证不会 panic 且输出结果正确。
  4. 验证cargo test 通过,该回归测试在修复移除后能复现 panic。
文件 模块 状态 重要度
rust/src/tokenizer/src/incremental.rs Tokenizer modified 6.73

关键符号

next_chunk

关键源码片段

rust/src/tokenizer/src/incremental.rs core-logic

核心文件,包含 `next_chunk` 方法的修复和新增的回归测试。

// rust/src/tokenizer/src/incremental.rsfn next_chunk(&mut self) -> Option<String> {
    // 计算 hold-back 截止字节偏移
    let cutoff = self.cumulative_output.len().saturating_sub(self.min_bytes_to_buffer);
    // 确保分割点位于 UTF-8 字符边界,防止切片 panic
    let cutoff = self.cumulative_output.floor_char_boundary(cutoff);
    (cutoff > self.output_index).then(|| {
        let chunk = self.cumulative_output[self.output_index..cutoff].to_string();
        self.output_index = cutoff;
        chunk
    })
}#[cfg(test)]
mod tests {
    // ...    #[test]
    fn next_chunk_cutoff_respects_char_boundary() {
        // Regression: next_chunk 的 hold-back 截止偏移必须对齐到 UTF-8 字符边界
        // 否则流式输出多字节字符(CJK/emoji)且设置了 hold-back(由 stop string 引起)时
        // 在 cumulative_output 中间位置切片会 panic
        let backend = Utf8Backend;
        // hold_back_bytes = 2 模拟 stop string 导致的小缓冲区
        let mut decoder = backend.create_decode_stream(&[], false, 2);
        let mut out = String::new();
        // 使用多字节字符串 " 你好 A" 的字节迭代,触发边界条件
        for byte in "你好A".bytes() {
            decoder.push_token(u32::from(byte)).unwrap();
            if let Some(chunk) = decoder.next_chunk() {
                out.push_str(&chunk);
            }
        }
        let (last_chunk, full_text) = decoder.flush(None).unwrap();
        if let Some(chunk) = last_chunk {
            out.push_str(&chunk);
        }
        assert_eq!(full_text, "你好A");
        assert_eq!(out, "你好A");
    }
}

评论区精华

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

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

风险与影响

低风险。修复仅添加一行 floor_char_boundary 调用,且已在 push_tokenflush 中验证过该模式。新增回归测试确保正确性。无需担忧回归。

直接影响:Rust 前端服务稳定性显著提升,避免因单次请求中的多字节字符导致整个进程崩溃。影响范围:所有使用 Rust 前端且配置了 stop string 的流式请求,特别是涉及 CJK、emoji 等非 ASCII 字符的场景。

进程崩溃修复

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论