Fix directed powi OOM on primitive-float underflow - #2
Open
cmpute wants to merge 2 commits into
Open
Conversation
DirectedPowI's native branch caught overflow (native not finite) but not
underflow: for an i32 exponent whose exact result is below the smallest
subnormal, f64::powi returns 0.0 (finite), so the guard did not fire and
execution fell through to eval_adaptive. dashu correctly returns the tiny
representable FBig (e.g. 5^(-2^31) ~= 2^-5e9, 53-bit significand), but the
adapter then materialized its exact rational in fbig_rat via
UBig::from(1) << 5.0e9 — exhausting memory (the fuzzer saw RSS climb to
600 MB+ before the kill).
Resolve it the same way overflow is resolved: a nonzero base that underflows
to ±0 is outside the primitive-finite range, so return the structural
directed endpoint from extreme_power (smallest subnormal under outward
modes, ±0 under inward/nearest) instead of computing it exactly. Mirrors
the existing outside-i32 branch's magnitude-based structural resolution.
This is an adapter-side fix: dashu's arbitrary-precision FBig legitimately
represents 2^-5e9 and returns it promptly; the OOM is the adapter's
unbounded materialization when bridging to a fixed-width float.
Verified: the OOM reproducer now runs in 0 ms (no allocation, no violation);
a 126-case boundary sweep (bases {0.5,0.999999,1.0000001,1.5,2,5,10} x
exponents {+-2^31, +-2^40, i32::MIN/MAX, +-10000} x Down/Up/Nearest) agrees
with the MPFR oracle on every case (0 mismatches).
Co-Authored-By: Claude <noreply@anthropic.com>
The previous commit's `native == 0.0` short-circuit was too aggressive: f64::powi is not correctly rounded and underflows to 0 even when the exact result is a representable subnormal, so it wrongly collapsed valid results like 2.0001^(-1024) (a real subnormal) to 0 — a correctness regression the fuzzer surfaced via opendp_sequences powi (differs from MPFR). Replace the native-zero trigger with the same magnitude gate the outside-i32 branch already uses: magnitude_log2 = exponent * log2(|base|), structural resolution only outside [-1200, 1200]. That still covers the OOM regime (5^(-2^31) ≈ 2^-5e9, far outside the range) while leaving every result inside the primitive range — including all subnormals — to exact, correctly-rounded evaluation (bounded ~1200-bit shift, no OOM). Verified: OOM reproducer still 0 ms / no allocation; the previously mis-rounded 2.0001^(-1024) now returns its correct subnormal; a 168-case boundary sweep agrees with MPFR on every case (0 mismatches). Co-Authored-By: Claude <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.
Summary
DirectedPowIcan drivefbig_ratto allocate gigabytes for a result whose magnitude is representable in dashu but astronomically beyond the primitive-float range. dashu returns that result promptly and correctly; the adapter then materializes its exact rational with an unbounded shift and exhausts memory.DirectedPowI::<f64, IBig>::eval(5.0, &IBig::from(-(1i64 << 31)), Direction::Up).5.0^(-2³¹) ≈ 2^-5.0e9;f64::powiunderflows this to0.0(finite), so the existing native guard does not fire;eval_adaptivecalls dashupowi, which correctly returns the tiny representableFBig(53-bit significand,repr.exponent() ≈ -5.0e9);fbig_ratthen runsUBig::from(1u8) << 5.0e9→ OOM (the fuzzer saw RSS climb to 600 MB+ before the kill). Reached viadirected_unary/opendp_sequences.Root cause
directed_powi!'s native branch catches overflow but not underflow (src/backend/dashu.rs):For an i32 exponent whose exact result is below the smallest subnormal,
f64::powireturns0.0— finite, so the guard does not fire and execution falls through toeval_adaptive→fbig_rat(which shifts by the huge exponent). Theelsearms only run when the exponent does not fit i32, so they never see this case.fbig_ratmaterializes the exact rational with an unbounded shift:dashu is innocent here:
FBig::<Up>::try_from(5.0).powi(IBig::from(-(1i64<<31)))returns in ~90 µs with a 53-bit significand andrepr.exponent() ≈ -5.0e9— a valid, representable value. The OOM is purely the adapter's unbounded materialization.Fix
Resolve underflow the same way overflow is already resolved — a nonzero base whose power underflows to ±0 is outside the primitive-finite range, so return the structural directed endpoint from
extreme_powerinstead of computing it exactly:This mirrors the existing outside-i32 branch's
magnitude_log2 outside [-1200, 1200]structural resolution, extended to the i32-exponent underflow case the native branch currently misses. (base == 0.0is excluded:0**n == 0exactly is cheap.)extreme_power(neg, overflow=false, …)yields the directed underflow endpoints: smallest positive/negative subnormal under outward modes,±0under inward/nearest.A defensive bound on
fbig_rat's shift is deliberately not included — it's belt-and-suspenders for other hypothetical huge-exponent paths, and this fix makes the known path unreachable; the exact policy (clamp vs. sentinel vs. panic) is better settled separately.Why the adapter, not dashu
dashu's
FBigis arbitrary-precision with anisizeexponent range (±~9.2e18 on 64-bit). A value like2^-5e9is genuinely representable and finite; returning it is correct. Capping dashu's notion of "representable" to what a primitive float can materialize would break arbitrary-precision users. The adapter, which bridges to fixed-width floats, is the right place to bound the materialization.Verification
OPENDP_NUM_VIOLATION) under a tightrss_limit_mb=512.{0.5, 0.999999, 1.0000001, 1.5, 2, 5, 10}× exponents{±2³¹, ±2⁴⁰, i32::MIN, i32::MAX, ±10000}×{Down, Up, Nearest}— agrees with the MPFR oracle on every case (0 mismatches), in ~635 µs total.powi(5, -2³¹)→Up=0x0000000000000001(min pos subnormal),Down/Nearest=0.🤖 Generated with Claude Code