Skip to content

Slurm - #420

Open
jtravs wants to merge 34 commits into
LupoLab:masterfrom
jtravs:slurm
Open

Slurm#420
jtravs wants to merge 34 commits into
LupoLab:masterfrom
jtravs:slurm

Conversation

@jtravs

@jtravs jtravs commented Apr 5, 2026

Copy link
Copy Markdown
Contributor

Improve Slurm handling of memory, paths, and julia environments

Copilot AI review requested due to automatic review settings April 5, 2026 10:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the SlurmExec execution mode to generate more robust Slurm array-job scripts, adding per-task memory handling, Julia environment selection, thread configuration, and safer path handling, along with expanded user documentation.

Changes:

  • Extend SlurmExec with memory, project, and nthreads options and generate SBATCH scripts accordingly.
  • Add memory parsing to derive Julia --heap-size-hint from Slurm --mem.
  • Add extensive Slurm usage documentation to the scans guide.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
src/Scans.jl Extends SlurmExec, adds heap-hint parsing, and updates Slurm job script generation (mem, threads, quoting, ulimit, Julia binary path).
docs/src/scans.md Documents SlurmExec behavior and provides usage examples and explanation of memory/threads/project handling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Scans.jl Outdated
Comment thread src/Scans.jl Outdated
Comment thread src/Scans.jl
Comment thread src/Scans.jl Outdated
Comment thread src/Scans.jl Outdated
Comment thread src/Scans.jl
Comment thread src/Scans.jl Outdated
@jtravs

jtravs commented Apr 5, 2026

Copy link
Copy Markdown
Contributor Author

@copilot check my fixes and resolve addressed threads

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Scans.jl Outdated
Comment thread src/Scans.jl Outdated
Comment thread src/Scans.jl
Comment thread src/Scans.jl
Comment thread src/Scans.jl Outdated
Comment thread src/Scans.jl Outdated
Comment thread docs/src/scans.md Outdated
@jtravs
jtravs requested a review from chrisbrahms June 23, 2026 11:55

@chrisbrahms chrisbrahms left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. I'll need to check that it works with my usual way of working using SSHExec, though I don't think that is a block to merging.

Please add a brief summary to #415 for the next release notes.

Comment thread docs/src/scans.md
jtravs and others added 10 commits June 23, 2026 16:52
…ints limit

- Added `step_on` parameter to `RK45.solve` and `RK45.solve_precon` functions to allow the stepper to land exactly on prescribed positions, improving accuracy for weak components.
- Updated `run` function in `Luna.jl` to support the new `step_on` parameter.
- Enhanced `solve` function in `RK45.jl` to handle `step_on` and ensure proper stepping behavior.
- Introduced `maxpoints` parameter in `QueueExec` to limit the number of scan points processed per worker, allowing for better memory management in long-running simulations.
- Updated `Scans` module to handle `maxpoints` in both `QueueExec` and `SlurmExec`, ensuring proper execution and resource management.
- Added tests to validate the behavior of `step_on` in RK45 and the functionality of `maxpoints` in queue scans, ensuring correctness and robustness of the new features.
test/runtests.jl: table-driven test list with substring filtering via
test_args (no arguments = full suite, unchanged order and testset names).
test/test_perf_bitident.jl: small envelope free-space Kerr propagation
run twice and compared with isequal, as the reference point for the
optimised code paths added in subsequent commits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Kerr_field/Kerr_env now return callable structs (KerrField/KerrEnv, same
constructor API) carrying the pointwise trait: their polarisation can be
applied to whole arrays in one broadcast (also the GPU-friendly form)
instead of column-by-column.

TransFree skips its oversampled buffers (Eωo/Pωo) when the grid is not
oversampled (scale == 1, complex field, no noise field) — the copies they
mediated are exact identities there. With purely pointwise responses the
polarisation additionally overwrites Eto in place, dropping Pto as well:
3 of 4 transform buffers gone at production size. The general path is
unchanged and remains selectable with fastpath=false; the free-space
EnvGrid setup also reuses the coarse FFT plan when the grids match.

All fast paths verified bit-identical in test_perf_bitident.jl; full
gates: test_kerr, test_raman, test_full_freespace, test_radial,
test_interface (298 tests) all pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- yi (dense-output) and yerr (error estimate) are now allocated lazily:
  with the default weaknorm the error metric is computed in a single fused
  pass (weaknorm_fused) and yerr never exists; with step_on-aligned saves
  yi never exists either. Custom norms keep the materialised yerr path via
  the fused_errnorm hook (opt in by defining a method).
- fbar! propagates its input in place instead of copying into a private
  scratch buffer (evaluate! rebuilds yn from y each stage; the
  locextrap=false case restores yn explicitly; the constructor passes a
  transient copy so the caller's y0 stays untouched).
- Butcher stage accumulation, locextrap and the interpolant are each one
  fused left-associated broadcast instead of sequential .+= passes
  (~26 field passes -> ~8 per step).
- preserve_input=false on solve/solve_precon/Luna.run adopts y0 as the
  stepper's yn buffer, saving one field-sized allocation.

Net for the free-space production case: 12 -> 9 persistent field buffers.
Verified bit-identical against the previous commit on six configurations
(Stepper, PreconStepper, locextrap=false, custom norm, function linop,
full 3D Kerr propagation with interpolated saves) via serialised
cross-commit comparison, plus test_rk45/full_freespace/harness gates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jtravs and others added 9 commits August 13, 2026 14:14
…a 1.12+

Utils gains tforeach/tchunks: chunked, bit-identical elementwise threading
helpers, dormant below THREADING_MINLEN elements (1M default) and with a
single Julia thread, so small-array (modal) users and CI are untouched.
Applied to the RK45 propagator kernels (the exp(linop*dz) passes — the
largest single-threaded cost in 3D runs), the fused Butcher/locextrap/
errnorm/interpolant passes, and the TransFree fast-path response,
apodisation and scaling kernels.

Utils.use_native_fftw_threads deregisters FFTW.jl's Julia-task (partr)
threading callback — which segfaults on Julia >= 1.12 — and hands FFT
threading back to libfftw3's own pthread pool (verified to be the NULL-
callback behavior of FFTW_jll's binary). set_fftw_threads applies this
automatically on Julia >= 1.12 with multiple Julia threads, so threaded
Julia kernels and threaded FFTs now combine safely.

Gates: harness + test_rk45 single-threaded (54) and with -t 4 (56,
threaded-vs-serial bitwise comparison active) all pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FactoredFreeLinop stores only the separable factors k²(ω) and k⊥²(ky,kx)
(8 MB at production size) instead of the materialised (nω,nky,nkx)
ComplexF64 array, with a specialised threaded RK45 propagator kernel that
computes elements and exponentials on the fly — removing a field-sized
array and ~14 field-sized reads per step. FreeNorm does the same for the
const_norm_free cache. Both subtype AbstractArray (lazy getindex), so
linoptype metadata, Stats dispatch and generic fallbacks are unchanged.
The element formulas are shared (inlined) with the materialising fill
loops, so lazy and dense can never drift: verified bit-identical
elementwise and through full propagation in the harness. Opt in with
make_const_linop(...; factored=true) / const_norm_free(...; factored=true);
defaults unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…adence

run's window-application buffer (Et = FT \ Eω, one field-sized array) is
replaced by the transform's own time-domain buffer via
NonlinearRHS.scratch(transform) when shapes match (TransFree without
oversampling); the buffer is dead between RHS evaluations so results are
unchanged. twin_period=N applies the spectral/temporal window projection
only every Nth accepted step and always immediately before a save (via
Output.willsave); default 1 = exact current behavior. Skipping is a
physics-level change (~5e-4 relative in the aggressive harness case) and
needs per-case A/B validation before production use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RamanPolarEnvBatched computes the Raman polarisation for all transverse
points at once: two batched dim-1 FFTs on the doubled anti-wraparound
grid per evaluation (threaded fills/multiplies) instead of two small
serial FFTs per transverse column (~2M per RK step at production size),
and the density-dependent response function is updated once per call
instead of once per column (exact — the density is a scalar). The
batched trait routes it through the TransFree fast path at array level;
the general path rejects batched responses with a clear error.

Agreement with columnwise RamanPolarEnv: rtol 1e-12 element-level and
1e-10 through a full propagation (not bitwise — batched FFT algorithms
differ from single-column ones). This is the stage that makes the
ModelPNPS Raman campaign computationally feasible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Luna.Scans parses ARGS itself (scan scripts accept --range/--batch/...),
so test files that construct a Scan reject leftover selection patterns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
step! performed the FSAL move (k7 of the completed step becomes k1 of the
next) inside the accept branch, but solve() consumes that step's dense
output *after* step! returns: the output saves at line 65 and the
interpolant closure handed to stepfun both run later. Every interpolated
save therefore evaluated the dense-output polynomial with k7 sitting in the
k1 slot, and the k1 weight is not zero (interpC column 1, b1(1) = 35/384),
so each value carried a spurious dt*b1(σ)*(k7 - k1) = O(h²) term. That
drops the dense output from 4th order to 2nd.

This is not a corner case: Luna.run leaves step_on unset by default and
Output.GridCondition saves via yfun(ts) with ts < tn, so every saved
z-slice of a default run is an interpolated value.

Move it to the top of step!, guarded by the previous step's s.ok. step! is
evaluate!'s only caller, so this is one site with one guard, and it stays
ahead of evaluate!(::PreconStepper)'s rebase of ks[1] into the new anchor
frame -- the copy must still happen in the old frame, exactly as before.
s.ok is false before the first step (ks[2:7] are `similar`, i.e. undef) and
after a rejection (k1 must survive for the retry); a min_dt clamp that
forces ok = true in steplims! still gets the move, as it did before.

Nothing between step! returning and the next step! reads or writes ks[1] --
errnorm consumes the k values before the old copy site -- so the stepped
trajectory is unchanged. Verified on the N=5 soliton problem at rtol=1e-8:
step count, hash of the z sequence, hash of the dz sequence, hash of the
final field and its energy are all bit-identical, for both the plain and
the preconditioned stepper. Only interpolated values move.

Measured on a fixed-step model problem with an exact solution
(y' = i(a + |y|²)y, h = 0.1 and 0.025):

  dense-output error at the step midpoint, h = 0.1
      plain    1.586e-02  ->  1.061e-05
      precon   9.973e-04  ->  4.953e-08
  convergence order of that error
      plain    2.00  ->  4.90
      precon   2.00  ->  4.99
  relative error of the interpolant's slope at the step start
      plain    3.97e-01  ->  2.24e-05
      precon   2.50e-02  ->  2.00e-07
  endpoint error and its order: unchanged to the last digit

and on the soliton problem, the summed energy of 51 interpolated saves
moves from 8.35581116e7 to 8.35584000e7 against an exact stepped-endpoint
value of 8.3558400113e7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dopri.jl had the two weight vectors of the Dormand-Prince RK5(4)7M pair
labelled the wrong way round: `b5`, commented "Weights for 5th order
method", held [5179/57600, ...], which is the embedded *4th*-order vector,
and `b4` held [35/384, ...], which is the 5th-order one. step! propagated
`b5`, so locextrap=true -- the default, and the only setting anything ever
uses -- ran the lower-order solution of the pair, while locextrap=false
propagated the higher-order one.

Told apart by the quadrature order conditions Σᵢbᵢcᵢᵖ = 1/(p+1), in exact
rational arithmetic: [35/384, ...] satisfies them through p = 4;
[5179/57600, ...] gives Σbc⁴ = 0.19974 ≠ 1/5, so it cannot be 5th order.
[35/384, ...] is also the final Butcher row B[6] and the σ=1 value of the
dense-output polynomial -- both of which follow from DOPRI5 being FSAL, and
neither of which is true of the other vector. It is what scipy's RK45 and
MATLAB's ode45 propagate.

Swap the values so the names mean what they say, and select between them in
step!. errest is unchanged: it was b5 - b4 and is now b4 - b5, the same
numbers, still the standard E vector = (4th order) - (5th order). Both
branches now form yn explicitly instead of one of them relying on the
stage-6 accumulation evaluate! happens to leave behind, so the result no
longer depends on the RHS leaving its input array alone; for locextrap that
is bit-identical to the old path, since b5[1:6] == B[6], b5[7] == 0 and the
accumulation order is the same.

Two things fall out. FSAL becomes exact: stage 7 is the RHS evaluated at the
solution that is now actually propagated, so ks[1] == f(t_{n+1}, y_{n+1})
(measured: exactly 0 residual, was 2.3e-5 at h = 0.1). And the dense output
becomes a continuous extension of the propagated solution -- at the step
endpoint the interpolant reproduces yn to round-off (1.9e-14) instead of
differing from it by the full embedded error estimate (2.1e-5).

This changes the result of every simulation. Measured on a fixed-step model
problem with an exact solution, h = 0.1 and 0.025:

  endpoint error, order of convergence
      plain    7.27e-04 / 2.09e-06, order 4.22  ->  7.89e-05 / 7.66e-08, order 5.00
      precon   2.31e-06 / 8.95e-09, order 4.01  ->  8.80e-08 / 5.74e-11, order 5.29
  dense-output error: unchanged (the interpolant is 4th order either way)

and on the N=5 soliton problem at rtol=1e-8, where the exact conserved
energy is 1638400:

      plain    error 2.2e-03 in 21944 steps  ->  1.1e-04 in 14328 steps
      precon   error 4.6e-01 in  5426 steps  ->  7.6e-03 in  5426 steps

i.e. 20-60x more accurate at the same tolerance, and 35% fewer steps for the
plain stepper: the old FSAL inconsistency (k1 was the RHS at the 5th-order
point while the solution carried forward was the 4th-order one) was
inflating the error estimate as well.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merges fix/rk45-dense-output-and-locextrap (LupoLab#436): 7747c93
"Defer RK45 FSAL move so dense output uses the right k1" and 1d7e4c3
"Propagate the 5th-order solution when locextrap is on". Merged rather than
cherry-picked so both lines share the commits themselves -- when LupoLab#436 lands
on master, git can see the change is already in both and the eventual merge
back only has to reconcile this branch's own RK45 rewrite.

Conflict resolution, all in src/RK45.jl and test/test_rk45.jl, where that
rewrite needs a different shape from the version on master:

- the propagation is a fused tchunks pass per branch rather than the .+=
  loop. With the corrected vectors locextrap needs only k1,k3..k6
  (b5[7] == 0), so that pass drops a k7 argument; the embedded branch keeps
  all seven. Bit-identical to the stage-6 accumulation, as before.
- evaluate!(::PreconStepper)'s stage-6 re-accumulation is gone: it existed
  because the !locextrap path relied on the accumulation that in-place fbar!
  had clobbered, and step! now rebuilds yn in both branches.
- the endpoint-snap rationale in interpolate(), and the step_on tests that
  pin it, were written around the σ=1 mismatch 1d7e4c3 removes. The snap
  stays -- a step_on landing can sit 1 ulp off the target, and a save asked
  for at the endpoint should be the stepped solution itself -- but the tests
  now assert the polynomial agrees with yn to round-off just outside the
  window (< 1e-9 relative, measured ~1e-16) instead of "differs by a finite
  amount", and the coefficient identities follow the corrected names.
- solve()'s step_on docstring no longer attributes percent-level scatter to
  the interpolant. With the FSAL fix it is 4th order, one below the stepped
  solution, which is the remaining reason to land weak-signal saves on step
  endpoints.

Full test suite on the merged tree: 4574 pass, 12 broken (pre-existing),
0 failures -- including test_perf_bitident.jl, whose bit-identity assertions
compare runs within one tree and so are unaffected by the accuracy change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jtravs

jtravs commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

@chrisbrahms this branch now contains a mess of a large performance rework aimed at 3D
free-space (FreeGrid) propagation. Apologies, this could be split, currently it
is in here as I work on making the TG-FROG work tractable.

Note that this will surely conflict with the big FreeSpace refactor #416 , I suggets we merge that first and then I will fix here, but it is here for the record anyway.

Performance work for large-array propagation

Headline numbers, measured on a production 3D free-space run (fused silica, Kerr,
EnvGrid 128 × FreeGrid 1024², rtol = 1e-8, AMD EPYC 7763):

before after
peak RSS per propagation ~46 GB 30.6 GiB
field-sized solver/transform buffers 18.5 10
wall time per propagation (5 FFTW threads) ~4.9 h (1 Julia thread) ~3.0 h (8 Julia threads)
Julia-level threading of the step none 4.18× on 8 threads

The performance changes are bit-identical and verified as such. I reproduced previously stored output
bit-for-bit (max relative difference exactly 0.0 on the k-space-integrated
spectra, two independent delay points at 1024²).

This also merges the #436 as I wanted to include that on my HPC runs. Ideally we merge that first.

Solver performance — applies to all propagation types

All of the following are bit-identical: the same floating-point operations in the same
order, enforced by tests.

  • Register reduction, 12 → 9 field-sized buffers. yi (dense-output) and yerr
    (error estimate) are now allocated lazily and, on the default path, never at all:
    with norm === weaknorm the error metric is computed in a single fused pass
    (weaknorm_fused) that never materialises yerr, and with step_on-aligned saves
    the endpoint snap means yi is never touched. make_fbar!'s private scratch array is
    gone — the preconditioned RHS now propagates its input in place, which is safe because
    evaluate! rebuilds yn from y at every stage.
    Custom norms keep the materialised path via a fused_errnorm(norm) hook (returns
    nothing by default; define a method to opt in).
  • Pass fusion, ~26 → ~8 field-sized passes per step. Butcher stage accumulation,
    local extrapolation, the error estimate and the interpolant are each now a single
    fused broadcast instead of a sequence of .+= passes. Julia's n-ary + is
    left-associated, so per element this matches the previous sequential accumulation
    bit-for-bit.
  • FSAL by pointer swap rather than copying a full array each step.
  • preserve_input=false (opt-in, on solve/solve_precon/Luna.run) lets the
    stepper adopt the caller's array as its yn buffer instead of copying it.
  • Zero-interval propagator calls are skipped (t1 == t2 was doing a full
    exp(0) multiply — this also removes the wasted pass on rejected steps).

Threading

Utils.tforeach / Utils.tchunks are chunked Threads.@threads :static helpers used
for the elementwise kernels — the exp(linop·dz) propagator passes (by far the largest
single-threaded cost in 3D runs), the fused stage/error/interpolant passes, and the
transform's response, apodisation and scaling kernels.

They are gated: below 2²⁰ elements, or with one Julia thread, they call straight
through to the serial code path. Small-array propagations (modal, mode-averaged) are
therefore untouched, and CI — which runs single-threaded — exercises exactly the
previous code. Elementwise threading is bit-identical by construction; a 1-thread vs
N-thread comparison is included in the test suite.

Measured scaling on one 3D propagation (128 × 640², 144 steps, EPYC 7763):

Julia threads wall speedup parallel efficiency
1 119.6 min
4 39.3 min 3.04× 76%
8 28.6 min 4.18× 52%

Before this branch there was no Julia-level parallelism anywhere in the propagation —
FFTW's own threads were the only parallelism available.

FFTW threading on Julia ≥ 1.12

FFTW.jl installs a Julia-task (partr) threading callback into libfftw3 when
Threads.nthreads() > 1, and that callback segfaults on Julia ≥ 1.12 — which previously
forced the choice between threaded FFTs and multithreaded Julia.
Utils.use_native_fftw_threads() deregisters it (fftw_threads_set_callback(NULL)),
handing FFT threading back to libfftw3's own pthread pool; set_fftw_threads now does
this automatically on Julia ≥ 1.12 when more than one Julia thread is running. Both
libraries are forced to load first, since the callback is installed lazily at first load
and would otherwise be silently reinstated. Guarded on the FFTW (non-MKL) provider and
feature-detected, so it degrades to a no-op rather than erroring on other versions.

This is what makes the threading above usable at all on current Julia.


3D free-space transform (TransFree)

  • Fast path, 4 → 1 working buffers. When the grid is not oversampled
    (length(grid.ωo) == length(grid.ω), scale == 1, complex field, no noise field —
    the usual EnvGrid/thg=false case), the copies through Eωo/Pωo are exact
    identities, so those buffers are no longer allocated and the FFTs run directly between
    the solution array, Eto and the output. If additionally every response is pointwise,
    the polarisation overwrites Eto in place and Pto goes too.
    The general path is unchanged and still selectable via fastpath=false.
  • Pointwise response protocol. Kerr_field/Kerr_env now return small callable
    structs (same constructor API) carrying a Nonlinear.pointwise trait plus a
    pointwise_P scalar kernel, so their polarisation can be applied as one whole-array
    broadcast rather than looping over ~10⁶ transverse columns with a function call each.
    Et_to_Pt_ordered! applies responses in tuple order, so each element accumulates its
    contributions exactly as the columnwise version did.
  • Batched Raman. RamanPolarEnvBatched computes the delayed response for all
    transverse points at once — two batched FFTs along the time dimension on the doubled
    anti-wraparound grid per RHS evaluation — instead of two length-Nto FFTs per
    transverse column (~2×10⁶ small serial FFTs per step at production size). It also
    updates the density-dependent response kernel once per call rather than once per
    column, which is exact since the density is a scalar. Agrees with the columnwise
    RamanPolarEnv to ~1e-15 element-wise and 1e-10 through a full propagation (not
    bitwise: batched FFT algorithms differ from single-column ones).
  • Factored linear operator and normalisation (opt-in, factored=true on
    make_const_linop / const_norm_free). FactoredFreeLinop and FreeNorm store only
    the separable factors — k²(ω) as a vector and k⊥²(ky,kx) as a small matrix, ~8 MB
    total — and compute elements on demand, replacing two field-sized arrays and removing
    ~14 field-sized reads per step. Both subtype AbstractArray with a lazy getindex,
    so linoptype metadata, Stats dispatch and generic fallbacks all behave as before.
    A specialised threaded make_prop! method computes the exponential on the fly.
    The element formulas are shared (inlined) with the materialising fill loops, so the
    lazy and dense forms cannot drift apart — verified bit-identical both element-wise and
    through a full propagation.
  • Luna.run scratch reuse. NonlinearRHS.scratch(transform) exposes the
    transform's time-domain buffer (dead between RHS evaluations) so run no longer
    allocates its own Et = FT \ Eω. Falls back to allocating for transforms that don't
    provide one.
  • twin_period (opt-in, default 1 = current behaviour) applies the
    spectral/temporal window projection every Nth accepted step instead of every step, and
    always immediately before a save (via a new Output.willsave). Saves the two extra
    coarse FFTs per step. Note this is a physics-level change — the windowing feeds back
    into the propagation — so it needs its own validation before use.

Buffer accounting for one 3D free-space Kerr propagation:

before after
RK45 12 9
TransFree 4 1 (2 with Raman)
run's Et 1 0
linop + norm cache 1.5 ~0 (factored)
total field-equivalents 18.5 10

Testing

  • test/runtests.jl gained substring filtering: Pkg.test("Luna", test_args=["rk45", "freespace"]), or julia --project test/runtests.jl rk45. No
    arguments runs the full suite exactly as before. (Selection args are consumed before
    the test files are included, since Scans parses ARGS itself.)
  • test/test_perf_bitident.jl is a small-grid harness that runs the same propagation
    through old and new code paths and asserts bitwise equality: fast path vs general
    path, factored vs materialised operators, threaded vs serial kernels, adopted vs
    copied input, fused vs materialised error norm, plus element-level agreement of the
    pointwise and batched responses against their columnwise references.

Because the old code paths remain selectable (fastpath=false, materialised operators,
twin_period=1, threading off), the bit-identity claims are checkable.


5. Applicability, and what could follow

Already general — every propagation type benefits: both correctness fixes, the
register reduction and pass fusion, preserve_input, the FSAL pointer swap, the FFTW
threading fix, and the threaded kernels (for arrays above the gate — modal runs stay on
the serial path unchanged).

Currently TransFree-only: the buffer fast path, array-level responses, factored
operators, scratch reuse and batched Raman.

No plasma yet this is all based on envelope propagation in glass. Plasma can be treated batched similar to Raman.

Radial (TransRadial) is the natural next step — the structure is nearly identical
(same four buffers, the same idcs column loop, the same trailing
nl .*= ωwin .* (-imω)./(2·normfun(z)), the same cached (nω, Nr) constant linop and
const_norm_radial):

  • array-level pointwise responses are a one-line switch (Et_to_Pt!
    Et_to_Pt_ordered! — the signatures already match);
  • buffer elimination needs the same non-oversampled + complex guard, so EnvGrid radial
    runs would qualify (RealGrid radial runs are oversampled and would keep the general
    path);
  • the linop/norm factorisation is the same separable k²(ω) × k⊥² structure — a
    smaller memory win at (nω, Nr), but the same removal of per-step array traffic;
  • batched Raman batches over transverse points either way.

Separately, and independent of this branch: the examples build Hankel.QDHT(R, N, dim=2), which routes through Hankel.dot!'s permutedims path and allocates two
field-sized arrays per call — four per RHS evaluation, ~24 per step. Laying the radial
arrays out as (Nr, Nt) with dim=1 would make the transverse transform
allocation-free. That looks like the single biggest available radial win.

Modal gets the solver improvements automatically, but the transform work doesn't map
onto it: TransModal's cost is the Cubature integration (up to mfcn RHS-equivalent
evaluations per call) and its arrays are (nω, nmodes), so buffer count isn't the
constraint. The analogous win there would be in the integration itself — a separate
piece of work.

GPU. Nothing here is CUDA-specific, but the array-level formulation is deliberately
GPU-shaped: whole-array broadcasts instead of scalar column loops, AbstractFFTs
planning, similar-based allocation, and separable operators stored as small factor
arrays. A kernel benchmark of the propagation primitives on an A40 (FP64) suggests a
GPU backend would be an additive, dispatch-based change rather than a rewrite — the
remaining CPU-specific pieces are the chunked threading helpers, the two column-loop
kernels in the fast path, and the scalar reduction in the error norms.

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.

3 participants