ENH: implement np.minmax - #32231
ENH: implement np.minmax#32231
np.minmax#32231Conversation
This comment has been minimized.
This comment has been minimized.
1 similar comment
This comment has been minimized.
This comment has been minimized.
|
I don't know what the above message is. |
This seems to be a mypy bug that's related to #32215 and only occurs for certain mypy configurations in there's a assignment to a variable called TLDR; it's unrelated |
This comment has been minimized.
This comment has been minimized.
|
@ngoldbaum I think this should be ready for a quick pass as well. |
|
@seiko2plus do you have any thoughts on the SIMD here and extending our current Maybe @mhvk has an opinion? IMO this is fine and the simplest way to do this. It does make it marginally more complicated for someone else to eventually move away from |
|
I'd argue that this was the simplest way to do this. We want |
This comment has been minimized.
This comment has been minimized.
4 similar comments
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
ngoldbaum
left a comment
There was a problem hiding this comment.
My AI model points out that np.minmax is silently incorrect for masked arrays:
ma = np.ma.masked_array([1.0, 100.0, 2.0], mask=[0, 1, 0])
np.min(ma), np.max(ma) # (1.0, 2.0)
np.minmax(ma) # (1.0, 100.0)I think we can add a simple minmax method on the MaskedArray class to fix this:
def minmax(self, axis=None, out=None, fill_value=None, keepdims=np._NoValue):
outs = (None, None) if out is None else out
return (self.min(axis=axis, out=outs[0], fill_value=fill_value, keepdims=keepdims),
self.max(axis=axis, out=outs[1], fill_value=fill_value, keepdims=keepdims))
np.ma.MaskedArray.minmax = minmaxIMO this is worth adding to paper over the issue, even if it is adding a (minor) new feature to MaskedArray. Of course I know people don't like the attribute-forwarding this relies on and may not want to do more of it. A np.ma.minmax is certainly more principled and I'm happy to defer until that if you think the method isn't a good idea.
Also it looks like StringDType isn't supported:
>>> np.minmax(arr)
Traceback (most recent call last):
File "<python-input-2>", line 1, in <module>
np.minmax(arr)
~~~~~~~~~^^^^^
numpy._core._exceptions._UFuncNoLoopError: ufunc 'minimummaximum' did not contain a loop with signature matching types (<class 'numpy.dtypes.StringDType'>, <class 'numpy.dtypes.StringDType'>) -> (None, None)
What do you think about implementing StringDType minimummaximum loops here like you do for the other dtypes, following the existing minimum and maximum implementations? It also looks like np.min and np.max are completely untested in test_strings.py - seems worth adding tests for those along with minmax to me.
|
@ngoldbaum - On the string dtype (and potentially for other user dtypes) how about we add a generic fallback to the For masked arrays, I'd argue to not add a |
|
A generic fallback for stringdtype and user dtypes would be nice! Fair enough on masked arrays. |
|
Agreed with @ikrommyd to just add a |
Should we really be adding new |
It's good for consistency and it's literally a one-liner function. The docstring what we're adding effectively to the review. |
I'm more worried that when we put it in the relnotes, it might lead to people thinking that it's a good idea to use |
|
We can sneak it in the single release note for I believe I have addressed this round of review comments from everyone. |
|
Hi @ikrommyd - On the top-level API question - np.minmax would definitely be helpful. Bounding boxes and plot axis limits both need min and max on the same array and currently that's two separate calls walking the data twice. Even more useful on large datasets. A nanminmax version alongside np.minmax would be useful too. If minmax ships without it, users will hit the gap immediately and end up writing data[~np.isnan(data)] every time. |
Signed-off-by: Iason Krommydas <[email protected]>
Signed-off-by: Iason Krommydas <[email protected]>
…in fromnumeric.pyi 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]>
|
Yap, thanks, fly-by fixes also looks right. Let's merge when CI is done and then follow-up if necessary :). |
Conflicts came from numpygh-32231 (np.minmax), which extended the hardcoded reduction fast path this branch replaces with the generic tuple-spec and forward= machinery. * arrayfunction_override.c: keep this branch's forwarding fast path. * fromnumeric.py: max/prod keep their tuple-spec dispatchers; minmax keeps the callable _minmax_dispatcher, since its out= may be a tuple of two arrays which a tuple-spec cannot unpack for the override check. It therefore has no C fast path for now and relies on the Python _UFuncNoLoopError fallback as before. * overrides._ReductionKind and the matching C enum stay removed.
|
Did anybody benchmark on how much of a difference this makes? |
@ikrommyd did but I can't find a public link offhand. However it's pretty easy to find cases that are wins: In fact, I think we checked at one point and couldn't find any cases where it isn't a win to use minmax if you really do need both min and max. |
|
@GniLudio - Yeah I had done some benchmarks. it's 1.25x to 2x better usually and never worse. Think about it this way, your core computation does not change, you still need to accumulate the min and max. What changes is how often you pass over the array and read it's data. If your operation is bottleneck is reading from memory (you are reducing along a contiguous axis), then the boost is around 2x cause you reduced the reading by half. If your bottleneck is actually calculating the min and max, then the speed up is much smaller at around 25%. These are usually platform dependent too and dependent on what instructions your cpu can run. If you find a case that it isn't faster, it's almost surely a bug that we should fix. |
I was just surprised to not see any mentions of benchmarking results. It's always easy to assume that something is faster, just to find out that it's not. Glad to see that you actually tested it and got the expected speedups. |
|
Very cool! Thank you for working on this Iason. Also thank you everyone who helped review 🙏 |
|
Sorry for commenting on a merged PR. Is it usual to add a |
|
I thought it usually is for features on an existing function. Will make a PR. Edit: #32607 |
PR summary
Closes #9836
Implements
np.minmaxusing the new ability to register reduction loops to ufuncs implemented in #31816The loops are implemented in the existing
.c.srcfiles with thenp.minimun/maximumloops and using exactly the same optimizations and SIMD as them. Two new filesminmax.cppandminmax.hare 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.srcto highway and using only c++ but that is a more complicated change with a way more difficult review so I did not do that.minmaxis not a method on the ndarray likeminandmaxare. The only thing exposed in the top-level API is the newnp.minmaxfunction. Theminimummaximumufunc is not exposed asnp.minimummaximumlikenp.minimumandnp.maximumare 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
nanminmaxin 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 anp.ma.minmax).First time committer introduction
N/A
AI Disclosure
AI has been used for writing most of the boilerplate such as import locations and the almost entirely copy-pasted doscstrings from min/max. AI has also been used to review the loops which are mostly copy-pasted from the minimum/maximum loops and find better SIMD optimizations on them that are specific to the minmax case. I'd really appreciate the opinion of a SIMD person here as the only thing I can argue about is SIMD is just trying out and seeing if it's faster or slower. My knowledge stops there.