core 0.5.0
Shared MPI block decomposition + asynchronous ghost-layer exchange (header-only C++20)
Loading...
Searching...
No Matches
pcg.hpp
Go to the documentation of this file.
1// core — device (Kokkos) multigrid-preconditioned CG for the AMR FV Poisson.
2//
3// A Krylov accelerator on top of the existing device machinery: the matvec is the
4// consistent conservative FV Laplacian `applyFv` (poisson.hpp), the
5// preconditioner is one (or a few) Multigrid V-cycle(s) (multigrid.hpp),
6// and the inner products / vector updates are Kokkos reductions / parallel_fors. This
7// is exactly flow's structured MG-PCG, ported onto the AMR octree CSR: CG accelerates
8// the geometric MG so a given residual is reached in far fewer fine-grid matvecs than
9// stationary V-cycling, on whatever backend Kokkos targets (CUDA / HIP / OpenMP).
10//
11// SPD subtlety: the FV operator L = D^{-1} S (D = diag(cell volume), S the symmetric
12// stencil) is *not* symmetric in the Euclidean inner product, but it is symmetric and
13// negative-definite in the volume-weighted inner product <u,v>_D = Σ V_i u_i v_i. So CG
14// runs on A := −L (SPD in <·,·>_D) and every dot product is volume-weighted. The V-cycle
15// preconditioner solves L z = −r (correction scheme) ⇒ z ≈ A^{-1} r.
16//
17// Singular (periodic, pure-Neumann) case: A has the constant nullspace. The RHS, the
18// residual, and the preconditioned residual are projected volume-weighted-mean-zero each
19// iteration (deflated CG) so the iteration stays in the range space. The homogeneous-
20// Dirichlet build is non-singular (bcDiag > 0) ⇒ no projection.
21//
22// The MG preconditioner is bit-exact deterministic (Jacobi smoother); CG itself depends
23// on global-reduction summation order, so this is the *performance* path — validated by
24// convergence + matching the V-cycle's converged solution, not by host bit-exactness.
25//
26// Requires a Kokkos build + the morton checkout (PECLET_CORE_HAVE_MORTON).
27#ifndef PECLET_CORE_AMR_PCG_HPP
28#define PECLET_CORE_AMR_PCG_HPP
29
30#ifdef PECLET_CORE_HAVE_MORTON
31
32#include <cmath>
33#include <functional>
34
38
39namespace peclet::core::amr {
40
41// ---- small device vector primitives (volume-weighted where the FV operator needs it) ----
42
45 Index n) {
46 double s = 0.0;
47 Kokkos::parallel_reduce(
48 "amr::pcg_dotvol", n,
49 KOKKOS_LAMBDA(const Index i, double& acc) { acc += (u(i) * v(i)) / invVol(i); }, s);
50 return s;
51}
52
58inline void buildFluidMask(const FvOp& op, View<double> mask, Index n) {
59 auto start = op.faceStart;
60 auto w = op.faceW;
61 auto bc = op.bcDiag;
62 Kokkos::parallel_for(
63 "amr::pcg_fluidmask", n, KOKKOS_LAMBDA(const Index i) {
64 double d = bc(i);
65 for (Index k = start(i); k < start(i + 1); ++k)
66 d += w(k);
67 mask(i) = (d > 1e-30) ? 1.0 : 0.0;
68 });
69}
70
73 Kokkos::parallel_for("amr::pcg_masksolid", n, KOKKOS_LAMBDA(const Index i) { u(i) *= mask(i); });
74}
75
83 const std::function<double(double)>& reduce) {
84 Kokkos::parallel_for("amr::pcg_masksolid", n, KOKKOS_LAMBDA(const Index i) { u(i) *= mask(i); });
85 double su = 0.0, sv = 0.0;
86 Kokkos::parallel_reduce(
87 "amr::pcg_meannum", n,
88 KOKKOS_LAMBDA(const Index i, double& a) { a += mask(i) * u(i) / invVol(i); }, su);
89 Kokkos::parallel_reduce(
90 "amr::pcg_meanden", n, KOKKOS_LAMBDA(const Index i, double& a) { a += mask(i) / invVol(i); },
91 sv);
92 if (reduce) {
93 su = reduce(su);
94 sv = reduce(sv);
95 }
96 const double m = (sv > 0.0) ? su / sv : 0.0;
97 Kokkos::parallel_for(
98 "amr::pcg_meansub", n, KOKKOS_LAMBDA(const Index i) { u(i) -= mask(i) * m; });
99}
100
102 Index n) {
103 removeMeanVolReduced(u, invVol, mask, n, {});
104}
105
107inline void axpy(View<double> y, double a, View<const double> x, Index n) {
108 Kokkos::parallel_for("amr::pcg_axpy", n, KOKKOS_LAMBDA(const Index i) { y(i) += a * x(i); });
109}
110
112inline void zpby(View<double> p, View<const double> z, double b, Index n) {
113 Kokkos::parallel_for(
114 "amr::pcg_zpby", n, KOKKOS_LAMBDA(const Index i) { p(i) = z(i) + b * p(i); });
115}
116
118inline void negate(View<double> x, Index n) {
119 Kokkos::parallel_for("amr::pcg_negate", n, KOKKOS_LAMBDA(const Index i) { x(i) = -x(i); });
120}
121
122// ---------------------------------------------------------------------------
123// MG-preconditioned CG over a Multigrid, driving the system L x = rhs on its
124// finest level. Owns the Krylov scratch; reuses the multigrid's own finest x/b as
125// transient preconditioner storage. Solves A x = b_A with A := −L (SPD in <·,·>_D).
126// ---------------------------------------------------------------------------
127template <int Dim, unsigned Bits = (Dim == 2 ? 32u : (Dim == 3 ? 21u : 16u))>
128class PCG {
129 public:
131
132 struct Result {
133 int iters = 0;
134 double res0 = 0.0;
135 double res = 0.0;
136 };
137
139 void setVcycle(int pre, int post, int bottom, double omega) {
140 pre_ = pre;
141 post_ = post;
142 bottom_ = bottom;
143 omega_ = omega;
144 }
146 void setCyclesPerPrec(int k) { cyclesPerPrec_ = k; }
149 void setSingular(bool s) { singular_ = s; }
150
159 void setDistributed(std::function<void(View<double>)> refresh,
160 std::function<double(double)> dotReduce, Index nExt) {
161 haloFn_ = std::move(refresh);
162 dotReduce_ = std::move(dotReduce);
163 nExt_ = nExt;
164 }
165
171 template <class MGT = MG>
173 double tol = 1e-10) {
174 const Index n = mg.numLeaves(0);
175 const FvOp& op = mg.op(0);
176 View<const double> invVol(op.invVol);
177 ensure(n);
178 buildFluidMask(op, mask_, n);
180 Result R;
181 // Project onto the fluid range: always zero the solid cells (their per-region null modes); for
182 // the singular (periodic/all-Neumann) operator also remove the fluid constant. Applied to every
183 // Krylov quantity so the iteration stays in the well-posed fluid range (mirrors flow
184 // removeMean∘maskSolid).
185 auto project = [&](View<double> u) {
186 if (singular_)
187 removeMeanVolReduced(u, invVol, mask, n, dotReduce_);
188 else
189 maskSolid(u, mask, n);
190 };
192 const double s = dotVol(a, b, invVol, n);
193 return dotReduce_ ? dotReduce_(s) : s;
194 };
195
196 // x = 0 ; r = b_A − A·0 = b_A = −rhs (A = −L, b_A = −rhs). Element copy over the local
197 // rows (not deep_copy): rhs may be local-sized while the scratch carries a ghost tail.
198 Kokkos::deep_copy(x, 0.0);
199 {
200 auto r = r_;
201 Kokkos::parallel_for(
202 "amr::pcg_r0", n, KOKKOS_LAMBDA(const Index i) { r(i) = -rhs(i); });
203 }
204 project(r_);
205 R.res0 = std::sqrt(vdot(View<const double>(r_), View<const double>(r_)));
206 if (R.res0 == 0.0)
207 return R;
208
209 applyPrec(mg, r_, z_, n); // z = M^{-1} r ≈ A^{-1} r
210 project(z_);
211 Kokkos::deep_copy(p_, z_);
212 double rz = vdot(View<const double>(r_), View<const double>(z_));
213
214 int it = 0;
215 double rnorm = R.res0;
216 for (; it < maxIters; ++it) {
217 // Ap = A p = −L p, projected back onto the fluid range (keeps the search directions there).
218 sync(p_); // ghost tail of the direction before the matvec (no-op single-rank)
219 applyFv(op, View<const double>(p_), Ap_);
220 negate(Ap_, n);
221 project(Ap_);
222 double pAp = vdot(View<const double>(p_), View<const double>(Ap_));
223 if (pAp == 0.0)
224 break;
225 double alpha = rz / pAp;
226 axpy(x, alpha, View<const double>(p_), n); // x += α p
227 axpy(r_, -alpha, View<const double>(Ap_), n); // r −= α Ap
228 project(r_);
229 rnorm = std::sqrt(vdot(View<const double>(r_), View<const double>(r_)));
230 if (rnorm <= tol * R.res0) {
231 ++it;
232 break;
233 }
234 applyPrec(mg, r_, z_, n);
235 project(z_);
237 double beta = rzNew / rz;
238 zpby(p_, View<const double>(z_), beta, n); // p = z + β p
239 rz = rzNew;
240 }
241 project(x); // solid cells exactly 0; fluid mean removed (singular)
242 R.iters = it;
243 R.res = rnorm;
244 return R;
245 }
246
247 private:
248 // z = M^{-1} r : solve L z = −r with `cyclesPerPrec_` V-cycles (correction scheme),
249 // using the multigrid's own finest x/b as scratch. (A = −L ⇒ A z = r ⟺ L z = −r.)
250 // The distributed multigrid sizes its finest x/b at the same nExt as this solver's scratch,
251 // so the deep_copies stay extent-matched.
252 template <class MGT>
253 void applyPrec(MGT& mg, View<double> r, View<double> z, Index n) {
254 Kokkos::deep_copy(mg.b(0), r);
255 negate(mg.b(0), n);
256 Kokkos::deep_copy(mg.x(0), 0.0);
257 for (int k = 0; k < cyclesPerPrec_; ++k)
258 mg.vcycle(pre_, post_, bottom_, omega_);
259 Kokkos::deep_copy(z, mg.x(0));
260 }
261
263 void sync(View<double> v) const {
264 if (haloFn_)
265 haloFn_(v);
266 }
267
268 void ensure(Index n) {
269 if (nExt_ > n)
270 n = nExt_; // scratch carries the ghost tail in distributed solves
271 if (r_.extent(0) == static_cast<std::size_t>(n))
272 return;
273 r_ = View<double>("pcg_r", static_cast<std::size_t>(n));
274 z_ = View<double>("pcg_z", static_cast<std::size_t>(n));
275 p_ = View<double>("pcg_p", static_cast<std::size_t>(n));
276 Ap_ = View<double>("pcg_Ap", static_cast<std::size_t>(n));
277 mask_ = View<double>("pcg_fluidmask", static_cast<std::size_t>(n));
278 }
279
280 View<double> r_, z_, p_, Ap_, mask_;
281 int pre_ = 2, post_ = 2, bottom_ = 40;
282 double omega_ = 0.8;
283 int cyclesPerPrec_ = 1;
284 bool singular_ = true;
285 std::function<void(View<double>)> haloFn_; // ghost-tail refresh (unset ⇒ no-op)
286 std::function<double(double)> dotReduce_; // global reduction (unset ⇒ local)
287 Index nExt_ = 0; // extended (local+ghost) scratch size
288};
289
290} // namespace peclet::core::amr
291
292#endif // PECLET_CORE_HAVE_MORTON
293#endif // PECLET_CORE_AMR_PCG_HPP
Result solve(MGT &mg, View< double > x, View< const double > rhs, int maxIters=200, double tol=1e-10)
Solve L x = rhs on mg's finest level into x (size n; nExt distributed).
Definition pcg.hpp:172
void setVcycle(int pre, int post, int bottom, double omega)
V-cycle parameters used for the preconditioner application.
Definition pcg.hpp:139
void setCyclesPerPrec(int k)
Number of V-cycles per preconditioner application (default 1).
Definition pcg.hpp:146
void setSingular(bool s)
Whether to project out the constant nullspace (default true; set false for the non-singular homogeneo...
Definition pcg.hpp:149
void setDistributed(std::function< void(View< double >)> refresh, std::function< double(double)> dotReduce, Index nExt)
Distributed solve (docs/amr_distributed_flow.md, rung 3): refresh re-fills the ghost tail [n,...
Definition pcg.hpp:159
void axpy(View< double > y, double a, View< const double > x, Index n)
y += a·x
Definition pcg.hpp:107
void removeMeanVol(View< double > u, View< const double > invVol, View< const double > mask, Index n)
Definition pcg.hpp:101
void applyFv(const FvOp &op, View< const double > u, View< double > Lu)
Hu = (c0·I + cD·L) u (consistent conservative FV Laplacian, c0=0/cD=1 ⇒ pure L).
Definition fv_op.hpp:114
std::vector< std::array< double, Dim > > transferFieldGradients(const BlockOctree< Dim, Bits > &oldT, const std::vector< double > &oldF)
Per-old-leaf minmod prolongation gradients (per fine-coordinate unit) — transferField's stencil,...
Definition adapt.hpp:53
void zpby(View< double > p, View< const double > z, double b, Index n)
p = z + b·p (CG direction update)
Definition pcg.hpp:112
void negate(View< double > x, Index n)
y = −x (negate in place)
Definition pcg.hpp:118
void maskSolid(View< double > u, View< const double > mask, Index n)
Zero the solid cells (project out the solid null modes).
Definition pcg.hpp:72
void buildFluidMask(const FvOp &op, View< double > mask, Index n)
Build the fluid mask: mask(i)=1 where the operator diagonal Σ_f w_f (+ bcDiag) is non-trivial,...
Definition pcg.hpp:58
void removeMeanVolReduced(View< double > u, View< const double > invVol, View< const double > mask, Index n, const std::function< double(double)> &reduce)
Project u onto the FLUID range: zero solid cells, then subtract the volume-weighted mean over the flu...
Definition pcg.hpp:81
double dotVol(View< const double > u, View< const double > v, View< const double > invVol, Index n)
Volume-weighted dot <u,v>_D = Σ_i V_i u_i v_i, V_i = 1/invVol_i.
Definition pcg.hpp:44
Kokkos::View< T *, MemSpace > View
1D device array.
Definition view.hpp:26
std::int64_t Index
Signed index type for grids and particles (supersedes block_decomposer's long int IndxT).
Definition types.hpp:15
View< double > invVol
1/V_i, size n
Definition fv_op.hpp:81
View< double > faceW
w_f = openness·A_f/d_f per face, size nFaces
Definition fv_op.hpp:84
View< Index > faceStart
CSR row offsets, size n+1.
Definition fv_op.hpp:82
View< double > bcDiag
Dirichlet boundary diagonal per cell (0 if periodic), size n.
Definition fv_op.hpp:85
double res0
initial volume-weighted residual norm
Definition pcg.hpp:134
double res
final volume-weighted residual norm
Definition pcg.hpp:135