Skip to content

Fuse GSPO into the monolithic compiled loss (#507)#552

Open
jlamypoirier wants to merge 1 commit into
jlp_monolithic_head_lossfrom
jlp_gspo_monolithic
Open

Fuse GSPO into the monolithic compiled loss (#507)#552
jlamypoirier wants to merge 1 commit into
jlp_monolithic_head_lossfrom
jlp_gspo_monolithic

Conversation

@jlamypoirier

Copy link
Copy Markdown
Collaborator

Claude Opus 4.8 authored this PR. Follow-up to #549 (base is that branch; retarget to main once it merges).

What

Lets GSPO participate as a child of the monolithic loss type, so it shares the vocabulary softmax with another combinable loss (realistically z-loss) instead of running its own softmax pass.

Why it needed special handling

The other combinable losses (CE, z-loss, distillation, GRPO) finish their loss + gradient in a single torch.compile boundary via fused_core. GSPO can't: its eager segment-aggregation seam (index_add_ over a per-batch num_segments + SDP/SP all-reduce + clipping) must sit between forward and backward and stay out of any compiled boundary (else it recompiles per num_segments). So GSPO's loss and gradient are both unknown until after an eager step.

Approach — deferred combinable child

  • GSPO's fused_core runs only its forward on the shared softmax and returns (None, None, forward_state).
  • CombinableLoss gains a finish hook (default no-op passthrough). After the compiled _monolithic_core, MonolithicLoss.forward_backward calls child.finish(...) per child; GSPO runs its existing gspo_segment_seam (eager) + gspo_backward_core (compiled) there, accumulating into the shared gradient buffer.
  • Non-seam children are unchanged — they finish inside fused_core and their finish is the no-op default.

Shared: the forward softmax (one softmax_base + its TP all-reduce) across GSPO and its sibling. Not shared: GSPO's backward stays a separate compiled kernel, since it depends on the eager seam output — inherent to the seam, unavoidable in the compiled path.

LanguageModelGSPOLossConfig becomes a CombinableLossConfig (mirroring GRPO), so use_triton is inherited and the monolithic validators accept it.

Tests

tests/layers/test_lm_head.py: GSPO added to the monolithic wrap-set, with new fused_gspo_loss and fused_gspo_and_z_loss configs (masked + unmasked, single-split — GSPO can't be split). Each is validated against the same independent reference as its per-loss equivalent. Full test_lm_head.py + test_lm_losses.py: 575 passed, 21 skipped (CPU).

Not in scope

The Triton monolithic kernel (that path is naturally three-phase and hosts GSPO's seam without this deferral machinery).

🤖 Generated with Claude Code

GSPO's eager segment-aggregation seam (index_add_ over a per-batch
num_segments) can't run inside the single torch.compile boundary that
fuses the other combinable losses. Make it a deferred combinable child:
`fused_core` runs only GSPO's forward on the shared softmax and returns
`(None, None, forward_state)`; `MonolithicLoss` then calls a new
`finish` hook per child, where GSPO runs its existing segment seam and
backward and accumulates into the shared gradient. Non-seam children
keep finishing inside `fused_core` (default no-op `finish`), so the
shared softmax is reused across e.g. GSPO + z-loss with no change to the
existing fused path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant