Repository navigation
Conversation
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/195077
Note: Links to docs will display an error until the docs builds have been completed. ✅ No FailuresAs of commit b84d28e with merge base 7fe5288 ( This comment was automatically generated by Dr. CI and updates every 15 minutes. |
This PR needs a
|
| return static_cast<scalar_t>(exponential(generator)); | ||
| auto sample = exponential(generator); | ||
| auto value = static_cast<scalar_t>(sample); | ||
| // Floor if the cast underflowed to 0; an exact 0 (lambda == inf) stays 0 |
There was a problem hiding this comment.
Feels like this should be a generic c10::safe_numerics utility maybe?
There was a problem hiding this comment.
I added as c10::cast_no_underflow in safe_numerics.h.
It picked up a second caller on the way, since the CUDA kernel's eps/2 floor is applied in accscalar_t and so still narrows to 0 for half once lambda >= 2.
3d0bc50 to
3d2ddb5
Compare
3d2ddb5 to
1531a9b
Compare
exponential_ samples in a wider type and narrows with static_cast<scalar_t>, and anything below float16's smallest subnormal becomes an exact 0. On CPU the kernel samples in double, so a sample under 2^-25 rounds to zero about 2.98e-8 of the time at lambda=1, growing with lambda. On CUDA the eps/2 floor inside transformation::exponential is applied in accscalar_t (float), which bounds the sample at 2^-24/lambda; that sits at or below half's round-to-even tie point once lambda >= 2, so it narrows to zero too. multinomial without replacement divides p by that sample in its Gumbel-trick path, 0/0 = NaN, and NaN wins argmax, so zero-probability categories get sampled. Only float16 is affected: bfloat16 and float32 subnormals are far below anything either kernel emits. c10::cast_no_underflow narrows and returns denorm_min with the input's sign when the cast would otherwise flush a nonzero value to zero, and both kernels use it in place of their static_cast. It lives in c10/util/safe_numerics.h rather than TypeCast.h because the template needs only <limits>, with the destination's numeric_limits instantiated at the call site, so the header stays cheap enough to include from CUDA kernels. An exact 0 stays 0, so exponential_(inf) still returns 0, which test_exponential asserts; the random stream is untouched and every other output value is bit-identical. Flooring before the cast cannot work: T is double in the CPU branch of transformation::exponential, and eps/2 there is about 1.1e-16, which still rounds to float16 0. Sampling on CPU in opmath float with the CUDA floor would change the random stream for fp16/bf16/fp32 and still emit zeros for lambda >= 2, so it would need this floor anyway. Fixes pytorch#192812 Test Plan: test_exponential_no_zero is now parametrized over lambda and also runs on CPU. At lambda=1024 about 3e-5 of the samples fall under the rounding threshold, so 5e7 samples give roughly 1500 zeros before the fix against 1 at lambda=1, which is what makes it a real regression detector on both backends. test_exponential_half_underflow pins a deterministic CPU case (seed 20 draws 4.5e-9 at index 5833), and test_multinomial_zero_prob_half is the issue repro batched as 200 rows over 151936 float16 categories. Both are red on f5f7ac4 and green with the fix. ``` python test/test_torch.py -k exponential python test/test_torch.py -k multinomial python test/test_torch.py -k kstest python test/test_mps.py -k exponential python test/test_mps.py -k multinomial ``` Exact zeros in 1e8 float16 CPU samples at lambda 1/2/8: 3/3/24 before, 0/0/0 after (seed 0, Apple M5). Throughput cost is within noise: float16 exponential_ on 1e7 samples 80.4 ms before, 81.3 ms after (minimum over 15 interleaved runs of the two builds); float64 unchanged since the compiler folds the check away there. No GPU on hand, so the CUDA change is unverified outside CI.
1531a9b to
b84d28e
Compare
Fixes #192812
Root cause
exponential_samples in a wider type and narrows withstatic_cast<scalar_t>. Anything below float16's smallest subnormal becomes an exact 0.On CPU the kernel samples in double, so a sample under
2^-25rounds to zero about 3 times per 1e8 draws atlambda=1. On CUDA theeps/2floor insidetransformation::exponentialis applied inaccscalar_t(float), which bounds the sample at2^-24/lambda; that sits at or below half's round-to-even tie point oncelambda >= 2, so it narrows to zero as well.multinomialwithout replacement dividespby the sample (Gumbel trick), so a zero turns a zero-probability category into0/0 = NaN, and NaN winsargmax. Only float16 is affected; bfloat16 and float32 subnormals are far below anything either kernel emits.Fix
c10::cast_no_underflow<To>(value)narrows, and returnsdenorm_minwith the input's sign when the cast would otherwise flush a nonzero value to zero. Both kernels use it in place of theirstatic_cast.It lives in
c10/util/safe_numerics.hrather thanTypeCast.hbecause the template itself needs only<limits>(the destination'snumeric_limitsis instantiated at the call site), so the header stays cheap enough to include from CUDA kernels.The random stream is untouched and an exact 0 stays 0, so
exponential_(inf)still returns 0 (asserted bytest_exponential) and every other output value is bit-identical.Flooring before the cast does not work, since
eps/2of a double still rounds to float16 0. Sampling on CPU in float with the CUDA floor would change the random stream for fp16/bf16/fp32 and still emit zeros forlambda >= 2, so it would need this floor anyway.Verification
test_exponential_no_zerois now parametrized over lambda and also runs on CPU. Atlambda=1024roughly 3e-5 of the samples fall under the rounding threshold, so 5e7 samples produce about 1500 zeros before the fix against 1 atlambda=1; that is what makes it a real regression detector on both backends.test_exponential_half_underflowpins a deterministic CPU case (seed 20, index 5833).Zeros in 1e8 float16 CPU samples (seed 0, Apple M5):
Perf is within noise: float16
exponential_on 1e7 samples goes 80.4 ms to 81.3 ms (minimum over 15 interleaved runs of the two builds); float64 is unchanged, the compiler folds the check away there.I have no GPU on hand, so the CUDA change is unverified outside CI.
cc @jgong5 @mingfeima @XiaobingSuper @sanchitintel @ashokei @jingxu10 @aditew01