Repository navigation
Inner products stay on the device - #98
Merged
Merged
Conversation
On the CuPy backend StencilVectorSpace.inner (and so StencilVector.inner, BlockVectorSpace.inner and dot_inner) returns a 0-d device array, in the serial and the MPI case, instead of copying the result to the host. The result is a copy of the reduction buffer. On NumPy it is a NumPy scalar, as before. StencilVectorSpace.axpy accepts a 0-d device array without copying it to the host. CG, PCG, BiCG, BiCGStab and PBiCGStab keep their step sizes on the device; the only copy per iteration is the residual norm of the convergence test (2/3/4/6/5 copies per iteration before, 1 after, counted under the fake CuPy). MINRES, LSMR and the Uzawa solver copy every inner product to the host, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 7, 2026
max-models
added this pull request to stack #99
October 8, 2026 05:42
spossann
approved these changes
Oct 8, 2026
spossann
added a commit
to struphy-hub/struphy
that referenced
this pull request
Oct 9, 2026
**Stack:** part 3 of 3 — based on #706 (`feectools-stencil-6d-views`, merge that first, after #703), next: none (top of the stack). Corresponding update in feectools: struphy-hub/feectools#98. Mirrors the feectools stack struphy-hub/feectools#96 → struphy-hub/feectools#97 → struphy-hub/feectools#98. `feectools-stencil-6d-views` is merged into this branch (no rebase), and the `feectools` submodule now points at the stacked head of struphy-hub/feectools#98, `6d88806`, which contains feectools#96 and #97. Against its base (#706) this PR only moves the submodule `d16a6ad` → `6d88806`. After each feectools PR of the stack is merged into `devel-tiny`, the `feectools` submodule of the struphy stack must be moved to the corresponding merged `devel-tiny` commit (the `pr-feectools-submodule` check fails until then). --- **Solves the following issue(s):** Part of #689 (`VlasovAmpereOneSpecies` end to end on the GPU, no host/device transfers in the time loop), tracked in #650. This PR moves the `feectools` submodule to struphy-hub/feectools#98 ("Inner products stay on the device"). On the CuPy backend, every CG iteration of a struphy solve copied two scalars from the device to the host; after this change it copies one (the convergence test). **The `pr-feectools-submodule` check fails until struphy-hub/feectools#98 is merged** into `devel-tiny`. After that, the submodule should point at the merge commit. **Core changes:** - `feectools` submodule: `d16a6ad` (base #706) → `6d88806` (branch `inner-on-device` of struphy-hub/feectools#98, stacked on feectools#96 and #97; originally `a15a8e8`). No struphy code changes. - What changes in feectools, and only on the CuPy backend: - `StencilVector.inner`/`BlockVector.inner`/`dot_inner` return a 0-d device array, in serial and with MPI. - `axpy` accepts such a scalar without copying it to the host. - CG, PCG, BiCG, BiCGStab and PBiCGStab keep alpha/beta on the device and copy only the residual norm, once per iteration. On NumPy, results and types are unchanged. Copies to the host per iteration, counted under cunumpy's fake CuPy: | Solver | before | after | | --- | --- | --- | | CG | 2 | 1 | | PCG | 3 | 1 | | BiCG | 4 | 1 | | BiCGStab | 6 | 1 | | PBiCGStab | 5 | 1 | - struphy call sites of `.inner` that run on CuPy now get a 0-d device array. I checked them: - the scalars in `models/scalars.py` write it into `xp` buffers (`local_value[0] = ...`), which works on the device; - the model energy methods (`linear_mhd.py`, `shear_alfven.py`, ...) return it, and the scalar machinery handles it like the existing `dot_inner` results; - `PolarVector.dot` adds a NumPy scalar to it, which works, but polar splines are not on CuPy yet (#695); - the multigrid smoothers run their own PCG with `.inner`. It works with device scalars: the arithmetic stays on the device, and the comparisons with 0 copy to the host implicitly. - This removes the first blocker listed in #705 ("CG inner products ... The Schur solve is outside the transfer guard because of this"). The remaining copy per CG iteration still counts as a transfer for `assert_no_transfers`. The transfer guard could then cover the Schur solve with an allowance of one 8-byte copy per iteration. **Model-specific changes:** None. **Documentation changes:** None in struphy. The feectools PR documents the change in feectools' `CUDA_STRATEGY.md` (section "Inner products on the device"). **Testing** (macOS, no GPU; struphy kernels compiled with GNU/Fortran; **GPU tests not run**): - feectools (see struphy-hub/feectools#98): - serial suite: 9456 → 9480 passed, with the same 6 unrelated failures in `ddm/tests/test_cart_*d.py`; - `mpirun -n 2` linalg MPI suite: 738 → 739 passed; - the new fake-CuPy tests check the copy counts and that the results equal NumPy's. - struphy, solver tests on the NumPy backend, with feectools `devel-tiny` (before) and this branch (after): - `propagators/tests/test_poisson.py -m "not mpi" -k "not multigrid"`: 40 passed before, 40 passed after; - `linear_algebra/tests/test_saddlepoint_massmatrices.py`: the first case (`SaddlePointSolverUzawaNumpy`) passed before and after. I stopped the remaining saddle-point and multigrid tests: with the feectools kernels uncompiled in the checkouts, each test took more than 10 minutes. On NumPy, feectools' change is limited to identity helpers and an equivalent loop structure in BiCGStab, and the feectools solver tests check the same iteration counts and results. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Stefan Possanner <stefan.possanner@ipp.mpg.de>
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.
Stack: part 3 of 3 — based on #97 (
stencil-6d-views, merge that first, after #96), next: none (top of the stack).stencil-6d-views(which contains #96) is merged into this branch (merge commitsa8bde31and6d88806, no rebase); the only conflict wasfeectools/linalg/tests/test_mpi_device.py, where both new tests are kept (test_fewer_diagonals_dot_and_transpose_match_global_referencefrom #97 andtest_inner_result_is_a_copy_of_the_reduction_bufferfrom this PR). Tests run on the merged head6d88806(macOS, no GPU):test_kron_device_solve.py,test_inner_on_device.py,test_device_matvec.py,test_mpi_device.pyserial (82 passed, 54 skipped),mpirun -n 2 ... --with-mpi test_mpi_device.py test_kron_device_solve.py(51 passed, 34 skipped),test_cuda_parity.py(4 passed, 49 skipped, GPU).Companion struphy PR: struphy-hub/struphy#716 (part 3 of the struphy stack struphy-hub/struphy#703 → struphy-hub/struphy#706 → struphy-hub/struphy#716); its
feectoolssubmodule points at this branch's new head6d88806, which contains #96 and #97.Solves the following issue(s):
Part of the CUDA work tracked in struphy-hub/struphy#650, for running a model end to end on the GPU (struphy-hub/struphy#689: no host/device transfers in the time loop). #88 moved the
innerreduction to the device, but in the serial case it still returned a NumPy scalar, so every CG iteration of a struphy solve copied two scalars from the device to the host. No issue closed.Companion struphy PR (moves the
feectoolssubmodule to this branch): struphy-hub/struphy#716Core changes
1.
innerreturns a device scalar for device vectorsStencilVectorSpace.inner(and soStencilVector.inner,BlockVectorSpace.inner,BlockVector.inneranddot_inner) returns a 0-d CuPy array when the data are on the device:to_numpyany more;Allreduceon the device buffers aftercunumpy.mpi.synchronize_for_mpi, and the result stays on the device as well;x._dot_recv_data[0], a view of that buffer.On NumPy the result is a NumPy scalar, as before (the same value and type).
2.
axpywith a device scalarThe axpy kernel takes
alphaby value, sofloat(a)would copy a device scalar to the host and wait for the device.StencilVectorSpace.axpywith a 0-d device array computesy += a * xwith array operations instead (also for the interface data); Python and NumPy scalars still go through the kernel.BlockVectorSpace.axpydelegates to it.3. Solvers keep their scalars on the device (
linalg/solvers.py)The design: one 8-byte copy per iteration, for the convergence test; the step sizes stay on the device.
axpy/*=with them run on the device. The residual norm is copied explicitly with a helper_host(an identity on NumPy) in the convergence test and forget_info(), which holds host scalars as before._host, i.e. as before._sqrt). Its Givens rotations still call.item()per entry (unchanged).xp.is_gpucheck (about 0.3 µs) per call.I did not implement "test the residual every k iterations": it would remove most of the remaining synchronizations, but it changes iteration counts compared with NumPy, and after convergence the extra iterations can divide by a zero residual. It is listed as an open question in
CUDA_STRATEGY.md.Copies to the host per iteration, before → after
Counted under cunumpy's fake CuPy (
CUNUMPY_FAKE_CUPY=1) on a 1D SPD stencil matrix (40 points, degree 2). Device kernel launches run the host kernel on the fake device buffers. Two counts are used:cunumpy.profiling.count_transfers().to_host, and all device → host copies, including implicitfloat()/bool()/.item()conversions thatcount_transfersdoes not see. These are counted by wrapping the fakecupy.ndarraymethods. Both counts agree below, except for GMRES.The iteration counts and solutions are identical to NumPy (bitwise in this test).
With MPI (
mpirun -n 2, fake CuPy, Open MPI reading the host memory behind the fake device arrays), I checked with an ad-hoc script that is not committed: the parallel inner product returns a 0-d device array with the global value, and distributed CG makes 1 copy per iteration and converges to the exact solution.Tests
New
linalg/tests/test_inner_on_device.py(+inner_on_device_child.py, the fake-CuPy child process):innerreturns NumPy scalars (stencil and block), andget_info()holds host scalars for every solver;innerreturns a 0-d device array that is independent of the reduction buffer,axpywith it makes no copy, the five Krylov solvers make exactly 1 copy per iteration and none to the device, and all eight solvers give the same iterations and solution as NumPy;count_transfers.New MPI test in
test_mpi_device.py: the result of the reduction survives the next inner product with the same vector, and it is a NumPy scalar on NumPy or a 0-d device array on CuPy.devel-tiny)pytest feectools -m "not mpi and not petsc"mpirun -n 2 pytest feectools/linalg -m "mpi and not petsc" --with-mpiThe 6 failures are the same on both (
ddm/tests/test_cart_2d.py,test_cart_3d.pywithout MPI) and are unrelated to this PR. The 6 new skips are the GPU tests.GPU tests were not run (no GPU here). The device paths were exercised only through the fake CuPy.
Documentation changes
CUDA_STRATEGY.md: new section "Inner products on the device" (with the table above). Theinneropen question is updated, and a new open question covers testing the residual less often.🤖 Generated with Claude Code