Skip to content

Inverse plans (inv, \, ldiv!) and in-place plans (plan_fft!, plan_bfft!) - #133

Open
pankgeorg wants to merge 7 commits into
JuliaMath:mainfrom
JuliaComputing:feat/inverse-inplace-plans
Open

Inverse plans (inv, \, ldiv!) and in-place plans (plan_fft!, plan_bfft!)#133
pankgeorg wants to merge 7 commits into
JuliaMath:mainfrom
JuliaComputing:feat/inverse-inplace-plans

Conversation

@pankgeorg

Copy link
Copy Markdown

Item from the plan in #130 that came out of checking what DSP.jl needs from a plan (stacked on #132; the diff includes its commit until it lands).

Before

  • inv(p) threw TypeError: in typeassert, expected Union{} for every FFTA plan: there was no plan_inv method, and the placeholder pinv::FFTAInvPlan field made AbstractFFTs.pinv_type resolve to Union{}. Consequently p \ x, ldiv!(y, p, x) and inv(plan_fft!(x)) (used by DSP.jl's convolution) were unavailable.
  • There were no in-place plans: plan_fft!, plan_bfft!, plan_ifft!, and therefore fft!, bfft!, ifft!, had no methods — and fft!(x) could never have worked, because FFTA's internal kernel was itself named fft!, shadowing AbstractFFTs.fft! in the exported namespace.

Now

  • The plan structs are mutable with a const payload and an initially undefined pinv field, exactly the pattern FFTW.jl uses, so AbstractFFTs.inv caches plan_inv(p) in the plan. plan_inv is defined for complex plans (opposite direction, 1/prod(size) scale) and real plans (rfftbrfft).
  • plan_fft!/plan_bfft! return an FFTAPlan_inplace wrapping an ordinary plan and a buffer shaped like the input. When the input and output alias, the input is copied to the buffer and transformed out of place (FFTA's kernels are out of place; the 1D pencil path is not alias-safe); mul!(y, p!, x) with distinct arrays uses the wrapped plan directly. plan_ifft! and fft!/bfft!/ifft! follow from AbstractFFTs.
  • The internal kernel is renamed fft_kernel! so the AbstractFFTs function is no longer shadowed.
  • test/inverse_inplace.jl covers inv, \, ldiv!, plan_ifft/plan_irfft, caching, in-place plans for 1D and N-d regions, fft!/bfft!/ifft!, allocation-free in-place execution for 1D plans, and the DSP.jl pattern p! = plan_fft!(x); ip! = inv(p!); p! * buf; ip! * buf.
  • The ExplicitImports check now ignores plan_inv, ScaledPlan and normalization (non-public AbstractFFTs names that a backend has to use, like Plan), and the Aqua piracy allowance covers plan_fft!/plan_bfft! like the other entry points.

With FFTW.jl loaded alongside FFTA, plan_rfft(::Vector{Float64}, ::Int) was
ambiguous between FFTW's StridedArray method and FFTA's method annotated
with region::RegionTypes, turning rfft(x) into a MethodError. Leave region
unannotated on the AbstractFFTs entry points (as plan_fft already does) and
normalise it in an internal function, so FFTW's methods are strictly more
specific and take over as AbstractFFTs intends.

A coexistence test runs in a subprocess (loading FFTW in the test process
would make every other test exercise FFTW).
Real-input/real-output plans only implemented *, so every rfft/irfft
allocated, mul!(y, p, x) with a preallocated output was a MethodError,
rfft along one dimension of an N-d array went through mapslices, and 2D
real plans ran a full complex transform and discarded half of it.

FFTAPlan_re now carries a scratch buffer and two pencil kernels
(_rfft_pencil!/_brfft_pencil!) implement the even-length half-size trick
and the odd-length transform on AbstractVectors, so views work as input
and output. mul! is defined for 1D plans on 1D and N-d arrays (looping
over pencils along the region) and for 2D plans on N-d arrays (real
transform along the first region dimension, complex along the second),
and * allocates the output and calls mul!. The 2D plan's first call
graph is built for the half length like the 1D plan's.
inv(p) threw a TypeError for every FFTA plan: no plan_inv method existed
and the dummy pinv::FFTAInvPlan field made AbstractFFTs' pinv_type
resolve to Union{}. The plan structs are now mutable with an initially
undefined pinv field, as in FFTW.jl, so AbstractFFTs.inv caches the
result of the new plan_inv methods for complex and real plans; p \ x,
ldiv!(y, p, x), plan_ifft and plan_irfft work through them.

plan_fft!/plan_bfft! return an FFTAPlan_inplace wrapping an ordinary plan
plus a buffer: when input and output alias, the input is copied to the
buffer and transformed out of place (FFTA's kernels are out of place, and
the 1D pencil path is not alias-safe); otherwise the wrapped plan is used
directly. fft!, bfft! and ifft! from AbstractFFTs now work — the internal
kernel that shadowed AbstractFFTs.fft! is renamed fft_kernel!.
…llow AbstractFFTs backend hooks in the ExplicitImports check
@codecov

codecov Bot commented Aug 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.19048% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.17%. Comparing base (7aeb327) to head (5fae53d).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
src/plan.jl 96.13% 8 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #133      +/-   ##
==========================================
- Coverage   98.80%   98.17%   -0.64%     
==========================================
  Files           5        4       -1     
  Lines         585      656      +71     
==========================================
+ Hits          578      644      +66     
- Misses          7       12       +5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…nels

Replacing mapslices with strided views made real transforms along dims=2
of wide matrices (64 x N) 1.2-1.3x slower on x86-64: the mapslices copy
had been an unlabelled copy-in that turned a stride-of-a-cache-line
gather into one contiguous pass before the kernel. Pencils whose parent
arrays are unit-stride along the transform dimension still go to the
kernels directly (the dims=1 gain stays); any other pencil is copied to
two plan-owned contiguous buffers first and copied back after, so
execution stays allocation-free and the dims=2 result is now identical
to the mapslices one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant