Skip to content

Commit ca9a2e8

Browse files
authored
Merge pull request numpy#105 from SwayamInSync/fix-pickle-scalar-99
Adding `__reduce__` method to QuadPrecision for supporting pickle
2 parents 567e9a0 + a821897 commit ca9a2e8

8 files changed

Lines changed: 419 additions & 20 deletions

File tree

.github/workflows/build_wheels.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626

2727
- name: Build wheels
2828
env:
29-
CIBW_BUILD: "cp311-manylinux_x86_64 cp312-manylinux_x86_64 cp313-manylinux_x86_64 cp313t-manylinux_x86_64 cp314-manylinux_x86_64 cp314t-manylinux_x86_64"
29+
CIBW_BUILD: "cp311-manylinux_x86_64 cp312-manylinux_x86_64 cp313-manylinux_x86_64 cp314-manylinux_x86_64 cp314t-manylinux_x86_64"
3030
CIBW_ENABLE: cpython-prerelease cpython-freethreading
3131
CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28
3232
CIBW_BUILD_VERBOSITY: "3"
@@ -88,7 +88,7 @@ jobs:
8888
8989
- name: Build wheels
9090
env:
91-
CIBW_BUILD: "cp311-* cp312-* cp313-* cp314-* cp313t-* cp314t-*"
91+
CIBW_BUILD: "cp311-* cp312-* cp313-* cp314-* cp314t-*"
9292
CIBW_ENABLE: cpython-prerelease cpython-freethreading
9393
# CIBW_ARCHS_MACOS: ${{ matrix.os == 'macos-13' && 'x86_64' || 'arm64' }}
9494
CIBW_BUILD_VERBOSITY: "3"
@@ -149,7 +149,7 @@ jobs:
149149
150150
- name: Build wheels
151151
env:
152-
CIBW_BUILD: "cp311-* cp312-* cp313-* cp314-* cp313t-* cp314t-*"
152+
CIBW_BUILD: "cp311-* cp312-* cp313-* cp314-* cp314t-*"
153153
CIBW_ENABLE: cpython-prerelease cpython-freethreading
154154
CIBW_ARCHS_WINDOWS: ${{ matrix.architecture == 'x86' && 'x86' || 'AMD64' }}
155155
CIBW_BUILD_VERBOSITY: "3"

src/csrc/casts.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ quad_to_string_adaptive_cstr(Sleef_quad *sleef_val, npy_intp unicode_size_chars)
416416
// Use scientific notation with full precision
417417
const char *scientific_str = Dragon4_Scientific_QuadDType_CStr(sleef_val, DigitMode_Unique,
418418
SLEEF_QUAD_DECIMAL_DIG, 0, 1,
419-
TrimMode_LeaveOneZero, 1, 2);
419+
TrimMode_LeaveOneZero, 1, 4);
420420
if (scientific_str == NULL) {
421421
PyErr_SetString(PyExc_RuntimeError, "Float formatting failed");
422422
return NULL;

src/csrc/dragon4.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1929,7 +1929,7 @@ Dragon4_PrintFloat_Sleef_quad(Sleef_quad *value, Dragon4_Options *opt)
19291929
/* mantissa_lo is unchanged */
19301930
exponent = floatExponent - 16383 - 112;
19311931
mantissaBit = 112;
1932-
hasUnequalMargins = (floatExponent != 1) && (mantissa_hi == 0 && mantissa_lo == 0);
1932+
hasUnequalMargins = (floatExponent != 1) && (mantissa_hi == (1ull << 48) && mantissa_lo == 0);
19331933
}
19341934
else {
19351935
/* subnormal */

src/csrc/quaddtype_main.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ get_sleef_constant(PyObject *self, PyObject *args)
6868
static PyMethodDef module_methods[] = {
6969
{"is_longdouble_128", py_is_longdouble_128, METH_NOARGS, "Check if long double is 128-bit"},
7070
{"get_sleef_constant", get_sleef_constant, METH_VARARGS, "Get Sleef constant by name"},
71+
{"from_raw_bytes", QuadPrecision_from_raw_bytes, METH_VARARGS,
72+
"from_raw_bytes(data, backend='sleef', ld_format=-1): reconstruct a "
73+
"QuadPrecision scalar from its raw little-endian bytes (used by pickle). "
74+
"For the 'longdouble' backend ld_format is the source LDBL_MANT_DIG and, "
75+
"if given, must match this platform's."},
7176
{"set_num_threads", py_quadblas_set_num_threads, METH_VARARGS,
7277
"Set number of threads for QuadBLAS"},
7378
{"get_num_threads", py_quadblas_get_num_threads, METH_NOARGS,

src/csrc/scalar.c

Lines changed: 151 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
#include <sleefquad.h>
44
#include <stdlib.h>
55
#include <math.h>
6+
#include <string.h>
7+
#include <float.h>
68

79
#define PY_ARRAY_UNIQUE_SYMBOL QuadPrecType_ARRAY_API
810
#define NPY_NO_DEPRECATED_API NPY_2_0_API_VERSION
@@ -40,6 +42,8 @@ QuadPrecision_raw_new(QuadBackendType backend)
4042
new->value.sleef_value = Sleef_cast_from_doubleq1(0.0);
4143
}
4244
else {
45+
// An 80-bit long double occupies 16 bytes but writes only 10
46+
memset(&new->value, 0, sizeof(new->value));
4347
new->value.longdouble_value = 0.0L;
4448
}
4549
return new;
@@ -335,19 +339,6 @@ QuadPrecision_str_dragon4(QuadPrecisionObject *self)
335339
}
336340
}
337341

338-
static PyObject *
339-
QuadPrecision_str(QuadPrecisionObject *self)
340-
{
341-
char buffer[128];
342-
if (self->backend == BACKEND_SLEEF) {
343-
Sleef_snprintf(buffer, sizeof(buffer), "%.*Qe", SLEEF_QUAD_DIG, self->value.sleef_value);
344-
}
345-
else {
346-
snprintf(buffer, sizeof(buffer), "%.35Le", self->value.longdouble_value);
347-
}
348-
return PyUnicode_FromString(buffer);
349-
}
350-
351342
static PyObject *
352343
QuadPrecision_repr_dragon4(QuadPrecisionObject *self)
353344
{
@@ -358,7 +349,7 @@ QuadPrecision_repr_dragon4(QuadPrecisionObject *self)
358349
.sign = 1,
359350
.trim_mode = TrimMode_LeaveOneZero,
360351
.digits_left = 1,
361-
.exp_digits = 3};
352+
.exp_digits = 4};
362353

363354
PyObject *str;
364355
if (self->backend == BACKEND_SLEEF) {
@@ -633,11 +624,157 @@ QuadPrecision_as_integer_ratio(QuadPrecisionObject *self, PyObject *Py_UNUSED(ig
633624
return PyTuple_Pack(2, numerator, denominator);
634625
}
635626

627+
static int
628+
quad_host_is_big_endian(void)
629+
{
630+
uint16_t probe = 1;
631+
return ((const unsigned char *)&probe)[0] == 0;
632+
}
633+
634+
635+
static void
636+
quad_copy_canonical(unsigned char *dst, const unsigned char *src, size_t n)
637+
{
638+
if (quad_host_is_big_endian()) {
639+
for (size_t i = 0; i < n; i++) {
640+
dst[i] = src[n - 1 - i];
641+
}
642+
}
643+
else {
644+
memcpy(dst, src, n);
645+
}
646+
}
647+
648+
649+
static PyObject *
650+
QuadPrecision_reduce(QuadPrecisionObject *self, PyObject *Py_UNUSED(ignored))
651+
{
652+
size_t nbytes = (self->backend == BACKEND_SLEEF)
653+
? sizeof(self->value.sleef_value)
654+
: sizeof(self->value.longdouble_value);
655+
const unsigned char *src = (self->backend == BACKEND_SLEEF)
656+
? (const unsigned char *)&self->value.sleef_value
657+
: (const unsigned char *)&self->value.longdouble_value;
658+
659+
unsigned char raw[sizeof(quad_value)];
660+
quad_copy_canonical(raw, src, nbytes);
661+
662+
PyObject *data = PyBytes_FromStringAndSize((const char *)raw, (Py_ssize_t)nbytes);
663+
if (data == NULL) {
664+
return NULL;
665+
}
666+
667+
PyObject *backend_obj = PyUnicode_FromString(
668+
self->backend == BACKEND_SLEEF ? "sleef" : "longdouble");
669+
if (backend_obj == NULL) {
670+
Py_DECREF(data);
671+
return NULL;
672+
}
673+
674+
PyObject *module = PyImport_ImportModule("numpy_quaddtype._quaddtype_main");
675+
if (module == NULL) {
676+
Py_DECREF(data);
677+
Py_DECREF(backend_obj);
678+
return NULL;
679+
}
680+
PyObject *reconstruct = PyObject_GetAttrString(module, "from_raw_bytes");
681+
Py_DECREF(module);
682+
if (reconstruct == NULL) {
683+
Py_DECREF(data);
684+
Py_DECREF(backend_obj);
685+
return NULL;
686+
}
687+
688+
PyObject *args;
689+
if (self->backend == BACKEND_LONGDOUBLE) {
690+
// Tag longdouble payloads with the platform's LDBL_MANT_DIG
691+
PyObject *fmt = PyLong_FromLong((long)LDBL_MANT_DIG);
692+
if (fmt == NULL) {
693+
Py_DECREF(data);
694+
Py_DECREF(backend_obj);
695+
Py_DECREF(reconstruct);
696+
return NULL;
697+
}
698+
args = PyTuple_Pack(3, data, backend_obj, fmt);
699+
Py_DECREF(fmt);
700+
}
701+
else {
702+
args = PyTuple_Pack(2, data, backend_obj);
703+
}
704+
Py_DECREF(data);
705+
Py_DECREF(backend_obj);
706+
if (args == NULL) {
707+
Py_DECREF(reconstruct);
708+
return NULL;
709+
}
710+
711+
PyObject *result = PyTuple_Pack(2, reconstruct, args);
712+
Py_DECREF(reconstruct);
713+
Py_DECREF(args);
714+
return result;
715+
}
716+
717+
PyObject *
718+
QuadPrecision_from_raw_bytes(PyObject *Py_UNUSED(module), PyObject *args)
719+
{
720+
Py_buffer view;
721+
const char *backend_str = "sleef";
722+
int ld_format = -1; // source LDBL_MANT_DIG for longdouble; -1 = not provided
723+
if (!PyArg_ParseTuple(args, "y*|si", &view, &backend_str, &ld_format)) {
724+
return NULL;
725+
}
726+
727+
QuadBackendType backend = BACKEND_SLEEF;
728+
if (strcmp(backend_str, "longdouble") == 0) {
729+
backend = BACKEND_LONGDOUBLE;
730+
}
731+
else if (strcmp(backend_str, "sleef") != 0) {
732+
PyBuffer_Release(&view);
733+
PyErr_SetString(PyExc_ValueError, "Invalid backend. Use 'sleef' or 'longdouble'.");
734+
return NULL;
735+
}
736+
737+
size_t expected = (backend == BACKEND_SLEEF) ? sizeof(Sleef_quad) : sizeof(long double);
738+
if (view.len != (Py_ssize_t)expected) {
739+
PyErr_Format(PyExc_ValueError,
740+
"from_raw_bytes expected %zu bytes for the '%s' "
741+
"backend, got %zd",
742+
expected, backend_str, view.len);
743+
PyBuffer_Release(&view);
744+
return NULL;
745+
}
746+
747+
if (backend == BACKEND_LONGDOUBLE && ld_format != -1 &&
748+
ld_format != LDBL_MANT_DIG) {
749+
PyErr_Format(PyExc_ValueError,
750+
"Cannot unpickle a 'longdouble' QuadPrecision created on a "
751+
"platform with a different long double format "
752+
"(LDBL_MANT_DIG=%d) than this one (LDBL_MANT_DIG=%d).",
753+
ld_format, (int)LDBL_MANT_DIG);
754+
PyBuffer_Release(&view);
755+
return NULL;
756+
}
757+
758+
QuadPrecisionObject *self = QuadPrecision_raw_new(backend);
759+
if (self == NULL) {
760+
PyBuffer_Release(&view);
761+
return NULL;
762+
}
763+
unsigned char *dst = (backend == BACKEND_SLEEF)
764+
? (unsigned char *)&self->value.sleef_value
765+
: (unsigned char *)&self->value.longdouble_value;
766+
quad_copy_canonical(dst, (const unsigned char *)view.buf, expected);
767+
PyBuffer_Release(&view);
768+
return (PyObject *)self;
769+
}
770+
636771
static PyMethodDef QuadPrecision_methods[] = {
637772
{"is_integer", (PyCFunction)QuadPrecision_is_integer, METH_NOARGS,
638773
"Return True if the value is an integer."},
639774
{"as_integer_ratio", (PyCFunction)QuadPrecision_as_integer_ratio, METH_NOARGS,
640775
"Return a pair of integers whose ratio is exactly equal to the original value."},
776+
{"__reduce__", (PyCFunction)QuadPrecision_reduce, METH_NOARGS,
777+
"Support pickling: return (from_raw_bytes, (raw_bytes, backend[, ld_format]))."},
641778
{NULL, NULL, 0, NULL} /* Sentinel */
642779
};
643780

src/csrc/utilities.c

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,16 @@ cstring_to_quad_internal(const char *str, const char *start, QuadBackendType bac
138138
memcpy(temp, str, len);
139139
temp[len] = '\0';
140140

141-
// Call Sleef_strtoq with the bounded string
141+
// Call Sleef_strtoq with the bounded string.
142+
//
143+
// NOTE: SLEEF's decimal strtoq is only non-correctly-rounded for inputs
144+
// whose *significant* digits exceed what a quad can hold (>~45); there it
145+
// may be <= 1 ULP off. This is unreachable for quad<->string round-trips:
146+
// every quad is exact within SLEEF_QUAD_DECIMAL_DIG (36) significant
147+
// digits and Dragon4 (str/repr) emits <= 36, so re-parsing is exact. Only
148+
// significant digits count - magnitude/exponent (e.g. 1e4932) is scaled
149+
// separately and is fine. Pickling uses raw bytes (from_raw_bytes), which
150+
// never goes through this path.
142151
char *sleef_endptr;
143152
out_value->sleef_value = Sleef_strtoq(temp, &sleef_endptr);
144153
free(temp);

src/include/scalar.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ QuadPrecision_raw_new(QuadBackendType backend);
2424
QuadPrecisionObject *
2525
QuadPrecision_from_object(PyObject *value, QuadBackendType backend);
2626

27+
PyObject *
28+
QuadPrecision_from_raw_bytes(PyObject *module, PyObject *args);
29+
2730
int
2831
init_quadprecision_scalar(void);
2932

0 commit comments

Comments
 (0)