Skip to content

Commit 706b103

Browse files
authored
BUG: fix StringDType distinct-allocator bugs and add tests (numpy#31609)
1 parent 211a570 commit 706b103

10 files changed

Lines changed: 618 additions & 51 deletions

File tree

numpy/_core/_internal.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from numpy import _NoValue
1414
from numpy.exceptions import DTypePromotionError
1515

16+
from ._multiarray_umath import _is_view_safe_cast
1617
from .multiarray import StringDType, array, dtype, promote_types
1718

1819
try:
@@ -484,7 +485,7 @@ def _getfield_is_safe(oldtype, newtype, offset):
484485
485486
"""
486487
if newtype.hasobject or oldtype.hasobject:
487-
if offset == 0 and newtype == oldtype:
488+
if offset == 0 and _is_view_safe_cast(oldtype, newtype):
488489
return
489490
if oldtype.names is not None:
490491
for name in oldtype.names:
@@ -514,9 +515,10 @@ def _view_is_safe(oldtype, newtype):
514515
515516
"""
516517

517-
# if the types are equivalent, there is no problem.
518-
# for example: dtype((np.record, 'i4,i4')) == dtype((np.void, 'i4,i4'))
519-
if oldtype == newtype:
518+
# more precise than ``oldtype == newtype``: e.g. dtype((np.record, 'i4,i4'))
519+
# views safely as dtype((np.void, 'i4,i4')), while two equal StringDType
520+
# instances with separate allocators do not
521+
if _is_view_safe_cast(oldtype, newtype):
520522
return
521523

522524
if newtype.hasobject or oldtype.hasobject:

numpy/_core/src/multiarray/compiled_base.c

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "ctors.h"
1717
#include "common.h"
1818
#include "dtypemeta.h"
19+
#include "dtype_transfer.h"
1920
#include "simd/simd.h"
2021

2122
#include <string.h>
@@ -393,18 +394,58 @@ arr_place(PyObject *NPY_UNUSED(self), PyObject *args, PyObject *kwdict)
393394
j = 0;
394395

395396
copyswap = PyDataType_GetArrFuncs(PyArray_DESCR(array))->copyswap;
396-
NPY_BEGIN_THREADS_DESCR(PyArray_DESCR(array));
397-
for (i = 0; i < ni; i++) {
398-
if (mask_data[i]) {
399-
if (j >= nv) {
400-
j = 0;
397+
if (copyswap == NULL) {
398+
NPY_cast_info cast_info;
399+
NPY_ARRAYMETHOD_FLAGS flags;
400+
const npy_intp one = 1;
401+
const npy_intp elsize = chunk;
402+
const npy_intp strides[2] = {elsize, elsize};
403+
404+
NPY_cast_info_init(&cast_info);
405+
if (PyArray_GetDTypeTransferFunction(
406+
PyArray_ISALIGNED(values) && PyArray_ISALIGNED(array),
407+
strides[0], strides[1],
408+
PyArray_DESCR(values), PyArray_DESCR(array), 0,
409+
&cast_info, &flags) < 0) {
410+
goto fail;
411+
}
412+
if (!(flags & NPY_METH_REQUIRES_PYAPI)) {
413+
NPY_BEGIN_THREADS;
414+
}
415+
for (i = 0; i < ni; i++) {
416+
if (mask_data[i]) {
417+
if (j >= nv) {
418+
j = 0;
419+
}
420+
421+
char *data[2] = {src + j*chunk, dest + i*chunk};
422+
if (cast_info.func(
423+
&cast_info.context, data, &one, strides,
424+
cast_info.auxdata) < 0) {
425+
NPY_END_THREADS;
426+
NPY_cast_info_xfree(&cast_info);
427+
goto fail;
428+
}
429+
j++;
401430
}
431+
}
432+
NPY_END_THREADS;
433+
NPY_cast_info_xfree(&cast_info);
434+
}
435+
else {
436+
NPY_BEGIN_THREADS_DESCR(PyArray_DESCR(array));
437+
for (i = 0; i < ni; i++) {
438+
if (mask_data[i]) {
439+
if (j >= nv) {
440+
j = 0;
441+
}
402442

403-
copyswap(dest + i*chunk, src + j*chunk, 0, array);
404-
j++;
443+
copyswap(dest + i*chunk, src + j*chunk, 0, array);
444+
j++;
445+
}
405446
}
447+
NPY_END_THREADS;
406448
}
407-
NPY_END_THREADS;
408449

409450
Py_XDECREF(values);
410451
Py_XDECREF(mask);

numpy/_core/src/multiarray/convert_datatype.c

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,36 @@ PyArray_SafeCast(PyArray_Descr *type1, PyArray_Descr *type2,
648648
}
649649

650650

651+
/*
652+
* Python-level helper answering whether reinterpreting data of dtype *from*
653+
* as dtype *to* is a view. Unlike descriptor equality or `np.can_cast` with
654+
* "no" casting, this distinguishes equivalent descriptor instances with
655+
* separate internal state (e.g. StringDType allocators).
656+
*/
657+
NPY_NO_EXPORT PyObject *
658+
_is_view_safe_cast(PyObject *NPY_UNUSED(module), PyObject *const *args,
659+
Py_ssize_t len_args)
660+
{
661+
if (len_args != 2) {
662+
PyErr_SetString(PyExc_TypeError,
663+
"_is_view_safe_cast() takes exactly two arguments");
664+
return NULL;
665+
}
666+
if (!PyArray_DescrCheck(args[0]) || !PyArray_DescrCheck(args[1])) {
667+
PyErr_SetString(PyExc_TypeError,
668+
"_is_view_safe_cast() arguments must be dtype instances");
669+
return NULL;
670+
}
671+
PyArray_Descr *from = (PyArray_Descr *)args[0];
672+
PyArray_Descr *to = (PyArray_Descr *)args[1];
673+
npy_intp view_offset = NPY_MIN_INTP;
674+
/* ignore_error=1: dtype pairs with no resolvable cast are simply not views */
675+
npy_intp is_safe = PyArray_SafeCast(from, to, &view_offset,
676+
NPY_NO_CASTING, 1);
677+
return PyBool_FromLong(is_safe && view_offset == 0);
678+
}
679+
680+
651681
/* Provides an ordering for the dtype 'kind' character codes */
652682
NPY_NO_EXPORT int
653683
dtype_kind_to_ordering(char kind)

numpy/_core/src/multiarray/convert_datatype.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ PyArray_GetCastingImpl(PyArray_DTypeMeta *from, PyArray_DTypeMeta *to);
1515
NPY_NO_EXPORT PyObject *
1616
_get_castingimpl(PyObject *NPY_UNUSED(module), PyObject *args);
1717

18+
NPY_NO_EXPORT PyObject *
19+
_is_view_safe_cast(PyObject *NPY_UNUSED(module), PyObject *const *args,
20+
Py_ssize_t len_args);
21+
1822
NPY_NO_EXPORT PyArray_VectorUnaryFunc *
1923
PyArray_GetCastFunc(PyArray_Descr *descr, int type_num);
2024

numpy/_core/src/multiarray/getset.c

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@
1212

1313
#include "npy_import.h"
1414

15+
#include "array_assign.h"
1516
#include "common.h"
1617
#include "conversion_utils.h"
1718
#include "ctors.h"
19+
#include "dtype_transfer.h"
1820
#include "dtypemeta.h"
21+
#include "lowlevel_strided_loops.h"
1922
#include "scalartypes.h"
2023
#include "descriptor.h"
2124
#include "flagsobject.h"
@@ -671,26 +674,43 @@ array_flat_set(PyArrayObject *self, PyObject *val, void *NPY_UNUSED(ignored))
671674
retval = 0;
672675
goto exit;
673676
}
674-
swap = PyArray_ISNOTSWAPPED(self) != PyArray_ISNOTSWAPPED(arr);
675677
copyswap = PyDataType_GetArrFuncs(PyArray_DESCR(self))->copyswap;
676-
if (PyDataType_REFCHK(PyArray_DESCR(self))) {
678+
if (copyswap == NULL || PyDataType_REFCHK(PyArray_DESCR(self))) {
679+
/* reference dtypes have copyswap, but the transfer path handles
680+
refcounts and is better for structured dtypes */
681+
NPY_cast_info cast_info;
682+
NPY_ARRAYMETHOD_FLAGS transfer_flags = 0;
683+
npy_intp one = 1;
684+
npy_intp itemsize = PyArray_ITEMSIZE(self);
685+
npy_intp transfer_strides[2] = {itemsize, itemsize};
686+
687+
NPY_cast_info_init(&cast_info);
688+
if (PyArray_GetDTypeTransferFunction(
689+
IsUintAligned(self) && IsUintAligned(arr),
690+
itemsize, itemsize,
691+
PyArray_DESCR(arr), PyArray_DESCR(self), 0,
692+
&cast_info, &transfer_flags) < 0) {
693+
goto exit;
694+
}
677695
while (selfit->index < selfit->size) {
678-
PyArray_Item_XDECREF(selfit->dataptr, PyArray_DESCR(self));
679-
PyArray_Item_INCREF(arrit->dataptr, PyArray_DESCR(arr));
680-
memmove(selfit->dataptr, arrit->dataptr, sizeof(PyObject **));
681-
if (swap) {
682-
copyswap(selfit->dataptr, NULL, swap, self);
696+
char *args[2] = {arrit->dataptr, selfit->dataptr};
697+
if (cast_info.func(&cast_info.context, args, &one,
698+
transfer_strides, cast_info.auxdata) < 0) {
699+
NPY_cast_info_xfree(&cast_info);
700+
goto exit;
683701
}
684702
PyArray_ITER_NEXT(selfit);
685703
PyArray_ITER_NEXT(arrit);
686704
if (arrit->index == arrit->size) {
687705
PyArray_ITER_RESET(arrit);
688706
}
689707
}
708+
NPY_cast_info_xfree(&cast_info);
690709
retval = 0;
691710
goto exit;
692711
}
693712

713+
swap = PyArray_ISNOTSWAPPED(self) != PyArray_ISNOTSWAPPED(arr);
694714
while(selfit->index < selfit->size) {
695715
copyswap(selfit->dataptr, arrit->dataptr, swap, self);
696716
PyArray_ITER_NEXT(selfit);

numpy/_core/src/multiarray/item_selection.c

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -457,9 +457,10 @@ PyArray_PutTo(PyArrayObject *self, PyObject* values0, PyObject *indices0,
457457
NPY_BEGIN_THREADS_THRESHOLDED(ni);
458458
}
459459
else {
460-
PyArray_Descr *dtype = PyArray_DESCR(self);
461460
if (PyArray_GetDTypeTransferFunction(
462-
PyArray_ISALIGNED(self), itemsize, itemsize, dtype, dtype, 0,
461+
PyArray_ISALIGNED(self) && PyArray_ISALIGNED(values),
462+
itemsize, itemsize,
463+
PyArray_DESCR(values), PyArray_DESCR(self), 0,
463464
&cast_info, &flags) < 0) {
464465
goto fail;
465466
}
@@ -755,7 +756,9 @@ PyArray_PutMask(PyArrayObject *self, PyObject* values0, PyObject* mask0)
755756

756757
NPY_cast_info_init(&cast_info);
757758
if (PyArray_GetDTypeTransferFunction(
758-
PyArray_ISALIGNED(self), itemsize, itemsize, dtype, dtype, 0,
759+
PyArray_ISALIGNED(self) && PyArray_ISALIGNED(values),
760+
itemsize, itemsize,
761+
PyArray_DESCR(values), PyArray_DESCR(self), 0,
759762
&cast_info, &flags) < 0) {
760763
goto fail;
761764
}
@@ -778,15 +781,15 @@ PyArray_PutMask(PyArrayObject *self, PyObject* values0, PyObject* mask0)
778781
}
779782
}
780783
}
784+
NPY_END_THREADS;
781785
NPY_cast_info_xfree(&cast_info);
782786
}
783787
else {
784788
NPY_BEGIN_THREADS;
785789
npy_fastputmask(dest, src, mask_data, ni, nv, itemsize);
790+
NPY_END_THREADS;
786791
}
787792

788-
NPY_END_THREADS;
789-
790793
Py_XDECREF(values);
791794
Py_XDECREF(mask);
792795
if (copied) {
@@ -1024,7 +1027,10 @@ PyArray_Choose(PyArrayObject *ip, PyObject *op, PyArrayObject *out,
10241027
PyArrayObject **mps, *ap;
10251028
PyArrayMultiIterObject *multi = NULL;
10261029
npy_intp mi;
1027-
NPY_cast_info cast_info = {.func = NULL};
1030+
/* PyArray_MultiIterFromObjects below bounds n by NPY_MAXARGS */
1031+
NPY_cast_info cast_infos[NPY_MAXARGS];
1032+
int needs_transfer = 0;
1033+
NPY_BEGIN_THREADS_DEF;
10281034
ap = NULL;
10291035

10301036
/*
@@ -1118,23 +1124,35 @@ PyArray_Choose(PyArrayObject *ip, PyObject *op, PyArrayObject *out,
11181124
npy_intp transfer_strides[2] = {elsize, elsize};
11191125
npy_intp one = 1;
11201126
NPY_ARRAYMETHOD_FLAGS transfer_flags = 0;
1121-
if (PyDataType_REFCHK(dtype)) {
1122-
int is_aligned = IsUintAligned(obj);
1127+
needs_transfer = PyDataType_REFCHK(dtype);
1128+
if (needs_transfer) {
11231129
PyArray_Descr *obj_dtype = PyArray_DESCR(obj);
1124-
PyArray_GetDTypeTransferFunction(
1125-
is_aligned,
1126-
dtype->elsize,
1127-
obj_dtype->elsize,
1128-
dtype,
1129-
obj_dtype, 0, &cast_info,
1130-
&transfer_flags);
1130+
for (i = 0; i < n; i++) {
1131+
NPY_cast_info_init(&cast_infos[i]);
1132+
}
1133+
for (i = 0; i < n; i++) {
1134+
int is_aligned = IsUintAligned(obj) && IsUintAligned(mps[i]);
1135+
if (PyArray_GetDTypeTransferFunction(
1136+
is_aligned,
1137+
PyArray_DESCR(mps[i])->elsize,
1138+
obj_dtype->elsize,
1139+
PyArray_DESCR(mps[i]),
1140+
obj_dtype, 0, &cast_infos[i],
1141+
&transfer_flags) < 0) {
1142+
goto fail;
1143+
}
1144+
}
11311145
}
11321146

1147+
if (!(transfer_flags & NPY_METH_REQUIRES_PYAPI)) {
1148+
NPY_BEGIN_THREADS_THRESHOLDED(multi->size);
1149+
}
11331150
while (PyArray_MultiIter_NOTDONE(multi)) {
11341151
mi = *((npy_intp *)PyArray_MultiIter_DATA(multi, n));
11351152
if (mi < 0 || mi >= n) {
11361153
switch(clipmode) {
11371154
case NPY_RAISE:
1155+
NPY_END_THREADS;
11381156
PyErr_SetString(PyExc_ValueError,
11391157
"invalid entry in choice "\
11401158
"array");
@@ -1161,22 +1179,28 @@ PyArray_Choose(PyArrayObject *ip, PyObject *op, PyArrayObject *out,
11611179
break;
11621180
}
11631181
}
1164-
if (cast_info.func == NULL) {
1182+
if (!needs_transfer) {
11651183
/* We ensure memory doesn't overlap, so can use memcpy */
11661184
memcpy(ret_data, PyArray_MultiIter_DATA(multi, mi), elsize);
11671185
}
11681186
else {
11691187
char *args[2] = {PyArray_MultiIter_DATA(multi, mi), ret_data};
1170-
if (cast_info.func(&cast_info.context, args, &one,
1171-
transfer_strides, cast_info.auxdata) < 0) {
1188+
if (cast_infos[mi].func(&cast_infos[mi].context, args, &one,
1189+
transfer_strides,
1190+
cast_infos[mi].auxdata) < 0) {
11721191
goto fail;
11731192
}
11741193
}
11751194
ret_data += elsize;
11761195
PyArray_MultiIter_NEXT(multi);
11771196
}
1197+
NPY_END_THREADS;
11781198

1179-
NPY_cast_info_xfree(&cast_info);
1199+
if (needs_transfer) {
1200+
for (i = 0; i < n; i++) {
1201+
NPY_cast_info_xfree(&cast_infos[i]);
1202+
}
1203+
}
11801204
Py_DECREF(multi);
11811205
for (i = 0; i < n; i++) {
11821206
Py_XDECREF(mps[i]);
@@ -1194,7 +1218,12 @@ PyArray_Choose(PyArrayObject *ip, PyObject *op, PyArrayObject *out,
11941218
return (PyObject *)obj;
11951219

11961220
fail:
1197-
NPY_cast_info_xfree(&cast_info);
1221+
NPY_END_THREADS;
1222+
if (needs_transfer) {
1223+
for (i = 0; i < n; i++) {
1224+
NPY_cast_info_xfree(&cast_infos[i]);
1225+
}
1226+
}
11981227
Py_XDECREF(multi);
11991228
for (i = 0; i < n; i++) {
12001229
Py_XDECREF(mps[i]);

0 commit comments

Comments
 (0)