Skip to content

BUG: Avoid binary64 intermediates in long-double/SLEEF casts - #117

Draft
SwayamInSync wants to merge 4 commits into
numpy:mainfrom
SwayamInSync:fix-inter-backend-casts-112
Draft

BUG: Avoid binary64 intermediates in long-double/SLEEF casts#117
SwayamInSync wants to merge 4 commits into
numpy:mainfrom
SwayamInSync:fix-inter-backend-casts-112

Conversation

@SwayamInSync

@SwayamInSync SwayamInSync commented Jul 25, 2026

Copy link
Copy Markdown
Member

Closes #112.

This PR implements direct conversion in both directions:

  • long double -> SLEEF is exact.
  • SLEEF -> long double is correctly rounded to the target format under the active IEEE rounding mode.

The same implementation is used for:

  • QuadPrecDType inter-backend casts
  • NumPy longdouble -> SLEEF QuadPrecDType
  • SLEEF QuadPrecDType -> NumPy longdouble
  • casting="same_value" roundtrip checks

Widening: long double -> SLEEF

The conversion selects the cheapest exact path available:

  • Native IEEE binary128 long double: direct representation copy
  • Native __float128: direct compiler widening
  • long double == double: SLEEF's direct binary64 conversion
  • Portable fallback: exact radix-2 reconstruction in 32-bit significand chunks

The portable path uses frexp to decompose the source value and reconstructs its complete significand in binary128. Every intermediate is exactly representable, so no additional rounding occurs.

For example, these values now retain their bits:

np.longdouble("9007199254740993")
np.longdouble("1.000000000000000000867361737988403547...")

Narrowing: SLEEF -> long double

The portable path rounds binary128 directly to the target long-double format:

  1. Determine the target quantum for normal or subnormal values.
  2. Scale the binary128 value to integer quantum units.
  3. Round once according to the active floating-point rounding mode.
  4. Reconstruct the rounded long-double value exactly.

Supported rounding modes:

  • round to nearest, ties to even
  • downward
  • upward
  • toward zero

Underflow, subnormal boundaries, the normal/subnormal transition, maximum finite values, and overflow midpoints are handled explicitly.
Native compiler casts are used where SLEEF exposes IEEE binary128 directly.

The long-double exponent range must fit within binary128.

Testing

Added a parameterized cast suite covering 72 generated cases:

  • Values beyond binary64 precision
  • Both Quad backend directions
  • NumPy longdouble cast surfaces
  • safe, same_kind, and same_value
  • Normal, subnormal, underflow, and overflow boundaries
  • Nearest-even halfway cases
  • All four IEEE rounding modes
  • Positive and negative values
  • Signed zero, infinities, and NaNs
  • Exponent-wide adjacent-value midpoint checks
  • 256 randomized exact long-double roundtrips
  • Empty, 1-D, 2-D, and 3-D arrays
  • Contiguous, reversed, and unaligned storage
  • Concurrent execution under pytest-run-parallel

Performance

For one million elements on x86-64:

Cast Before After
NumPy long double -> SLEEF 24.5 ms 17.5 ms
Quad long double -> SLEEF 25.7 ms 18.8 ms
SLEEF -> NumPy long double 22.9 ms 37.5 ms
SLEEF -> Quad long double 28.5 ms 46.6 ms

Widening is faster. Correct binary128-to-x87 narrowing is slower than the old binary128-to-double-to-x87 shortcut because it retains the target's full 64-bit significand and performs the required target-format rounding.

@SwayamInSync
SwayamInSync marked this pull request as draft July 25, 2026 12:34
@SwayamInSync

SwayamInSync commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

Sleef's intermediate triple-double is lossy and impacts cast_to_double

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: inter-backend casts lose precision by converting through double

1 participant