Monolithic fused head-loss kernel (torch.compile) (#507)#549
Open
jlamypoirier wants to merge 26 commits into
Open
Monolithic fused head-loss kernel (torch.compile) (#507)#549jlamypoirier wants to merge 26 commits into
jlamypoirier wants to merge 26 commits into
Conversation
Introduce a single torch.compile head-loss kernel that runs the vocab softmax once and
emits all requested losses + the combined gradient in one pass, replacing the per-loss
loop where each loss runs its own softmax. This commit lands the scaffolding and the
first loss (cross-entropy from labels); further losses are added incrementally.
- `_monolithic_core` (loss/monolithic.py) is one @torch.compile boundary that inlines the
plain softmax cores (`_softmax_base`, `_predicted_logits_from_labels`, newly factored out
of functional/entropy_loss.py while keeping the public `fused_*` wrappers byte-identical),
so compile fuses the work across enabled losses. Losses toggle on by passing their inputs;
gradients accumulate in fp32 and cast once.
- Opt-in via `LanguageModelHeadConfig.loss_implementation` ({per_loss (default), fused, triton}).
The head keeps the per-loss path as the baseline/oracle and falls back to it for losses not
yet in the kernel (e.g. DPO), accumulating into the same gradient. Per-loss bookkeeping stays
in the loss objects via `get_monolithic_spec` / `register_monolithic_outputs`.
- Validation requires a single shared effective logits scale factor across softmax losses.
Tests: `test_monolithic_loss` (kernel vs the fused CE baseline) and `fused_*` head configs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a z-loss branch to the monolithic kernel, reusing the shared softmax's sum_exp_logits and logits_max (z-loss adds logits_max back; cross-entropy cancels it) so the flagship CE + z-loss combo runs one softmax pass. z-loss has no required tensor input, so it toggles on a static `z_loss_enabled` flag rather than a None-gated tensor. Grad terms accumulate in fp32 and cast once. Functional parity tests now cover z-loss alone and the CE + z-loss combo against the summed per-loss baselines, plus a tensor-parallel subtest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the from-distribution entropy losses (cross-entropy / forward-KL / reverse-KL over a teacher distribution given as logits or probabilities, with temperature) to the monolithic kernel. These share the student softmax with cross-entropy and z-loss, and add one teacher softmax in the same boundary when the target is logits. Factor the post-student-softmax math of the from-distribution helpers into plain cores (_cross_entropy_from_distribution_core, _reverse_kl_from_distribution_core) that accept the precomputed student softmax, and have both the public fused wrappers and the monolithic kernel call them — one source of truth, baseline math unchanged. Functional parity tests cover all three loss types over both target formats and a non-unit temperature, against fused_entropy_loss_forward_backward, plus tensor-parallel subtests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ure (#507) Wire LanguageModelDistillationLoss into the monolithic kernel via get_monolithic_spec (the from-distribution kernel branch already exists), so distillation and the label-CE + distillation combo run on one shared softmax. Also fix a pre-existing bug: LanguageModelDistillationLoss._forward_backward never passed the configured `temperature` to the loss kernel, so the teacher softmax always used temperature 1.0 regardless of the config. Pass it in both the per-loss and monolithic paths. Default is unchanged (1.0); this only affects configs that explicitly set a non-unit temperature. Head-level parity tests add distillation and label+distillation combos on the fused path, plus a non-unit-temperature case on both paths. The reference now scales the teacher logits by logits_scale_factor / temperature before the softmax (identity for the existing scale-1, temperature-1 configs). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the GRPO policy-gradient objective to the monolithic kernel: per-token IS-ratio clipping over the shared softmax, plus the new_logprobs_mean metric as a side output. Reuses the shared predicted-logits; uses out-of-place unsqueeze on sum_exp_logits (the per-loss kernel mutates it in place). LanguageModelGRPOLoss gets get_monolithic_spec / register_monolithic_outputs; it falls back to the per-loss path when metrics are enabled (kernel metrics land in a later step). Validated against fused_grpo_loss_forward_backward (loss, grad, new_logprobs) at the functional, head, and tensor-parallel levels. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…softmax (#507) Emit the full GRPO metric family (ratio/KL/clip/advantage stats + optional per-token entropy) from the monolithic kernel's shared softmax instead of a second softmax pass over the logits (the #494 redundancy). Entropy is the only metric needing the vocab axis; the rest reuse the already-computed new_log_probs. Extract GRPOMetrics + the pure metric aggregation into a new grpo_metrics module so both the baseline compute_grpo_metrics and the kernel share one source of truth (and to avoid a monolithic<->policy_gradient import cycle). The GRPO loss no longer falls back to the per-loss path when metrics are enabled. Validated vs the independent loop reference: functional (with/without entropy), head (per-loss + fused metrics configs), and tensor-parallel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Split fused_gspo_loss_forward_backward into a compiled forward core (one softmax + predicted-logits -> new_log_probs), the eager segment seam (index_add_ + SDP/SP all-reduce + clipping, where the symbolic num_segments stays out of every compiled boundary), and a compiled backward core (the O(vocab) scatter-add grad, previously eager). This is the proven three-phase layout already used by the Triton GSPO kernel; the backward is now fused and num_segments never triggers a recompile. GSPO runs in the monolithic (fused) loss path via the existing spec-less fallback, threading the shared gradient buffer; a standalone GSPO computes one softmax, as before. Math is byte-equivalent to the prior eager implementation. Validated vs reference_gspo_loss (functional across num_segments/dtypes, head fused + per-loss configs, and a new tensor-parallel distributed subtest). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Validate the monolithic kernel composes arbitrary loss kinds over one shared softmax. Functional level adds CE+z-loss+distillation and GRPO+z-loss combos, comparing total loss + combined gradient against the sum of the per-loss baselines (extends `_monolithic_baseline` to the distribution and GRPO kinds and builds the shared input union). Head level adds the fused three-way label+distillation+z-loss combo and GRPO+metrics+z-loss, checked against the independent reference end to end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GSPO now runs inside the monolithic kernel sharing the single softmax, rather than through the spec-less fallback. The kernel splits into three phases when a GSPO spec is present: a compiled forward (shared softmax + every enabled non-GSPO loss + GSPO's per-token new_log_probs), the eager segment seam (index_add_ aggregation + SDP/SP all-reduce + clipping, the symbolic num_segments staying out of all compiled boundaries), and a compiled backward whose GSPO grad term composes into the shared fp32 accumulator and casts once. The per-loss math is factored into a plain _apply_combinable_losses helper shared by the single-pass and GSPO-split forwards; the GSPO seam/backward cores move to monolithic.py (the import leaf) so policy_gradient can reuse them without a cycle. GSPO gets get_monolithic_spec / register_monolithic_outputs; the standalone fused_gspo_loss_forward_backward is unchanged and remains the oracle. Adds monolithic-GSPO functional + tensor-parallel tests and a fused GSPO+z-loss head combo (the shared-softmax payoff). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The plain math cores are inlined into the monolithic kernel (and the standalone GSPO path) from other modules, so a leading underscore — which marks a module-private name that should not be imported elsewhere — was wrong. Drop it from the cross-module-shared cores: softmax_base, predicted_logits_from_labels, cross_entropy_from_distribution_core, reverse_kl_from_distribution_core (entropy_loss), grpo_metrics_core (grpo_metrics), and gspo_segment_seam / gspo_backward_core (monolithic). The compiled `fused_*` wrappers and the module-local cores (e.g. _monolithic_core, _gspo_forward_core) are unchanged. Pure rename. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hare CE/z-loss cores - GRPO `get_monolithic_spec` now skips `new_logprobs_mean` and the GRPO metric family when `losses is None` (nothing logged this step), mirroring the per-loss path; threaded `losses` through `get_monolithic_spec`. GSPO stays ungated to match its standalone. - Drop the unused `triton` `LossImplementation` value (it silently ran the fused path); it returns when the Triton kernel lands. - Extract `cross_entropy_from_labels_core` / `z_loss_core` (plain, taking the shared softmax tensors) so the monolithic kernel and the standalone `fused_*` kernels share one copy of the CE/z-loss math, matching the distribution cores. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the head-level `loss_implementation` toggle + `MonolithicLossSpec` marshalling with a `MonolithicLoss` / `MonolithicLossConfig` loss type that holds the combinable child losses as nested entries and shares one softmax across them. The head is untouched: the monolithic loss flows through the ordinary per-loss loop, and non-combinable losses (e.g. DPO) are plain siblings. - Each combinable loss (label-CE, z-loss, distillation, GRPO) owns a plain post-softmax `combinable` function + `combinable_extract` / `combinable_core`; the standalone `fused_*` kernels call the same function, so the per-loss math has one source of truth. - `_monolithic_core` iterates entries via object-method dispatch (no flat arg list, no `if enabled` chain) and fuses into a single graph (no graph breaks). - Removed `LossImplementation` enum + field, the `get_monolithic_spec` / `register_monolithic_outputs` hooks, `MonolithicLossSpec` / `MonolithicLossOutput`, the flat-arg kernel and its dispatcher; reverted head.py to main. - GSPO reverts to standalone-only (segment seam / backward moved back to policy_gradient); folding it into `MonolithicLoss` is a follow-up. - `_get_reference_model_logits` resolves the head logits by splitting on `.losses.`, so distillation works whether flat or nested in the composite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Kill the four `*_combinable` module-level indirection functions (`z_loss_combinable`, `cross_entropy_from_labels_combinable`, `distillation_combinable`, `grpo_combinable`): inline each into its loss's `combinable_core`, now a `@staticmethod` so both the monolithic loop and the standalone `fused_*` kernel call the one function. Add an intermediate `CombinableLoss` base owning the shared combinable machinery — `register_combinable_extras` (moved off the general `LanguageModelLoss` base) and a `_accumulate_grad` staticmethod that replaces the cast-and-accumulate tail duplicated across the z-loss / GRPO standalone kernels and the monolithic core. `fused_z_loss_forward_backward` and `fused_grpo_loss_forward_backward` now route through their `combinable_core`. Verified: staticmethod dispatch still fuses with no graph breaks (`fullgraph=True`); head + functional + distributed TP suites pass. Net -80 lines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Delete the `fused_z_loss_forward_backward` / `fused_grpo_loss_forward_backward` module functions — their body was just `softmax -> combinable_core -> accumulate` (a one-child monolithic), kept module-level only so tests could import them. Add `CombinableLoss.combinable_forward_backward` (compiled) as that standalone path; z-loss / GRPO `_forward_backward` route their non-triton branch through it, and the tests now construct the loss object and exercise the method directly (the tensor-parallel `group` is passed per call, so a trivial single-rank object suffices — including in the distributed subtests). GRPO's non-triton path now gets its metric family from the shared softmax via `combinable_core` instead of a separate `compute_grpo_metrics` pass (numerically identical, one fewer softmax); the triton path keeps the separate pass since its kernel emits no metrics. Verified: `combinable_forward_backward` fuses with no graph breaks (`fullgraph`); head + functional + distributed TP suites pass (651 / 21 skipped). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Label-entropy and distillation now run their non-triton standalone path through
`CombinableLoss.combinable_forward_backward` (softmax -> combinable_core ->
accumulate), the same as z-loss / GRPO. This makes the general
`fused_entropy_loss_forward_backward` kernel and its three compiled base-wrappers
(`_fused_cross_entropy_base_from_{labels,distribution}`,
`_fused_reverse_kl_base_from_distribution`) unused, so they are deleted — the
shared `_core` functions remain the single source of truth. Numerically
identical: both paths already went through the same cores.
Tests build the label / distillation loss object and exercise the method (triton
path still calls its kernel directly). The `probabilities` target format is
dropped from the entropy suite — no loss config consumes it (distillation is
logits-only), so it was general-kernel-only coverage of an unreachable path.
Verified: all entropy variants fuse with no graph breaks (`fullgraph`); head +
functional + distributed TP suites pass (531 / 21 skipped).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The float32-vs-float16 tolerance selector compared `data_type` (the imported module `fast_llm.engine.config_utils.data_type`) to `DataType.float32`, which is always False — so the fused/triton comparisons always used the loose 1e-4 threshold, even in float32. Compare the `dtype` parameter instead, so float32 cases use the intended 1e-5; drop the now-unused module import. All float32 entropy/z-loss cases pass at the tightened tolerance. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#507) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The forward_backward/_forward_backward template pair (skip -> _forward_backward -> register -> weight) is a property of single-scalar losses, not the loss abstraction itself. Move it into a new SingleLoss intermediate; make the base LanguageModelLoss.forward_backward abstract. Composite losses (MonolithicLoss) now satisfy forward_backward directly instead of carrying a dead _forward_backward override that only existed to satisfy the abstract method. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fine-review follow-up: remove bench_monolithic.py from tracking (a stale local
dev script that should never have been committed), and drop downstream-consumer
references from the new loss/functional docstrings and comments per project
style ("describe what the thing does, not which caller uses it"). Also fixes the
cross_entropy_from_labels_core docstring, which claimed GRPO routes through it
(GRPO calls predicted_logits_from_labels directly).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-drop the stale local dev benchmark that a prior git add re-staged; it should remain an untracked local file, not part of the PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… TP (#507) - Reject `loss_type: reverse_kl` for a `label` loss at config time (labels are one-hot, so it is undefined; the triton path already asserted this, the compiled path silently computed cross-entropy). - Reject a `use_triton` flag on a monolithic child loss, which the fused path ignores. - Add a monolithic composite test (cross-entropy + z-loss sharing one softmax) checked against the two losses run standalone, covering the tensor-parallel distributed path where several per-loss all-reduces meet one shared softmax. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop unnecessary forward-ref quotes and parameterize the core's list return type; add strict=True to the child zip; fix the log-prob docstring (/ -> -). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A zero-weight loss contributes an all-zero gradient. Return grad_output None for it so the backward term drops out of both the standalone and fused paths, while its scalar is still logged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce a CombinableLossConfig marker base owning use_triton; the combinable losses inherit it (GRPO via multiple inheritance alongside the policy-gradient base). The monolithic validation now checks isinstance and reads the narrowed use_triton directly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Children were built with the raw head scale, dropping the composite's own logits_scale_factor field; pass self._logits_scale_factor so it stacks like the weight field does. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Declare get_inputs/fused_core on CombinableLoss; drop the redundant losses-not-None guard (metrics-not-None already implies it); remove a no-op f-string prefix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Authored by Claude Opus 4.8 (Claude Code).
Summary
Implements the
torch.compileportion of #507: a monolithic head loss that runs the vocabulary softmax once and shares it across a set of combinable losses, emitting each loss's scalar / metrics and the combined logits gradient in a single fused pass — instead of the per-loss loop where each loss re-softmaxes the full vocab and re-issues its own tensor-parallel all-reduces.It is exposed as a loss type, not a head mode: configure a
monolithicloss whose nestedlossesare the combinable set.Approach
MonolithicLoss/MonolithicLossConfigis adynamic_typeloss. The head is unchanged — it loops overhead.lossesand threads one gradient buffer, so a non-combinable loss (e.g. DPO) is just a sibling entry that runs its ownforward_backward. There is no head-level toggle, no fallback dispatch, and no central spec object.z_loss_combinable) pluscombinable_extract(eager kwargs → tensors) andcombinable_core(the math). Its standalonefused_*kernel and the monolithic loop both call the same function, so the per-loss math lives once._monolithic_corecomputes the student softmax once, then loops the child losses callingchild.combinable_core(...). The child set is fixed per config, so the loop unrolls and eachcombinable_coreinlines inside one@torch.compilegraph — confirmed to fuse with no graph breaks. Gradient contributions accumulate in fp32 and cast to the logits dtype once.MonolithicLossConfig._validaterequires its entries to be combinable and to agree onlogits_scale_factor(one softmax = one scale). Duplicates are allowed and simply run twice over the shared student softmax.Loss coverage
In the monolithic loss: label cross-entropy, z-loss, from-distribution cross-entropy / forward-KL / reverse-KL (distillation, with temperature), and the GRPO objective +
new_logprobs+ the GRPO metric family + entropy — all from the shared softmax, which removes the second softmax pass introduced in #494.Out of scope / follow-ups. DPO (span-based + autograd + reference model) stays a standalone sibling loss. GSPO is a standalone loss for now (its per-segment eager seam doesn't fit the uniform
core -> (loss, grad)shape); folding it intoMonolithicLossis a planned follow-up. The Triton port is also a follow-up.Correctness
The per-loss kernels remain the equivalence oracle: the standalone
fused_*kernels (now sharing the same combinable cores) validate against independent references, and the monolithic loss is validated end-to-end at the head level against the same references — across masking × cross-entropy-splits × dtypes, multi-token-prediction, tied embeddings, logit scaling, and softcap. GSPO's standalone three-phase form (compiled forward → eager segment seam → compiled backward) is unchanged and keeps its tensor-parallel coverage.Validated on CPU (torch.compile) plus the distributed gloo
world_size=2chain for the tensor-parallel vocab-sharded path: head suite129 passed, functional suite260 passed, distributed262 passed.Performance
Opt-in, so this is upside rather than a default change. The fusion mechanism is a single shared softmax + one fused gradient pass across the enabled losses, so combined-loss configs avoid the redundant softmax/all-reduce passes of the per-loss loop. (Indicative single-GPU functional numbers from the earlier kernel structure showed ~1.5–1.7× on combined-loss configs with identical peak memory; a re-benchmark of the loss-type path is a follow-up. Tensor-parallel configs additionally save the redundant all-reduce set.)
Bundled fix
Per-loss distillation now honors its
temperatureconfig — the standalone path previously dropped it and defaulted to 1.0. Existing distillation runs withtemperature != 1.0will see corrected behavior.Files
loss/monolithic.py—MonolithicLoss+ the compiled_monolithic_coreentry loop.loss/config.py—MonolithicLossConfig+ acombinableflag on each loss config.loss/z_loss.py,loss/entropy_loss.py,loss/policy_gradient.py— per-losscombinable_*methods + the shared post-softmax functions; standalonefused_*kernels routed through them. GSPO's segment seam / backward moved back here.functional/entropy_loss.py— the shared math cores (softmax_base,predicted_logits_from_labels,cross_entropy_from_labels_core,z_loss_core, the from-distribution cores).loss/loss.py—register_combinable_extrashook;_get_reference_model_logitsresolves the head logits robustly for nested losses.language_model/head.py— reverted tomain(no monolithic-specific code).tests/layers/test_lm_head.py,tests/layers/test_lm_losses.py— head-level parity for themonolithicloss type; standalone kernel + reference coverage retained.🤖 Generated with Claude Code