后训练库 TRL 发了 v1.13.0,更新日志里最值得注意的不是新功能,而是一次几乎让人脸红的修复:默认 chunked 损失的 lm_head 投影,此前一直先把两个操作数升到 fp32 再做矩阵乘,把这个最重的 GEMM 从张量核挤到了 SIMT 通路。官方在 8×H100 上对 Qwen3.6-35B-A3B 跑 trl sft 剖析,这两个 fp32 SIMT GEMM 占了全部 GPU kernel 时间的 21.6%。
一处 dtype 决策,吃回两成 kernel 时间
机理不复杂:h.float() @ w.float().t() 里两个操作数本来就是 bf16,升精度没有带来任何信息增益,反而每次都要物化一份完整 lm_head 权重的 fp32 副本——248k 词表下是 2.03 GB,而且每个 chunk 重建一次、每次梯度重计算再重建一次。修法就是把投影留在 bf16 张量核上。
官方微基准(单 chunk,256 token × 词表 248,320 × hidden 2048,1×H100,bf16 前向+反向):23.37 ms / 5.99 GB 降到 3.86 ms / 3.03 GB,6.0 倍。落到端到端(tokens/s/GPU,每步 16,384 token):Qwen3-8B 全参微调在 2×H100 FSDP2 上从 3554 提到 6009(1.69×),LoRA r16 从 4531 到 7125(1.57×),Qwen3-30B-A3B(MoE)1.20×,gemma-3-270m 1.32×。蒸馏 trainer 原本每个 chunk 要付两次这笔开销(学生一份、教师一份),同样受益;在 accelerate 混合精度下数值位级一致。
百万 token 训练,从 trick 变成官方文档路径
v1.13.0 同步发布了长上下文训练指南和一个可跑的示例:单节点 8×H100,每步训练一整本书长度的序列。实测配置是 Qwen3-8B、单条序列 1,048,576 token,380 秒/步,每卡 56.2 GB(bf16,loss_type="chunked_nll")。指南按你实际会撞到的顺序给出杠杆:chunked_nll、梯度检查点 offload、YaRN RoPE;约束也写得很直白——只支持 full attention、不支持 packing,checkpointing offload 需要 transformers ≥ 5.16。
PPOTrainer:2020 年的起点,2026 年删掉
Breaking 部分才是有历史感的:PPOTrainer、PPOConfig 和 value-head 封装整体移除。这条代码 2020-03-28 随仓库首个 commit(dfb6a580)落地,当时包还叫 lm_ppo——官方更新日志的原话是,它是 TRL 里最老的东西、原初代码库的最后一块。移除理由也列得清楚:一年多没有功能维护、全部 trainer 里唯一从未对齐输入格式的那个(还在收 tokenized input_ids)、记录在案的使用量趋近于零,还持续吸引自动 bug 猎手对没人运行的代码提真实报告。from trl import PPOTrainer 在 v1.10 就已经失效,真正的 PPO 用户本来就钉在旧版本上;create_reference_model 保留(BCO、A2PO、Online DPO 还在用)。
其他值得扫一眼的:Liger-Kernel 的 fused linear DPO/KTO/GRPO/JSD 损失被 vendor 进 trl.losses(与 Liger v0.8.2 位级一致,use_liger_kernel 开关行为不变);vLLM 支持升到 0.28.0、弃掉 0.19.0;依赖下限抬到 peft ≥ 0.13.0、deepspeed ≥ 0.18.6。
这件事的启示:训练栈里真正的浪费,往往不在算法,而在"默认就这么写了很多年"的数值习惯。21.6% 的 kernel 时间,一个 dtype 决策就拿回来了——你的训练脚本里,是不是也藏着一份没人看过第二眼的 fp32 lm_head?
完整更新日志见 GitHub Release。