Skip to content

Speed up FFT demag: parallelise glue loops + FFT-friendly padding - #174

Merged
davidcortesortuno merged 2 commits into
port-to-uv-scikit-core-buildfrom
demag-fft-opt
Aug 13, 2026
Merged

Speed up FFT demag: parallelise glue loops + FFT-friendly padding#174
davidcortesortuno merged 2 commits into
port-to-uv-scikit-core-buildfrom
demag-fft-opt

Conversation

@davidcortesortuno

Copy link
Copy Markdown
Collaborator

Speed up the FFT demag (CPU-only)

Profiling an LLG run shows the demag field evaluation is ~66% of runtime (the FFT convolution + the spectral tensor multiply), so any win here compounds across every CPU user. The FFTs were already well set up (r2c/c2r, FFTW_MEASURE, threaded, fftw_malloc-aligned); this PR fixes two things around them.

1. Parallelise the O(N) glue loops

  • The spectral tensor multiply (H = N·M, 9 complex-mul + 6 add per point) ran single-threaded while the FFTs used all cores. It also iterated over total_length instead of the lenz·leny·(lenx/2+1) valid r2c spectrum — ~2× the necessary work. Now bounded to nfreq and OpenMP-parallelised.
  • The pack / zero / unpack loops are parallelised too (collapse(2) so 2D meshes, nz=1, also thread).

2. FFT-friendly padding

  • Pad to the next even 7-smooth (2·3·5·7) size ≥ 2n instead of exactly 2n, so awkward mesh sizes avoid FFTW's slow large-prime path (e.g. 2·127 = 254 = 2×127).
  • The tensor Nyquist-zeroing is generalised i==nx → i==lenx/2identical for the 2n case, so good sizes are byte-for-byte unchanged.

Measured (2D, 4 threads) — demag field per call

n before after speedup
32 0.078 ms 0.037 ms 2.1×
64 0.301 ms 0.121 ms 2.5×
128 1.052 ms 0.468 ms 2.2×
127 (large-prime pad) 2.184 ms 0.464 ms 4.7×

1.4× overall for demag-bound simulations. No GPU required — this is the CPU path everyone uses.

Correctness

  • Field magnitudes are bit-for-bit unchanged across all sizes (including the re-padded n=127).
  • tests/test_demag_libraries.py (FFT demag vs brute-force DemagFull, cuboid and hexagonal meshes) passes.

Not included (measured, rejected)

  • Batching the 3 FFTs via fftw_plan_many: measured; only helped tiny n=32 (−18%, already negligible in absolute terms) and was within noise for the sizes that matter, since each FFT already saturates the cores. Not worth the added allocation/stride complexity.
  • RHS N_Vector↔numpy zero-copy (separate investigation): the marshalling copies are <2% of runtime — no measurable effect.

Notes

  • Stacked on port-to-uv-scikit-core-build (one commit) — intended for review after the uv branch is accepted.
  • demag.c is shared with master, so this applies cleanly to either line.
  • A useful step toward the Fidimag 4.0 transition: better CPU demag throughput without changing the public API or numerical results.

🤖 Generated with Claude Code

The demag field eval is ~66% of LLG runtime. The FFTs were already r2c/c2r,
FFTW_MEASURE, threaded and aligned, but two things were leaving CPU on the table:

1. Parallelise the O(N) glue loops around the FFTs. The spectral tensor
   multiply (H = N.M; 9 complex-mul + 6 add per point) ran single-threaded
   while the FFTs used all cores; also it iterated over total_length instead of
   the lenz*leny*(lenx/2+1) valid r2c spectrum, doing ~2x the needed work. Now
   bounded to nfreq and OpenMP-parallelised, along with the pack/zero/unpack
   loops (collapse(2) so 2D meshes parallelise too).

2. Pad to the next even 7-smooth (2,3,5,7) size >= 2n instead of exactly 2n, so
   awkward mesh sizes avoid FFTW's slow large-prime path (e.g. 2*127 = 254 =
   2x127). The tensor Nyquist-zeroing is generalised from i==nx to i==lenx/2,
   which is identical for the 2n case, so good sizes are unchanged.

Measured (2D, 4 threads), demag field per call:
  n=32   0.078 -> 0.037 ms   (2.1x)
  n=64   0.301 -> 0.121 ms   (2.5x)
  n=128  1.052 -> 0.468 ms   (2.2x)
  n=127  2.184 -> 0.464 ms   (4.7x, was large-prime bound)

Field magnitudes are bit-for-bit unchanged; test_demag_libraries (FFT vs
brute-force DemagFull, cuboid + hexagonal) passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Unrelated to the FFT-speedup change, but surfaced by its CI: fill_demag_tensors_c
mapped padded indices to the physical grid via x = abs(i-nx+1), which reaches
x == nx at the last padded index (i = lenx-1). Reading tensors[id + c*nx*ny*nz]
there indexes past the caller's 6*nx*ny*nz buffer, returning heap garbage -
an intermittent NaN, most visible in tensor_yz (largest component offset, so it
reads furthest past the end). Flaky by heap luck, which is why only the 3.14 CI
job caught it.

Zero the padded wrap edge (x>=nx || y>=ny || z>=nz), matching the Nyquist
zeroing in compute_demag_tensors; those points have no physical counterpart.

Strengthen test_demag_2d_pbc: assert np.isfinite (catches inf too, not just
NaN) and that the padded wrap planes are exactly zero.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@davidcortesortuno
davidcortesortuno merged commit 23ee98a into port-to-uv-scikit-core-build Aug 13, 2026
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant