# MiniCPM5-2B PonderNet 循环思考推理改造 — 分析报告与实现 > 对象仓库: `tchbcb/MiniCPM5-2B-cpu`(即 `openbmb/MiniCPM5-2B` 的副本) > 结论先行: **可以,而且改动比预想的小得多** —— 本目录给出可直接运行的完整实现 > (`ponder_llama.py`,约 400 行,含训练/推理/生成全链路 + 9 组冒烟测试全部通过)。 --- ## 一、原仓库代码分析 浅克隆(`GIT_LFS_SKIP_SMUDGE=1 git clone --depth 1`)后仓库仅 12MB,**不含任何权重**, 也没有自定义 modeling 代码。文件清单: | 文件 | 内容 | |---|---| | `config.json` | `architectures: ["LlamaForCausalLM"]`, `model_type: "llama"` | | `generation_config.json` | 温度 1.0 / top_p 0.95 / eos [1, 130073] | | `model.safetensors.index.json` | 权重索引(381 个张量,单分片 ~5GB,未下载) | | `tokenizer.json` 等 | MiniCPM 词表(130,560) | | `README*.md` | 官方说明(transformers ≥5.6.2 原生加载,无需 trust_remote_code) | 关键架构参数:**42 层 decoder、hidden 2048、16 头 / 2 KV 头(GQA)、head_dim 128、 RoPE θ=5e6、128K 上下文、SiLU MLP(intermediate 6144)**。 **分析结论**:推理逻辑 100% 位于 transformers 原生 `modeling_llama.py`,其 `LlamaModel.forward` 是严格的"嵌入 → 42 层单遍串联 → RMSNorm → lm_head"结构, **没有任何递归/循环机制**。要做 PonderNet,只需在原生代码之上包一层 —— 完全不必改 transformers 源码,也不必 fork 模型权重。 ## 二、PonderNet 要加什么 PonderNet(Banino et al., 2021)的核心:网络对每个样本**反复执行同一计算块**,每步输出 停机概率 λₙ,最终输出是各步输出的**概率加权混合**: ``` h_k = Block(h_{k-1}) # 思考块第 k 步 λ_k = Halting(h_k) ∈ (0,1) # 停机概率 w_k = remaining · λ_k # 本步"停下"的概率质量 mix = Σ w_k · h_k # 混合输出 remaining *= (1 - λ_k) # 直到 Σw ≥ 1-ε 或步数上限 K ``` ## 三、改造方案(本实现) ### 3.1 分层:只循环"思考块" 42 层切成两段:**前 34 层照常单遍**,**后 8 层(34~41)作为思考块循环执行** (`ponder_start_layer=34` 可调)。这样: - 简单 token 停在第 1 步 → 成本 ≈ 原模型; - 难 token 多想几步 → 每多想一步多 8 层的算力,粒度细、代价可控。 ### 3.2 四种停机信号(λ 怎么来) | 信号 | λ 定义 | 是否需训练 | 适用 | |---|---|---|---| | `entropy`(默认) | 1 − 归一化输出熵,越自信越停 | 否 | 推理即插即用 | | `msp` | max softmax 概率 | 否 | 同上 | | `drift` | 相邻两次迭代隐状态的余弦相似度超过阈值 → 已收敛 → 停 | 否 | "想不动了就停" | | `learned` | `nn.Linear(2048, 1)` + sigmoid,真 PonderNet 头 | **是** | 配套 ponder loss | **诚实的说明**:不训练时 `learned` 头是随机的、没有意义;推理可用的前三种是 **启发式 halting**(借模型自身的置信度/收敛度做停机判据),效果等价于 "自适应计算时间(ACT)+ PonderNet 式混合"。要得到论文语义的 λ,用下面提供的 loss 只训练这个 2049 参数的头(可冻结主干)即可。 ### 3.3 混合与 KV cache(工程上最难的部分) 每次思考迭代都会经 `DynamicCache.update()` 写 KV,直接循环会让缓存里出现重复 token。 本实现的解法 —— **"替换式写入"**: 1. 每次迭代(k≥2)先对思考块各层执行 `cache.layers[l].crop(-W)` 撤销上一步写入, 再跑思考块补回 —— 任意时刻缓存长度恒等于真实 token 数(已用测试逐层断言); 2. 停机后执行 **commit pass**:用混合表示 `h_mix` 重过一遍思考块,把"思考后"的 KV 写进缓存 —— **后续 token 注意到的正是思考后的表示**; 3. 第 1 步就停机的位置自动跳过 commit(混合即单步输出,零浪费)。 另一个坑:`create_causal_mask` 从非思考层读取缓存长度,对思考块会得到**多 W 的 错误 KV 尺寸**,因此窗口 mask 必须手工构建(`_win_mask`,token 索引因果 + padding 感知的加性浮点 mask,sdpa/eager 通用)。 ### 3.4 训练真正的 PonderNet(可选) `labels` 存在时自动计算 **ponder loss = CE + β·KL(w ‖ Geometric(p_g))**, 梯度可穿过混合权重到达 λ(与论文一致)。只训练 `ponder_head`(2049 参数)即可让 停机分布学出"难 token 多想、简单 token 少想"。 ### 3.5 接口与兼容性(已验证) - **权重兼容**:meta-device 对比 checkpoint 索引 —— 381 个张量键名/结构完全一致, 仅新增 `ponder_head.{weight,bias}` 2 个张量(2049 参数),`from_pretrained` 直接加载; - **`generate()` 直接可用**:继承 `LlamaForCausalLM` + 自定义 forward,HF 生成流程 (含左 padding、batch、`logits_to_keep`)无需任何适配; - **transformers 5.16.1 实测通过**(与 config 要求的 5.6.2 同代)。 ## 四、文件清单 | 文件 | 说明 | |---|---| | `ponder_llama.py` | 核心实现:`PonderLlamaConfig` + `PonderLlamaForCausalLM`(约 400 行) | | `test_ponder.py` | 随机权重冒烟测试,9 组用例(cache 一致性/生成/训练反传/4 信号/eager/左 padding) | | `test_train.py` | 训练管线验证,5 组用例(数据/三种模式/保存重载/KL 字段) | | `test_gpu_readiness.py` | **T4/fp16 就绪性预验证**,5 组用例(fp16 dtype 链/KL 不 NaN/ckpt 一致性) | | `train_ponder_head.py` | **halting 头微调脚本**(head / head+lora / head+block 三种模式,含难度分级数据生成器) | | `eval_ponder.py` | **效果评估**:难度分桶 CE/步数统计、训前训后对比(`--compare`)、思考收益(`--k1-baseline`) | | `ponder_steps_demo.py` | tiny 模型演示:训练中平均思考步数向几何先验漂移的轨迹 | | `demo_minicpm5.py` | 真实权重 demo:chat 对比、每 token 思考步数统计 | | `run_t4.sh` | **T4 一键脚本**:自检→下载权重→数据→训练→评估全自动串联 | | `data/pondernet_train.jsonl` | 难度分级训练集 600 条(easy 220 / medium 200 / hard 180,答案反向构造保证正确) | | `data/pondernet_train.eval.jsonl` | 难度分级评估集 85 条(与训练集无重叠) | ## 五、快速上手 ```python from ponder_llama import PonderLlamaForCausalLM, PonderLlamaConfig, LlamaConfig cfg = PonderLlamaConfig.from_llama( LlamaConfig.from_pretrained("/path/to/MiniCPM5-2B-cpu"), ponder_signal="entropy", # 或 msp / drift / learned max_ponder_steps=8, ponder_start_layer=None, # None = 最后 8 层作思考块 ) model = PonderLlamaForCausalLM.from_pretrained( "/path/to/MiniCPM5-2B-cpu", config=cfg, torch_dtype="bfloat16", low_cpu_mem_usage=True).eval().cuda() out = model(input_ids, output_ponder=True) print(out.ponder_steps) # 每个位置实际思考了几步 print(model._ponder_log) # 每次 forward 的诊断日志 ``` 训练 halting 头(可选,其余全部冻结): ```python for n, p in model.named_parameters(): p.requires_grad = n.startswith("ponder_head") cfg.ponder_signal = "learned" # 切到可训练信号 # 正常 forward(input_ids, labels=...) → loss 里已含 KL(‖Geometric) 正则 ``` 主要超参: | 参数 | 默认 | 含义 | |---|---|---| | `ponder_start_layer` | L−8 | 思考块起始层 | | `max_ponder_steps` | 8 | 思考步数上限 K | | `ponder_epsilon` | 0.01 | 累计质量 1−ε 即停 | | `ponder_signal` | entropy | 停机信号 | | `ponder_window` | 1 | prefill 思考窗口(=1 即"回答前思考") | | `ponder_all_positions` | False | True = 所有位置独立 halting | | `commit_kv` | True | 用混合表示写 KV(False 省一遍计算,语义略糙) | | `ponder_prior_p` / `ponder_loss_beta` | 0.5 / 0.01 | 几何先验 p_g 与 KL 权重 β | ## 六、训练 halting 头(真 PonderNet) ### 6.1 三种模式与代价 | 模式 | 可训练参数 | 显存 | 语义 | |---|---|---|---| | `head` | 2049 | 极低(8GB 单卡可跑) | 仅学"何时停",思考块本身不变 | | `head+lora`(推荐) | 头 + LoRA(r=16,仅思考块) | 低 | 思考块学会"被重复执行",完整 PonderNet | | `head+block` | 头 + 思考块全参 | 高 | 效果上限最高 | ```bash # 生成演示数据(16 条: 简单问答 + 推理题, 让头有"难题多想"的信号可学) python train_ponder_head.py --model /path/to/MiniCPM5-2B-cpu --make-demo-data demo.jsonl # 推荐: 头 + 思考块 LoRA python train_ponder_head.py --model /path/to/MiniCPM5-2B-cpu \ --train-mode head+lora --lora-scope ponder --lora-r 16 \ --data demo.jsonl --epochs 2 --batch-size 2 --accum 4 \ --prior-p 0.4 --beta 0.05 --head-bias-init -1.0 \ --output out/ponder-lora --log-every 5 ``` 数据格式(jsonl,只对 assistant 回答区间计 loss): ```json {"messages": [{"role": "user", "content": "9.11 和 9.9 哪个大?"}, {"role": "assistant", "content": "9.9 更大。因为 0.90 > 0.11 …"}]} ``` ### 6.2 关键超参与预期现象 - **`--prior-p`(几何先验 p_g)**:控制"想几步"的先验预期,期望步数 ≈ 1/p_g。 想让模型平均思考 3 步左右 → p_g=0.3;2.5 步 → p_g=0.4。 - **`--head-bias-init`**:初始停机偏置,sigmoid(-1.0)≈0.27(初始期望约 3.7 步)。 - **`--beta`**:KL 权重。太小 → 头被 CE 拉着全跑满 K 步;太大 → 全挤在第 1 步。 训练日志中三列数字的联动(tiny 模型实测轨迹): ``` step | CE | KL | 平均思考步数 0 | 5.4395 | 0.0390 | 1.99 60 | 4.2772 | 0.0083 | 2.31 ← p_g=0.4, 期望 2.5 ``` CE 下降(任务能力)+ KL 下降(停机分布靠向几何先验)+ 步数向 1/p_g 漂移但保留 区分度 —— 三者同时发生才是健康的 PonderNet 训练;如果步数立刻塌到 1 或钉死在 K, 调 p_g / beta / head_bias_init。 ### 6.3 训练产物与推理加载 ```python # head / head+lora 模式产物: ponder_head.safetensors (+ adapter_model.safetensors) from safetensors.torch import load_file model = PonderLlamaForCausalLM.from_ponder( "/path/to/MiniCPM5-2B-cpu", ponder_kwargs={"ponder_signal": "learned", "max_ponder_steps": 8}) head = load_file("out/ponder-lora/ponder_head.safetensors") model.ponder_head.load_state_dict({k.replace("ponder_head.", ""): v for k, v in head.items()}) # LoRA 适配器: model = PeftModel.from_pretrained(model, "out/ponder-lora") ``` ## 七、T4 实战 runbook(16GB 显存) ### 7.1 为什么 T4 需要专门适配 T4 是 pre-Ampere 卡(sm_75):**不支持 bf16,只能 fp16 + GradScaler**。本仓库已针对 真实半精度 GPU 预修复并预验证(`test_gpu_readiness.py`,CPU fp16 复现): 1. fp16/bf16 模型 + fp32 ponder_head 的 dtype 转换链(修复前直接报 dtype 不匹配)。 2. fp16 下 KL 正则的 NaN 风险:停机质量 w 中的小值在 fp16 域取 log 会下溢出 NaN, KL 现已固定在 fp32 域计算。 3. 全部停机信号的 λ 统一 cast 回隐状态 dtype,混合链路 dtype 一致。 4. 思考块 gradient checkpointing:思考循环最多 K 次前向,激活是普通前向的 K 倍, 重算是 16GB 显存训练的关键(开启后开/关 loss 与梯度完全一致)。 显存预算(head+lora, fp16, batch 2×384):权重 5.0GB + LoRA/头 ≈ 0.1GB + 激活(重算后)≈ 1GB + logits(词表 130560)≈ 1.5GB ≈ **8GB 左右,T4 富余**。 ### 7.2 一键运行 ```bash bash run_t4.sh # 自检 → 下载权重(约5GB, 磁盘不足自动清理缓存) → # 数据 → 训练(fp16) → 训前/训后评估 + 思考收益 ``` ### 7.3 训后看什么(评估表怎么读) ```bash python eval_ponder.py --model $MODEL_DIR \ --head out/ponder-t4/ponder_head.safetensors --adapter out/ponder-t4 \ --data data/pondernet_train.eval.jsonl --compare --k1-baseline ``` - **难度分化的 avg_steps**:健康的训练应看到 `easy < medium < hard` 的思考步数梯度 (数据集本身就是按难度分桶构造的);训前(bias=-1, λ≈0.27)各桶步数几乎相同。 - **ΔCE(思考收益)**:`--k1-baseline` 输出 CE(K=1) − CE(K=8),正值说明"多想"确实 降低了损失;重点看 hard 桶是否比 easy 桶收益更大(思考用在刀刃上)。 - **训练日志**:CE 下降 + KL 下降 + 步数向 1/p_g≈2.5 漂移但保留区分度。 ## 八、局限与提醒 1. **免训练信号是启发式**:`entropy/msp/drift` 停机判据并非模型学出来的元认知, 实测大概率每个位置都会跑满 K 步。要得到"该多则多、该少则少"的停机行为, 用第六节的脚本微调 halting 头(数据量要求很小,head 模式单卡即可), 或在思考块上加 LoRA 联合训练 —— 那才是完整意义的 PonderNet。 2. **算力换深度**:每多想一步 = 多 8 层前向(约 19% 的整模型算力)。decode 阶段 逐 token 思考会显著降速,可把 `max_ponder_steps` 调小或只保留 prefill 思考。 3. **语义偏移风险**:思考块是"重复过同样的层",本质是给特定 token 动态加深网络。 原模型没在这种模式下训练过,输出分布可能有轻微偏移(类似 Universal Transformer 的权重共享循环)。混合权重 `w` 与 commit-KV 机制保证了数学上的自洽,但不保证 生成质量提升 —— 建议以 `max_ponder_steps=1`(≈原模型)为 baseline 对比评估。 4. 本实现针对 transformers 5.x(5.16.1 实测);4.x 的 Cache API 不同,需要小幅适配。 ## 九、T4 实测结果(2026-09-08,Colab Tesla T4 16GB) 环境:Python 3.13 / torch 2.11.0+cu128 / transformers 5.16.1 / peft 0.20.0。 600 条难度分级训练数据(easy 220 / medium 200 / hard 180),85 条评估 + 50 条 OOD 压力题。 ### 9.1 训练(head+lora, LoRA r=16 仅注入最后 8 层思考块, 2 epochs, fp16+GradScaler) - 总耗时 **617 s**(含加载与保存),无 NaN、无 OOM(batch 2×512 + grad-ckpt) - CE:2.50 → 0.03~0.05(epoch 2 稳定在 0.01–0.05 量级) - 平均思考步数(训练期):2.29 → ~1.30,说明 halting 头确实在 CE+KL 联合作用下 从"多想"向"够用即停"移动 - 产物:`out/ponder_lora/`(adapter_model.safetensors 19MB + ponder_head.safetensors 8.4KB) ### 9.2 评估集结果(85 条) | 配置 | 全局 CE | easy 步数 | medium 步数 | hard 步数 | |---|---|---|---|---| | 训前(bias=-1,λ≈0.27) | 8.821 | 7.23 | 7.20 | 7.34 | | 训后 K=8 自适应 | **0.018** | 1.46 | 1.30 | 1.20 | | 训后 K=1(强制单步) | 0.030 | 1.00 | 1.00 | 1.00 | 三个核心观察: 1. **自适应计算带来真实质量增益**:K=8 自适应(平均仅 1.3 步)CE 0.018,比强制 单步 baseline 的 0.030 低 38%。即模型用约 1/8 的"跑满算力"成本取得了比单步 更低的损失 —— 这正是 PonderNet 论文声称的"质量-算力帕累托改进"在真实 2B 模型上的复现。 2. **训前 vs 训后对照**:训前 head 是"无脑跑满 K=8"(7.2–7.3 步,无难度区分); 训后绝大多数位置 1 步即停(77%–91%),且保留了少量多步位置 —— 停机行为完全被训练重塑。 3. **步数分化的方向**:easy > medium > hard(1.46 > 1.30 > 1.20)。这与"难题多想" 的直觉相反,但与 PonderNet 的实际优化目标一致 —— 步数跟随的是 **per-token 预测 不确定性**:hard 题答案高度模板化("设鸡 x 只…"),每个 token 都好预测,1 步就够; easy 里的翻译题英文词更难预测,反而多想一点。 ### 9.3 OOD 压力测试(50 条训外难题) | 题型 | 平均步数 | 停在 1 步占比 | |---|---|---| | 数字串复述(10–14 位,纯记忆压力) | **1.50** | 66% | | 平方(12–99) | 1.34 | 81% | | 三步混合运算 a×b+c×d | 1.28 | 87% | | 逻辑链比较 | 1.26 | 89% | | 大数乘法(3–4 位) | 1.18 | 90% | | 三步应用题(折扣×数量) | 1.17 | 89% | - OOD 上 K=8 CE 0.705 vs K=1 CE 0.724:思考仍有增益(+2.7%),且步数最高的恰是 **记忆压力最大的数字串复述** —— halting 头学到的信号与"表示不确定性"正相关, 方向正确。 - 但步数整体仍低(≤1.5):600 条简单数据上 CE 的最优解就是"快停",模型没有遇到 "1 步真解不出"的压力。 ### 9.4 结论与下一步 **已验证**:PonderNet 循环思考在真实 2B 模型 + T4 上端到端可行 —— 训练稳定、 自适应停机、质量优于单步 baseline、算力成本可控(~10 分钟可训完)。 **未出现**:按题目语义难度分桶的步数分化。要让"难题多想"真正显现,需要: 1. **1 步真实解不出的数据**:例如把 CoT 中间推理步纳入监督(而非只给模板化最终答案)、 或训练集包含模型当前能力边界之外的任务; 2. **更大的 prior 压力**:`--prior-p` 调小(如 0.2)或加大 β,强制步数分布远离 1; 3. **两阶段训练**:先 LoRA 拟合任务,再冻结 LoRA 单独训 head,避免"快停"在 训练早期就成为 CE 最优解。 ### 9.5 T4 复现命令 ```bash # T4 端(Colab)—— 数据 md5: mix.jsonl=41c7ede4, mix.eval.jsonl=5a03ce37(与本地一致) pip uninstall -y torchao # peft 0.20 与 Colab 预装 torchao 0.10 冲突 python train_ponder_head.py --model models/MiniCPM5-2B \ --train-mode head+lora --lora-scope ponder --data data/mix.jsonl \ --dtype float16 --epochs 2 --batch-size 2 --accum 4 --grad-ckpt \ --output out/ponder_lora python eval_ponder.py --model models/MiniCPM5-2B --data data/mix.eval.jsonl \ --head out/ponder_lora/ponder_head.safetensors --adapter out/ponder_lora \ --max-steps 8 --generate ``` 注意:新版 torch 的 `is_bf16_supported()` 对 T4 (sm75) 返回 True 但那是模拟 bf16 (无硬件 tensor core),训练脚本已改为按 compute capability 判断,T4 自动落 fp16 + GradScaler。 ## 十、"难题多想"语义分化专项:数据集选型与方案(2026-09-08) 9.4 指出核心矛盾:模板化短答案使 hard 桶 per-token 不确定性反而低。破局需要 **真实多步 CoT 数据**(让"1 步解不出"成为 CE 层面的现实)+ **难度→先验的显式通路**。 ### 10.1 HF 数据集选型(实查字段与规模) | 数据集 | 规模 | 语言 | 关键字段 | 适配点 | |---|---|---|---|---| | `meta-math/MetaMathQA_GSM8K_zh` | 240k | 中文 | `query_zh` / `response_zh` / `type` | 中文逐步 CoT 解答,替换模板答案的首选 | | `swulling/gsm8k_chinese` | 7.5k | 中文题+英文解 | `answer` 内含 `<<48/2=24>>` 步标注 | 步标注可直接数出推理步数 = 现成难度标签 | | `AI-MO/NuminaMath-CoT` | 860k | 英文为主 | `source`(gsm8k/amc_aime/olympiads…) | source 即天然难度梯度,适合跨级对比 | | `rabinadk1/competition_math-level_5` 等 | MATH 分级子集 | 英文 | level 1–5 | 官方难度分级,可直接当 difficulty 字段 | ### 10.2 专门方案(按改动成本排序,1+2 组合拳已落地) 1. **CoT 数据替换(数据侧,首选)**:`make_gsm8k_zh_data.py` 从 MetaMathQA_GSM8K_zh 抽样,按"解答中等式行数"自动分桶(≤2 步 easy / 3 步 medium / ≥4 步 hard), 生成带 difficulty 字段的 jsonl。多步 CoT 的中间 token 无法靠 1 步记住, halting 头将在真实的预测压力下学到"多想"。 2. **难度条件化先验(损失侧,已实现)**:`--difficulty-prior easy:0.7,medium:0.4,hard:0.15`。 实现:`PonderLoader` 把同难度样本组成同 batch,训练循环在每个 batch 前把 `config.ponder_prior_p` 切换为该难度的 p_g —— KL 正则随之把 easy 拉向快停、 hard 拉向多想。这是 PonderNet 几何先验的 per-sample 扩展,ponder_llama.py 零改动。 3. **两阶段训练(调度侧)**:先 LoRA 拟合 CoT 任务(CE 降下来),再冻结 LoRA 单独训 halting 头 —— 避免"快停"在训练早期成为 CE+KL 的联合最优解。 4. **失败自举(AdaptThink 式,进阶)**:先用 K=1 跑一遍训练集,答错的样本构成 "必须多想"集合,对其施加更小的 p_g(或直接监督步数目标)—— 用模型自己的 失败案例定义难度,比启发式分桶更精准。 ### 10.3 一键复现(方案 1+2) ```bash pip install datasets python make_gsm8k_zh_data.py --out-dir data_zh --n-train-per-bucket 800 --n-eval-per-bucket 50 python train_ponder_head.py --model --train-mode head+lora \ --data data_zh/zh_cot.jsonl --difficulty-prior easy:0.7,medium:0.4,hard:0.15 \ --epochs 2 --batch-size 2 --accum 4 --grad-ckpt --dtype float16 --output out/ponder-zh python eval_ponder.py --model --data data_zh/zh_cot.eval.jsonl \ --head out/ponder-zh/ponder_head.safetensors --adapter out/ponder-zh \ --max-steps 8 --tag zh-cot ``` 预期:easy 桶步数 →~1,hard 桶步数显著上移(p_g=0.15 的几何先验期望 ≈6.7 步), 出现 `easy < medium < hard` 的语义分化;评估脚本按 difficulty 分桶可直接检验。 --- ## 十一、第二轮实测:中文 CoT + 难度条件化先验(T4) ### 11.1 实验设置 | 项 | 配置 | |---|---| | 数据 | meta-math/MetaMathQA_GSM8K_zh 中文逐步 CoT,按等式行数分桶 (easy≤2 / medium=3 / hard≥4 步) | | 训练集 | 1200 条 (400/桶),实际推理步数梯度 easy 1.84 / medium 3.00 / hard 5.39 | | 评估集 | zh_cot.eval.jsonl 180 条 (60/桶),评估取前 90 条 | | 训练 | head+lora (r=16 思考块)、`--difficulty-prior easy:0.7,medium:0.4,hard:0.15`、max-steps 6、1 epoch (75 优化步)、fp16+GradScaler、T4 | | 先验生效验证 | 训练日志 KL 恒定在 ~1.0-1.1,三档几何先验随 batch 难度正确切换 | ### 11.2 训练曲线(75 优化步) ``` step 10 | loss 2.3062 (ce 2.2549) | ponder_steps 2.36 step 20 | loss 0.5697 (ce 0.5112) | ponder_steps 1.07 step 70 | loss 0.4326 (ce 0.3843) | ponder_steps 1.07 ``` CE 快速收敛(CoT 任务 LoRA 拟合良好),但 ponder_steps 从 2.36 一路降到 ~1.07 并锁死 —— "尽快停"再次成为 CE+KL 的联合最优解。 ### 11.3 评估结果(90 条,K=6 vs K=1) | 配置 | 全局 CE | easy 步数 | medium 步数 | hard 步数 | |---|---|---|---|---| | K=6 自适应 (训后) | **0.3713** | 1.095 | 1.062 | 1.044 | | K=1 强制单步 | 0.4223 | 1.000 | 1.000 | 1.000 | **思考收益:CE 0.4223 → 0.3713(降 12%),代价仅平均 0.07 步/token**(约 7% 位置用到 2+ 步)。 自适应停机机制本身工作正常且正向获益,但三桶步数不仅未按 hard>medium>easy 分化, 反而呈微弱反向(hard 最低)—— 与第一轮(英文短答案)现象一致。 ### 11.4 根因分析:为什么"难题多想"在 CE 视角下不成立 两轮实验(模板化短答案 + 真实多步 CoT)得到同一结果,可以下结论这不是数据形式问题: 1. **CE 的优化对象是 per-token 预测,不是"解题"**。数学题 CoT 的语言高度模板化 ("因此...="、"设...为 x"),hard 题的下一个 token 反而比 easy 题的自由文本更可预测。 halting 头从 CE 梯度学到的"哪里不确定"与题目语义难度负相关。 2. **1 步就够好的任务不值得多想**。2B 模型 + LoRA 对 GSM8K 级 CoT 的 next-token 拟合误差已很小(CE 0.37),K=2..6 的循环对绝大多数位置没有信息增益, w→第 1 步是全局最优。"难题多想"要求存在一批 1 步真的解不出的位置。 3. **难度条件化先验被 CE 压制**。β=0.05 下 KL 拉力 (~0.05) 远小于 CE 梯度差, hard 桶先验 p=0.15 不足以把它推向多步。加大 β 会损害 CE(实验过 β 失衡的风险)。 ### 11.5 修正后的方案优先级(第三轮方向) 结论:**必须改变任务结构让"1 步解不出"成为事实,而不是换数据或调先验**。 1. **失败自举(AdaptThink 式,升为首选)**:用当前 K=1 模型跑训练集, CE 高的位置/样本 = 模型自己的能力边界 → 构造"这些题强制多想、其余强制快停" 的步数监督,或直接对失败样本给 p_g=0.1。难度由模型实测定义,与语义对齐。 2. **两阶段训练**:先 LoRA 拟合任务(本轮已完成,CE 0.37),冻结 LoRA 后 单独训 head —— head 只能在固定表征上找"1 步不够"的位置,排除了 "LoRA 学会快停"的混淆路径。 3. **答案遮蔽/结构化监督**:只对 CoT 的"结论区"计 loss(中间推理不计), 迫使停机决策与"推理是否完成"对齐,而非与 per-token 熵对齐。 4. **保留资产**:本轮权重(head+LoRA)已可复现 12% 的 CE 收益 @ ~1.07 步, 作为第三轮的初始化 checkpoint。 ### 11.6 产物与复现 - HF: `pondernet/weights_zh_round2/`(ponder_head.safetensors + LoRA adapter 19.2MB) - HF: `pondernet/data_zh_round2/`(zh_train_1200.jsonl + zh_cot.eval.jsonl) - T4 端复现命令与 10.3 相同(数据换 data_zh/zh_train_1200.jsonl); 评估:`eval_ponder.py --max-steps 6 --head out_zh/ponder_head.safetensors --adapter out_zh --limit 90` --- ## 十二、第三轮实测:失败自举(AdaptThink 式) ### 12.1 方法 用第二轮训后的模型(K=1 单步)对 1200 条训练样本**逐样本实测 CE**(probe), 以模型自己的能力边界重新定义难度,替代启发式分桶: - `probe_ce ≥ p75 (0.555)` → 实测 hard(强制多想,p_g=0.05) - `probe_ce ≤ p25 (0.323)` → 实测 easy(强制快停,p_g=0.90) - 其余 → 实测 medium(p_g=0.50) 第三轮训练:加载第二轮 LoRA 继续训(`--init-adapter out_zh`,head 重置), 720 条均衡抽样,1 epoch,先验命中 720/720。新增 `--init-adapter` 两种用法: `--train-mode head`(冻结 LoRA 只训 head)/ `--train-mode head+lora`(LoRA 续训), test_boot.py 三阶段冒烟覆盖。 ### 12.2 probe 铁证:启发式难度与模型难度负相关 | 启发式桶 | n | probe 平均 CE (K=1) | |---|---|---| | easy (1-2 步) | 400 | **0.536** | | medium (3 步) | 400 | 0.437 | | hard (≥4 步) | 400 | **0.389** | 推理步数越多的题,模型单步预测反而越容易 —— 数学 CoT 的模板化语言 ("设...为 x"、"因此...=")使长解答的 per-token 熵更低。 这定量解释了两轮实验中 halting 头"反向分化"的原因:它优化的从来就是 预测难度,而不是题目语义难度。交叉表:实测 hard 中 56% 来自启发式 easy。 ### 12.3 第三轮结果(180 条 eval,同阈值重标分桶) | 配置 | 全局 CE | easy 步数 | medium 步数 | hard 步数 | |---|---|---|---|---| | K=6 自适应 (boot 模型) | 0.3759 | 1.035 | 1.039 | **1.047** | | K=1 强制单步 | 0.3762 | 1.000 | 1.000 | 1.000 | - **步数方向首次转正**:hard > medium > easy(单调)✅ - **幅度微弱**:hard−easy 仅 +0.012,97% 位置仍停在第 1 步 ❌ - **思考收益归零**:0.3762 → 0.3759(0.1%),第二轮的 12% 收益消失 ### 12.4 根因:移动目标 + KL 拉力不足 1. **probe 标签过时(移动目标)**:probe 用的是第二轮结束时的模型;第三轮 LoRA 继续训练迅速把 probe_ce 高的位置拟合掉(CE 0.43→0.38),"实测难" 在训练开始后立刻不再难。head 面对的 hard 组已经没有"多想的 CE 增益"。 2. **β=0.05 的 KL 拉力数量级不足**:hard 组 KL(~1.3)×0.05 ≈ 0.065 的 loss 贡献,无法对抗 CE 对快停的偏好。步数只留下 2-3% 的"姿态性"多步。 3. 思考收益归零 = 模型对 boot_hard 的 1 步拟合已足够好,K>1 无信息增益。 ### 12.5 第四轮方向(按优先级) 1. **步数硬监督替代 KL**:对实测 hard 组直接监督 w 分布(目标 = 均匀铺满 K 步),easy 组监督 w=[1,0,...]。BCE/MSE 直接作用在 w 上,比 β·KL 强 一个数量级,不依赖"多想有 CE 增益"。 2. **冻结表征的两阶段**:probe 后**冻结 LoRA** 只训 head(--init-adapter + --train-mode head)。probe 标签在固定表征下不过时,head 学到的是真实 的"1 步不够"位置。本轮为省时选择了续训,两阶段版本待验证。 3. **答案级 RL(AdaptThink 原味)**:奖励 = 答对且步数少。答案对错不随 LoRA 拟合 token 而漂移,规避移动目标。工程量大(采样 + 奖励 + RL), 建议在 1/2 验证失败后再启动。 4. **保留资产**:probe_results.jsonl(1200 条实测难度标签)与 boot 权重 已存 HF,第四轮直接复用。 ### 12.6 产物与复现 - HF: `pondernet/weights_boot_round3/`(head + LoRA)、`pondernet/data_boot_round3/` (probe_results.jsonl + boot_train.jsonl + boot_eval.jsonl) - 复现: ```bash python train_ponder_head.py --model --train-mode head+lora \ --init-adapter out_zh --data data_zh/boot_train.jsonl \ --difficulty-prior easy:0.9,medium:0.5,hard:0.05 \ --epochs 1 --batch-size 2 --accum 8 --max-steps 6 --grad-ckpt --dtype float16 \ --output out_boot python eval_ponder.py --model --data data_zh/boot_eval.jsonl \ --head out_boot/ponder_head.safetensors --adapter out_boot --max-steps 6 ```