TRL, Hugging Face's post-training library, shipped v1.13.0 this week, and the most consequential change in the log is a fix that is almost embarrassing in hindsight: the default chunked loss was upcasting its lm_head projection to fp32 before the matrix multiply, pushing the heaviest GEMM off the tensor cores onto the SIMT path. In an 8×H100 profile of trl sft on Qwen3.6-35B-A3B, those two fp32 SIMT GEMMs accounted for 21.6% of all GPU kernel time.
One dtype decision buys back a fifth of kernel time
The mechanism is simple: both operands of h.float() @ w.float().t() were already bf16, so the upcast bought no information. It did, however, materialize an fp32 copy of the entire lm_head weight — 2.03 GB for a 248k vocabulary — rebuilt for every chunk and again on every gradient-checkpoint recompute. The fix keeps the projection on bf16 tensor cores.
Official micro-benchmark (one chunk, 256 tokens × vocab 248,320 × hidden 2048, 1×H100, bf16, fwd+bwd): 23.37 ms / 5.99 GB before, 3.86 ms / 3.03 GB after — 6.0×. End to end (tokens/s/GPU, 16,384 tokens per step): Qwen3-8B full fine-tuning on 2×H100 FSDP2 goes 3554 → 6009 (1.69×); LoRA r16 goes 4531 → 7125 (1.57×); Qwen3-30B-A3B (MoE) 1.20×; gemma-3-270m 1.32×. Distillation trainers paid the fp32 cost twice per chunk (student and teacher) and benefit equally; under accelerate mixed precision the numerics are bit-identical.
Million-token training becomes a documented path
v1.13.0 also ships a long-context guide and a runnable example that trains a book-length sequence per step on a single 8×H100 node. The measured config: Qwen3-8B at 1,048,576 tokens per sequence, 380 s/step, 56.2 GB per GPU (bf16, loss_type="chunked_nll"). The guide documents the levers in the order you actually hit them — chunked_nll, gradient-checkpointing offload, YaRN RoPE — and states the constraints plainly: full attention only, no packing, and checkpointing offload needs transformers ≥ 5.16.
PPOTrainer: the 2020 starting point, deleted in 2026
The breaking change is the one with history: PPOTrainer, PPOConfig, and the value-head wrappers are gone. That code landed on 2020-03-28 in the repository's first commit (dfb6a580), when the package was still called lm_ppo — in the release notes' own words, the oldest thing in TRL and the last piece of the original codebase. The stated reasons: no feature work in over a year, the only trainer never aligned on input format (it still took tokenized input_ids), near-zero recorded usage, and a magnet for automated bug hunters filing real reports against code nobody runs. from trl import PPOTrainer already stopped working in v1.10, so anyone actually running PPO was pinned to an older TRL anyway; create_reference_model stays (BCO, A2PO and Online DPO use it).
Also worth a glance: the fused linear DPO/KTO/GRPO/JSD losses from Liger-Kernel are vendored into trl.losses (bitwise identical to Liger v0.8.2, use_liger_kernel behavior unchanged); vLLM support moves to 0.28.0 and drops 0.19.0; dependency floors rise to peft ≥ 0.13.0 and deepspeed ≥ 0.18.6.
The takeaway: in modern training stacks, the real waste is often not in the algorithm but in numerical habits that survived because "that's how it was always done". 21.6% of kernel time, recovered by one dtype decision — how many unexamined fp32 lm_head projections are hiding in your training script?
Full changelog: GitHub Release.