Repository navigation
Conversation
Rows whose target is ignore_index add nothing to the loss or the gradients, but fused_linear_cross_entropy_forward ran the logits, grad_input and grad_weight GEMMs on them. Project only the other rows. chunk_size still comes from all rows, so skipped rows remove whole chunks; rows are skipped only when that removes a chunk, and torch.compile keeps the current path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
fused_linear_cross_entropy_forwardruns its three GEMMs (logits,grad_input,grad_weight) on every row.Rows whose target is
ignore_indexadd nothing to the loss or the gradients.In SFT they are often most of the batch: masked prompts, padding, image tokens.
This PR runs the projections only on the other rows.
chunk_sizestill comes from allBTrows, so skipped rows remove whole chunks and the logits memory budget stays the same.Rows are skipped only when that removes a chunk; otherwise the current code path runs.
nonzero()replaces the existing.item(), so there is still one host sync.Under
torch.compilethe current code path runs.Training step time, forward + backward, RTX 4090:
No change when every token is labelled.
The fused loss alone gets about 1 / (labelled share) faster: 1.3x at 75%, 2x at 50%, 7.8-9.0x at 10%.
Testing Done
make testto ensure correctness (5140 passed; the 22 failures fail the same way onmainin this environment)make checkstyleto ensure code stylemake test-convergenceto ensure convergence (168 passed; the 2 failures fail the same way onmainin this environment)Details
Ignored rows keep what they get today: a zero
grad_inputrow, 0 per-token loss, z-loss and accuracy, and-1as predicted token. Per-token outputs are scattered back to allBTrows.Each chunk's GEMMs read the full
V x Hweight, so the saving comes from removing chunks. When no chunk is removed (for example, only the shifted last label of each sequence is ignored), the contiguous slices run as before and results are bit-identical tomain.With
grad_inputrows no longer contiguous, the dX GEMM writes into one reused chunk buffer (mm(out=)) andindex_copy_moves the rows.Numerics: in bf16, the loss matches
mainto 1e-6 (random weights) and to 2e-5 relative (public 4-bit checkpoints); gradients are bit-identical when chunk boundaries line up and otherwise differ by bf16 rounding (up to 1.3e-3 relative). The difference comes from the last, shorter chunk.Memory: transient peak +2 to +4 MB (row index, one
chunk x Hbuffer).torch.compile:torch._dynamo.explaingives the same graphs and breaks onmainand this PR, and compiled results equal eager, withcapture_scalar_outputson and off.Benchmark:
LIGER_FLCE_BENCH_IGNORE_FRACTION=0.5 python benchmark/scripts/benchmark_fused_linear_cross_entropy.pysets that share of targets toignore_index. Onllama_3_8bshapes it gives 0.52-0.53x ofmain's full time forligerandliger-fp32-accum, memory within 2 MB.Tests
New in
test/transformers/test_fused_linear_cross_entropy.py(54 cases):test_correctness_ignored_rows: prompt-first, scattered, all and no ignored rows;mean,sum,none(per-token upstream gradient); bias on and off; againsttorch.test_ignored_rows_do_not_enter_projection: NaN in ignored rows must not reach loss or gradients. Fails onmain: the NaN reachesgrad_weightthrough0 x NaNin the dW GEMM.test_per_token_outputs_with_ignored_rows: loss, z-loss, accuracy and predicted tokens withreduction="none".In
test/ops/test_fused_linear_cross_entropy.py:test_ignored_rows_skip_whole_chunks_onlypins the rule (0, 1, 8, 24, 40 ignored rows of 40 give 3, 3, 2, 1, 0 logits GEMMs).test_chunk_storage_released_before_next_chunk_and_final_castandtest_reduction_none_reenters_with_reusable_buffersignore the whole first chunk, so they now expect one chunk fewer.Results:
transformerstests onmainopstests onmainmake test, this PRmake test-convergence, this PRmainThe 24 tests that fail on both:
test_mlp.py(19),test_fused_neighborhood_attention.py(2),test_fused_linear_jsd.py::test_correctness_with_ignore_index(1), fp32mini_qwen2_vlmultimodal and fp32mini_llavawith logits (2).test/cutedsl/test_fused_linear_cross_entropy.py: 11 passed, 226 skipped (native CuTe DSL FLCE needs SM100), same onmain.Environment: torch 2.14.1+cu132, triton 3.8.0, transformers 5.20.2, ruff 0.15.22.
FLCE alone (BT = 8192, bf16, forward + backward, ms)
lm_headtrained:lm_headfrozen (LoRA):With every row labelled, all shapes are within 1.2% of
main(same code path apart fromnonzero()).All training-step results (ms)
With every token labelled, all setups are within 0.03% of
main.Setup: Liger-patched Hugging Face models, SDPA, gradient checkpointing, bf16, optimizer step not included.
Full and LoRA runs use random weights from the public configs; QLoRA runs load the
unsloth/*-bnb-4bitcheckpoints withlm_headin bf16.LoRA: r = 16 on all linear layers.
Labels: the first (1 - share) of each sequence is
-100.Qwen3-32B runs at 2048 tokens because 4096 does not fit in 24 GB.
Medians of 5 (3 for the QLoRA runs) interleaved
main/PR steps in one process, GPU clocks locked at 2520/10501 MHz.Labelled share of common SFT datasets
tatsu-lab/alpacaHuggingFaceH4/ultrachat_200k(multi-turn)trl-lib/tldr(prompt-completion)NousResearch/hermes-function-calling-v1bespokelabs/Bespoke-Stratos-17k(reasoning)HuggingFaceH4/llava-instruct-mix-vsftwith Qwen2.5-VL image tokensQwen3 chat template, assistant tokens labelled, everything else
-100(TRLcompletion_only_loss/assistant_only_loss, LLaMA-Factorytrain_on_prompt=False, Axolotltrain_on_inputs: false).Token-weighted over 2000 samples (300 for LLaVA), packed.
Padding to the longest sample lowers these shares further.
Generated with Claude Code and Human In The Loop 🙈