Repository navigation
ENH: add mechanism to register reduction loops to ufuncs for multi-output reductions - #31816
Conversation
|
CC @mhvk I would be very curious if you have some high-level thoughts on this? |
|
@seberg - thanks for the ping! @ikrommyd - this looks very nice indeed, and I like the general idea of having a new slot available to register reduction functions. Like you, I'm also pleased that in the end the changes are not very intrusive. On the PR itself, one thing I think is important is to have something in numpy that exercises the new function; possibly, your minmax, maybe just for integers or floats, could serve that purpose? You'll have seen that there are several |
|
Thanks @mhvk! Yeah I was planning to open a separate PR targetting this PR's branch that implements minmax in numpy but it wouldn't be much different than what the gist already has. I mean one just registers the reduction loops using the slot and |
|
If Marten's OK with a minmax reduction landing as part of this PR, I'm fine with it too. IMO a NEP isn't necessary to merge this, so it would be purely for documentation purposes. It would also add some process overhead that you might not want to deal with. |
|
Yeah I wanted to keep the diff minimal for easier first look here on the design. We also need to touch reduceat and accumulate in a similar manner to reduce so that all 3 reduction-like ops work with this. Happy to add minmax + reduceat + accumulate changes here. I generally find smaller PRs easier to work with from the reviewers' perspective and then I would apply all the comments made here for reduce to accumulate and reduceat so what I originally had in mind was to have these and minmax as separate PRs but happy to adjust according to your liking 😃 |
|
All sounds good. I agree with the sense to stick to just |
| Traceback (most recent call last): | ||
| ... | ||
| ValueError: reduce only supported for functions returning a single value | ||
| ValueError: divmod.reduce is not supported: the resolved loop does not register a reduction loop |
There was a problem hiding this comment.
just wanted to make the docs ci pass, this whole section needs refactoring now that multi-output reductions are allowed
|
Okay since I have not seen any opposing feedback on the design here, I plan to turn this into a more proper PR this week with testing and documentation. As Marten suggested, I will add the minmax toy example as a C test. |
|
I've added documentation updates and I took my minmax and turned it into a C test by adding a double loop only + promoter for python scalars. I didn't want to add full dtype support as that will be done in the actual minmax implementation that will follow. The linting failure is unrelated and is on a file I'm not touching here. IMO this is ready for the first review pass. There may be some doc displaying issues cause I didn't check the website preview but will do that later. Gonna do a rebase to main too. |
0ddc006 to
c9f76ae
Compare
seberg
left a comment
There was a problem hiding this comment.
Overall, very nice, I have only some small comments. I think that with the current minmax + tests using initial= we should have a fairly reasonable test coverage, which is also good (the no "identity" and identity logic part).
One thing we could make sure to test is fortran order (it shouldn't matter but that way we have strides != 0 for the reduced array), object dtype could be nice, but I don't want to bloat this too much (i.e. the initial buffer bug).
| keeping their own dtype. A multi-dtype ufunc would instead register a | ||
| loop per dtype and pick the common one, as in the built-in | ||
| :func:`numpy.minimum`/:func:`numpy.maximum`. | ||
| - It has no :c:macro:`NPY_METH_get_reduction_initial`, so |
There was a problem hiding this comment.
And I guess unfortunately that would need a char **initial or so (I don't have a thought beyond adding that and maybe considering to actually do a deprecation of the current one).
But yeah, we can follow up...
| * cross-operand constraint is the structural one that `acc_i` aliases `out_i`. | ||
| */ | ||
| static PyArrayMethodObject * | ||
| reducelike_promote_and_resolve_multi(PyUFuncObject *ufunc, |
There was a problem hiding this comment.
I should re-read this once more eventually, but it looks not all that scary! Hopefully we don't find weird corner cases :).
There was a problem hiding this comment.
Yeah if you have any ideas on the algorithm, please let me know. This is the the algorithm I came up with but I don't know much about dtype resolution edge cases.
|
Merge conflict is simple. It's just the new member in the array method struct from the loop caching PR. Will fix on the next rebase. Just don't wanna ruin the state right now. |
018f134 to
082caa2
Compare
|
I have implemented one more small |
ngoldbaum
left a comment
There was a problem hiding this comment.
Meaty review for a meaty PR!
I think this is well on its way but there are still a bunch of issues my AI-assisted review found, see below.
|
Whew that took a while to get done haha. I feel like I've addressed all your review comments @ngoldbaum! |
|
@ikrommyd OK, I think this is ready now. You'll need to clear the merge conflict and then I think I'll go ahead and merge. While you're doing that, I also think this test would add some coverage for the object forward loop: |
Signed-off-by: Iason Krommydas <[email protected]>
Signed-off-by: Iason Krommydas <[email protected]>
Signed-off-by: Iason Krommydas <[email protected]>
Signed-off-by: Iason Krommydas <[email protected]>
Signed-off-by: Iason Krommydas <[email protected]>
Signed-off-by: Iason Krommydas <[email protected]>
Signed-off-by: Iason Krommydas <[email protected]>
Signed-off-by: Iason Krommydas <[email protected]>
Signed-off-by: Iason Krommydas <[email protected]>
Signed-off-by: Iason Krommydas <[email protected]>
Signed-off-by: Iason Krommydas <[email protected]>
Signed-off-by: Iason Krommydas <[email protected]>
Signed-off-by: Iason Krommydas <[email protected]>
Signed-off-by: Iason Krommydas <[email protected]>
Signed-off-by: Iason Krommydas <[email protected]>
Signed-off-by: Iason Krommydas <[email protected]>
Signed-off-by: Iason Krommydas <[email protected]>
Signed-off-by: Iason Krommydas <[email protected]>
Signed-off-by: Iason Krommydas <[email protected]>
5ed1a82 to
572cc56
Compare
Signed-off-by: Iason Krommydas <[email protected]>
|
@ngoldbaum - rebased, fixed the conflict and added the test you suggested. |
|
OK, no one else weighed in since I suggested merging this yesterday, so in it goes! If anyone would like to register any comments or suggestions, just ping Iason and he'll get to it on a second pass. |
Implements np.minmax using the new ability to register reduction loops to ufuncs implemented in #31816 The loops are implemented in the existing .c.src files with the np.minimun/maximum loops and using exactly the same optimizations and SIMD as them. Two new files minmax.cpp and minmax.h are created just to create the ufunc and initialize the array methods. I found this to be by far the easiest setup to do now. The other option would be porting the whole loops_minmax.dispatch.c.src to highway and using only c++ but that is a more complicated change with a way more difficult review so I did not do that. minmax is not a method on the ndarray like min and max are. The only thing exposed in the top-level API is the new np.minmax function. The minimummaximum ufunc is not exposed as np.minimummaximum like np.minimum and np.maximum are as I do not believe there's much use in that. If we ever need it, it's a very simple change. We should also consider adding a nanminmax in a follow-up PR for completeness and also adding a masked array aware implementation (either by making minmax a method on the ndarray or only by a np.ma.minmax). Signed-off-by: Iason Krommydas <[email protected]>
PR summary
This is adding the ability to add reduction loops to ufuncs as it was being recently discussed in the numpy community meetings. The current reductions are implemented using the binary ufunc loops which are 2 -> 1 (2 inputs 1 output) and by setting the output pointer equal to the first input pointer which makes it the accumulation location and not moving it. We generalize this logic for ufuncs that return N outputs. However the normal ufunc loop cannot be used in a similar way in this case. The trick is not possible. So we add the ability to add reduction loops to ufuncs that are N + 1 -> N loops while their nominal/forward loops are 2 -> N.
This is done via a single new function pointer on the array method called
get_reduction_loopsimilar toget_strided_loop. The whole reduction machinery is then generalized to support loops that are N + 1 -> N instead of hardcoding the assumption that the loop is 2 -> 1. In the nominal binary ufunc case, everything collapses down to the old 2 -> 1 assumption. This generalization was the "easy" part. I kept it toreduceright now to minimize the diff for comments but can done forreduceatandaccumulatein a similar manner. The "difficult" algorithmically part for me was thereducelike_promote_and_resolve_multifunction mainly because since the reduction loops live on the array method via theget_reduction_loopfunction pointer, we have to resolve the dtypes for the "forward pass" loop, get the array method from ufunc object's_loopsand then get the reduction loop from it. This potentially needs some discussion about how it's best to do so. I generally like the fact that I have to touch only a few files to implement this whole machinery here.An example of how this machinery can be used by numpy or an extension is here: https://gist.github.com/ikrommyd/51dddb52e54c6f99aa86637b16a11d22
The PR/design should be reviewed in alignment with that.
Here I have a
minmax.cppextension with asetup.pythat implements aminmummaximumufunc and registers reduction loops for it which would beminmax. I also include a test filetest_minmax.pyin the gist.This is obviously not ready, I'm just opening it as a draft for comments about the core ideas. We definitely need to consolidate the design and add better checks and error messages in a few places. Currently on my macos and linux machines both the numpy testing suite and
test_minmax.pypass so I'm curious to see what the full CI does.I plan to make a PR on my fork soon (next couple of days) that targets the PR branch that implements minmax as part of numpy using the reduction loop mechanism instead of having it in a separate extension.
This is towards #9836
First time committer introduction
N/A
AI Disclosure
Claude Opus 4.8 via the Claude Code harness has been used for A) querying around the numpy codebase as a fancier grep B) reviewing for logic or reference counting bugs and C) to write comments that I was too lazy to write myself. The whole design and ideas are my own and most of the C code is my own. The AI has mainly fixed my bugs and wrote code to register the reduction loops on the ufunc in my minmax.cpp extension in the gist.
cc @ngoldbaum @seberg please tag anyone else who might be interested in taking a look at the design.
P.S. ignore the changes in
numpy/_core/src/umath/loops_minmax.dispatch.c.src, they are just for benchmarking purposes because my minmax.cpp extension has no SIMD right now.