Repository navigation
Stop the log and rsqrt epsilon defaults underflowing to zero in fp16 - #2887
Open
NishchayMahor wants to merge 1 commit into
Open
NishchayMahor wants to merge 1 commit into
NishchayMahor wants to merge 1 commit into
Conversation
`log` and `rsqrt` cast their default epsilon to the input dtype:
epsilon=nptype_from_builtin(self.x.dtype)(1e-45) # log
epsilon=nptype_from_builtin(self.x.dtype)(1e-12) # rsqrt
Both values are representable in fp32 but underflow to exactly 0 in
fp16, so the stabilizer both docstrings promise disappears for the dtype
most Core ML models run in. Constant folding then returns infinities:
rsqrt fp16: epsilon=0.0 -> [inf inf]
rsqrt fp32: epsilon=1e-12 -> [1.0e+06 9.9995e+03]
log fp16: epsilon=0.0 -> [-inf -inf]
log fp32: epsilon=1e-45 -> [-103.27893 -18.420681]
(x = [0.0, 1e-8] in each case.)
`_epsilon_for_dtype` floors the cast value at the smallest positive
subnormal for the dtype, so it can never land on zero. fp32 is
unaffected -- both defaults are representable there and come back
unchanged, which the tests pin. fp16 gets 6e-08 instead of 0.
A side effect worth noting: fp16 `rsqrt(0)` now folds to 4096.0, which is
what the Core ML runtime already returns for that input, so constant
folding and the runtime agree for fp16 where they previously did not.
`inverse` casts its epsilon the same way but defaults to 1e-4, which is
representable in fp16, so it is left alone.
This branch has not been deployed
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 #2886.
What was wrong
logandrsqrtcast their default epsilon to the input dtype:np.float16(1e-45)andnp.float16(1e-12)are both exactly0.0. Both docstrings describe epsilon as "a small constant that is added to the input ... for stability", and in fp16 it was not being added at all:What I changed
_epsilon_for_dtype(builtin_dtype, value)casts as before, then floors the result at the smallest positive subnormal for that dtype so it can never land on zero.logandrsqrtcall it instead of casting directly.I used
np.nextafter(dtype(0), dtype(1))rather thannp.finfo(...).smallest_subnormalbecausesmallest_subnormalonly arrived in numpy 1.22 andsetup.pystill allowsnumpy >= 1.14.5.fp32 is untouched. Both defaults are representable there and come back bit-identical; there is a test pinning that, because the alternative — picking a new epsilon magnitude for every dtype — would silently change existing fp32 models. fp16 gets
6e-08instead of0.0, which is the smallest change that restores the documented behaviour.inversecasts its epsilon the same way but defaults to1e-4, which is representable in fp16, so I left it alone rather than touch all three for symmetry.How this helps
Any fp16 model where an
rsqrtorloginput can reach zero at convert time was folding toinf/-inf, which then propagates asNaNthrough whatever consumes it.rsqrtis the core of RMSNorm, so a zero-variance row is a realistic way to hit it. After this change those fold to finite values in fp16 as they already did in fp32.There is a second effect I did not plan for and think is worth flagging: fp16
rsqrt(0)now folds to4096.0, which is exactly what the Core ML runtime returns for that input. So for fp16, constant folding and the runtime now agree where they previously did not. The fp32 disagreement described in #2886 is unaffected by this change and still open.Tests
Two classes in
test_elementwise_unary.py:TestEpsilonForDtype— unit tests on the helper: never zero for fp16 or fp32, fp32 returns the value unchanged, fp16 falls back to the smallest subnormal.TestFp16EpsilonKeepsFoldedValuesFinite— the behavioural consequence, written so it does not depend on the helper existing. On currentmainonly the two fp16 cases fail and both fp32 cases pass, so it discriminates rather than failing everywhere:With the change,
tests/iOS15/test_elementwise_unary.pyis 20 passed.What I did not verify
tests/iOS15/as a whole has two pre-existing failures on this machine —TestCropResize::test_builder_to_backend_smokeandTestReshape::test_reshape_with_zero, both on the fp16 mlprogram backend. They fail identically on cleanmain, so they are not from this change, but I have not looked into them.I also have not measured the effect on a real converted model. The evidence here is at the op and folding level.