Losses¶
Two loss functions are registered in
kempnerforge/training/loss.py:
cross_entropy and chunked_cross_entropy. z-loss is a regularizer
that wraps either — not a separate registry entry.
cross_entropy¶
[train]
loss_fn = "cross_entropy"
Direct call to F.cross_entropy over flattened
(batch·seq_len, vocab_size) logits and (batch·seq_len,) labels.
ignore_index = -100 is hard-coded; labels with -100 are skipped in
the reduction. This is how
document-packed sequences mask out cross-document
attention — tokens at packing boundaries get label -100 so the loss
doesn’t cross document edges.
When every label is -100 it returns an exact fp32 zero that stays on
the autograd graph of the logits, so there is no NaN from dividing by
zero valid tokens and backward and FSDP’s collectives still run on
that rank.
chunked_cross_entropy¶
[train]
loss_fn = "chunked_cross_entropy"
ce_chunk_size = 4096 # 0 -> auto 4096
Identical output to cross_entropy, but computed in chunks along the
flattened token dimension. Use this when the full logit tensor would
be large enough to matter — a 7B Llama-3 with batch=4, seq=4096,
vocab=128000 materializes an 8 GB float32 cross-entropy
intermediate if computed end-to-end.
Mechanics:
The loop iterates
for i in range(0, num_tokens, chunk_size)and accumulates per-chunk loss withreduction="sum".At the end, divides by the number of non-ignored tokens across all chunks to recover the mean.
ignore_index = -100is threaded through per chunk.
Important: chunking saves memory inside the loss kernel, not in the
output projection. The (batch, seq_len, vocab_size) logit tensor
that the model produces is still materialized — chunked_cross_entropy
only avoids the second full-size tensor that F.cross_entropy
internally allocates for the softmax.
ce_chunk_size = 0 in the config is a sentinel for “use the default
4096” — see
build_loss_fn.
fp32 cross-entropy¶
[train]
loss_fn = "cross_entropy" # or chunked_cross_entropy
fp32_cross_entropy = true # default false
Under mixed_precision = "bf16" the output head returns bf16 logits,
and both losses above compute the log-softmax, the loss and its
gradient in bf16. Stacks that run the forward pass under
torch.autocast compute cross-entropy in fp32 instead.
fp32_cross_entropy = true does the same for either loss, without the
fp32 copy of the logits (8.4 GB for batch=4, seq=4096,
vocab=128256) that upcasting them would allocate, and without the
fp32 log-probabilities autograd would keep for backward. It changes
training numerics slightly. With the default false, both losses are
unchanged.
Both losses then run one custom autograd function in fp32 (fp64 logits stay fp64):
Forward: takes the log-sum-exp and target logit in fp32 and keeps only the per-token log-sum-exp (one fp32 value per token).
Backward: recomputes the softmax from that log-sum-exp, subtracts the one-hot target, and writes the gradient in the logits’ dtype. Rows labeled
-100get a zero gradient.
cross_entropy does this over the whole batch at once;
chunked_cross_entropy does it ce_chunk_size tokens at a time. On
CUDA both steps are torch.compiled (once per process, as
FlexAttention is) into fused kernels that read the bf16 logits
directly. On CPU the same functions run eagerly.
Measured on one H100 for batch=4, seq=4096, vocab=128256 (bf16
logits of 3.9 GiB), loss forward and backward:
time |
memory beyond the logits |
loss error vs fp64 |
|
|---|---|---|---|
|
12.4 ms |
11.7 GiB |
2.1e-3 |
|
35.4 ms |
23.5 GiB |
4.3e-8 |
|
8.8 ms |
4.9 GiB |
7.5e-8 |
The first call on each new shape compiles for a few seconds.
Limits:
If
torch.compileis disabled or falls back to eager on CUDA, whole-batchcross_entropyholds several fp32 tensors the size of the logits at once in its backward;chunked_cross_entropybounds them to one chunk. Usechunked_cross_entropyif compile is off.The backward cannot itself be differentiated: with
create_graph=Truethe gradient comes back detached, so a second backward through it raises.The flag is rejected for any
loss_fnother than these two.
z-loss (logit magnitude regularizer)¶
[train]
loss_fn = "cross_entropy" # or chunked_cross_entropy
z_loss_weight = 1e-4 # 0 -> disabled
z-loss penalizes large log-sum-exp values — equivalent to penalizing the softmax normalizer from running off. It is a PaLM / Gemma trick for keeping logits in a numerically sane range without clamping.
Formula:
z_loss = z_loss_weight * mean(logsumexp(logits)^2)
How it wires in: build_loss_fn wraps the base loss with a closure
that adds z_loss(logits, z_weight) to the returned loss. The wrap
happens once at setup; the per-step overhead is one logsumexp over
the logit tensor.
Recommended weights from the literature:
Value |
Source |
|---|---|
|
PaLM |
|
Gemma |
|
off; default |
z_loss_weight = 0.0 makes the wrap a no-op — the closure still runs
but z_loss() returns 0.0 immediately without computing
logsumexp.
MoE auxiliary loss is separate¶
The MoE load-balancing loss is not part of this pipeline. It
lives on the model (model.get_moe_aux_loss()) and is added to the
base loss in the training loop:
loss = loss_fn(logits, labels)
if mc.is_moe:
loss = loss + mc.moe_aux_loss_weight * model.get_moe_aux_loss()
See MoE for how get_moe_aux_loss() sums the
router aux losses: one per MoE layer, and on a MoT model one per
modality on each MoE layer.
Picking one¶
Situation |
Loss |
z-loss |
|---|---|---|
Small model, tight compile, small vocab |
|
off |
7B+ with large vocab (≥ 50K) |
|
optional |
Muon / aggressive LRs / long runs |
either |
|
PaLM / Gemma-style recipe |
|
|
The shipped
7b_16gpu_muon.toml
is the reference combination: Muon + chunked cross-entropy +
z-loss — exercises all three paths together.
See also¶
Training loop § Non-PP step — where
loss_fn(logits, labels)is called.Configuration § TrainConfig —
loss_fn,ce_chunk_size,fp32_cross_entropy,z_loss_weight.MoE — how the MoE aux loss composes with the cross-entropy / z-loss stack.