Prhub

#2760 feat(tito): support Qwen3.8 27B and Flash-Next templates

原始 PR 作者 guapisolo 合并时间 2026-09-01 04:12 文件变更 14 提交数 9 评论 6 代码增减 +316 / -3

执行摘要

Qwen3.8 系列新增双 TITO 家族名共享固定渲染模板

Qwen3.8 将 reasoning effort 序列化进 system prefix,当 Miles 供应 pretokenized input_ids 时需要一个家族专属 renderer。27B 与 Flash-Next 的 tokenizer 共享该序列化契约,但模型架构仍需不同的公共 TITO 家族名。作者明确说明本 PR 只支持注册的默认 reasoning 模式:per-request 定制需要单独的 request-argument 契约重构,否则缓存的 session prefix 可能静默改变渲染模式。

值得精读,重点看三处设计决策:(1) 两个公共枚举映射到同一个 tokenizer 类的工厂写法,如何避免复制 renderer;(2) 固定默认 reasoning 模式并显式拒绝冲突 kwargs 的保护机制,防止缓存前缀静默失配;(3) FIXME 驱动的临时行为显式化,为后续 request-argument 优先级重构留出清晰的演进路径。若团队要新增类似“共享序列化契约、不同公共名”的模型家族,可直接复用本 PR 的模式。

讨论亮点

本 PR 无实质性的 review 评论交锋。claude[bot] 前后触发两次自动 review,最终结论为“Code review found no issues”;人工评审者 Shi-Dong 评论“@claude review always”并直接 APPROVED(LGTM!)。真正的设计讨论体现在 PR body 与 commit 演进中:作者明确解释了 qwen38small / qwen4exp 两个外部名共享一个序列化实现的取舍,以及固定 xhigh 是直到 request-argument 优先级统一前的临时 workaround。

实现拆解

实现按以下 4 步展开:

  1. 新增 tokenizer 家族与枚举注册:在 miles/utils/chat_template_utils/tito_tokenizer.py 中定义 Qwen38SmallTITOTokenizer,继承 Qwen3TITOTokenizertool_call_parser = "qwen3_coder"FIXED_TEMPLATE 指向新模板并固定 preserve_thinking=True, reasoning_effort="xhigh"(带 FIXME 注释标明临时性);TITOTokenizerType 新增 QWEN38_SMALL = "qwen38small"QWEN4_EXP = "qwen4exp"get_tokenizer_class 中用 case cls.QWEN38_SMALL | cls.QWEN4_EXP 让两个公共名共享同一个实现类,避免复制 Jinja renderer。
  2. 新增固定模板 miles/utils/chat_template_utils/templates/qwen3.8_small_and_flash_next_fixed.jinja(167 行):从 HuggingFace 上 Qwen3.8-27B commit 1d4bf0f 与 Qwen3.8-Flash-Next commit de4b8e4 的 tokenizer_config.json 复制而来,仅删除 synthetic-prefix 的 no-user guard;核心包含 reasoning effort 校验与 system 前缀指令注入、vision 占位符渲染、tool_call 格式、multi_step_tool 反向扫描逻辑。
  3. 扩展验证器参数透传miles/utils/test_utils/session_verify_runner.pynamespace_to_train_args 新增 --sglang-kv-cache-dtype--sglang-mamba-full-memory-ratio 两个 opt-in 参数的序列化,默认 None 时不输出,确保 Qwen3.8 的 FP8 KV cache 与 Mamba 全内存驻留配置能从 verify runner 传到训练命令,同时不影响其他家族默认值。
  4. 测试与文档配套:新增 tests/e2e/sglang/test_session_server_multi_role/test_qwen38small.py(2 GPU H200 lane,ModelConfig 使用 Qwen3.8-27B-FP8、fp8_e4m3、mamba_full_memory_ratio=4.59、2 cycles,注册两个 mismatch_rate CI gate);fast 测试覆盖枚举 dispatch(test_tito_tokenizer.py)、固定模板解析表(test_fixed_templates.py,新增 QWEN38_SMALL / QWEN4_EXP 两行共享同一模板)、kwargs 覆盖保护参数化、pretokenized append-only 契约(test_pretokenized_chat.py,Qwen3.8 因默认保留思维被排除在跨 user turn 负向测试外)、session 预 tokenize 冒烟(test_session_pretokenized_e2e.py)、参数序列化(test_session_verify_runner.pytest_arguments.py);文档更新 docs/user-guide/agentic-rollout.md
文件 模块 状态 重要度
miles/utils/chat_template_utils/tito_tokenizer.py 模板渲染 modified 6.71
miles/utils/chat_template_utils/templates/qwen3.8_small_and_flash_next_fixed.jinja 渲染模板 added 5.52
miles/utils/test_utils/session_verify_runner.py 参数透传 modified 5.11
tests/e2e/sglang/test_session_server_multi_role/test_qwen38small.py E2E 测试 added 6.04
tests/fast/utils/chat_template_utils/test_fixed_templates.py 模板测试 modified 5.51
tests/fast/utils/test_utils/test_session_verify_runner.py 参数测试 modified 5.88
tests/fast/utils/chat_template_utils/test_pretokenized_chat.py 渲染测试 modified 4.92
tests/fast/utils/test_arguments.py 参数测试 modified 4.92
docs/user-guide/agentic-rollout.md 用户文档 modified 1.52

关键符号

Qwen38SmallTITOTokenizer TITOTokenizerType.get_tokenizer_class namespace_to_train_args test_qwen38small test_qwen38_families_resolve_default_template test_registered_kwargs_cannot_be_overridden test_namespace_to_train_args_omits_model_mamba_cache_config_by_default test_namespace_to_train_args_emits_model_mamba_cache_config

关键源码片段

miles/utils/chat_template_utils/tito_tokenizer.py core-logic

核心源码:新增 Qwen38SmallTITOTokenizer 类,并在 TITOTokenizerType 枚举与 get_tokenizer_class 工厂中注册 qwen38small / qwen4exp 两个共享实现的家族名。

# Qwen3.8 家族:两个公共名共享同一个序列化实现。
# Qwen3.8 会把 reasoning effort 写进 system prefix,因此 Miles 供应
# pretokenized input_ids 时必须由家族专属 renderer 处理,否则前后缀会失配。
class Qwen38SmallTITOTokenizer(Qwen3TITOTokenizer):
    """Qwen3.8 reasoning-effort 模板 + Qwen3 token 边界。"""
​
    tool_call_parser = "qwen3_coder"
​
    FIXED_TEMPLATE = FixedTemplate(
        template="qwen3.8_small_and_flash_next_fixed.jinja",
        # FIXME: 在 request-argument 优先级统一之前,临时固定 reasoning effort 为
        # xhigh,避免缓存的 session prefix 因 per-request 参数而静默改变渲染模式。
        extra_kwargs={"preserve_thinking": True, "reasoning_effort": "xhigh"},
        allowed_append_roles=frozenset({"tool", "user", "assistant"}),
    )
​
​
class TITOTokenizerType(StrEnum):
    # ... 省略其他家族枚举 ...
    # qwen38small 对应 Qwen3.8-27B,qwen4exp 对应 Qwen3.8-Flash-Next。
    # 二者 tokenizer 序列化契约相同,但公共名分开,给模型架构独立演进留空间。
    QWEN38_SMALL = "qwen38small"
    QWEN4_EXP = "qwen4exp"
​
    @classmethod
    def get_tokenizer_class(cls, t: TITOTokenizerType) -> type[TITOTokenizer]:
        """Resolve the concrete ``TITOTokenizer`` subclass for *t*."""
        match t:
            # ... 省略其他 case ...
            case cls.QWEN38_SMALL | cls.QWEN4_EXP:
                # or-pattern:两个公共名指向同一个实现类,避免复制 Jinja renderer。
                return Qwen38SmallTITOTokenizer
            # ... 省略其他 case ...
            case _:
                raise ValueError(f"Unknown TITOTokenizerType: {t!r}")
miles/utils/chat_template_utils/templates/qwen3.8_small_and_flash_next_fixed.jinja core-logic

新增 167 行固定 Jinja 模板,是 Qwen3.8 两个家族共享的渲染核心:包含 reasoning effort 校验与 system 前缀指令注入、vision 占位符、tool_call 渲染与 multi_step_tool 逻辑。

{# reasoning effort 解析与指令注入:这是 Qwen3.8 与 Qwen3 系列的关键差异,
   effort 被序列化进 system 前缀,本地渲染必须与远端 SGLang 默认一致,
   否则 pretokenized 前缀会与远端生成失配。 #}
{%- set reasoning_instructions = '' %}
{%- if enable_thinking is undefined or enable_thinking is true %}
    {%- set resolved_reasoning_effort = reasoning_effort|default('xhigh') %}
    {%- if resolved_reasoning_effort not in ('xhigh', 'medium', 'low') %}
        {{- raise_exception('Unexpected reasoning effort ' ~ reasoning_effort ~
            '. Supported types are xhigh (default), medium, and low.') }}
    {%- endif %}
    {# xhigh 与 low 注入不同的思考指令;medium 走默认空指令。 #}
    {%- if resolved_reasoning_effort == 'xhigh' %}
        {%- set reasoning_instructions = 'Reasoning effort is set to xhigh. Please think
            carefully through the task, validate key assumptions, consider plausible
            alternatives, and prioritize correctness, consistency, and clarity in the
            final answer.' %}
    {%- elif resolved_reasoning_effort == 'low' %}
        {%- set reasoning_instructions = 'Reasoning effort is set to low. Keep your
            thinking brief and focused, moving directly to the conclusion without
            unnecessary elaboration.' %}
    {%- endif %}
{%- endif %}{# 有工具时 system 前缀包含指令;否则把指令单独拼进 system 消息。 #}
{%- if tools and tools is iterable and tools is not mapping %}
    {{- '<|im_start|>system\n' }}
    {%- if reasoning_instructions %}
        {{- reasoning_instructions + '\n\n' }}
    {%- endif %}
    {{- "# Tools\n\nYou have access to the following functions:\n\n<tools>" }}
    {%- for tool in tools %}
        {{- "\n" }}
        {{- tool | tojson }}
    {%- endfor %}
    {{- "\n</tools>" }}
{%- else %}
    {%- if messages[0].role == 'system' %}
        {%- set content = render_content(messages[0].content, false, true)|trim %}
        {%- if content %}
            {{- '<|im_start|>system\n' + (reasoning_instructions + '\n\n'
                if reasoning_instructions else '') + content + '<|im_end|>\n' }}
        {%- elif reasoning_instructions %}
            {{- '<|im_start|>system\n' + reasoning_instructions + '<|im_end|>\n' }}
        {%- endif %}
    {%- elif reasoning_instructions %}
        {{- '<|im_start|>system\n' + reasoning_instructions + '<|im_end|>\n' }}
    {%- endif %}
{%- endif %}

评论区精华

claude[bot] 自动代码审查 other

claude[bot] 两次回复提示仓库配置了手动 review 流程(@claude review),随后一次审查输出“Code review found no issues”,未发现高置信度问题。

结论:自动审查未发现问题,PR 通过。 · 已解决

Shi-Dong 人工批准 other

Shi-Dong 评论“@claude review always”订阅持续审查,随后直接 APPROVED 并给出“LGTM!”。

结论:人工评审通过,无未解决疑虑。 · 已解决

风险与影响

  1. reasoning effort 被硬编码为 xhighQwen38SmallTITOTokenizer.FIXED_TEMPLATE 中的 FIXME 已标注此行为临时性;在当前契约下,用户无法在 session 内请求 low / medium 推理档位,且 xhigh 会导致更长的 thinking token 与更高的推理成本。若未来取消固定而 request-argument 优先级未统一,缓存的 session prefix 会静默改变渲染模式,破坏不变量。
  2. Flash-Next 仅 tokenizer 层落地:PR body 明确 Qwen3.8-Flash-Next 的完整执行仍需独立 sglang-miles-qwen38next runtime/image,本 PR 的 qwen4exp 单独使用可能无法端到端跑通。
  3. namespace_to_train_args 直接访问新属性ns.sglang_kv_cache_dtype / ns.sglang_mamba_full_memory_ratio 若调用方 Namespace 未提供会抛 AttributeError;测试 _build_args 已显式补 None 默认,但其他调用路径需同步跟进。
  4. 模板差异:从 HF 复制时删除了 synthetic-prefix no-user guard,若存在空 system 前缀场景,本地渲染与 HF 原生模板可能存在细微行为差异,依赖 fast 测试的 append-only 契约兜底。
  5. E2E 时长:test_qwen38small 曾在 4 GPU lane 耗时 1h32m,后续迁到 2 GPU lane 缩短到 10-22 分钟,但仍属较重的 CI 门禁。

对用户:新增 --tito-model qwen38small(Qwen3.8-27B)与 --tito-model qwen4exp(Qwen3.8-Flash-Next)两个可用家族名,均默认 preserve_thinking=truereasoning_effort=xhigh;现有 qwen3 / qwen35 / qwen36 / qwennext 家族行为完全不变,属纯增量。对系统:固定模板渲染链路、枚举工厂 dispatch、验证器参数透传三处扩展,全部 opt-in。对团队:确立了一个“多个公共家族名共享同一实现类”的模板复用模式,并为后续 Qwen3.8 系列其他变体(预留 qwen38 名称空间)与 request-argument 契约重构铺路。

固定 reasoning effort 为临时行为 Flash-Next 依赖独立 runtime 参数透传新属性依赖调用方提供 模板与 HF 源存在删改差异 E2E 门禁耗时较长

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论