Repository navigation
Conversation
danielhanchen
added a commit
to unslothai/unsloth-zoo
that referenced
this pull request
Oct 25, 2026
…hunked JSD, TRL and Liger (#2079) ## Summary Adds `fused_linear_jsd`, a fused two-head generalized JSD for TRL's `GKDTrainer`, as an opaque custom op (`unsloth_zoo::fused_linear_jsd_v1`). Rows are processed in chunks: each chunk projects the student and the teacher head with fp32 logits, takes the closed-form student gradient (hidden, plus head and bias when they train) in the forward, and frees the chunk before the next one, so neither model ever holds tokens x vocab logits and the backward only rescales the stored gradient. - Matches TRL's `generalized_jsd_loss` (0.22.2, 0.25.1, 1.15.0): logits / T, log-softmax on both, beta = 0 forward KL, beta = 1 reverse KL, logsumexp mixture in between, `labels != -100` mask, batchmean or `num_items_in_batch`. - The teacher can have its own hidden size, head, bias, device, logit scale and softcap; it is never differentiated. - CUDA runs a Triton body (rows split over vocab blocks); without Triton an Inductor-compiled torch body, then eager. - Peaked rows stay correct: the gradient term is shifted by its value at the student's argmax, so a row whose top logit leads by 20-80 still has a zero-sum fp32 gradient (the softmax collapse described in linkedin/Liger-Kernel#1555). - GradScaler `scaling` is supported; engagement is counted in `CALLS["jsd"]`; chunk budget `UNSLOTH_FUSED_LINEAR_JSD_CHUNK_MB`. The unsloth side that routes `GKDTrainer` here follows in a separate unsloth PR; with an older unsloth nothing calls this op. ## Numbers Exclusive B200, N = 2048 rows, student hidden 2048, teacher hidden 2560, V = 151936, bf16, forward + backward, median of 3 (spreads do not overlap): | beta | this op | existing chunked JSD | TRL dense | Liger `LigerFusedLinearJSDLoss` (compiled) | |---|---|---|---|---| | 0 | 389 MiB, 4.96 ms | 406 MiB, 6.42 ms | 4162 MiB, 7.24 ms | 1500 MiB, 7.44 ms | | 0.5 | 389 MiB, 6.58 ms | 1148 MiB, 11.20 ms | 7724 MiB, 23.22 ms | 3276 MiB, 9.96 ms | | 1 | 389 MiB, 5.96 ms | 406 MiB, 6.25 ms | 4671 MiB, 7.79 ms | 1500 MiB, 7.73 ms | Student gradient relative error vs an fp64 oracle: 2.2e-3 for this op, 1.5e-2 for the existing chunked path, 1.5e-2 to 8.3e-2 for Liger, 1.6e-2 to 1.5e-1 for TRL dense in bf16. ## Tests `tests/test_fused_linear_jsd.py`: fp64 TRL oracle for beta in {0, 0.1, 0.5, 1} x T in {1, 0.9} with masks, bias + scale + softcap, trainable head and `num_items_in_batch`, cross-checked against the installed TRL function; chunking, fully masked batch, GradScaler scaling, peaked rows, Triton vs fp64 body, 0 graph breaks under `torch.compile(fullgraph = True)`. On this branch with main: the new file plus `test_distillation_chunked_jsd.py`, `test_fused_linear*.py`, `test_fused_loss_fullgraph.py`, `test_chunked_ce.py`, `test_chunked_logprob.py`: 545 passed, 3 skipped on GPU. The new tests fail on main.
danielhanchen
added a commit
to unslothai/unsloth-zoo
that referenced
this pull request
Oct 25, 2026
…tmax collapse) (#2080) ## Summary The fused linear CE / log-prob kernels lose all gradient signal on confident rows. When the target logit leads every other class by more than about 16.6 (fp32), `exp(z_t - m) = 1` absorbs the sum of the other classes, so `lse == z_t`, `log p_t` reads exactly 0 and the target term of the gradient, `1 - p_t`, reads exactly 0. The hidden-state gradient of such a row is then 100% wrong (relative), and on the Triton log-prob routes with a temperature the log-prob can even come out positive (up to +1.8e-6, from the target logit being recomputed after the sweep). This is the softmax collapse described in arXiv 2610.31642, section 2; Liger has the same issue open (linkedin/Liger-Kernel#1555). Fix, in `unsloth_zoo/fused_losses/fused_linear.py`: - Forward (`_fl_forward_kernel`, `_fl_partial_kernel`, `_fl_combine_kernel`): the running sum excludes the target. The sweep skips the target's block as a whole (one scalar choice per block, no per-element mask in the hot loop) and the other classes of that block are added back by one re-read after the sweep. Then `log p_t = -softplus(log(s_others) + m - z_t)`, which never adds `e^0 = 1` to `e^-margin` and is never positive. The exact-target-logit mode uses the same form. Entropy takes `log s` from the same term when the target is the row max; the split merge still rescales with each split's full sum. - Backward (`_fl_backward_kernel`, `_torch_dz`) and the flash `u` kernel: the target column uses `-expm1(log p_t)` (label smoothing included). - Torch fallback (`_torch_forward`): same split with `logaddexp` / `softplus`. - Op schema bumps (Inductor / AOTAutograd caches key on neither): `fused_linear_logprobs_v5 -> _v6` (one extra output, `log p_t` of the logits the gradient reads; equal to the log-probs unless the exact-target mode is on) and `fused_linear_logprobs_bwd_v3 -> _v4`. The CE op is unchanged. Nothing outside this file references the op names. - Triton builds without libdevice get Kahan forms of `expm1` / `log1p` (exact to an ulp for tiny arguments with `tl.exp` / `tl.log` only). ## Accuracy (fp64 oracle, margins 17 / 20 / 30 / 50, V in {1003, 40001, 151936}) | | before | after | |---|---|---| | hidden-grad, worst row, fp32 logits | 1.0 relative | 1.8e-5 | | hidden-grad, worst row, bf16 | 1.0 relative | 3.9e-3 (bf16 rounding itself 1.8e-3) | | log p_t, collapsed rows | 0 (100% relative), positive on Triton + temperature | 1.7e-5 relative, never positive | | entropy of collapsed rows | 5.3% (Triton) / 6.0% (torch) | 1.5e-5 | | CE loss, absolute | 2.3e-7 of loss read as 0 | 9e-13 | New `tests/test_fused_linear_collapse.py` (Triton and torch, logits modes 0 / 1 / 2, temperature 1 / 0.7, softcap 0 / 30, CE, log-probs, entropy, trainable head; a negative control checks ordinary rows are unchanged): 24 failed / 1 passed (the control) on main, 25 passed with this PR (14 skipped are torch-fallback combinations the file does not run). ## Speed B200, exclusive, 2560 x 151936 frozen head, 3 rounds, median ratio (this PR / main): CE 1.004, GRPO 0.999, GRPO + entropy 1.017, DPO 1.068 with min-ratio 0.995 (A/A noise on the same harness: 0.97 to 1.006 at 8192 rows, up to 20% at 2048). Kernel profile of the CE flash route: `_fl_partial_kernel` 0.84 -> 0.87 ms of a 9.3 ms step. ## Tests GPU, all fused-linear / chunked CE / chunked log-prob / GRPO recompute / fullgraph / preference / JSD / distillation suites plus the new file: 733 passed, 17 skipped. The fullgraph tests confirm no new graph breaks. Not covered here: a ROCm / HIP run (only the half-logit mode ROCm defaults to was exercised, on CUDA) and pre-Volta GPUs.
1 of 3 tasks
|
Some independent measurements of the near-optimum JSD gradient error this PR removes on the fused Triton path, plus two places where the same residual survives — in case they help the review. The numbers below come from an op-for-op CPU emulation of the kernel arithmetic (fp32; the same formulas as
|
3 tasks
Tcc0403
reviewed
Nov 5, 2026
Co-authored-by: Tcc0403 <76503978+Tcc0403@users.noreply.github.com>
Collaborator
2 of 3 tasks
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the softmax collapse from A Fix for Softmax and Cross-Entropy in the kernels. Once the target logit leads the logsumexp of the rest by more than ~16.6 (fp32),
softmax(x_y)rounds to 1, sosoftmax - 1andlse - x_ybecome exactly 0 while the other classes keep their gradients. The gradient no longer sums to zero, and the loss reads 0. This happens for very confident tokens, which are common in LLM training.dx_y = -sum_others / dandloss = softplus(log(sum_others) + m - x_y). Vocab-parallel all-reduces both sums in one call.logpinstead oflse(the backward rebuildslsefrom it) and uses-expm1(logp)for1 - p_y.fused_selective_log_softmaxreturns the accurate logp too and drops its unusedlse=arg.tl.float32(...)) and were unreachable. Rows wider than 65536 now go through them instead of raising.log1p/expm1/target_nllhelpers inops/utils.pyonly usetl.log/tl.exp, so no libdevice is needed.Details
New tests build rows with the target 5 to 40 above the rest and compare against an fp64 reference written as
softplus(lse_others - x_y), since plain fp64F.cross_entropycollapses too past ~36.7. All of them fail on main.Overhead on a 3090 Ti, bf16, H=4096, fwd+bwd: fused KL is 2-3% slower, fused JSD 1-2%. I didn't benchmark the CE / GRPO / softmax changes.
Not touched: the cutile / cute / flydsl / ascend variants, and the chunked distillation losses (torch autograd, same as torch).
Testing Done
make testto ensure correctness (ran the suites of the touched ops on both GPUs: CE, FLCE, GRPO, CE+TVD, selective log softmax, vocab-parallel CE, fused KL / JSD, JSD, KL, softmax, neighborhood attention)make checkstyleto ensure code stylemake test-convergenceto ensure convergence