Prhub

#56378 [Rust Frontend] Support `generation` blocks in HF chat template

原始 PR 作者 BugenZhao 合并时间 2026-09-12 12:56 文件变更 3 提交数 2 评论 2 代码增减 +207 / -0

执行摘要

Rust 前端支持 HF 聊天模板的 generation 块

Transformers 与 TRL 在 chat template 中使用 {% generation %}...{% endgeneration %} 标记 assistant 输出,分别服务于 assistant-token masks 与 TRL 的 assistant-only 训练 loss,公共模板如 SmolLM3-3B checkpoint 与 TRL Qwen3 training 均依赖该语法;而 Rust 前端的 MiniJinja 渲染器会直接报 unknown statement generation,导致含该标签的模板无法编译。PR body 明确说明这是通过 Open-PR 搜索 generation minijinja、generation blocks、endgeneration 发现的兼容性缺口,目标是在不破坏渲染语义的前提下让这些模板在 Rust 链路中可用。

值得精读。这是一个把 Jinja2 方言兼容层做得干净利落的案例:用 lexer span 做局部替换而非正则/字面量替换,用 call 块保留局部作用域,重写前校验原生 call 配对以防止歧义。相比 Mistral.rs、TGI 的删除式方案,本实现语义保持更完整,适合作为 Rust 前端模板兼容性处理的参考实现;同时其测试设计(非法语法矩阵 + 渲染语义矩阵)也值得借鉴。

讨论亮点

该 PR 没有实质性的技术 review 评论(review_comments 为 0):claude[bot] 指出这是 fork PR、自动 review 被禁用,维护者 njhill 直接 approve 并运行 /ci run 触发 Buildkite CI #88468。技术权衡集中体现在 PR body 的方案对比中:Mistral.rs 用正则直接删除标签、TGI 与 Dynamo renderer 用字面量替换,作者指出这些方案会丢失空白控制与块内作用域语义,因此本实现选择 scoped call block 重写,既保留渲染语义(包括 whitespace trimming 与块内赋值),又额外承担了语法配对校验,属于比同类前端更保守、语义更完整的做法。

实现拆解

  1. 新增重写模块 generation.rs:核心函数 rewrite_generation_blocks 调用 minijinja::machinery::tokenize 对模板源码做词法分析,遍历相邻 token 对 (Token::BlockStart, Token::Ident(name)) 识别 generation / endgeneration 关键字;同时用 CallBlockKind 栈记录原生 call / endcall 与 generation 块的配对关系,在重写前完成语法校验(endgeneration 写成 endcall、多余的 endgeneration、未闭合块都会返回 SyntaxError),避免重写后两类块共用 endcall 而丢失区分信息。
  2. span 级替换策略:只替换关键字的字节区间(span.start_offset..span.end_offset),保留 {%、%} 定界符及其空白控制符(- / +),字符串字面量、注释、raw 块因 lexer 只产出 Ident token 而天然不受影响;替换按 rev() 从后往前执行,避免前一次 replace_range 改变后续 span 偏移。render_generation 从 kwargs 取出 caller 并调用,行为等价于 Transformers AssistantTracker 的局部作用域。
  3. 编译路径接线(template.rs):CompiledChatTemplate::new 在 detect_chat_template_content_format 与 build_environment 之前先调用 rewrite_generation_blocks,保证 content-format 检测看到的是可编译源码;同时在 build_environment 中通过 env.add_function("__hf_generation", render_generation) 注册回调,使默认模板与 per-request override 两条编译路径都生效。
  4. 模块声明与集成测试(mod.rs):声明 mod generation;新增端到端测试 generation_blocks_allow_content_format_detection_and_request_overrides,验证同一模板作为默认模板与 request.chat_options.chat_template override 两种方式都能渲染出 hello。
  5. 测试与验证配套:generation.rs 内两个单元测试——非法语法用例(未闭合、多余参数、失配 end tag、交叉嵌套)全部被拒;7 个渲染语义用例(空白裁剪、换行保留、字面量、raw、注释、块内赋值作用域、嵌套 call)输出与期望一致。HF renderer 套件 79 项通过;对 SmolLM3-3B 与 TRL Qwen3 训练模板的 8 组渲染探针与 Transformers 输出一致,覆盖 thinking 开关与 generation prompt 开关。
文件 模块 状态 重要度
rust/src/chat/src/renderer/hf/generation.rs 模板渲染 added 8.57
rust/src/chat/src/renderer/hf/template.rs 模板渲染 modified 4.59
rust/src/chat/src/renderer/hf/mod.rs 模板渲染 modified 5.91

关键符号

rewrite_generation_blocks render_generation generation_blocks_allow_content_format_detection_and_request_overrides

关键源码片段

rust/src/chat/src/renderer/hf/template.rs dependency-wiring

编译路径接线点:在 CompiledChatTemplate::new 编译前调用 rewrite_generation_blocks,并在 build_environment 注册 __hf_generation 回调,使默认模板与 per-request override 两条路径同时生效。

/// 构建预配置环境:注册 `__hf_generation` 回调,重写后的模板才能调用它
fn build_environment(template: String) -> Result<Environment<'static>> {
    let mut env = Environment::new();
​
    env.set_trim_blocks(true);
    env.set_lstrip_blocks(true);
​
    env.add_template_owned("chat".to_owned(), template)?;
​
    env.set_unknown_method_callback(minijinja_contrib::pycompat::unknown_method_callback);
    env.add_filter("tojson", hf_tojson_filter);
    // 与 generation.rs 中的重写配套:`call __hf_generation()` 由此得到实现
    env.add_function("__hf_generation", render_generation);
​
    Ok(env)
}
​
impl CompiledChatTemplate {
    /// 编译模板:先重写 generation 块,再做 content-format 检测与编译
    pub fn new(template: String, content_format: ChatTemplateContentFormatOption) -> Result<Self> {
        // 重写发生在 content-format 检测之前,
        // 保证 Auto 检测与 per-request override 编译看到的是同一份可编译源码
        let template = rewrite_generation_blocks(template)?;
        let content_format = match content_format {
            ChatTemplateContentFormatOption::Auto => detect_chat_template_content_format(&template),
            ChatTemplateContentFormatOption::String => ChatTemplateContentFormat::String,
            ChatTemplateContentFormatOption::OpenAi => ChatTemplateContentFormat::OpenAi,
        };
        let env = build_environment(template)?;
        Ok(Self {
            env,
            content_format,
        })
    }
}

评论区精华

fork PR 自动 review 被禁用 other

claude[bot] 评论指出该 PR 来自 fork,自动 review 被禁用,需要维护者手动触发 `@claude review` 或直接人工评审;随后无任何行内技术评论。

结论:维护者 njhill 直接 approve,并通过 `/ci run` 触发 Buildkite CI #88468 后合入;技术权衡说明主要来自 PR body 中对 Mistral.rs、TGI、Dynamo 方案的对比。 · 已解决

风险与影响

  1. 语法严格化风险:重写逻辑在编译期对模板做严格配对校验,{% generation %} 缺 endgeneration、与原生 call 块交叉嵌套都会直接编译失败。这类模板在 MiniJinja 下原本就无法编译(unknown statement generation),所以实际效果是从不可用变为可用或明确报错,不会让原本能用的模板变坏;但实现依赖 MiniJinja tokenize 的当前词法行为与 ignore_unknown=false 语义,未来 MiniJinja 升级若改变词法规则需要回归。
  2. 语义等价性风险:generation 块在 Transformers 中承载 assistant-token mask 等训练语义,本实现只保证推理渲染文本与 Transformers 一致(PR body 明确说明 GPU 模型生成验证不在测试结果内),若未来要在 Rust 链路支持训练或掩码能力需要另行处理。
  3. 覆盖范围风险:改动只作用于 Rust 前端(rust/src/chat),Python 端不受影响;Rust 渲染器与 Python/Transformers 渲染器在复杂模板并存场景下的行为一致性仍需持续维护。

用户侧:加载含 {% generation %} 块的公共模板(SmolLM3-3B、TRL Qwen3 training 等)从编译失败变为可正常渲染,直接解锁 Dynamo 等 Rust 前端链路对这类模型的 chat template 支持,对已有可用模板无侵入。系统侧:重写仅在模板编译期执行一次,无运行时开销,CompiledChatTemplate 缓存语义不变。团队侧:为 Rust 渲染器与 Transformers 的 Jinja 方言对齐提供了可复用的兼容层范式,同类标签(如未来其他 HF 专有语句)可照此模式扩展。

词法级模板重写 缺少 GPU 端到端验证 仅覆盖 Rust 前端

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论