Fix rms_norm eps inflation from max-|x| rescale; guard all-zero input - #2822
Open
nschlaepfer wants to merge 1 commit into
Open
Fix rms_norm eps inflation from max-|x| rescale; guard all-zero input#2822nschlaepfer wants to merge 1 commit into
nschlaepfer wants to merge 1 commit into
Conversation
The torch rms_norm handler rescales x by max(|x|) to prevent fp16
overflow on ANE, but adds eps after the rescale. Algebraically:
sqrt(mean((x/m)^2) + eps) * m == sqrt(mean(x^2) + eps * m^2)
so the effective epsilon is inflated by max(|x|)^2. For spiky
activations (max >> rms) this produces percent-level output errors at
any compute precision, including FLOAT32 / CPU_ONLY.
Fix:
- Rescale eps by 1/m^2 so the rescaled computation is exactly
sqrt(mean(x^2) + eps).
- Clamp the scale to >= 1: inputs with max|x| <= 1 cannot overflow
fp16 when squared, and the clamp fixes a 0/0 NaN on all-zero rows
that the unclamped rescale introduces.
Adds regression tests for both defects (spiky-input parity at fp32
with 1e-5 rel-L2 tolerance; converted-model all-zero input).
Fixes apple#2821
Co-Authored-By: Claude Fable 5 <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.
Fixes #2821.
Problem
The torch
rms_normhandler rescales the input bym = max(|x|)to prevent fp16 overflow on ANE, but addsepsafter the rescale:The effective epsilon is inflated by
m^2. For spiky activations (max|x| >> rms(x)— common in transformer residual streams and register/embedding vectors) this produces percent-level output errors at any compute precision, includingFLOAT32/CPU_ONLY. The rescale also introduces a0/0→ NaN on all-zero rows, which the epsilon was originally there to prevent.Fix
Two changes inside the handler, preserving the ANE overflow protection:
1/m^2so the emitted math is algebraically identical tosqrt(mean(x^2) + eps)— the rescale becomes exact up to floating-point rounding.>= 1(mb.maximum(m, 1.0)): inputs withmax|x| <= 1cannot overflow fp16 when squared, so no rescale is needed there, and the clamp fixes the all-zero-row NaN.The clamp also keeps the rescaled epsilon term well-behaved in fp16: whenever
mis the true max,mean((x/m)^2) >= 1/N, so the (possibly underflowing)eps/m^2term is only ever needed whenmis clamped to 1 — where it equalsepsexactly.Verification (M3 Max, torch 2.11, python 3.11)
0.01·randn, one 25.0 component), fp32/CPU_ONLY, rel-L2 vs PyTorchrandn, fp32, rel-L2300·randn), FLOAT16 — overflow protectiontest_rms_norm.pysuite¹
test_dynamic_shapescrashes identically before and after this change on my machine (EnumeratedShapes predict crash on a macOS beta) — unrelated to this fix.Both new regression tests fail on current
main's translation (spiky:rel_l2=3.19e-04vs 1e-5 gate; zero-input: NaN) and pass with this change.Tests added
test_spiky_input_eps_not_inflated— spiky-input parity at fp32 with a tight (1e-5) rel-L2 gate. The existing suite'srtol=1e-2tolerance is exactly why this bug was invisible to it.test_zero_input_converted_model— all-zero input through the converted model (the existingtest_edge_casesonly exercised the PyTorch reference, not the conversion).🤖 Generated with Claude Code