Fix fft vmap and jvp for transforms over a subset of axes - #4138
Open
kapellirohith wants to merge 1 commit into
Open
Fix fft vmap and jvp for transforms over a subset of axes#4138kapellirohith wants to merge 1 commit into
kapellirohith wants to merge 1 commit into
Conversation
FFT::jvp dropped axes_ and transformed every axis of the tangent, so the forward mode derivative of any fft over a subset of the axes was silently wrong. This is the common case since a batch axis is not transformed. FFT::vmap resized every transformed axis for a real transform, but only the last one changes size. vmap of rfftn or irfftn returned a wrongly shaped output, and for irfftn the output was larger than the data the transform produces.
kapellirohith
marked this pull request as draft
August 10, 2026 16:39
kapellirohith
marked this pull request as ready for review
August 10, 2026 19:32
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.
Problem
The forward mode derivative of an fft that does not cover every axis of the
array is silently wrong. Since a batch axis is never transformed, that is the
ordinary batched case.
Minimal reproducer (M3 Pro, macOS 26.x, main @ e78d894):
The second line is the positive identification: before this change the tangent
is not merely wrong, it is exactly
fftnover every axis, to 0.0.A 1-D input, where the single axis is all the axes: 0.0 before and after.
mx.gradof the same function: correct before and after.mx.fft.fftn(a, axes=(0, 1))on a 3-D array: 16.038 before, 0.0 after.Only forward mode, and only when the transformed axes are a strict subset.
Mechanism
FFT::jvpcalled the no-axesfftn,rfftn,ifftnandirfftnoverloads.Those resolve to
fft_impl(a, real, inverse, norm, s), which buildsstd::vector<int> axes(a.ndim())andstd::iotas it, so the tangent wastransformed over every axis regardless of
axes_.For
rfft(axis=0)on a(4, 6)real input the shape itself comes back(4, 4)instead of(3, 6):rfftnover both axes halves the last axis,rfft(axis=0)halves the first.FFT::vjpimmediately above already threadsaxes_into all four branches,which is why reverse mode is correct and this survived.
Separately,
FFT::vmapapplied the real transform size change to everytransformed axis, while
fft_implresizes onlyvalid_axes.back().vmapofrfft2on a(3, 8, 8)array returned(3, 5, 5)instead of(3, 8, 5), andvmapofirfft2returned an output larger than the input supports. Note thatvalid_axesis in the order the caller passed, not sorted, so the axis thatchanges size is the last one passed; the fix keys off
fft_axes.back()andpreserves that.
Both date to the initial commit d1f8627 (2023-11-29).
Why existing tests missed it
test_fft_gradscoversmx.gradonly, and there was no vmap or jvp coveragefor the fft anywhere in the Python or C++ suites.
Known limitation, not fixed here
vmapofirfftnwith an explicitly requested odd output length still returns2 * (n - 1):This is a genuine collision, not an oversight in the rule. The primitive stores
only
(axes_, inverse_, real_), andn = 6andn = 7both produce anirfftninput whose last axis isn // 2 + 1 == 4, so the requested size isunrecoverable from the primitive state by the time
vmapruns. Fixing it meansadding an output size field, which changes
state()and therefore exportserialization, so I left it out of this change.
Validation
M3 Pro, macOS 26.x, against main @ e78d894. Identical results on GPU and with
mx.set_default_device(mx.cpu), as expected for a graph level change.jvp(f)(x,t) == f(t)and the vjp transpose identityRe<L t, c> == Re<t, L* c>vmap(jvp),jvp(vmap),jvp(jvp)andvjp(jvp)(the last two exactly zero, as required for a linear map)norm=backward, ortho and forward under jvp and vmapgrad(vmap(f))against per-slice grads, andvmap(grad(f))mx.compilecold and warm, including compiledvmaptest_fft_too_largeon cpu onlyNot verified locally: CUDA, Linux, Windows.
Tests
test_fft_vmapchecks vmap against the stack of the op applied to each slice,and
test_fft_jvpchecks the tangent againstf(t), both over real and inverseand five axis orders. Both fail on main.
Checklist
Put an
xin the boxes that apply.pre-commit run --all-filesto format my code / installed pre-commit prior to committing changes