perf: avoid a redundant full-array copy in scipy.fft hfft/hfftn adapters - #382
Open
intel-python-devops wants to merge 2 commits into
Open
intel-python-devops wants to merge 2 commits into
intel-python-devops wants to merge 2 commits into
Conversation
intel-python-devops
requested review from
antonwolfy,
jharlow-intel,
ndgrigorian,
vlad-perevezentsev and
xaleryb
as code owners
September 21, 2026 15:19
Contributor
|
seems to be a legitimate performance improvement |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
This pull request is entirely AI-generated. Please review thoroughly.
perf: avoid a redundant full-array copy in scipy.fft hfft/hfftn adapters
Track: performance
Summary
I read the root AGENTS.md, .github/copilot-instructions.md, and the nested AGENTS.md files, then read _pydfti.pyx, src/mklfft.c.src, _fft_utils.py, _mkl_fft.py, and the interfaces/ adapter modules end to end looking first for descriptor/memory bugs and then for avoidable copies. I checked one suspicious pattern closely — out=x aliasing routed into a DFTI_NOT_INPLACE descriptor — but existing tests (test_fft1d.py test_vector5/6, test_matrix4) already assert that exact case is correct, so I left it alone rather than reopen settled behaviour I can't verify by running. I found and fixed a genuine avoidable copy: mkl_fft.interfaces._scipy_fft.hfft and hfftn each did
x = np.array(x, copy=True)followed bynp.conjugate(x, out=x), i.e. two full passes over the array, where the sibling _numpy_fft.hfft already uses the equivalent single-passnp.conjugate(x). I replaced both occurrences withx = np.conjugate(x), which allocates the output and negates the imaginary part in one pass instead of two, and verified by re-reading the edited file that this was the only change and that behaviour (result values, dtype, shape, memory layout, and non-mutation of the caller's array) is preserved. I did not touch ihfft/ihfftn (they conjugate a freshly-allocated FFT output in place, so there is no caller-owned buffer at risk there) and did not attempt the higher-ranked but riskier stride-relaxation TODOs in _pydfti.pyx, leaving those to a human who can run the MKL-backed test suite.Mechanism
hfft/hfftn built the conjugated input via copy-then-mutate:
np.array(x, copy=True)(one full memory pass to allocate and copy every element) followed bynp.conjugate(x, out=x)(a second full pass to negate every imaginary part in place). Since x is already a plain ndarray by the time this code runs (it went through np.asarray in _validate_input, so it isn't a subclass needing normalization) and the copy's only purpose was to protect the caller's array from the in-place conjugate,np.conjugate(x)withoutout=does the identical job in one pass: it allocates a new array and writes the conjugated values directly, never touching the input. This halves the memory traffic per element for every call to scipy.fft.hfft/hfftn routed through mkl_fft.interfaces, and is the exact "avoidable copy" pattern ranked second in the mandate's performance priorities.Why this is safe
The two forms are computed over the same dtypes: _validate_input only rejects float16/float128/complex256, and conjugate is dtype-preserving in both forms (identity-copy for real dtypes, imaginary-negation for complex). Shape is unaffected since conjugate is elementwise. Strides/memory layout are unaffected because both np.array(..., copy=True) and the conjugate ufunc default to order='K' (preserve layout), so the freshly-allocated array in the new code has the same layout characteristics the old two-step form produced. Aliasing/mutation safety is preserved by a different mechanism: the old copy existed so the in-place conjugate wouldn't mutate the caller's x; np.conjugate(x) without out= never writes into x at all, so the caller's array is equally untouched. norm and out parameters are untouched by this diff; hfft/hfftn in this file don't pass out= through to mkl_fft.irfft/irfftn (noted in the existing "overwrite_x is not utilized" comment), so there's no interaction with a caller-supplied output buffer to reason about.
Candidates rejected
Left to a human