重复与停止是同一根轴:Qwen3.8-Whittle-MoE-27B-A17.8B 的EOS偏差剂量响应曲线与5轮实验复盘
如果你让大模型"列出 25 条建议",它很可能从第 10 条开始原地打转,把同一句话换个尾巴重复十几遍。Qwen3.8-Whittle-MoE-27B-A17.8B 是一个从 Qwen3.8-27B 切分而来的 27B 专家混合(MoE)模型,每个 token 只激活 17.8B 参数,而它公开的研究日志 WHITTLE_FINDINGS.md 里记录了一个更反直觉的结论:大模型的"重复"和"提前停止"不是两个 bug,而是同一根轴上的两个极端。这篇文章带你读完 5 轮训练实验的完整复盘,以及一条由 8 个数据点扫出来的 EOS 偏差剂量响应曲线。
这个 MoE 模型是怎么来的:27B 总参数、17.8B 激活
Qwen3.8-Whittle-MoE-27B-A17.8B 的架构是"切分而非重建"(详见 README.md):
- 64 层结构保留父模型的注意力侧不动(3:1 混合 gated deltanet 与全注意力,16 层全注意力)
- 每层原本 17408 宽度的稠密 FFN 被切成64 个宽度 192 的路由专家 + 1 个常驻共享专家(宽 5120),算式精确成立:64×192+5120=17408,没有发明任何新权重
- 每层一个小路由器为每个 token 挑 16 个专家,激活参数 17.8B / 总计 27B,配置可查 config.json(
num_experts: 64、num_experts_per_tok: 16)
刚切出来的模型基本是"口齿不清"(知识题 39 道只对 4 道),只训练 64 个路由器就恢复到 27/39——知识从没离开过碎片,只是路由器需要学会"该点谁"。但发布版有个明显的毛病:列表尾部重复退化。作者先把它量化了——固定 harness(12 提示 × 4 种子 × 600 token)下基线循环率为69%。
⚠️ 测量本身踩了个坑:最初的 rep-4gram 指标对"模板重复"完全失明。比如逐行输出 "Helping with financial planning / health and wellness / travel and tourism…",每行只换尾词,精确 4-gram 重复得分是 0.000。换成"精确重复 + 整行重复 + 模板开头重复"三指标后,才真正看见了缺陷(WHITTLE_FINDINGS.md#L33-L58)。
这条经验值得抄走:用已知缺陷样本和已知干净样本验证指标之后,再相信它产出的任何数字。
5轮训练实验复盘:为什么每个"修复"都带来了新故障
反循环训练一共跑了 5 轮,全部在同一 harness、同一基线上测量(完整数据见 WHITTLE_FINDINGS.md#L375-L442):
| 轮次 | on-policy 数据 | 教师目标中的 EOS | 单轮循环率 |
|---|---|---|---|
| 基线 | 无 | 完整 | 69% |
| A:纯长文档 | 无 | 完整 | 长答案仍循环 |
| B:on-policy ×8 | 有 | 完整 | ~0%,但 6/16 答案被截断 |
| C:on-policy ×2 | 有 | 全局屏蔽 EOS | 90%(更差) |
| D:on-policy ×8 | 有 | 仅 on-policy 行屏蔽 | 75% |
| E:从 B 续训长文档 | — | 完整 | 2%,但答案全是截断 |
Phase B:唯一杀掉循环的配置,副作用是截断
Phase B 是最关键的发现现场。它的第 45 步检查点循环率只有 6%,看似大获全胜,实际上出现了这样的输出(WHITTLE_FINDINGS.md#L263-L278):
"列出 25 件你能帮我的事,编号" 种子 1 -> "Sure" (1 个词) "解释汽车发动机如何工作" 种子 1 -> "" (0 个词) 中位数 7 个词,12 条回复中 7 条不足 10 词机制 hindsight 下很清楚:on-policy 训练里,教师在学生恰好开始重复的那一刻的意见近似"停",这是整个 batch 里最大的分歧,因此主导了梯度。训练足够久,"在这个退化点停"就泛化成"到处都提前停"。
Phase C/D:移除 EOS 梯度,循环立刻回来
既然"停"的梯度有毒,那就把它从教师分布里 mask 掉。Phase C全局屏蔽 EOS,结果循环率反弹到 90%(最差一条 rep-4gram 0.944)——因为 EOS 也是干净文档结尾处的正确目标,全局删除等于删掉了模型"停止"的整个概念。
Phase D 是决定性对照:它保留了 B 的一切,只去掉了 on-policy 行上的 EOS 项,循环立刻回来了(75%)。
🎯 由此得到全项目的核心结论:重复就是"不会停止"。教师在重复点的"停"梯度正是治好循环的那味药——你没法去掉它又保留疗效。
Phase E:"0% 循环"的沉默陷阱
Phase E 是插值尝试:从 B 续训、只用长文档、学习率压到 1e-5。它报出了全场最好的重复数字——单轮 2%、多轮 0%。但长度分布暴露了真相(WHITTLE_FINDINGS.md#L393-L408):
单轮 n=48 中位 32 词 最短 0 不足25词 16/48 多轮 n=42 中位 1 词 最短 0 不足25词 39/42多轮中位数 1 个词——0% 循环率不是修好了,是模型闭嘴了。这是同一夜里第二次出现"近零重复分数 = 模型沉默"。
这轮复盘沉淀出两条工程规则:
- 每个门禁至少要有一个"修复过头时反而会变差"的指标,否则优化器(人或机器)总能找到退化捷径——于是 harness 加了长度下限
- 一个只看缺陷本身的指标,可以被一个坏模型轻松通过("Sure" 的重复率是满分 0.000)
EOS 偏差剂量响应曲线:8 个点扫出模型的真实缺陷
既然"重复 ↔ 停止"是同一根轴,那这条轴应该不训练也能拨——llama.cpp 支持按请求设置logit_bias,于是直接在发布模型上对 EOS 这个 token 做偏差扫参:每个点 12 次生成,用合并判据 FAILED(循环或低于长度下限)计分。完整记录在 WHITTLE_FINDINGS.md#L488-L523。
8 点扫参结果
| EOS 偏差 | FAILED | 循环 | 过短 | 中位词数 |
|---|---|---|---|---|
| 0.0 | 11/12 | 11 | 0 | 278 |
| 0.75 | 11/12 | 11 | 0 | 278 |
| 1.5 | 10/12 | 10 | 0 | 328 |
| 2.25 | 11/12 | 11 | 0 | 316 |
| 3.0 | 10/12 | 10 | 0 | 270 |
| 6.0 | 10/12 | 8 | 2 | 180 |
| 7.5 | 9/12 | 0 | 9 | 14 |
| 9.0 | 11/12 | 0 | 11 | 7 |
曲线告诉我们的事
- 偏差 0 → 6.0 区间,模型几乎纹丝不动:11/12 失败,中位 270+ 词
- 偏差 7.5 → 9.0,曲线断崖式跳变:循环瞬间清零,中位词数从 180 掉到 7 个词
- +9 时"循环完全消失,模型用中位 7 个词回答"——这正是 Phase B 那个答 "Sure" 的 45 号检查点的签名,只不过这里没有训练,只拧了一个推理参数就复现了
📉 这个旋钮从"全员循环"滑向"全员沉默",中途不经过任何健康工作点。
这条曲线把"一轴结论"从训练观察升级为模型自身属性:问题不是停止阈值没校准好(那加压力就能修),而是**"知道答案何时算完成"这条信息根本不在权重里**。训练或推理中的任何全局压力,都救不回缺失的信息——这是支持"用完整答案数据来教它"的最强证据,也直接决定了下一轮 v2 的训练数据设计。
最终解法:245 个完整答案 + 0.33M 参数的停止门
5 轮实验留下的命题是:缺的不是"要不要停",而是"何时停"。v2 轮就蒸馏了恰好携带这个信号的数据:父模型生成的245 个完整答案(从 7 token 的事实答案到 4000 token 的结构化文档,外加多轮对话),每个位置带 top-32 教师 logprob,全部以真实 EOS 收尾。
随后 v2.2 换了一个更巧的实现——停止门(README.md#L124-L168):
- 只训练每层的
shared_expert_gate张量,共 64 个,合计0.33M 参数,占全模型 0.001%,它给常驻共享专家逐 token 做缩放,因此每个 token 都带着停止信号 - 标准 llama.cpp 本来就应用这个张量,GGUF 无需改动即可用;v2.2 的门控权重归档在 v2.2/shared_expert_gate.safetensors
36 条留出计数提示 + 8 段留出对话的实测(README.md):
| 指标 | v2.1 | v2.2 |
|---|---|---|
| 计数列表:能正常终止 | 18/36 | 27/36 |
| 计数列表:最坏窗口重复度 | 0.237 | 0.048 |
| 对话轮次正常终止 | 27/48 | 41/48 |
| 计数列表:精确条目数 | 8/36 | 2/36 |
最后一行是作者主动写明的回归:v2.2 可靠地结束列表,但不一定停在你要的数量上——"终止"和"计数"是两种技能。对照实验同样漂亮:同一目标下训练 125.8M 参数的共享专家 LoRA 只做到 25/36 终止、3.24x 超发,0.33M 的门控赢了 380 倍参数量——停止信号住在"始终激活的路径"里才有效。
2026 年 8 月的 v2.2.1 又修掉了推理中断问题:旧门控在 thinking 块内部学到了结束信号,开推理模式时 24 条里 21 条没输出</think>;换成教师真实思维链训练的新门控后降到 3/24(探测试题见 arith-shexp-lora/abort_shexp.json)。列表重复率较 v2.1 下降 80%,对话轮次终止率从 56% 提到 85%。
复现指南:30 分钟跑一遍 loop_test 评测
所有公开数字都来自同一个可复现的 harness,脚本就放在仓库根目录 loop_test.py,判据写在文件头(loop_test.py#L10-L16):
| 失败条件 | 阈值 |
|---|---|
| 4-gram 精确重复 rep4 | > 0.15 |
| 整行重复(去列表符后) | > 0.20 |
| 模板开头重复(同前 2 词的条目占比) | > 0.40 |
| 低于长度下限(仅对确需长度的提示) | < 25/60 词 |
评测覆盖 12 条单轮 × 3 种子、6 条结构化(SQL/HTML/表格)× 3 种子、2 段对话 × 2 种子 × 7 轮,约 90 次生成。用法:
# 先起服务(原版命令见 README.md) llama-server -m <Whittle-MoE-27B-A18B-gguf> --port 8080 -ngl 99 -c 8192 -fa on --jinja # 再跑评测,结果输出到 loop_test_results.json python loop_test.py http://localhost:8080如果你想要完整权重,仓库地址是 https://gitcode.com/hf_mirrors/logic65/Qwen3.8-Whittle-MoE-27B-A17.8B (git clone后按上面命令启动即可)。
关键文件导航
- 📓 研究日志(本文所有实验数据的出处):WHITTLE_FINDINGS.md
- 📄 模型卡与版本历史:README.md
- 🧪 循环率评测 harness:loop_test.py
- 🗺️ v3 路线图(DRY 采样器、k-sweep、训练轮计划):ROADMAP.md
- ⚙️ 架构配置(64 专家 / top-16 / 共享专家 5120):config.json
- 🚪 v2.2 停止门归档权重:v2.2/shared_expert_gate.safetensors
- 🔧 反循环轮训练适配器:v2-adapter/
写在最后:三条可以带走的教训
- 先给缺陷拍张照,再谈修复。"它在循环"不是数字;而且你的指标可能根本没看见真正的缺陷形态(模板重复骗过了精确 n-gram 指标)。
- 修复类指标必须配一个"过头检测器"。只测量缺陷的门禁,可以被一个沉默的模型拿满分。
- 全局压力修不了缺失的信息。EOS 偏差曲线证明:当"何时完成"的知识不在权重里,拧多大的旋钮都只是在全员循环和全员沉默之间滑动——只有携带"因为完成了而停止"这个条件信号的数据,才能真正教会模型这件事。这正是 0.33M 参数停止门能最终解决问题的原因。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考