core 0.5.0
Shared MPI block decomposition + asynchronous ghost-layer exchange (header-only C++20)
Loading...
Searching...
No Matches
graph_amg_device.hpp
Go to the documentation of this file.
1// core — device (Kokkos) APPLY for the smoothed-aggregation GraphAMG hierarchy.
2//
3// Setup (strength graph, greedy aggregation, smoothed prolongator, Galerkin RAP) stays on the
4// HOST oracle (graph_amg.hpp) — it is sequential-greedy and runs once per operator rebuild. This
5// class mirrors the finished hierarchy into device CSR Views and runs the HOT path — the V-cycle
6// apply (per-level SpMV, 4th-kind-Chebyshev / damped-Jacobi smoothing, transfers, tiny coarse CG)
7// — entirely on the device, so a device consumer (voro's device mesh optimiser, flow's device
8// PCG) never round-trips the iterate to the host.
9//
10// Determinism vs the host oracle: SpMV sums each row sequentially in the same order, transfers are
11// deterministic gathers (restriction uses a host-built transpose R = Pᵀ whose per-coarse-row
12// entries keep increasing-fine-index order — the exact order of the host scatter), and the vector
13// updates are elementwise ⇒ the V-cycle matches the host bit-for-bit EXCEPT the coarsest-level CG
14// dot products (parallel_reduce reorders the sums), which perturb only the tightly-converged
15// coarse correction (validated ~1e-12 relative in tests/test_graph_amg_device.cpp).
16#ifndef PECLET_CORE_SOLVER_GRAPH_AMG_DEVICE_HPP
17#define PECLET_CORE_SOLVER_GRAPH_AMG_DEVICE_HPP
18
19#include <Kokkos_Core.hpp>
20#include <string>
21#include <vector>
22
26
27namespace peclet::core::solver {
28
30 public:
33
35 void build(const HostCsrOp& A, const AmgParams& prm = {}) {
36 host_.build(A, prm);
37 prm_ = prm;
38 mirror();
39 }
40
42 void apply(const DView& r, const DView& z) const {
43 const DLevel& l0 = lv_[0];
44 Kokkos::deep_copy(l0.b, r);
45 Kokkos::deep_copy(l0.x, 0.0);
46 vcycle(0);
47 Kokkos::deep_copy(z, l0.x);
48 }
49
50 int numLevels() const { return (int)lv_.size(); }
51 Index size(int L = 0) const { return lv_[(std::size_t)L].n; }
52 const GraphAMG& hostHierarchy() const { return host_; }
53
54 // DLevel is public for the same nvcc extended-lambda stub-generation reason as the methods.
55 struct DLevel {
56 Index n = 0, nc = 0;
57 IView start, nbr; // off-diagonal CSR
59 IView Pstart, Pcol; // prolongation (fine rows)
61 IView Rstart, Rcol; // restriction = Pᵀ (coarse rows, entries in fine-index order)
63 double lmax = 1.0;
64 DView x, b, res, t0, t1; // scratch
65 };
66
67 template <class T>
68 static View<T> toDev(const std::vector<T>& v, const char* name) {
69 View<T> d(Kokkos::view_alloc(std::string(name), Kokkos::WithoutInitializing), v.size());
70 auto h = Kokkos::create_mirror_view(d);
71 for (std::size_t i = 0; i < v.size(); ++i)
72 h(i) = v[i];
73 Kokkos::deep_copy(d, h);
74 return d;
75 }
76
77 void mirror() {
78 lv_.clear();
79 for (const auto& hl : host_.levels()) {
80 DLevel d;
81 d.n = hl.A.n;
82 d.nc = hl.nc;
83 d.lmax = hl.lmax;
84 d.start = toDev(hl.A.start, "amgd_start");
85 d.nbr = toDev(hl.A.nbr, "amgd_nbr");
86 d.coef = toDev(hl.A.coef, "amgd_coef");
87 d.diag = toDev(hl.A.diag, "amgd_diag");
88 d.invDiag = toDev(hl.invDiag, "amgd_invDiag");
89 if (!hl.Pstart.empty()) {
90 d.Pstart = toDev(hl.Pstart, "amgd_Pstart");
91 d.Pcol = toDev(hl.Pcol, "amgd_Pcol");
92 d.Pval = toDev(hl.Pval, "amgd_Pval");
93 // Host-built transpose R = Pᵀ. Two-pass CSR transpose scanning fine rows in increasing
94 // order, so each coarse row's entries keep increasing-fine-index order — the exact
95 // accumulation order of the host oracle's restriction scatter (bit-compatible sums).
96 std::vector<Index> Rstart((std::size_t)hl.nc + 1, 0), Rcol(hl.Pcol.size());
97 std::vector<double> Rval(hl.Pval.size());
98 for (Index c : hl.Pcol)
99 ++Rstart[(std::size_t)c + 1];
100 for (std::size_t c = 0; c < (std::size_t)hl.nc; ++c)
101 Rstart[c + 1] += Rstart[c];
102 std::vector<Index> cur(Rstart.begin(), Rstart.end() - 1);
103 for (Index i = 0; i < hl.A.n; ++i)
104 for (Index k = hl.Pstart[(std::size_t)i]; k < hl.Pstart[(std::size_t)i + 1]; ++k) {
105 const Index c = hl.Pcol[(std::size_t)k];
106 const Index at = cur[(std::size_t)c]++;
107 Rcol[(std::size_t)at] = i;
108 Rval[(std::size_t)at] = hl.Pval[(std::size_t)k];
109 }
110 d.Rstart = toDev(Rstart, "amgd_Rstart");
111 d.Rcol = toDev(Rcol, "amgd_Rcol");
112 d.Rval = toDev(Rval, "amgd_Rval");
113 }
114 auto scratch = [n = (std::size_t)d.n](const char* nm) {
115 return DView(Kokkos::view_alloc(std::string(nm), Kokkos::WithoutInitializing), n);
116 };
117 d.x = scratch("amgd_x");
118 d.b = scratch("amgd_b");
119 d.res = scratch("amgd_res");
120 d.t0 = scratch("amgd_t0");
121 d.t1 = scratch("amgd_t1");
122 lv_.push_back(std::move(d));
123 }
124 }
125
126 // nvcc requires member functions containing extended (device) lambdas to be PUBLIC — the
127 // OpenMP/host build accepts them private, so the breakage only shows on the CUDA backend.
128 public:
129 // y = A x (diag + off-diagonal CSR; each row summed sequentially — the host order).
130 static void spmv(const DLevel& lv, const DView& x, const DView& y) {
131 IView start = lv.start, nbr = lv.nbr;
132 DView coef = lv.coef, diag = lv.diag;
133 Kokkos::parallel_for(
134 "peclet::core::amgd_spmv", Kokkos::RangePolicy<ExecSpace>(0, lv.n),
135 KOKKOS_LAMBDA(const Index i) {
136 double s = diag(i) * x(i);
137 for (Index k = start(i); k < start(i + 1); ++k)
138 s += coef(k) * x(nbr(k));
139 y(i) = s;
140 });
141 }
142
143 void vcycle(int L) const {
144 const DLevel& lv = lv_[(std::size_t)L];
145 if (L + 1 == (int)lv_.size()) {
146 coarseSolve(lv);
147 return;
148 }
149 smooth(lv, prm_.pre);
150 spmv(lv, lv.x, lv.res); // res = b − A x
151 {
152 DView res = lv.res, b = lv.b;
153 Kokkos::parallel_for(
154 "peclet::core::amgd_resid", Kokkos::RangePolicy<ExecSpace>(0, lv.n),
155 KOKKOS_LAMBDA(const Index i) { res(i) = b(i) - res(i); });
156 }
157 const DLevel& cl = lv_[(std::size_t)L + 1];
158 { // coarse b = Pᵀ res via the transpose gather (fine-index order per coarse row)
159 IView Rstart = lv.Rstart, Rcol = lv.Rcol;
160 DView Rval = lv.Rval, res = lv.res, cb = cl.b;
161 Kokkos::parallel_for(
162 "peclet::core::amgd_restrict", Kokkos::RangePolicy<ExecSpace>(0, cl.n),
163 KOKKOS_LAMBDA(const Index c) {
164 double s = 0.0;
165 for (Index k = Rstart(c); k < Rstart(c + 1); ++k)
166 s += Rval(k) * res(Rcol(k));
167 cb(c) = s;
168 });
169 }
170 Kokkos::deep_copy(cl.x, 0.0);
171 vcycle(L + 1);
172 { // x += P x_c
173 IView Pstart = lv.Pstart, Pcol = lv.Pcol;
174 DView Pval = lv.Pval, x = lv.x, cx = cl.x;
175 Kokkos::parallel_for(
176 "peclet::core::amgd_prolong", Kokkos::RangePolicy<ExecSpace>(0, lv.n),
177 KOKKOS_LAMBDA(const Index i) {
178 double s = 0.0;
179 for (Index k = Pstart(i); k < Pstart(i + 1); ++k)
180 s += Pval(k) * cx(Pcol(k));
181 x(i) += s;
182 });
183 }
184 smooth(lv, prm_.post);
185 }
186
187 void smooth(const DLevel& lv, int sweeps) const {
188 if (sweeps <= 0)
189 return;
190 for (int s = 0; s < sweeps; ++s)
191 if (prm_.chebDegree <= 0)
192 jacobiSweep(lv);
193 else
194 chebSweep(lv);
195 }
196
197 void jacobiSweep(const DLevel& lv) const {
198 const double step = prm_.jacobiOmega / lv.lmax;
199 spmv(lv, lv.x, lv.res);
200 DView x = lv.x, b = lv.b, res = lv.res, invD = lv.invDiag;
201 Kokkos::parallel_for(
202 "peclet::core::amgd_jacobi", Kokkos::RangePolicy<ExecSpace>(0, lv.n),
203 KOKKOS_LAMBDA(const Index i) { x(i) += step * invD(i) * (b(i) - res(i)); });
204 }
205
206 void chebSweep(const DLevel& lv) const {
207 const int k = prm_.chebDegree;
208 const double lam = 1.1 * lv.lmax;
209 DView r = lv.res, d = lv.t0, Ad = lv.t1, x = lv.x, b = lv.b, invD = lv.invDiag;
210 spmv(lv, lv.x, r); // r = b − A x
211 Kokkos::parallel_for(
212 "peclet::core::amgd_cheb_r0", Kokkos::RangePolicy<ExecSpace>(0, lv.n),
213 KOKKOS_LAMBDA(const Index i) { r(i) = b(i) - r(i); });
214 Kokkos::deep_copy(d, 0.0);
215 for (int i = 1; i <= k; ++i) {
216 const double c1 = (2.0 * i - 3.0) / (2.0 * i + 1.0);
217 const double c2 = (8.0 * i - 4.0) / ((2.0 * i + 1.0) * lam);
218 Kokkos::parallel_for(
219 "peclet::core::amgd_cheb_d", Kokkos::RangePolicy<ExecSpace>(0, lv.n),
220 KOKKOS_LAMBDA(const Index j) {
221 d(j) = c1 * d(j) + c2 * invD(j) * r(j);
222 x(j) += d(j);
223 });
224 if (i < k) {
225 spmv(lv, d, Ad);
226 Kokkos::parallel_for(
227 "peclet::core::amgd_cheb_rup", Kokkos::RangePolicy<ExecSpace>(0, lv.n),
228 KOKKOS_LAMBDA(const Index j) { r(j) -= Ad(j); });
229 }
230 }
231 }
232
233 static double dot(const DView& a, const DView& b, Index n) {
234 double s = 0.0;
235 Kokkos::parallel_reduce(
236 "peclet::core::amgd_dot", Kokkos::RangePolicy<ExecSpace>(0, n),
237 KOKKOS_LAMBDA(const Index i, double& acc) { acc += a(i) * b(i); }, s);
238 return s;
239 }
240
241 // Coarsest level: short unpreconditioned CG (tiny n). The dot products are the ONE place the
242 // device apply reorders sums vs the host oracle.
243 void coarseSolve(const DLevel& lv) const {
244 if (prm_.coarseSweeps > 0) {
245 smooth(lv, prm_.coarseSweeps);
246 return;
247 }
248 const Index n = lv.n;
249 DView x = lv.x, r = lv.res, p = lv.t0, Ap = lv.t1, b = lv.b;
250 spmv(lv, x, r);
251 Kokkos::parallel_for(
252 "peclet::core::amgd_cg_r0", Kokkos::RangePolicy<ExecSpace>(0, n),
253 KOKKOS_LAMBDA(const Index i) {
254 r(i) = b(i) - r(i);
255 p(i) = r(i);
256 });
257 double rr = dot(r, r, n);
258 const double rr0 = rr;
259 const int maxit = (int)std::min<Index>(n, 200);
260 for (int it = 0; it < maxit && rr > 1e-24 * rr0; ++it) {
261 spmv(lv, p, Ap);
262 const double pAp = dot(p, Ap, n);
263 if (pAp <= 0.0)
264 break;
265 const double alpha = rr / pAp;
266 Kokkos::parallel_for(
267 "peclet::core::amgd_cg_up", Kokkos::RangePolicy<ExecSpace>(0, n),
268 KOKKOS_LAMBDA(const Index i) {
269 x(i) += alpha * p(i);
270 r(i) -= alpha * Ap(i);
271 });
272 const double rrn = dot(r, r, n);
273 const double beta = rrn / rr;
274 Kokkos::parallel_for(
275 "peclet::core::amgd_cg_p", Kokkos::RangePolicy<ExecSpace>(0, n),
276 KOKKOS_LAMBDA(const Index i) { p(i) = r(i) + beta * p(i); });
277 rr = rrn;
278 }
279 }
280
281 private:
282 GraphAMG host_;
283 AmgParams prm_;
284 std::vector<DLevel> lv_;
285};
286
287} // namespace peclet::core::solver
288
289#endif // PECLET_CORE_SOLVER_GRAPH_AMG_DEVICE_HPP
void jacobiSweep(const DLevel &lv) const
static View< T > toDev(const std::vector< T > &v, const char *name)
void coarseSolve(const DLevel &lv) const
void build(const HostCsrOp &A, const AmgParams &prm={})
Host setup + one-time device mirror. Rebuild whenever the operator changes.
static void spmv(const DLevel &lv, const DView &x, const DView &y)
static double dot(const DView &a, const DView &b, Index n)
void smooth(const DLevel &lv, int sweeps) const
void apply(const DView &r, const DView &z) const
z = M⁻¹ r: one V-cycle (correction scheme) from a zero initial guess. Device views,...
void chebSweep(const DLevel &lv) const
Smoothed-aggregation AMG hierarchy usable as z = M⁻¹ r (one symmetric V-cycle from a zero initial gue...
Definition graph_amg.hpp:89
void build(const HostCsrOp &A, const AmgParams &prm={})
Definition graph_amg.hpp:91
const std::vector< Level > & levels() const
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
int coarseSweeps
coarsest level: 0 ⇒ a near-exact CG solve; >0 ⇒ this many smoother sweeps instead (with maxLevels=1 t...
Definition graph_amg.hpp:81
double jacobiOmega
damped-Jacobi smoother relaxation (used when chebDegree == 0)
Definition graph_amg.hpp:80
int chebDegree
Chebyshev smoother polynomial degree; 0 ⇒ damped-Jacobi smoother.
Definition graph_amg.hpp:79
int post
smoother sweeps per level (pre == post keeps the V-cycle symmetric)
Definition graph_amg.hpp:78
A general assembled sparse operator in CSR form: the diagonal is stored separately and the CSR (start...
Definition graph_amg.hpp:52