Repository navigation
BUG: fix StringDType distinct-allocator bugs and add tests - #31609
Conversation
|
that debug interpreter test crash looks real |
|
I think this is ready for review now. @seberg @MaanasArora @mhvk - hope you don't mind the ping. The C diff isn't so huge, most of the changes are new tests. |
| * Convert all inputs to arrays of a common type | ||
| * Also makes them C-contiguous | ||
| */ | ||
| mps = PyArray_ConvertToCommonType(op, &n); |
There was a problem hiding this comment.
For the PyArray_Choose fix here, I noticed we actually discussed this bug in #28206 (comment). Is it better to fix the PyArray_ConvertToCommonType function directly to convert all arrays to use an identical descriptor? Then I think we will lose the complexity below, as we only have one descriptor to cast from!
There was a problem hiding this comment.
(have to catch up with actual review still; we forgot to open a follow-up issue back then, I guess.)
Theoretically, but it would require copying all the inputs even if they have the right dtypes already basically; and it would be a bit strange to force that (i.e. the point normally is for all arrays to have their own dtype for StringDType).
But without that, each casts needs the identity dtype passed in and that requires one cast per array.
There was a problem hiding this comment.
OK yes, might be a bit strange to copy and then cast separately later, thanks.
There was a problem hiding this comment.
have to catch up with actual review still; we forgot to open a follow-up issue back then, I guess.
Sorry for dumping a bunch of code on you on a Friday. Take your time!
5915100 to
ef88174
Compare
seberg
left a comment
There was a problem hiding this comment.
Thanks, a few comments. In some places I think you/the bot thought the RefCheck was a pattern to generalize, but it is used in a few places due to bare memcpy use not to avoid copyswap (although it may be fine to do so additionally).
Otherwise a few small nits, many take-it-or-leave it. I do also think the view safety check should be used generally if we create it, there is nothing special about StringDType (and it can short-circuit the ==, I think.
| if (j >= nv) { | ||
| j = 0; | ||
| if (PyDataType_REFCHK(PyArray_DESCR(array))) { | ||
| /* dtypes with references may not define copyswap */ |
There was a problem hiding this comment.
Please change this to an explicit check for ->copyswap being NULL, while it may be true in practice, I don't think the comment really adds up.
| NPY_cast_info cast_info; | ||
| NPY_ARRAYMETHOD_FLAGS flags; | ||
| const npy_intp one = 1; | ||
| const npy_intp strides[2] = {chunk, chunk}; |
There was a problem hiding this comment.
This is better as elsize (zero is also OK, but elsize is probably better in practice to hit the contiguous loops).
chunk just doesn't make sense here at all.
| NPY_cast_info_init(&cast_info); | ||
| if (PyArray_GetDTypeTransferFunction( | ||
| PyArray_ISALIGNED(values) && PyArray_ISALIGNED(array), | ||
| chunk, chunk, |
There was a problem hiding this comment.
Should be identical to strides above, just use:
| chunk, chunk, | |
| strides[0], strides[1], |
| * separate internal state (e.g. StringDType allocators). | ||
| */ | ||
| NPY_NO_EXPORT PyObject * | ||
| _is_view_safe_cast(PyObject *NPY_UNUSED(module), PyObject *args) |
There was a problem hiding this comment.
Thanks, I really like this helper! I do have a bit of a dislike for PyArg_ParseTuple (it's just silly slow API...), but I don't mind keeping it.
There was a problem hiding this comment.
Changed it to METH_FASTCALL.
| swap = PyArray_ISNOTSWAPPED(self) != PyArray_ISNOTSWAPPED(arr); | ||
| copyswap = PyDataType_GetArrFuncs(PyArray_DESCR(self))->copyswap; | ||
| if (PyDataType_REFCHK(PyArray_DESCR(self))) { | ||
| /* dtypes with references may not define copyswap */ |
There was a problem hiding this comment.
Again, check explicitly for copyswap.
Effectively it means that StringDType shouldn't implement it even if we allowed that (it's far better off with this pattern due to lock handling) but that seems reasonable.
Now... what is completely crazy here is the whole manual PyArray_Item_XDECREF nonsense, which looks like copyswap() would handle it just fine for objects (and structured dtypes).
For structured dtypes the new path may well be better (for larger arrays). So I would be fine with keeping the REFCHCK additionally to copyswap == NULL.
(Another interesting thing, but let's not act on it, I guess... This function should release the GIL for larger inputs but doesn't seem to.)
| dest = PyArray_DATA(self); | ||
|
|
||
| if (PyDataType_REFCHK(PyArray_DESCR(self))) { | ||
| NPY_cast_info cast_info; |
There was a problem hiding this comment.
Just to note that I am not insane about the copyswap thing above. This path is different, because we assume that memcpy is sufficient (copyswap isn't even used).
(There is a new flag for optimizing structured dtypes that we should possibly check here as well, NOT_TRIVIALLY_COPYABLE but not this PR really... Won't complain if you add it, it's basically an extremely niche fix on structured dtypes.)
| dtype, | ||
| obj_dtype, 0, &cast_info, | ||
| &transfer_flags); | ||
| cast_infos = PyMem_New(NPY_cast_info, n); |
There was a problem hiding this comment.
Could use NPY_ALLOC_WORKSPACE or... just use NPY_MAXARGS the MultiIter is limited to that anyway (although of course it isn't explitily from here).
(I don't care though, I doubt micro-optimizations matter much for choose.)
| obj_dtype->elsize, | ||
| PyArray_DESCR(mps[i]), | ||
| obj_dtype, 0, &cast_infos[i], | ||
| &transfer_flags) < 0) { |
There was a problem hiding this comment.
We could release the GIL here based on transfer_flags (although, I suppose while it can't matter, in principle we should check all transfer flags).
This doesn't matter for objects, but it does matter a bit for strings.
| if (y_cast_info.func( | ||
| &y_cast_info.context, args, &one, | ||
| y_strides, y_cast_info.auxdata) < 0) { | ||
| NPY_END_THREADS; |
There was a problem hiding this comment.
Seems nicer to me to just put it into fail: here (requires moving the THREADS_DEF to the start of the function).
| raise TypeError( | ||
| "Cannot view StringDType array data through a different dtype " | ||
| "instance; use astype to copy the data to the new dtype " | ||
| "instance") |
There was a problem hiding this comment.
If you care for the StringDType error that is fine, but I don't think you need to guard for it here, in theory StringDType isn't special.
Maybe _is_view_safe_cast is not the best name here, but that function can completely replace the newtype == oldtype, I think?
|
I think I applied all your suggestions in the last commit. If you could give it another look I'd appreciate it. |
seberg
left a comment
There was a problem hiding this comment.
Thanks, only had a brief look through the new changes, but looks all good. (If you feel like it and haven't done so, I might have thrown an agent once more but don't have it set up here right now.)
Please go ahead and merge!
|
In it goes, thanks for taking a look and giving me feedback Sebastian! |
BUG: fix StringDType distinct-allocator bugs and add tests (#31609)
(cherry picked from commit 706b103)
Fixes bugs when multiple distinct stringdtype arrays are passed to
np.choose,np.put,np.putmask,np.place, flatiter reads, and a bug that could lead to unsafe views being created for StringDType and object arrays.Also exposes a new C helper function
_is_safe_view_castwhich takesview_offsetinto account and uses it in the Python helper functions for view safety. It only uses this function for StringDType. We could use it more generally but that might lead to breakage, so just fixing StringDType seemed like a practical first pass.The C changes all work out to fixing the same couple patterns: legacy paths that used copyswap (e.g. #24859, which this might obviate the need for?) or code paths that assumed all input arrays have the same descriptor instance.
Also adds tests for functionality that is fixed here or is poorly tested currently. It makes use of a new helper function that allocates two distinct arrays of strings with allocations that live in the arena buffer to trigger these bugs.
I milestoned this for 2.5.0 and marked it for backport because these are all bugs that can cause segfaults.
I used Claude Fable 5 to work on this, including for the implementation and new tests.