Skip to content

BUG: incorrect temp elision for new-style (NEP 43) user-defined dtypes#31193

Merged
seberg merged 2 commits into
numpy:mainfrom
MaartenBaert:main
Apr 13, 2026
Merged

BUG: incorrect temp elision for new-style (NEP 43) user-defined dtypes#31193
seberg merged 2 commits into
numpy:mainfrom
MaartenBaert:main

Conversation

@MaartenBaert

@MaartenBaert MaartenBaert commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

PR summary

can_elide_temp() in temp_elide.c incorrectly identifies new-style user-defined dtypes as numeric types eligible for in-place buffer reuse. For large arrays this silently rewrites a*a + b*b into (a*a) += (b*b), which raises a TypeError when the result dtype of the in-place add does not match the pre-allocated buffer.

I encountered this bug while implementing a custom fixed-point dtype, which automatically increases its bit width on every operation as required to avoid overflow. Since addition increases the bit width by one, attempting to reuse the temporary buffer results in an incompatible destination dtype being provided to the add ufunc and raises a TypeError.

The root cause is that PyArray_ISNUMBER evaluates to true for new-style user dtypes since it checks (type) <= NPY_CLONGDOUBLE and type_num = -1 for these types.

This could be fixed either in PyArray_ISNUMBER (probably more correct, but may have unexpected side effects) or with a minimal extra check in can_elide_temp (the approach used by the PR). Let me know if you'd like me to fix PyArray_ISNUMBER instead.

AI Disclosure

This bug was identified and fixed by Claude Sonnet 4.6, and then reviewed by me.
Full AI-generated explanation: https://gist.github.com/MaartenBaert/7540176c4005d26a0b292baefbec8519

@jorenham jorenham changed the title Fix incorrect temp elision for new-style (NEP 43) user-defined dtypes BUG: incorrect temp elision for new-style (NEP 43) user-defined dtypes Apr 10, 2026
@MaanasArora

Copy link
Copy Markdown
Contributor

Thanks, seems right! FWIW I think it is OK to change PyArray_ISNUMBER at the level of PyTypeNum_ISNUMBER (doing it locally for me didn't break NumPy tests at least, and type number checks can often be legacy). If it breaks any code it is probably unearthing bugs. That said not entirely familiar with these macros so maybe wait for someone else.

@seberg

seberg commented Apr 10, 2026

Copy link
Copy Markdown
Member

Nice, thanks for looking into this! I think it is probably good enough to reject only parametric dtypes here? I.e. with other dtypes we assume that things are fine.

@MaartenBaert

Copy link
Copy Markdown
Contributor Author

Just pushed a new fix which doesn't check type_num but instead checks for presence of the NPY_DT_PARAMETRIC flag. I'm checking the flag manually rather than through NPY_DT_is_parametric to avoid pulling in dtypemeta.h.

@seberg

seberg commented Apr 11, 2026

Copy link
Copy Markdown
Member

I am sorry, I remembered last night late and re-remembering only right now... But what slipped my mind completely is that the Py_NUMBER macro was just wrong, it should include the > 0 check that you correctly added.
I.e. I think your solution was already better than what I said, just not quite the best spot :(.

(I would be happy to broadening it up to the newer NPY_DT_is_numeric() && !parametric, but I am not fully confident that it is guaranteed correct always, it might need a flag to say "go ahead an use in-place for homogeneous operations".)

EDIT: If it's annoying, I may just push it later.

@MaartenBaert

MaartenBaert commented Apr 11, 2026

Copy link
Copy Markdown
Contributor Author

No problem, I will try changing PyTypeNum_ISNUMBER and let you know what happens.
Regarding the parametric check, I'm actually not sure whether it is appropriate to assume that all non-parametric types can use the temp elision optimization, since you can't really predict what kind of conventions custom dtypes may choose to use. E.g. consider a datetime-like dtype where the minus operator produces a time delta type instead.

@MaartenBaert

Copy link
Copy Markdown
Contributor Author

The macro fix seems to work equally well, both for my custom dtype test suite and the numpy test suite.

@seberg

seberg commented Apr 11, 2026

Copy link
Copy Markdown
Member

Thanks. Yeah, it seems like anything would be guess-work, while usually true, weird promotion could always happen.

@SwayamInSync maybe you have time for a quick look. I pushed one more hot-fix for the single case that looked like it might cause regressions here. I.e. that quaddtype_arr.conjugate() works via the ufunc.

Now, it seems for NumPy conjugate() actually just returns self (although I think a view should be OK). But, I think that fix isn't back-portable.
I have tested it locally, we could add a test, but it wouldn't run anyway for the time being.

For a better fix, I think we could:

  1. Move the numeric check first, to error out better (but broaden it to actually use the new definition that user-dtypes can match). -- maybe not strictly necessary.
  2. Check if .imag has a definition. If it does use the ufunc path.

The above would mean that quaddtype_arr.conjugate() would return self. That is a theoretical regression for user defined new-style complex dtypes.
Since I think we can be very confident those don't actually exist right now, I think that is fine.

@MaartenBaert

Copy link
Copy Markdown
Contributor Author

Now, it seems for NumPy conjugate() actually just returns self (although I think a view should be OK). But, I think that fix isn't back-portable.

That's related to a different issue which also affects my complex fixed-point dtype: there is no way to inform numpy that my type is complex, so it assumes it is real and uses the trivial implementation of .real (return self), .imag (return zero) and .conj() (return self). I wouldn't mind making a fix for that too, but would need some guidance on how to proceed. This might require another dtype flag?

@MaanasArora

MaanasArora commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

That's related to a different issue which also affects my complex fixed-point dtype: there is no way to inform numpy that my type is complex

#30984 included ufuncs for real and imag, so for now you can register them via PyUFunc_AddLoopsFromSpecs as in the docs. These would be then used for your dtype. A flag to use defaults would still be nice though.

@MaartenBaert

Copy link
Copy Markdown
Contributor Author

Good to know! Is there a similar ufunc for conj?

@MaanasArora

Copy link
Copy Markdown
Contributor

Yes pretty sure "conjugate" is the one! That is a very "real" ufunc so you should be able to register it directly even IIRC.

{
if (PyArray_ISCOMPLEX(self) || PyArray_ISOBJECT(self) ||
PyArray_ISUSERDEF(self)) {
PyArray_ISUSERDEF(self) || !NPY_DT_is_legacy(PyArray_DESCR(self))) {

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.

Thanks @seberg this works well. LGTM
Also noticed in quaddtype's we don't have test for quad_arr.conj() (we use np.conj which directly through the ufunc dispatch machinery and works)

@seberg

seberg commented Apr 13, 2026

Copy link
Copy Markdown
Member

Thanks for having a look @SwayamInSync, I'll put this in then.

I'll try to follow up with something that makes this work better for quaddtype in the future (e.g. also returning a view).

I think we can backport this @charris, but if it doesn't matter to @MaartenBaert then I don't mind either way.
(There is a small risk I missed another case like the .conj() one, but I don't think so and it would be even more niche probably.)

@seberg seberg merged commit 7a0dfad into numpy:main Apr 13, 2026
86 checks passed
@seberg seberg added the 09 - Backport-Candidate PRs tagged should be backported label Apr 13, 2026
@charris charris removed the 09 - Backport-Candidate PRs tagged should be backported label Apr 25, 2026
charris pushed a commit that referenced this pull request Apr 25, 2026
#31193)

Co-authored-by: Maarten Baert <maarten.baert@keysight.com>
Co-authored-by: Sebastian Berg <sebastianb@nvidia.com>
charris added a commit that referenced this pull request Apr 25, 2026
BUG: incorrect temp elision for new-style (NEP 43) user-defined dtypes (#31193)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants