Hybrid Fourier/tridiagonal preconditioner, multi-GPU fixes, rank-independent initial design - #17
Merged
Merged
Conversation
_resolve_device binds each MPI rank to GPU local_rank % nb_gpus, but cupy was never told: it places arrays and launches kernels on its *current* device, device 0 unless switched. On two GPUs every cupy temporary of rank 1 (to_device(), the reductions in _reference_lame, ...) therefore landed on GPU 0 while its fields lived on GPU 1 -- slow peer traffic at best, and an illegal-address fault for code that mixes the two in one kernel, as muGrid's hybrid preconditioner did. Homogenization now makes the resolved GPU cupy's current device right after resolving it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
--preconditioner hybrid applies the same reference stiffness as 'green', but inverts it with muGrid's HybridFourierTridiagonalPreconditioner: FFT in the rank-local axes and a block-tridiagonal (PCR) solve in the distributed one, so no all-to-all. hybrid-jacobi wraps it in the same J-FFT Jacobi scaling as green-jacobi, with the same refresh() on a material update. The FFT engine's real-space split is already the slab ([1, ..., P]) the hybrid needs, so it runs on the engine directly. It is exact: on a fixed design it matches 'green' to ~1e-14 per apply and takes identical CG iterations. In a full trust-region run (fp64, fixed rtol 1e-8, 128^3) the optimizer takes the same path to 7 digits, with ~1% more CG iterations from round-off. Time per CG iteration, solve_bench.py, fp32, Q1, 2x GTX TITAN X: n^3 green-jacobi 1 GPU / 2 GPU hybrid-jacobi 1 GPU / 2 GPU 128 11.4 / 8.78 ms 13.6 / 8.50 ms 160 23.3 / 17.7 ms 32.2 / 15.8 ms 192 40.4 / 31.1 ms 62.6 / 30.2 ms So it is slower on one GPU, where it only adds work, and scales ~2x from one GPU to two against 1.3x for the distributed FFT. solve_bench.py gains the new choices and now runs under mpirun: it used to build a serial muGrid.Communicator on every rank, so each rank solved the whole problem alone. Rank 0 reports; the CSV gains nb_ranks, so it refuses to append to a file written with the old columns. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
simulate.py and solve_bench.py called initial_density(homog.nb_pixels, ...) -- the rank-local shape -- with the same seed on every rank. Each rank thus drew the *same* noise and filtered it periodically on its own subdomain, so a 2-rank run started from two copies of a half-size design instead of the serial one, and the problem being solved changed with the number of ranks (checksum of the gathered 64^3 start: 0188fc8aca1b on one rank, 8acb9d447da1 on two). It showed up as iteration counts that moved with the rank count: 161 vs 143 CG iterations per solve at 128^3 in solve_bench.py. initial_density now takes the global shape and, optionally, the rank's subdomain_locations / nb_subdomain_grid_pts, and returns that rank's slice of the field generated on the whole grid. simulate_conduction.py already did this by hand; it now uses the same arguments. Serial callers are unchanged. With the fix the start is 0188fc8aca1b on one and two ranks, and the iterations per solve agree (160.8 vs 161.0). The new test checks that 2-, 3- and 4-slab subdomains reassemble bit-for-bit into the global field. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Base automatically changed from
fix/float32-reductions-and-tolerance
to
main
September 25, 2026 17:54
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three commits, from testing muGrid's
HybridFourierTridiagonalPreconditioneron two GPUs (2× GTX TITAN X, one MPI rank per GPU). The companion muGrid fix is muSpectre/muGrid#214.1. FIX: Put cupy on the rank's GPU
_resolve_devicegives each rank the GPUlocal_rank % nb_gpus, but cupy was never switched to it. cupy works on its current device (0 by default), so rank 1's cupy temporaries landed on GPU 0 next to fields on GPU 1.Homogenizationnow makes the resolved GPU cupy's current device.2. ENH:
--preconditioner hybrid/hybrid-jacobiThese apply the same reference stiffness as
green/green-jacobi, but invert it with FFT in the rank-local axes and a tridiagonal solve along the distributed axis, so there is no all-to-all. The FFT engine already splits the grid into the slabs the hybrid needs, so no new decomposition is required.benchmarks/solve_bench.pynow also runs undermpirun(before, every rank built a serial communicator and solved the whole problem alone) and recordsnb_ranksin its CSV.3. FIX: Rank-independent initial design
initial_density(homog.nb_pixels, seed=…)generated the random field on each rank's local subdomain with the same seed. A 2-rank run therefore started from two copies of a half-size design, so the problem changed with the number of ranks.initial_densitynow takes the global shape plus optionalsubdomain_locations/nb_subdomain_grid_ptsand returns this rank's slice.simulate.py,simulate_conduction.py(which already did this by hand) andsolve_bench.pyuse it. A new test checks that 2/3/4-slab subdomains reassemble exactly into the global field.Correctness
greento ~1e-14 per apply and needs identical CG iterations on a fixed design.Performance (time per CG iteration)
Measured with
solve_bench.py, Q1, rtol 1e-2, identical design on 1 and 2 GPUs:End to end in fp64 on 2 GPUs the two tie: 39.2 vs 39.0 ms/iteration at 128³ and 130.0 vs 130.8 at 192³. The hybrid is always slower on one GPU, where it only adds work. Its advantage is scaling: about 2× from 1 to 2 GPUs versus 1.3× for the distributed FFT, whose all-to-all goes through the host here because the two GPUs have no P2P link. The default therefore stays
green-jacobi.🤖 Generated with Claude Code