From 4f13365ffa8ba62a6cfcae7d83cb24979d0f3f66 Mon Sep 17 00:00:00 2001 From: iomgaa Date: Sat, 18 Jul 2026 10:25:11 -0400 Subject: [PATCH] =?UTF-8?q?=E5=B1=821:=20=E6=8D=9F=E5=A4=B1=E7=BC=A9?= =?UTF-8?q?=E6=94=BE=E7=BB=88=E8=A7=A3=E2=80=94=E2=80=94=E6=98=BE=E5=BC=8F?= =?UTF-8?q?=E9=80=80=E5=87=BA=20HF=20=E6=96=B0=E5=BC=8F=E6=A2=AF=E5=BA=A6?= =?UTF-8?q?=E7=B4=AF=E7=A7=AF=E5=A5=91=E7=BA=A6=EF=BC=88model=5Faccepts=5F?= =?UTF-8?q?loss=5Fkwargs=3DFalse=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 第二幕根因:新式契约的 ×world_size 补偿在基类 compute_loss 尾部(v5 trainer.py:2028),整体重写会绕过它 → loss 与梯度 ÷4(sanity 0.244≈0.85/4)。 按 HF 文档建议(trainer.py:1977)退出新式契约回经典行为;docs/02 勘误改为 两幕全记录。 Co-Authored-By: Claude Fable 5 --- ars_opd/trainer.py | 20 ++++++++++++-------- docs/02-sft-baseline.md | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/ars_opd/trainer.py b/ars_opd/trainer.py index e467a7d..0f32363 100644 --- a/ars_opd/trainer.py +++ b/ars_opd/trainer.py @@ -111,6 +111,15 @@ class SFTTrainer(Trainer): for module in self.model.modules(): if isinstance(module, torch.nn.Dropout): module.p = 0.0 + # 非显然约束:显式退出 HF 的新式梯度累积契约(v5 trainer.py:1977 文档 + # 原话:"If you are not using num_items_in_batch ... overwrite + # self.model_accepts_loss_kwargs to False")。新式契约要求返回 + # sum/全局token数,且依赖基类 compute_loss 尾部的 ×world_size 补偿 + # (trainer.py:2028)——我们整体重写了 compute_loss,那段补偿不会执行, + # 曾致 loss 与梯度 ÷4(2026-07-18,第二幕;第一幕是返回裸 mean 被 ×8, + # 全程记录见 docs/02 §5)。退出后回到经典契约:返回本微批 mean, + # Trainer 负责 ÷累积步数,日志跨卡平均,版本稳定 + self.model_accepts_loss_kwargs = False self._token_counts: list[int] = [] def compute_loss( @@ -133,14 +142,9 @@ class SFTTrainer(Trainer): inputs["labels"], inputs["attention_mask"], ) - # 非显然约束:HF 梯度累积契约(transformers 4.46 修正后的新式)。模型 - # forward 接受 loss_kwargs(Qwen3 是)时,Trainer 不再帮你除以累积步数, - # 而是期望 compute_loss 返回 sum/num_items_in_batch(整个累积组的总有效 - # token 数),各微批直接相加得到精确的全局 per-token 均值。返回裸 mean - # 会导致日志与梯度同时放大"累积步数"倍——2026-07-18 远程 sanity 的 - # loss 7.5 ≈ 0.94×8 正是此坑(诊断记录见 docs/02 §5) - if num_items_in_batch is not None: - loss = loss * num_tokens / num_items_in_batch # mean → sum/N_total + # num_items_in_batch 有意忽略:已在 __init__ 退出新式契约(见彼处注释), + # 本函数返回微批 mean,÷累积步数由 Trainer.training_step 负责。 + # 代价是微批按相同权重而非 token 数加权(百分之几的偏差,与参考实现同行为) self._token_counts.append(num_tokens) return (loss, outputs) if return_outputs else loss diff --git a/docs/02-sft-baseline.md b/docs/02-sft-baseline.md index 0cabec5..4047e84 100644 --- a/docs/02-sft-baseline.md +++ b/docs/02-sft-baseline.md @@ -83,5 +83,5 @@ F.cross_entropy(..., ignore_index=-100) ## 5. 验证方式 1. **本地(CPU)**:collator 单测对拍参考行为——构造超长解答样本断言 prompt 未被截空;断言 -100 位置分布;断言 enable_thinking 两种取值下边界正确。数据加载单测:断言 `except:pass` 已变显式报错。 -2. **远程**:先 sanity(官方 SFTTrainer 或我们管线跑 50 步)确认 loss 稳定下降;再全量 1 epoch。W&B 或实时日志盯 `sft/loss` 与有效 token 数。⚠️ 两条实测勘误(2026-07-18):① 起点不是想象的 ~2-3——预训练 Qwen3-0.6B 对 M3 风格数学文本的真实 CE ≈ **0.85**(探针 scripts/diag_loss_probe.py 实测),健康曲线 ≈ 0.9→0.4;② **HF 梯度累积契约坑**:模型 forward 接受 loss_kwargs 时(Qwen3 是),自定义 compute_loss 必须返回 `sum/num_items_in_batch` 而非裸 mean,否则日志与梯度都放大"累积步数"倍(首跑 loss 7.5 ≈ 0.94×8 即此坑;修复在 trainer.py compute_loss)。 +2. **远程**:先 sanity(官方 SFTTrainer 或我们管线跑 50 步)确认 loss 稳定下降;再全量 1 epoch。W&B 或实时日志盯 `sft/loss` 与有效 token 数。⚠️ 两条实测勘误(2026-07-18):① 起点不是想象的 ~2-3——预训练 Qwen3-0.6B 对 M3 风格数学文本的真实 CE ≈ **0.85**(探针 scripts/diag_loss_probe.py 实测),健康曲线 ≈ 0.9→0.4;② **HF 梯度累积契约坑(两幕剧)**:模型 forward 接受 loss_kwargs 时(Qwen3 是),Trainer 默认按"新式契约"对待自定义 compute_loss——第一幕:返回裸 mean 会被 ×累积步数(首跑 loss 7.5 ≈ 0.94×8);第二幕:改成 sum/num_items 后又 ÷world_size(0.244 ≈ 0.85÷4),因为新式契约的 ×num_processes 补偿在**基类** compute_loss 尾部(v5 trainer.py:2028),整体重写 compute_loss 会绕过它。终解 = 按 HF 文档(trainer.py:1977)显式 `self.model_accepts_loss_kwargs = False` 退出新式契约,回到"返回 mean、Trainer ÷累积步数"的经典行为(代价:微批等权而非 token 加权,偏差百分之几,与参考实现同行为)。教训:**整体重写框架方法时,必须检查基类同名方法里除了你替换的逻辑还捎带了什么**。 3. 关账判据:1k 子集 1 epoch 跑完,loss 曲线正常,checkpoint 能被 `from_pretrained` 加载并生成通顺文本。