Skip to content

Stencil 3D device kernels: 6D matrix views instead of the 2p+1 assumption - #97

Merged
max-models merged 3 commits into
device-kronecker-solvefrom
stencil-6d-views
Oct 8, 2026
Merged

max-models merged 3 commits into
device-kronecker-solvefrom
stencil-6d-views

Conversation

@max-models

@max-models max-models commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Stack: part 2 of 3 — based on #96 (device-kronecker-solve, merge that first), next: #98 (inner-on-device).

device-kronecker-solve is merged into this branch (merge commit d16a6ad, no rebase, so review comments stay); the only conflict was CUDA_STRATEGY.md, where both new sections are kept ("Kronecker solver on the device (#96)" first, then "6D matrix views"). The diff against the base shows only this PR's change. When #96 is merged, GitHub retargets this PR to devel-tiny.

Companion struphy PR: struphy-hub/struphy#706 (part 2 of the struphy stack struphy-hub/struphy#703 → struphy-hub/struphy#706 → struphy-hub/struphy#716); its feectools submodule points at this branch's new head d16a6ad, which contains #96.


Corresponding PR in struphy: struphy-hub/struphy#706

Summary

The 3D device stencil kernels from #88 (stencil_dot_3d, stencil_transpose_3d) take the matrix data as 6D views (Array6D<double>, cunumpy >= 0.6.1) instead of raw pointers, and all dot/transpose kernels read the number of diagonals from the matrix data instead of assuming 2 * p + 1. StencilMatrix.dot, vdot and transpose therefore no longer raise NotImplementedError for matrices with fewer diagonals than the pads of their spaces allow (blocks between spaces of different degree, derivative-type stencils). Related to struphy-hub/struphy#650 and struphy-hub/struphy#688 (6D array views in cunumpy).

Changes

  • CUDA, 3D: stencil_dot_3d(Array6D<double> mat, Array3D<double> x, Array3D<double> out, s_in, p_in, add, s_out, e_out, p_out) and stencil_transpose_3d(Array6D<double> mat, Array6D<double> matT, s_in, p_in, add, s_out, e_out, p_out). Strided views as in 1D/2D (Array2D/Array4D), so the hand-computed strides are gone. The pyccel versions take float[:, :, :, :, :, :] with the same arguments in the same order.
  • Diagonals from the data (all six kernels, pyccel and CUDA, 1D to 3D): along each direction the matrix has n = mat.shape[ndim + k] diagonals, its pads are q = (n - 1) // 2, diagonal d of row i is the column i - q + d (at x[i - q + d - s_in + p_in]), and the last owned row uses n - 1 + add. The transpose maps diagonal d of matT to diagonal q + i - j of mat. With q = p this is the old loop, in the same order. The pyccel kernels are one loop nest that picks the number of diagonals per row, instead of the 2/4/8 spelled-out (interior, last row) combinations.
  • e_in removed from stencil_transpose_1d/2d/3d. It was only needed for the row extents of the raw pointer in 3D, and the argument list is the same for all dimensions.
  • StencilMatrix: the 2 * p + 1 check is gone from dot, vdot and transpose. transpose(out=...) asserts that out has the pads of the matrix. Spaces with shifts > 1 still raise NotImplementedError (see below).
  • Tests:
    • MATRIX_CASES (GPU parity and CPU emulation) gets 2 new 1D cases, 2 in 2D and 4 in 3D. They cover fewer diagonals in some directions, non-periodic rectangular blocks between spaces of different size per direction, a derivative-type rectangular block with fewer diagonals in every direction, and pads 0 (one diagonal).
    • test_device_matvec.py: the old "reject" test is replaced by test_fewer_diagonals_match_dense_reference, which checks dot, vdot, transpose and transpose(out=...) against toarray() for 8 such matrices. A new test checks that shifts > 1 raise.
    • test_mpi_device.py: a matrix with fewer diagonals against the global field, and the adjoint identity of its transpose for a non-symmetric (one-sided) stencil.
  • CUDA_STRATEGY.md: new section "6D matrix views". The 2 * p + 1 / raw-pointer limitation and the open question about 6D views are removed, and shifts > 1 are listed as an open question.
  • Dependency: cunumpy >= 0.6.1 was already required on devel-tiny (Update to cunumpy 0.6.1 #94), so it is unchanged.

Behaviour changes

  • Matrices with fewer diagonals than 2 * p + 1 (StencilMatrix(V, W, pads=q) with q < p) work in dot, vdot and transpose on both backends, where they used to raise.
  • Matrices that already worked give bitwise the same results. I checked this by running the old and new pyccel kernels uncompiled on every old case.
  • stencil_transpose_<n>d takes one argument fewer (e_in). Only StencilMatrix calls these kernels; struphy does not call them directly.
  • Shifts > 1 still raise. The stencil kernels never handled them, and there is no reference to match. psydac's general kernels (matvec_<n>d, transpose_<n>d) disagree with toarray() for shifts > 1. Their transpose is also not the adjoint of their product, and psydac never tested them ("TODO: verify for s>1"). Supporting shifts would first need a verified data layout.
  • NumPy timings are unchanged (32×32×16, degree 3, compiled C): dot 5.9 → 5.1 ms, transpose 6.8 → 6.8 ms.

Testing

Local runs on macOS without a GPU (cunumpy 0.6.1, pyccel 2.2.1 with C, Open MPI 5). The GPU tests were not run. Before → after:

  • test_cuda_parity.py, test_cuda_emulation.py, test_device_matvec.py: 29 passed / 31 skipped → 37 passed / 49 skipped. More GPU parity cases are skipped now, and the CPU emulation covers all cases, the new ones included.
  • Serial feectools/linalg -m "not mpi and not petsc": 7942 passed → 7950 passed.
  • Serial feectools -m "not mpi and not petsc": 9456 passed, 6 failed → 9464 passed, 6 failed. The 6 failures are the pre-existing ones in ddm/tests/test_cart_2d.py/test_cart_3d.py.
  • mpirun -n 2 … feectools/linalg -m "mpi and not petsc" --with-mpi: 738 passed → 739 passed. test_mpi_device.py also passes with 1, 3 and 4 ranks.
  • Extra checks (not committed):
    • CPU emulation of every matrix case compiled with -DCUNUMPY_BOUNDS_CHECK: no out-of-bounds index.
    • dot, vdot and transpose against toarray() for every case with a dense meaning.

🤖 Generated with Claude Code

The 3D CUDA kernels stencil_dot_3d and stencil_transpose_3d take the matrix
data as Array6D<double> views (cunumpy >= 0.6.1) instead of raw pointers
whose shape was derived from 2 * p + 1 diagonals. All six dot/transpose
kernels (pyccel and CUDA, 1D to 3D) read the number of diagonals from the
matrix data: the pads of the matrix are q = (n - 1) // 2, diagonal d of row
i is the column i - q + d, the last owned row uses n - 1 + add. With q = p
the results are bitwise those of the previous kernels.

- stencil_transpose_<n>d: drop the e_in argument (only needed for the raw
  pointer in 3D).
- StencilMatrix: dot, vdot and transpose no longer raise for matrices with
  fewer diagonals than 2 * p + 1; spaces with shifts > 1 still raise.
  transpose(out=...) asserts that out has the pads of the matrix.
- Parity/emulation cases: matrices with fewer diagonals, non-periodic
  rectangular blocks, pads 0; dense references in test_device_matvec.py,
  a distributed check in test_mpi_device.py.
- CUDA_STRATEGY.md: notes for this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stack #96 -> #97: keep both CUDA_STRATEGY.md sections (Kronecker solver first).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
max-models added a commit that referenced this pull request Oct 7, 2026
Stack #96 -> #97 -> #98: test_mpi_device.py keeps both new tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
max-models added a commit that referenced this pull request Oct 7, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@max-models
max-models changed the base branch from devel-tiny to device-kronecker-solve October 7, 2026 21:56
max-models added a commit to struphy-hub/struphy that referenced this pull request Oct 7, 2026
…6d-views

Stack #703 -> #706: feectools submodule points to the head of
struphy-hub/feectools#97, which now contains #96.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@max-models
max-models added this pull request to stack #99 October 8, 2026 05:42
@max-models
max-models requested a review from spossann October 8, 2026 09:26
Comment thread feectools/linalg/kernels/stencil_dot_1d/stencil_dot_1d_kernels.py
Comment thread feectools/linalg/kernels/stencil_dot_1d/stencil_dot_1d_kernels.py
@max-models max-models mentioned this pull request Oct 8, 2026
@max-models
max-models merged commit e18c993 into devel-tiny Oct 8, 2026
9 checks passed
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>
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.

2 participants