Skip to content

fix: apply loss cotangent to hidden-state grad in vocab tiling - #4699

Open
shadowlilac-oss wants to merge 2 commits into
AI-Hypercomputer:mainfrom
shadowlilac-oss:fix/vocab-tiling-loss-cotangen
Open

fix: apply loss cotangent to hidden-state grad in vocab tiling#4699
shadowlilac-oss wants to merge 2 commits into
AI-Hypercomputer:mainfrom
shadowlilac-oss:fix/vocab-tiling-loss-cotangen

Conversation

@shadowlilac-oss

Copy link
Copy Markdown

Description

Both vocab tiling backward rules apply the incoming loss cotangent to the parameter
gradients but not to the hidden-state gradient, so the cotangent returned for
hidden_states is off by a factor of 1/loss_cotangent. This PR adds the missing
multiply in vocab_tiling_linen_loss and vocab_tiling_nnx_loss, plus a regression
test for the NNX path.

The problem

_bwd_scan_body calls vjp_fn(1.0) in both paths, so the scan produces an unweighted
d(total_loss)/d(hidden). A custom_vjp backward has to return
cotangent * d(output)/d(input) for every input. The params get that:

grad_head = jax.tree_util.tree_map(lambda g: g * loss_cotangent, grad_head)

but grad_reshaped_hidden_states goes to the return statement unscaled.

In train.py, loss = xent_sum / (total_weights + EPS), so loss_cotangent is
1/total_weights. Because hidden_states is the decoder output, every gradient
reaching the transformer body is inflated by total_weights, which is 65,528 at a
batch of 8 and sequence length 8192. The output head is scaled correctly, so the
head-to-body gradient ratio is wrong by that same factor, and clip_by_global_norm
then hands nearly the entire update budget to the body.

Observed on a Qwen3-VL-4B pretraining run with vocab tiling enabled: a raw gradient
norm around 2e6 where the correctly scaled value is around 30, and a loss that spikes
far above ln(vocab_size) within the first steps and then descends slowly from a few
hundred.

This is not model specific. The path is gated only on config.num_vocab_tiling > 1,
so it affects any configuration that turns vocab tiling on.

Why the existing tests pass

Every test in VocabTilingNNXTest and LossAndGradientCorrectnessTest differentiates
the unnormalized total_loss returned by _tiled_loss_fn, so jax.grad seeds the
cotangent at exactly 1.0 and g * 1.0 == g. The missing multiply is algebraically
invisible. test_nnx_vocab_tiling_grad_over_hidden_states targets this exact cotangent
and still passes for that reason. The harness never composes vocab tiling with a
downstream scalar normalization, which is what train.py does.

Implementation

One line in each backward rule, placed beside the existing parameter scaling. No extra
dtype handling is required: both return statements already apply
.astype(reshaped_hidden_states.dtype), which covers the fp32 promotion introduced by
the scalar multiply.

Shortcomings and follow-ups

The regression test covers the NNX path only. The Linen change is the same one-line fix
verified by inspection, and the Linen tests share the cotangent-of-1.0 blind spot, so a
matching test there is a reasonable follow-up.

Tests

Added test_nnx_vocab_tiling_grad_applies_loss_cotangent to VocabTilingNNXTest in
tests/unit/tiling_test.py. It scales the loss by 1/total_weights to match train.py
and compares the hidden_states gradient against the full-vocab reference.

pytest tests/unit/tiling_test.py -k test_nnx_vocab_tiling_grad_applies_loss_cotangent

I could not execute it: every test in VocabTilingNNXTest is marked
@pytest.mark.tpu_only and I only have GPU hardware. Removing the marker locally
reproduces the failure, which appears as a jnp.allclose mismatch scaled by
batch_size * seq_len. A maintainer with TPU access should confirm red before and
green after.

Checklist

Before submitting this PR, please make sure (put X in square brackets):

  • I have performed a self-review of my code. For an optional AI review, add the gemini-review label.
  • I have necessary comments in my code, particularly in hard-to-understand areas.
  • I have run end-to-end tests tests and provided workload links above if applicable.
  • I have made or will make corresponding changes to the doc if needed, including adding new documentation pages to the relevant Table of Contents (toctree directive) as explained in our documentation.

The custom_vjp backward scales the parameter gradients by the incoming loss cotangent but not the hidden-state gradient, so the decoder body receives gradients total_weights times too large when num_vocab_tiling > 1. Fixes both the Linen and NNX paths.
Every existing test differentiates the unnormalized total_loss, so the
cotangent is always 1.0 and a missing multiply in the custom_vjp
backward is invisible. Scales the loss by 1/total_weights, matching
train.py, and checks the hidden_states gradient against the full-vocab reference.
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@shadowlilac-oss shadowlilac-oss changed the title Fix/vocab tiling loss cotangen fix: apply loss cotangent to hidden-state grad in vocab tiling Aug 2, 2026
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