Skip to content

Adding __reduce__ method to QuadPrecision for supporting pickle#105

Open
SwayamInSync wants to merge 6 commits into
numpy:mainfrom
SwayamInSync:fix-pickle-scalar-99
Open

Adding __reduce__ method to QuadPrecision for supporting pickle#105
SwayamInSync wants to merge 6 commits into
numpy:mainfrom
SwayamInSync:fix-pickle-scalar-99

Conversation

@SwayamInSync
Copy link
Copy Markdown
Member

@SwayamInSync SwayamInSync commented May 19, 2026

closes #99

Fixing exp_digits=4 (since float128 can have max exponent of 4932) at each place of Dragon4 call to keep consistency in codebase

@SwayamInSync SwayamInSync requested a review from ngoldbaum June 5, 2026 06:46
Copy link
Copy Markdown
Member

@ngoldbaum ngoldbaum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked claude to review this and it spotted some issues (unverified by me):

  • The parsing relies on Sleef_strtoq, which Claude identified as not being lossless for the decimal case: sleef/src/quad/sleefsimdqp.c:3504-3546. Only hex-encoded values are losslessly converted.
  • You chose to serialize to the shortest representable form, but that leaves no slack for rounding error.

Elsewhere in the codebase you use quad_to_string_same_value_check to guard against this possibility but didn't use it in the new __reduce__ codepath, so it's possible to write values to disk that can't be round-tripped.

The new tests don't check places where the rounding error would be particularly bad: subnormals like 6.5e-4966 and values near the maximum representable value.

It also found a pre-existing issue in the dragon4 implementation: the hasUnequalMargins flag is computed after the mantissa bit it is OR'd in, so it's always false. It suggests this instead:

diff --git a/src/csrc/dragon4.c b/src/csrc/dragon4.c
index f86f269..405d751 100644
--- a/src/csrc/dragon4.c
+++ b/src/csrc/dragon4.c
@@ -1925,7 +1925,7 @@ Dragon4_PrintFloat_Sleef_quad(Sleef_quad *value, Dragon4_>
     /* factor the value into its parts */
     if (floatExponent != 0) {
         /* normal */
-        mantissa_hi = (1ull << 48) | mantissa_hi;
+        mantissa_hi = (1ull << 48) && mantissa_lo == 0;
         /* mantissa_lo is unchanged */
         exponent = floatExponent - 16383 - 112;
         mantissaBit = 112;

That's a pre-existing issue though and I haven't confirmed it.

It also generated this script which finds values that don't round-trip using several different methods: https://gist.github.com/ngoldbaum/0bbe4ed5d2479a76885b3f3c2ed7233a#file-rt_hunt-py

Comment thread src/csrc/casts.cpp
const char *scientific_str = Dragon4_Scientific_QuadDType_CStr(sleef_val, DigitMode_Unique,
SLEEF_QUAD_DECIMAL_DIG, 0, 1,
TrimMode_LeaveOneZero, 1, 2);
TrimMode_LeaveOneZero, 1, 4);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exp_digits is the minimum width of the exponent field (zero-padded).
Dragon4 always emits every significant exponent digit regardless, so this has no effect on round-trip correctness.
e+4932 prints in full at exp_digits=2 just as at 4.
The change is purely for consistency width across all formatting paths __repr__ and __reduce__ already use exp_digits=4, and binary128 exponents span up to 4 digits (±4932), so a 4-digit field formats every value consistently. casts.cpp was the only site still at 2, which produced variable-width exponents (...e+05 vs ...e+0005) for the same dtype depending on code path so setting it to 4 makes the scientific output uniform everywhere.

Comment thread src/csrc/scalar.c Outdated

static PyObject *
QuadPrecision_str(QuadPrecisionObject *self)
// This is just define here for debugging, we actually use QuadPrecision_str_dragon4 for __str__ method.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which means it's dead code, right? why not delete it? If you really want to use it for debugging then expose a private interface for it, IMO

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup this and its repr cousin both, will delete them in this PR

Comment thread tests/test_quaddtype.py Outdated

@pytest.mark.parametrize("backend", ["sleef", "longdouble"])
def test_pickle_scalar_issue_repro(self, backend):
"""The exact repro from #99: was RuntimeError on loads()."""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sort of comment isn't very helpful, IMO the test name is enough. I'd adjust your prompting to encourage the model to not comment like this.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it

Comment thread tests/test_quaddtype.py Outdated
assert loaded[3] == original[3]

@pytest.mark.parametrize("backend", ["sleef", "longdouble"])
def test_pickle_scalar_loads_does_not_raise(self, backend):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this test doing the same thing as test_pickle_scalar_issue_repro? I also think it's kind of silly to do pytest.fail in an except block: the test will fail if an exception is raised either way

Comment thread tests/test_quaddtype.py Outdated
"""Diagnostic precision-loss test. Two values with the same printed
repr can still differ at the full bit width, so check via subtraction
(which preserves precision) — `loaded - original` must be exactly zero.
Catches the regression where the longdouble path went through (double)."""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd delete this last line too - this sort of detail about the code's history or historical bugs shouldn't show up in comments IMO - LLMs tend to generate comments like this.

@SwayamInSync
Copy link
Copy Markdown
Member Author

The dragon4 roundtrip issue was actually real (means might have to update NumPy upstream too)

Reason this kept ignored even having roundtrip test-cases is because none of them purely targets the power of 2s. We only checked the subnormal, max and in between ones.
This was a really good find.

@SwayamInSync
Copy link
Copy Markdown
Member Author

Let me merge #104 since it is approved and update here

@SwayamInSync
Copy link
Copy Markdown
Member Author

Hey @ngoldbaum if you get some time have an eye on this again. I mostly resolved all the reviews and the discovered bug.

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] Pickle of scalar QuadPrecision fails on loads

2 participants