Repository navigation
Conversation
…="none" gradients in place The per-token loss and z_loss buffers were allocated in the input dtype, so bf16/fp16 inputs rounded every token before the mean/sum reduction. They now stay in fp32, are reduced in fp32 and are cast once to the input dtype. The new `loss_dtype` option (None or torch.float32) returns loss and z_loss in fp32 for any reduction; it is only supported by the Triton backend and the module raises a ValueError when another backend is selected. With reduction="none" the backward used `_input * grad_output.unsqueeze(1)`, which allocates a BT x V temporary. A small Triton kernel now scales each row of the saved gradient in place (fp32 multiply, one rounding to the input dtype, so the result is bitwise identical) with no device-to-host sync. Because the saved buffer is modified, a second backward after it was scaled (reduction="none", or a scalar gradient other than 1.0) now raises a RuntimeError instead of returning a silently wrong gradient; a scalar gradient of exactly 1.0 still supports retain_graph=True. Adds tests for the fp32 loss, the in-place gradient (including strided gradients and V not a multiple of the block size), the memory behavior and the retain_graph cases, plus a --reduction/--dtype option for the cross entropy benchmark and a note in docs/Low-Level-APIs.md. Co-authored-by: GitHub Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Congmin Qiu <cqiu+1@linkedin.com>
|
@JobQiu 1.The new guard breaks |
…n and benchmark backward on a fresh graph A review found two problems with the in-place gradient scaling and its second-backward guard: - The shared run_speed_benchmark calls y.backward(do, retain_graph=True) repeatedly on one graph with a random 0-D grad, so even reduction="mean" scales the saved gradient in place and the guard makes benchmark_cross_entropy.py fail on the second call. - Removing retained-graph support for reduction="none" broke first-order usage that worked before, such as backpropagating separately through different slices of the per-token losses. Changes: - Add an in_place option (default False, Triton backend only) to LigerCrossEntropyFunction, cross_entropy_triton, liger_cross_entropy and LigerCrossEntropyLoss, threaded like loss_dtype. By default reduction="none" again multiplies the saved gradient out of place, so retain_graph and slice-wise backward work. in_place=True keeps the rowwise in-place kernel (no BT x V temporary) and makes the graph single-use. The other backends reject in_place=True. - benchmark_cross_entropy.py: add --in-place and time the backward mode on a fresh graph per call, since a retained graph cannot be reused once the saved gradient is scaled in place. - Tests: cover both in_place values for the reduction="none" gradient checks, add retain_graph/slice-wise backward and backend-rejection tests. - docs: describe the default behaviour, in_place=True and the remaining mean/sum caveat. Co-authored-by: GitHub Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Congmin Qiu <cqiu+1@linkedin.com>
|
Thanks for the review, both points were valid. Fixed in a148e0c.
The description is updated with the re-run on H100 (default path matches main in time and peak memory, A possible follow-up (not in this PR): when the |
Summary
Two changes to the standalone cross entropy (
LigerCrossEntropyLoss/LigerCrossEntropyFunction).Per-token loss precision. Standalone CE stores the per-token loss in the input dtype (
cross_entropy.py:379on main), so bf16 per-token losses carry bf16 rounding (FLCE has kept fp32 since 2024-08). Per-token loss and z_loss are now always computed and reduced in fp32 internally, and mean/sum are cast once to the input dtype (output dtype unchanged). A newloss_dtype=torch.float32option returns the fp32 loss; it is supported by the Triton backend only, other backends raiseValueErrorwhen it is set. With the default bf16 output the returned loss error is bounded by the final bf16 cast, so the default path shows no measurable difference in the benchmark below; the precision gain is for callers who setloss_dtype=torch.float32.reduction="none"backward memory, as an opt-in. By default (in_place=False) the backward is exactly main's code path: it multiplies the saved gradient out of place by the per-token upstream gradient, soretain_graph=Trueand backward through different slices of the per-token losses keep working. The newin_place=Trueoption (Triton backend only; other backends raiseValueErrorwhen it is set) uses a row-wise in-place Triton kernel that scales the saved gradient bygrad_output[row]for any 1-D grad (mean, sum, weighted, strided), with an fp32 multiply and one rounding. It avoids the extra BT x V temporary and has no device-to-host sync, and gradients are bitwise identical to main. Within_place=Truethe graph is single-use: a second backward raisesRuntimeError.reduction="mean"/"sum"are unchanged from main, except that a second backward after a 0-D upstream gradient other than 1.0 now raisesRuntimeErrorinstead of silently returning a wrong gradient (with an upstream gradient of exactly 1.0,retain_graph=Truestill works).Related: #678 / #680 (introduced the per-row multiply), #1292 and #1251 (open; textual conflicts only, and this PR tests
grad_output.ndim > 0first to match #1292's layout), and the FLCE precedent.Details
The benchmark script gains
--reduction {mean,sum,none_sum,none_mean},--dtypeand--in-placeoptions (defaults unchanged;--in-placerequires anone_*reduction and otherwise exits with an error). Thebackwardspeed mode now times each backward on a fresh graph: the forward is rerun untimed,_input.gradis reset and the 256 MB L2 cache is flushed before the timed region, and the loop is sized liketriton.testing.do_bench(one discarded call, a 5-call average estimate, 25 ms warmup, 10 ms rep, quantiles 0.5/0.2/0.8). This fixes aRuntimeErrorthe benchmark hit when a backward was repeated on the same graph. The forward, full and no-grad-forward modes and the memory benchmark are unchanged (they already build a fresh graph per call).Benchmark: 1x NVIDIA H100 80GB HBM3, torch 2.13.0.2+cu130, triton 3.7.1, BT=8192, bf16 random logits x4, targets random with every 17th ignored, 20 iterations after warmup, forward + backward on a fresh graph per iteration, public vocab sizes. "default" is
in_place=False; extra peak memory is in GiB.Gradients are bitwise equal to main for both paths in all 24 combinations (12 cases x {default,
in_place=True}). On the default path, the retain_graph slice case (loss[:k].sum().backward(retain_graph=True)thenloss[k:].sum().backward(), BT=8192, k=BT/3) succeeds and the accumulated gradient equals a single backward bitwise, for all three vocab sizes.The B300 tables below were measured on the previous head (ddd3ec4), on 1x NVIDIA B300 SXM6, torch 2.13.0, same setup as above. Their "PR" columns are the
in_place=Truepath: the forward kernel and the in-place kernel are unchanged since. The default path was not re-measured on B300 because it is main's code path. Extra peak memory is main -> PR.Per-token loss precision (forward unchanged since the previous head, so measured there). On B300, gradients are bitwise equal to main in all 12 cases, and the per-token loss max abs error vs an fp32 PyTorch reference is:
loss_dtype=torch.float32On H100 (also on the previous head) the per-token loss max abs error is 0.093-0.124 with the default bf16 output and 3.8e-6 with
loss_dtype=torch.float32.The code, tests, docs and this description were written with AI assistance (GitHub Copilot). I reviewed every changed line; the tests and benchmarks listed above were run on the hardware described.
Testing Done
On 1x NVIDIA H100 80GB HBM3 (torch 2.13.0.2+cu130, triton 3.7.1), head a148e0c:
test/transformers/test_cross_entropy.pyplustest/cutedsl/test_cross_entropy.pyon this branch: 480 passed, 1 skipped.This branch's test file run against main's code: 121 failed / 191 passed, and the failures are exactly the tests that need the change: the
in_placekwarg tests (per-row multiply 48, matches torch 24, no-BT-x-V-temporary memory 12,loss_dtypekeeps input dtype 2, retain_graph slices 2, in_place second backward 1, in_place backend check 1), theloss_dtypetests (24 + 4 + 1 + 1), and the scalar second-backward guard test (1).Repo benchmark script (
--sweep-mode model_config --bt 4096): on the previous head the reported failure reproduces (RuntimeErrorin the backward speed mode with the default arguments, exit 1). On this head the script completes (exit 0) with the default arguments, with--reduction none_sum, and with--reduction none_sum --in-place;--in-placewith the defaultmeanreduction exits with--in-place requires --reduction none_sum or none_mean. These runs shared the GPU with the full test suite, so their timings are not quoted.Full suite (
pytest test/ --ignore=test/convergence) on this head: 2 failed, 6172 passed, 1558 skipped, 21 xfailed, 1 error. The failing set is exactly the pre-existing one seen on main in the previous H100 round (test_rope.py::test_qwen3_5_vision_rope_matches_hf, andtest_low_probability_vocab_block_is_finiteintest_jsd.pyandtest_fused_linear_jsd.py); the 41 extra passes are the new cross-entropy tests.ruff checkandruff format --checkpass on every touched file.Convergence on the B300 (previous round,
pytest test/convergence): the run hitsCUDA error: unspecified launch failureafter the first 7 mini-model tests, and every later test errors at setup. main and the branch behave identically (7 passed, 16 skipped, 12 xfailed, 310 errors with-k "not gpt_oss"; same first failing test), so it is an environment issue on that machine, not this change. It would be good to see the convergence job in CI.make testto ensure correctness (CE tests, see above)make checkstyleto ensure code style (touched files)make test-convergenceto ensure convergence (blocked by an environment crash that also happens on main, see above)