Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
afe80fc
Update feectools (device ghost exchange plans, single-reduction PCG);…
max-models Oct 16, 2026
4b4c565
Merge branch 'devel' into gpu-stack-1-feectools
max-models Oct 16, 2026
aaf14a7
Update feectools: stencil_dot_3d without warp shuffles on the fake CuPy
max-models Oct 16, 2026
8c7d0c6
Update feectools: CachedLaunch for the stencil axpy and inner kernels
max-models Oct 16, 2026
1c29d50
Merge branch 'devel' into gpu-stack-1-feectools
max-models Oct 16, 2026
0166e83
pcg_sr also for the multigrid and Hiptmair-Xu preconditioners of the …
max-models Oct 16, 2026
f02b02e
Merge branch 'devel' into gpu-stack-1-feectools
max-models Oct 22, 2026
f118350
Merge branch 'devel' into gpu-stack-1-feectools
max-models Oct 23, 2026
93a4a52
Merge branch 'devel' into gpu-stack-1-feectools
max-models Oct 23, 2026
2f091cd
Bump feectools version number
max-models Oct 23, 2026
515dac7
Merge branch 'devel' into gpu-stack-1-feectools
max-models Oct 23, 2026
20b77a5
Allow fused solver inner products in the GPU transfer budget
max-models Oct 23, 2026
9e6eeec
polar MPI decomposition parity requires more than one rank
max-models Oct 23, 2026
3f090ce
Fix fake CuPy MPI and solver paths
max-models Oct 23, 2026
29e9180
Account for MPI scalar diagnostic transfers
max-models Oct 23, 2026
080cead
Fix fake CuPy block operations and serial inner products
max-models Oct 23, 2026
710a083
Merge branch 'devel' into gpu-stack-1-feectools
max-models Oct 23, 2026
01ed148
Merge branch 'devel' into gpu-stack-1-feectools
max-models Oct 23, 2026
352a4bb
Set feectools to devel-tiny
max-models Oct 24, 2026
b3d5f2e
Update feectools dependency to 0.10.0
max-models Oct 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ dependencies = [
"cunumpy>=0.6.3, <=0.7.0",
"pyccel>=2.2.0, <=2.2.3",
"maybempi>=0.1.2",
"feectools>=0.9.0, <=0.9.0",
"feectools>=0.10.0, <=0.10.0",
"scipy<=1.18.0",
"h5py<=3.16.0",
"h5netcdf<=1.8.1",
Expand Down
6 changes: 3 additions & 3 deletions src/struphy/feec/preconditioner.py
Original file line number Diff line number Diff line change
Expand Up @@ -1944,7 +1944,7 @@ def supported(derham) -> bool:
@staticmethod
def resolve(precond: str, derham, algo: str, solver: str, u_space: str) -> str:
"""The preconditioner to use for the option ``precond`` of a shear Alfven propagator:
``"StiffnessPreconditioner"`` only if its requirements hold (implicit algorithm with ``solver="pcg"``,
``"StiffnessPreconditioner"`` only if its requirements hold (implicit algorithm with ``solver="pcg"`` or ``"pcg_sr"``,
``u_space="Hdiv"`` and :meth:`supported`), else the mass matrix preconditioner of the velocity space
(``"PolarMassMatrixPreconditioner"`` with polar splines), with a warning. Other options are returned as
they are."""
Expand All @@ -1953,8 +1953,8 @@ def resolve(precond: str, derham, algo: str, solver: str, u_space: str) -> str:
missing = []
if algo != "implicit":
missing += ['algo="implicit"']
if solver != "pcg":
missing += ['solver="pcg"']
if solver not in ("pcg", "pcg_sr"):
missing += ['solver="pcg" or "pcg_sr"']
if u_space != "Hdiv":
missing += ['u_space="Hdiv"']
if derham.polar_splines:
Expand Down
2 changes: 1 addition & 1 deletion src/struphy/io/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class LiteralOptions:
GivenInBasis = Literal["0", "1", "2", "3", "v", "physical", "physical_at_eta", "norm", None]

# solvers
OptsSymmSolver = Literal["pcg", "cg"]
OptsSymmSolver = Literal["pcg", "pcg_sr", "cg"]
OptsGenSolver = Literal["pbicgstab", "bicgstab", "gmres"]
OptsMassPrecond = Literal["MassMatrixPreconditioner", "PolarMassMatrixPreconditioner", None]
OptsShearAlfvenPrecond = Literal[
Expand Down
22 changes: 15 additions & 7 deletions src/struphy/models/tests/gpu_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,18 @@ def device_context():
yield


# most inner products a feectools solver iteration copies to the host at once (pipelined PCG: 3)
MAX_FUSED_REDUCTIONS = 3


@dataclass
class TransferBudget:
"""Transfers recorded per phase of the time loop (the setup before it is not counted).

``integrate`` (the propagators) may only copy scalars to the host: the convergence test of the Krylov solvers
reads the 8-byte residual norm once per iteration (feectools keeps everything else of the solve on the device).
No arrays, no host-to-device copies, no host kernels. ``diagnostics`` (scalars, saved markers, binned
``integrate`` (the propagators) may only copy scalars to the host: the Krylov solvers copy the inner products
of one iteration (residual norm and step coefficients, at most ``MAX_FUSED_REDUCTIONS`` of them) to the host
together, once per iteration (feectools keeps everything else of the solve on the device). No other arrays, no
host-to-device copies, no host kernels. ``diagnostics`` (scalars, saved markers, binned
distribution functions) may copy small results to the host: at most one scalar per tracked scalar and call,
never a host-to-device copy or a host kernel. ``output`` (``DataContainer.save_data``) may copy each saved
dataset to the host once, nothing more.
Expand All @@ -99,18 +104,21 @@ def check(self, n_scalars: int):
report = self.report()
assert self.n_integrate_calls == self.n_steps, report

# 1. the propagators: only scalars to the host (solver convergence tests), nothing else
# 1. the propagators: only (fused) scalars to the host (solver inner products), nothing else
for e in self.integrate.events:
if e.kind == "to_host":
assert e.nbytes is not None and e.nbytes <= 8, report
assert e.nbytes is not None and e.nbytes <= 8 * MAX_FUSED_REDUCTIONS, report
else:
assert e.kind not in HOST_DEVICE_KINDS, report

# 2. diagnostics: no host-to-device copies, no host kernels, only scalar-sized downloads
for kind in ("to_device", "kernel_conversion", "fallback"):
assert self.diagnostics.count(kind) == 0, report
assert self.diagnostics.to_host <= n_scalars * self.n_diagnostics_calls, report
assert self.diagnostics.bytes_to_host <= 8 * n_scalars * self.n_diagnostics_calls, report
# MPI scalar reductions copy one value per scalar, followed by the consolidated scalar buffer.
max_diagnostic_copies = (n_scalars + 1) * self.n_diagnostics_calls
assert self.diagnostics.to_host <= max_diagnostic_copies, report
max_diagnostic_bytes = 16 * n_scalars * self.n_diagnostics_calls
assert self.diagnostics.bytes_to_host <= max_diagnostic_bytes, report

# 3. output: each saved dataset once to the host
for kind in ("to_device", "kernel_conversion", "fallback"):
Expand Down
6 changes: 3 additions & 3 deletions src/struphy/propagators/curl_curl_solve.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ def __post_init__(self):
check_option(self.solver, LiteralOptions.OptsSymmSolver)
check_option(self.precond, LiteralOptions.OptsCurlCurlPrecond)
if self.precond is not None:
assert self.solver == "pcg", f"precond={self.precond!r} requires solver='pcg'."
assert self.solver in ("pcg", "pcg_sr"), f"precond={self.precond!r} requires solver='pcg' or 'pcg_sr'."

# defaults
if self.precond_params is None:
Expand Down Expand Up @@ -323,8 +323,8 @@ def verify_rhs(j) -> Vector | FEECVariable | AccumulatorVector:
pc_class = getattr(preconditioner, precond)
pc = pc_class(self.mass_ops.M1, **self.options.precond_params)

# (only "pcg" takes a preconditioner)
pc_kwargs = {"pc": pc} if self.options.solver == "pcg" else {}
# (only "pcg" and "pcg_sr" take a preconditioner)
pc_kwargs = {"pc": pc} if self.options.solver in ("pcg", "pcg_sr") else {}
self._solver = inverse(
lhs,
self.options.solver,
Expand Down
2 changes: 1 addition & 1 deletion src/struphy/propagators/gyrokinetic_poisson_solve.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def __post_init__(self):
check_option(self.precond, LiteralOptions.OptsDiffusionPrecond)
check_option(self.which_geometry, self.OptsGeometry)
if self.precond is not None:
assert self.solver == "pcg", f"precond={self.precond!r} requires solver='pcg'."
assert self.solver in ("pcg", "pcg_sr"), f"precond={self.precond!r} requires solver='pcg' or 'pcg_sr'."

# defaults
if self.precond_params is None:
Expand Down
8 changes: 4 additions & 4 deletions src/struphy/propagators/implicit_diffusion.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ class Options(OptionsBase):
- ``None``: no preconditioner.
- ``"MultiGrid"``: geometric multigrid V-cycle
(:class:`~struphy.linear_algebra.multigrid.preconditioner.MultiGridPreconditioner`,
requires ``solver="pcg"``).
requires ``solver="pcg"`` or ``"pcg_sr"``).
- ``"MassMatrixPreconditioner"``: Kronecker
approximation of the inverse of the 0-form mass matrix ``M0``
(see :mod:`struphy.feec.preconditioner`).
Expand Down Expand Up @@ -300,7 +300,7 @@ def __post_init__(self):
check_option(self.solver, LiteralOptions.OptsSymmSolver)
check_option(self.precond, LiteralOptions.OptsDiffusionPrecond)
if self.precond is not None:
assert self.solver == "pcg", f"precond={self.precond!r} requires solver='pcg'."
assert self.solver in ("pcg", "pcg_sr"), f"precond={self.precond!r} requires solver='pcg' or 'pcg_sr'."
if self.diffusion_mat is None:
assert self.precond not in ("MultiGrid", "StiffnessPreconditioner"), (
f"precond={self.precond!r} needs a diffusion term (diffusion_mat=None)."
Expand Down Expand Up @@ -470,8 +470,8 @@ def verify_rhs(rho) -> StencilVector | FEECVariable | AccumulatorVector:
pc = pc_class(self.mass_ops.M0, **self.options.precond_params)

# solver just with A_2, but will be set during call with dt
# (only "pcg" takes a preconditioner)
pc_kwargs = {"pc": pc} if self.options.solver == "pcg" else {}
# (only "pcg" and "pcg_sr" take a preconditioner)
pc_kwargs = {"pc": pc} if self.options.solver in ("pcg", "pcg_sr") else {}
self._solver = inverse(
stab_mat if self._diffusion_op is None else self._diffusion_op,
self.options.solver,
Expand Down
2 changes: 1 addition & 1 deletion src/struphy/propagators/poisson_solve.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def __post_init__(self):
check_option(self.solver, LiteralOptions.OptsSymmSolver)
check_option(self.precond, LiteralOptions.OptsDiffusionPrecond)
if self.precond is not None:
assert self.solver == "pcg", f"precond={self.precond!r} requires solver='pcg'."
assert self.solver in ("pcg", "pcg_sr"), f"precond={self.precond!r} requires solver='pcg' or 'pcg_sr'."
_parse_directions(self.stab_average)

# defaults
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ def __post_init__(self):
if self.algo == "explicit":
self.precond = "MassMatrixPreconditioner"
else:
assert self.solver == "pcg", 'precond="HiptmairXu" requires solver="pcg".'
assert self.solver in ("pcg", "pcg_sr"), 'precond="HiptmairXu" requires solver="pcg" or "pcg_sr".'

# defaults
if self.solver_params is None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class Options(OptionsBase):
:class:`~struphy.feec.preconditioner.ShearAlfvenSchurPreconditioner`, built from the equilibrium part
:math:`\mathbb T` of the basis projection operator; with ``nonlinear=True`` it approximates the Schur
operator with :math:`\mathbb T + \mathbb T^B`, since :math:`|\tilde{\mathbf B}| \ll |\mathbf B_0|`). The
Kronecker preconditioner needs ``algo="implicit"``, ``solver="pcg"``, ``u_space="Hdiv"``, tensor-product
Kronecker preconditioner needs ``algo="implicit"``, ``solver="pcg"`` or ``"pcg_sr"``, ``u_space="Hdiv"``, tensor-product
splines (no polar splines) and the global commuting projectors; otherwise it falls back (with a warning) to
``"MassMatrixPreconditioner"`` (``"PolarMassMatrixPreconditioner"`` with polar splines). ``"MultiGrid"`` is
not available here.
Expand Down
4 changes: 2 additions & 2 deletions src/struphy/propagators/shear_alfven_propagator.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ class Options(OptionsBase):
(fast diagonalization) preconditioner (``"StiffnessPreconditioner"``, the default,
:class:`~struphy.feec.preconditioner.ShearAlfvenSchurPreconditioner`: block Jacobi in the velocity
components with the auxiliary space correction of the :math:`\mathbf E \times \mathbf B` flows if
``exb_correction``, see there). Both need the implicit algorithm with ``solver="pcg"``. The Kronecker
``exb_correction``, see there). Both need the implicit algorithm with ``solver="pcg"`` or ``"pcg_sr"``. The Kronecker
preconditioner also needs ``u_space="Hdiv"``, tensor-product splines (no polar splines) and the global
commuting projectors; otherwise ``"StiffnessPreconditioner"`` falls back (with a warning) to
``"MassMatrixPreconditioner"`` (``"PolarMassMatrixPreconditioner"`` with polar splines). The Schur operator
Expand Down Expand Up @@ -147,7 +147,7 @@ def __post_init__(self):
check_option(self.precond, LiteralOptions.OptsShearAlfvenPrecond)
if self.precond == "MultiGrid":
assert self.algo == "implicit", f'precond="{self.precond}" requires algo="implicit".'
assert self.solver == "pcg", f'precond="{self.precond}" requires solver="pcg".'
assert self.solver in ("pcg", "pcg_sr"), f'precond="{self.precond}" requires solver="pcg" or "pcg_sr".'
if self.precond == "MultiGrid" and self.multigrid is None:
self.multigrid = MultiGridOptions()

Expand Down
6 changes: 4 additions & 2 deletions src/struphy/propagators/variational_density_evolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,9 @@ def allocate(self):
self._Mrho = kinetic_metric(self.derham, self.mass_ops, self.basis_ops, self.divdiv_alpha)
# "cg" takes no preconditioner; the density-weighted mass matrix also preconditions the regularized metric
self._Mrho_pc = (
MassMatrixPreconditioner(self.mass_ops.WMMnew, dim_reduce=None) if self.options.solver == "pcg" else None
MassMatrixPreconditioner(self.mass_ops.WMMnew, dim_reduce=None)
if self.options.solver in ("pcg", "pcg_sr")
else None
)
self._Mrho_inv = inverse(
self._Mrho,
Expand Down Expand Up @@ -442,7 +444,7 @@ def _initialize_projectors_and_mass(self):
self.mass_ops.Mv,
self.options.solver,
# "cg" takes no preconditioner
**({"pc": self.pc_Mv} if self.options.solver == "pcg" else {}),
**({"pc": self.pc_Mv} if self.options.solver in ("pcg", "pcg_sr") else {}),
tol=1e-10,
maxiter=1000,
verbose=False,
Expand Down
6 changes: 4 additions & 2 deletions src/struphy/propagators/variational_momentum_advection.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ def allocate(self):
)
# "cg" takes no preconditioner
self._Mrho_pc = (
MassMatrixPreconditioner(self.mass_ops.WMMnew, dim_reduce=None) if self.options.solver == "pcg" else None
MassMatrixPreconditioner(self.mass_ops.WMMnew, dim_reduce=None)
if self.options.solver in ("pcg", "pcg_sr")
else None
)
self._Mrho_inv = inverse(
self._Mrho,
Expand Down Expand Up @@ -308,7 +310,7 @@ def _initialize_mass(self):
self.mass_ops.Mv,
self.options.solver,
# "cg" takes no preconditioner
**({"pc": self.pc_Mv} if self.options.solver == "pcg" else {}),
**({"pc": self.pc_Mv} if self.options.solver in ("pcg", "pcg_sr") else {}),
tol=1e-10,
maxiter=1000,
verbose=False,
Expand Down
Loading