Slurm - #420
Conversation
There was a problem hiding this comment.
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
SlurmExecwithmemory,project, andnthreadsoptions and generate SBATCH scripts accordingly. - Add memory parsing to derive Julia
--heap-size-hintfrom 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.
|
@copilot check my fixes and resolve addressed threads |
There was a problem hiding this comment.
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.
chrisbrahms
left a comment
There was a problem hiding this comment.
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.
…threading support
…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>
…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>
|
@chrisbrahms this branch now contains a mess of a large performance rework aimed at 3D 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 propagationHeadline numbers, measured on a production 3D free-space run (fused silica, Kerr,
The performance changes are bit-identical and verified as such. I reproduced previously stored output 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 typesAll of the following are bit-identical: the same floating-point operations in the same
Threading
They are gated: below 2²⁰ elements, or with one Julia thread, they call straight Measured scaling on one 3D propagation (128 × 640², 144 steps, EPYC 7763):
Before this branch there was no Julia-level parallelism anywhere in the propagation — FFTW threading on Julia ≥ 1.12FFTW.jl installs a Julia-task (partr) threading callback into libfftw3 when This is what makes the threading above usable at all on current Julia. 3D free-space transform (
|
| 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.jlgained substring filtering:Pkg.test("Luna", test_args=["rk45", "freespace"]), orjulia --project test/runtests.jl rk45. No
arguments runs the full suite exactly as before. (Selection args are consumed before
the test files are included, sinceScansparsesARGSitself.)test/test_perf_bitident.jlis 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
EnvGridradial
runs would qualify (RealGridradial 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.
… instances finish
…g and prevent infinite respawning
Improve Slurm handling of memory, paths, and julia environments