Clip gradients with the norm the engine already computed - #5143
Open
NuojCheng wants to merge 1 commit into
Open
Conversation
`optax.clip_by_global_norm` -- which `maxtext_utils.apply_gradient_clipping` wraps -- recomputes `global_norm(grads)` internally, one line after `_update_kernel` computed it for reporting. That was a second full pass over every gradient plus a second cross-replica reduction. This is a numerical change, not only a saving, which is why it is its own PR rather than a drive-by. The engine's norm is computed in float32 because a sum of squares over bf16 overflows on production-size models; optax's runs in the gradients' own dtype. So under `grad_dtype=bfloat16` the engine reported a guarded norm and then clipped with an unguarded one, and it now clips with the guarded one. At the default `grad_dtype=float32` the two are the same number and nothing changes. Having the norm in hand is also what makes the division guardable. `jnp.where` evaluates both branches, so an all-zero gradient tree divides by zero on the branch it discards. optax has this today -- it divides unguarded and leans on `lax.select` to drop the result -- and stops under `debug_nans`; the test asserts both that this does not and that optax does. The fp8 path stays with optax: `apply_gradient_clipping` holds `OVERWRITE_WITH_GRADIENT` out of both the norm and the scaling, and that carve-out is not worth reproducing for the saving.
NuojCheng
requested review from
A9isha,
RissyRan,
SurbhiJainUSC,
abhinavclemson,
aireenmei,
bvandermoon,
darisoy,
dipannita08,
gagika,
gobbleturk,
hengtaoguo,
huytransformer,
igorts-git,
jiangjy1982,
khatwanimohit,
richjames0,
shralex,
shuningjin,
vipannalla and
xibinliu
as code owners
September 4, 2026 16:56
4 tasks
There was a problem hiding this comment.
Code Review
This pull request introduces a custom _clip_by_grad_norm method in maxtext_engine.py to optimize gradient clipping by reusing the already computed gradient norm, avoiding redundant computations and cross-replica reductions. It also safely guards against division-by-zero errors on all-zero gradients, preventing issues under debug_nans. Unit tests are added to verify correctness and the division-by-zero guard. I have no feedback to provide as there are no review comments.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
4 tasks
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.
Split out of #5138, which was a deferral change and had no business carrying this.
What
_update_kernelcomputesgrad_normfor reporting, then one line later callsmaxtext_utils.apply_gradient_clipping, whoseoptax.clip_by_global_normcomputesglobal_norm(grads)again. This clips with the norm already in hand instead. The scale isoptax's exactly:
1under the threshold,threshold / normover it.This is a numerical change, not only a saving
The engine's norm is computed in float32 -- deliberately, because a sum of squares over
bf16 overflows on production-size models -- while optax's runs in the gradients' own dtype.
So under
grad_dtype=bfloat16the engine has been reporting a guarded norm and clippingwith an unguarded one, and it now clips with the guarded one.
At the default
grad_dtype=float32the two are the same number and nothing changes.Only configs that set
grad_dtype: bfloat16see different clipped gradients, and for thosethe new behaviour is the correct one. That divergence is the reason this is a separate PR:
it should be reviewed on its own terms rather than approved as part of a deferral change.
Division-by-zero
Addresses the review left on #5138. Having the norm in hand is what makes the division
guardable, so the guard comes for free here:
This is the same idiom the accumulated-denominator division 10 lines above already uses.
Worth noting the direction: optax is the side that fails this.
clip_by_global_normdivides by the norm unguarded and leans on
lax.selectto discard the result, so on anall-zero gradient tree the value is right but a NaN is computed to get there, and
JAX_DEBUG_NANSstops on it. So this removes a hazard MaxText has today rather thanavoiding one it would have introduced.
test_clip_by_grad_norm_does_not_divide_by_zeroasserts both halves of that.
fp8
Unchanged, still routed to optax:
apply_gradient_clippingholdsOVERWRITE_WITH_GRADIENTout of both the norm and the scaling, and reproducing that carve-out is not worth the
saving.
Tests
tests/post_training/unit/maxtext_engine_test.py-- 52 passed on CPU.test_clip_by_grad_norm_matches_optax-- both sides of the threshold, against optaxtest_clip_by_grad_norm_does_not_divide_by_zero-- all-zero tree underjax.debug_nansChecklist
Before submitting this PR, please make sure (put X in square brackets):
gemini-reviewlabel.