Skip to content

Vacuum - PERF - Serve conjugate toroidal residue classes from one factorization - #438

Draft
jhalpern30 wants to merge 2 commits into
feature/vacuum-3d-inductancefrom
performance/vacuum-3d-conjugate-classes
Draft

Vacuum - PERF - Serve conjugate toroidal residue classes from one factorization#438
jhalpern30 wants to merge 2 commits into
feature/vacuum-3d-inductancefrom
performance/vacuum-3d-conjugate-classes

Conversation

@jhalpern30

@jhalpern30jhalpern30 commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Release note

  • Audience: developers
  • Numerical impact: none (harness @ 185acd0)
  • Migration: none

Toroidal residue classes k and -k now share one operator assembly and one LU factorization, since the field-period blocks are real and so D̂₋ₖ = conj(D̂ₖ). A 3D stellarator run whose mode set spans conjugate classes gets 1.55-1.95x off the vacuum solve for no change in results.

Regression report

regress --cases diiid_n1,solovev_n1 --refs feature/vacuum-3d-inductance,performance/vacuum-3d-conjugate-classes @ 185acd08

casequantities trackedchanged
diiid_n147none
solovev_n122none

Both cases are nfp = 1, which has exactly one self-conjugate class, so no pairing can occur in either — they confirm nothing regressed, not that the reduction works.

Notes for reviewers

Base branch is feature/vacuum-3d-inductance, not develop — this stacks on that PR and should merge into it.

Review package (diagrams, derivations, benchmarks):https://claude.ai/code/artifact/17fac702-393d-4993-9b49-e290234d3ecc
It covers this change and the branch below it as one story.

The identity

The field-period blocks D_d, S_d are real for any boundary — that is already why the code picks Float64 when nfp == 1. So D̂₋ₖ = conj(D̂ₖ) and Ŝ₋ₖ = conj(Ŝₖ), and with F := conj(E') for the partner class's Fourier rows:

wv[k'] = c·E' (conj(A) \ conj(B) E'ᴴ)|p = conj( c·F (A \ B Fᴴ)|p ) A = D̂ₖ, B = Ŝₖ

So the partner needs no new code path: run the existing per-class body with conj(E') as its mode basis against the representative's factorization, then conjugate the output block. That expression is invariant under a simultaneous unitary change of basis, which is why the same rule works verbatim on the stellarator-symmetric path — the class-k basis is reused and the real transformed operator is the same matrix for both classes.

I_v is accumulated pre-conjugation and the loop already applies a trailing conj!, so for a partner the two cancel and that call is skipped. That one-line asymmetry is the likeliest place for a sign errorVacuum.jl, the partner || conj!(Iv_block) line — and is what the A/B matrix exists to catch.

Verification

checkresult
pairing on vs off — 64 configs (nfp 1-5, wall/nowall, use_symmetry on/off, compute_Iv on/off)<=1.3e-14
nfp=1, nfp=2, single-class mode family (no pair possible)bitwise identical
asymmetric boundary (falls through the stellarator path, still pairs)1.1e-16
test/runtests_vacuum.jl384/384
examples/Solovev_ideal_example_3D vs base branch, 135 numeric datasetsbitwise identical

Agreement is roundoff rather than bitwise on purpose: the unpaired path builds cis(-2*pi*(nfp-k)*d/nfp) while the paired path conjugates cis(-2*pi*k*d/nfp), and those differ in the last bits.

Measured

nfp=5, 64x32 per period, conformal wall, 6 threads, back-to-back in one process:

modesKneithersymmetry onlypairing onlyboth
n=[2,3]23.189 s1.393 s1.777 s0.769 s (4.15x)
n=1:446.523 s2.825 s3.337 s1.570 s (4.15x)
n=0:458.179 s3.158 s5.093 s2.036 s (4.02x)

The two mechanisms are multiplicative and do not interfere. Memory is neutral — peak is still one class's operator.

Worth pushing on

  • Zero benefit for anything currently runnable end-to-end. Every example and both regression cases are nfp = 1, which has exactly one, self-conjugate, class. The gain is entirely prospective.
  • It does not asymptote to 2x. Full class coverage always leaves 1 (odd nfp) or 2 (even nfp) unpairable self-conjugate classes, so the realistic band is 1.3-1.6x; 1.79x needs a mode set that avoids k=0 and k=nfp/2.
  • Neither use_symmetry nor use_conjugate_pairing is reachable from a full GPEC run. All three callers outside the Vacuum module (Free.jl:105, Free.jl:240, ResponseMatrices.jl:304) omit them. I kept them as kwargs rather than VacuumInput fields because VacuumInput states the physics problem and these do not change the answer — but if you want an escape hatch from gpec.toml, ForceFreeStatesControl is the natural home. Not done here; it pairs naturally with plumbing nfp through.

Commit message

The single commit subject is informal and says the work is unreviewed. Worth rewording before merge.

…operators via conjugate pairs to reduce the number of solves by around 2x. Again, this was all Claude and I need to look at this to clean it up
@github-actions

Copy link
Copy Markdown
Contributor

This pull request is missing a reviewer.

If you are not ready to name them, mark this pull request as a draft.
docs/development/contributors.md suggests lead developers to ask.
Merging is not blocked here, but no pull request may be merged without human review.

…es, ~2.9x faster at nfp=2
The operator element type was chosen once per call rather than per mode family: without
stellarator symmetry it was ComplexF64 for every family whenever nfp > 1. That included the
self-conjugate families k=0 and, for even nfp, k=nfp/2, whose phases exp(-2πikd/nfp) = ±1 are
real and whose reduced operators are therefore real. The cause was the shared flat buffer, which
carries a single element type, so one general family forced complex on all of them.
Both element types are now carved from one Float64 backing store, the complex families through a
strided reinterpret (still a StridedArray, so LAPACK applies). Peak allocation is therefore
byte-for-byte unchanged when any family is complex, and halved when every family is real, which
is the case for nfp <= 2 and for any stellarator-symmetric run.
Selecting the type alone would have been a net 2.6x regression: BLAS has no mixed real/complex
triangular solve, and Julia's generic fallback is ~30x slower than a matched one, costing more
than the real factorization saves. A real family now carries its complex right-hand side through
both the projection and the solve as a real [Re Im] pair, which is exact because a real operator
propagates the two parts independently. That path is 30x faster than the generic fallback and
1.9x faster than the matched complex solve, so this also removes the same latent penalty from the
existing nfp == 1 and stellarator-symmetric paths, which were already building real operators.
Measured, 8 threads, no wall, non-stellarator-symmetric boundary, min of 3:
nfp=2 N=9216 Np=4608 2.891 s -> 1.003 s 2.9x
nfp=3 N=9216 Np=3072 1.066 s -> 0.707 s 1.5x
nfp=4 N=9216 Np=2304 0.800 s -> 0.526 s 1.5x
nfp=3 N=9216 stell-sym 0.476 s -> 0.286 s 1.7x
The factorization itself gains ~3.5x; end-to-end is diluted by assembly, so the speedup grows
with resolution (nfp=2 goes 1.2x -> 2.2x -> 2.9x over N = 2048, 4608, 9216). nfp=2 gains most
because every family there is self-conjugate; it was previously the case where the field-period
reduction bought no factorization speedup at all. Peak RSS at Np=6144 drops 2.57 GB -> 2.23 GB.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

perfSame answers, less time or memory

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jhalpern30