-
-
Notifications
You must be signed in to change notification settings - Fork 12k
ENH: Use itertools.product for ndindex to improve performance #29165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ENH: Use itertools.product for ndindex to improve performance #29165
Conversation
| print(f"Time to consume first 1000 elements: {partial_time * 1000:.3f} ms") | ||
| print("--> Memory efficient: Only generates indices as needed") | ||
|
|
||
| if __name__ == "__main__": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be more sensible to follow our usual approach to writing/running benchmarks with asv. It has builtin harnesses for timing (time_) and peak memory usage (peakmem_). It will probably be easier to evaluate changes when sticking to this common approach we use for benchmarking perf/memory usage. It shouldn't be too hard to navigate around the other asv benchmarking modules we have to get a sort of "template" for how that works.
numpy/lib/_index_tricks_impl.py
Outdated
| New in version X.Y.Z. | ||
| As of NumPy 2.3.0.dev0, this iterator is implemented using `itertools.product` | ||
| from Python's standard library. This change provides significant improvements | ||
| in both performance and and memory efficiency, particularly for large iteration |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
repeated and; we'll probably want to change the version numbering you use above since i.e., 2.3.0 has already been released
numpy/lib/tests/test_index_tricks.py
Outdated
| # 1. Empty Shapes and Zero Dimensions | ||
| def test_ndindex_empty_and_zero_dimensions(): | ||
| # Empty shape | ||
| assert list(np.ndindex()) == [()] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It probably does make sense to add a few more test cases, but note that the already-existing test_ndindex already has this particular test case for example, and may cover one or two other cases.
numpy/lib/tests/test_index_tricks.py
Outdated
| with pytest.raises(TypeError): | ||
| list(np.ndindex([2, 3])) | ||
| with pytest.raises(TypeError): | ||
| list(np.ndindex((2.0, 3))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor: could use pytest.mark.parametrize for concision and separation of case execution
numpy/lib/tests/test_index_tricks.py
Outdated
| expected_count = 20 * 30 * 40 | ||
| assert_equal(count, expected_count) | ||
|
|
||
| assert elapsed_time < 1.0, f"ndindex took {elapsed_time:.3f}s, which seems slow" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't usually do this in our testsuite--we typically guard against performance regressions using asv benchmarks separate from our regression tests proper (see my other comment about asv)
numpy/lib/_index_tricks_impl.py
Outdated
| Notes | ||
| ----- | ||
| New in version X.Y.Z. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This text seems fine for a release note. But since the interface is unchanged, I would leave it out if the user documentation.
jorenham
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should be able to fix the linter errors by running ruff check --fix locally. If you don't have ruff installed yet, then you can run pip install -r requirements/linter_requirements.txt to install the same ruff version as we use in CI.
|
Hi @jorenham @tylerjereddy and @eendebakpt, I've pushed updates that include: Switched to product(*map(range, shape)) for cleaner ndindex initialization. Ran ruff check --fix across all relevant files — no issues reported locally. Amended the commit message and force-pushed. That said, the CI lint jobs (ComprehensiveTests Lint, Linux tests / lint) are still failing — I’m currently investigating. Most other CI failures (macOS, Windows, BLAS, FreeBSD, Emscripten, etc.) appear unrelated to this PR. The core focus remains the ndindex rewrite using itertools.product for better performance and memory usage, along with an expanded and refactored test suite. Let me know if you'd like me to dive into any specific check. Thanks again for your feedback and support! |
mattip
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, needs a release note. Some of the comments in the tests are redundant
c75e95a to
94f2a73
Compare
numpy/lib/_index_tricks_impl.py
Outdated
| strides=_nx.zeros_like(shape)) | ||
| self._it = _nx.nditer(x, flags=['multi_index', 'zerosize_ok'], | ||
| order='C') | ||
| if any(dim < 0 for dim in shape): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if any(dim < 0 for dim in shape): | |
| if min(shape) < 0: |
Is perhaps faster, as it avoids creation of a generator.
numpy/lib/tests/test_index_tricks.py
Outdated
| def test_ndindex_negative_dimensions(negative_shape_arg): | ||
| """Test that negative dimensions raise ValueError.""" | ||
| with pytest.raises(ValueError): | ||
| # ndindex should raise ValueError immediately for negative dimensions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you are testing ndindex raises immediately, then remove the list in the next line
numpy/lib/tests/test_index_tricks.py
Outdated
| """Test that negative dimensions raise ValueError.""" | ||
| with pytest.raises(ValueError): | ||
| # ndindex should raise ValueError immediately for negative dimensions | ||
| list(ndindex(negative_shape_arg)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| list(ndindex(negative_shape_arg)) | |
| ndindex(negative_shape_arg) |
|
@samruddhibaviskar11 The PR looks like a nice improvement overall. Would you be able to address the remaining comments and look at the CI failures? |
Yes, @eendebakpt I will look into the remaining comments and CI failures. |
…ts, and benchmarks
|
Note that there are still several problems that are causing the CI to fail. |
numpy/lib/_index_tricks_impl.py
Outdated
| strides=_nx.zeros_like(shape)) | ||
| self._it = _nx.nditer(x, flags=['multi_index', 'zerosize_ok'], | ||
| order='C') | ||
| if min(shape) < 0: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if min(shape) < 0: | |
| if min(shape, default=0) < 0: |
(to handle the case where shape is empty)
- Use min(..., default=0) to handle empty shapes safely. - Fix cascade issues in ndindex implementation. - Add regression tests to cover empty shapes and previous bugs.
a4cb71d to
cf267eb
Compare
|
Hi all the CI is green ✅. Please take another look and let me know if anything else is needed. Thanks! |
| providing significant improvements in both performance and memory efficiency, | ||
| particularly for large iteration spaces, while maintaining the original behavior and interface. | ||
|
|
||
| For example, for a (50, 60, 90) array, this results in a 5.2× speedup on average, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| For example, for a (50, 60, 90) array, this results in a 5.2× speedup on average, | |
| For example, for an array of shape (50, 60, 90) the NumPy `npindex` benchmark improves performance by a factor 5.2. |
Maybe leave of the memory. Is there really a significant amount saved compared to the size of the array?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Updated to your phrasing and removed the memory mention as suggested. ✅
eendebakpt
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left a minor comment for the release notes, but otherwise this looks good.
Thanks @samruddhibaviskar11
jorenham
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code itself looks good now AFAIK. Once the other comments have also been addressed, and the other maintainers are also happy, this should be ready to merge.
|
Hi all, The CI doc build failed due to network issues fetching external references (pandas/matplotlib). Core tests are passing, so this seems like an infrastructure issue. Could someone help resolve this CI issue? |
|
Indeed looks like a network issue. You could either trigger a new CI build to be sure (make an empty commit), or the numpy maintainers could (if pr is approved) merge anyway. |
|
Thanks for confirming! I’ll trigger a new CI build with an empty commit to be safe. |
seberg
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think plenty reviewed and a quick look through, this looks nice, thanks a lot. Covered a lot of ground iwth benchmarks and tests.
|
Thank you all for reviewing and guiding me ✅ — I’ve learned a lot! |
…29165) * PERF: Rewrite ndindex using itertools.product for speed and memory efficiency * Refactor ndindex using itertools.product and add ASV benchmarks and expanded tests * Fix linting issues flagged by Ruff across ndindex implementation, tests, and benchmarks * Add release note and finalize ndindex improvements * BUG: Raise ValueError for negative dimensions in ndindex * MAINT: Expose ndindex in numpy.lib for public usage and benchmarks * MAINT: Fix ndindex __module__ attribute for public API test * Remove numpy.distutils tests after dropping distutils in NumPy 2.0 * Format imports in numpy.lib.__init__.py with Ruff * Set ndindex.__module__ = 'numpy.lib' to match test expectation * BUG: Raise ValueError for negative dimensions in ndindex; MAINT: Remove ndindex from numpy.lib public API; TEST: Update negative dimension test * Revert distutils test deletions as requested * Revert numpy/lib/__init__.py to upstream/main state as requested * lib: ndindex—handle empty shape and fix implementation bugs - Use min(..., default=0) to handle empty shapes safely. - Fix cascade issues in ndindex implementation. - Add regression tests to cover empty shapes and previous bugs. * TST: remove imports to fix lint for test_index_tricks.py * STYLE: Remove ruff formatting and lint fixes * REF: ndindex test cases — added functional tests and applied Ruff fixes * Add benchmark comparing ndindex vs itertools.product * MAINT: remove unused in ndindex * DOC: clarify release note for ndindex performance improvement * trigger CI rebuild
…29165) * PERF: Rewrite ndindex using itertools.product for speed and memory efficiency * Refactor ndindex using itertools.product and add ASV benchmarks and expanded tests * Fix linting issues flagged by Ruff across ndindex implementation, tests, and benchmarks * Add release note and finalize ndindex improvements * BUG: Raise ValueError for negative dimensions in ndindex * MAINT: Expose ndindex in numpy.lib for public usage and benchmarks * MAINT: Fix ndindex __module__ attribute for public API test * Remove numpy.distutils tests after dropping distutils in NumPy 2.0 * Format imports in numpy.lib.__init__.py with Ruff * Set ndindex.__module__ = 'numpy.lib' to match test expectation * BUG: Raise ValueError for negative dimensions in ndindex; MAINT: Remove ndindex from numpy.lib public API; TEST: Update negative dimension test * Revert distutils test deletions as requested * Revert numpy/lib/__init__.py to upstream/main state as requested * lib: ndindex—handle empty shape and fix implementation bugs - Use min(..., default=0) to handle empty shapes safely. - Fix cascade issues in ndindex implementation. - Add regression tests to cover empty shapes and previous bugs. * TST: remove imports to fix lint for test_index_tricks.py * STYLE: Remove ruff formatting and lint fixes * REF: ndindex test cases — added functional tests and applied Ruff fixes * Add benchmark comparing ndindex vs itertools.product * MAINT: remove unused in ndindex * DOC: clarify release note for ndindex performance improvement * trigger CI rebuild
Summary
This PR rewrites
numpy.ndindexto useitertools.productinstead ofnditer, significantly improving both runtime performance and memory efficiency while preserving full backward compatibility.It addresses Issue #28921: ndindex is slower than itertools alternative.
Motivation
The current implementation of
ndindexusesnditer, which is flexible but adds unnecessary overhead when generating Cartesian product indices. Sinceitertools.productis a well-optimized C implementation for this exact pattern, switching to it results in a notable performance improvement.Changes Made
ndindex.__init__and__next__methods innumpy/lib/_index_tricks_impl.pyto utilizeitertools.product.numpy/lib/tests/test_index_tricks.pyto cover various scenarios (e.g., empty/zero dimensions, non-integer dimensions, iterator independence, stop iteration behavior, large dimensions, and consistency withndenumerate).benchmarks/benchmarks/bench_ndindex.pyto provide a clear performance comparison.ndindexin_index_tricks_impl.pyto include aNotessection explaining the implementation change and its benefits.Benchmark Results
Benchmarks were added in
benchmarks/benchmarks/bench_ndindex.py. Here is a summary:Memory Usage Test:
SUCCESS! Average performance improvement: 9.8x
This demonstrates excellent performance improvement.
Test Cases & Functional Correctness
The following new tests were added to ensure correctness across a variety of use cases:
Tests live in numpy/lib/tests/test_index_tricks.py.
Environment/Testing Notes:
✅ The optimized implementation was developed and tested locally in a standalone file (ndindex_optimized.py), separate from the NumPy codebase.
✅ A full set of unit tests were written and run successfully in isolation using pytest, validating correctness across all intended behaviors.
🔁 When attempting to run the full NumPy test suite inside a GitHub Codespace (editable install with pip install -e . and meson/ninja), I encountered the following persistent import error:
ImportError: cannot import name 'version' from partially initialized module 'numpy'🔍 This appears to be a build tooling issue, not a problem with the PR's logic or correctness.
📦 Submitting this PR in hopes that GitHub Actions CI will successfully build and test the full suite, or maintainers can help identify the issue with the local dev environment
Docstring Update
A Notes section has been added to the ndindex docstring to reflect the new implementation:
✳️ Replace X.Y.Z with the release version when merging.
Related Issue
Closes: #28921
Author Note
👋 I'm a first-time contributor to NumPy and would love feedback on this PR. Thanks to @seberg and @ricardoV94 for the helpful context in the original issue!
📎 GitHub: @samruddhibaviskar11