flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_cutcellmg_aniso_mpi.cpp
Go to the documentation of this file.
1// G4 under MPI — the ANISOTROPIC pressure hierarchy, multi-rank
2// (flow/doc/anisotropic_metric.md §8.5 last bullet + §5.3/§5.4 and trap 6; Phase 2 commit C3).
3//
4// The SAME problem the single-rank `cutcellmg_aniso` gate solves — the Zick & Homsy SC sphere
5// (phi = 0.216, R = 0.3722 L) on the stretched box (N, 2N, N/2) of the cube L^3, apertures from
6// the solver's own `buildOpenness` with hp = (1, 1/2, 2), weights w = (1, 4, 1/4) — driven through
7// the PRODUCTION `CutcellMG::initMpi` and compared against the validated single-rank `init()`.
8//
9// Four statements:
10// 1. the LEVEL TABLE the aspect rule chooses is IDENTICAL on every rank. It has to be: every
11// rank computes it from the same doubles and the same replicated decomposition, with no
12// communication (§5.3). It is also the single-rank table wherever the even-block gate does
13// not bite.
14// 2. np = 1 is BIT-EXACT to the single-rank solve (the gating is byte-identical), np = 2/4 agree
15// to the MG-PCG reduction-order floor — the tolerance pattern of `test_cutcellmg_mpi`.
16// 3. TRAP 6: an axis DEFERRED by the aspect rule is NOT "blocked". With the economic trigger
17// off (`setTelescopeMinExtent(0)`, so a merge happens only when an axis that CAN coarsen
18// globally is not even on every rank) and telescoping ON, NO level telescopes — even though
19// the rule defers x and z for two levels. Deferral alone must never merge ranks.
20// 4. a FORCED telescope at level 1 (`setTelescopeForceLevel(1)`) still reaches the same answer
21// on the deferred hierarchy: the merge path and the aspect rule compose.
22#include <mpi.h>
23
24#include <cmath>
25#include <cstdio>
26#include <Kokkos_Core.hpp>
27#include <utility>
28#include <vector>
29
30#include "mac_cutcell.hpp" // buildOpenness (the solver's own aperture model)
31#include "mac_cutcell_mg.hpp"
32#include "peclet/core/common/types.hpp"
33
34using peclet::core::IVec;
40
41static constexpr int G = 1, NLEV = 6;
42static constexpr int N = 32; // stretched grid (N, 2N, N/2) == N^3 cells
43static constexpr double LBOX = 1.0, RSPH = 0.3722; // phi = 0.216 in the unit cell
44
45// d' = (|x - c|_minimage - R)/dref at the PHYSICAL centre of the GLOBAL cell (gx, gy, gz).
46static double sdfAt(int gx, int gy, int gz, int nx, int ny, int nz, double dref) {
47 double px = (gx + 0.5) * (LBOX / nx) - 0.5 * LBOX, py = (gy + 0.5) * (LBOX / ny) - 0.5 * LBOX,
48 pz = (gz + 0.5) * (LBOX / nz) - 0.5 * LBOX;
49 px -= LBOX * std::round(px / LBOX);
50 py -= LBOX * std::round(py / LBOX);
51 pz -= LBOX * std::round(pz / LBOX);
52 return (std::sqrt(px * px + py * py + pz * pz) - RSPH * LBOX) / dref;
53}
54
55// Assemble the operator on `mg`'s level-0 block and solve MG-PCG to 1e-10; returns the host
56// iterate. `og` is this block's global inner origin (0 single-rank).
57using HostVec = decltype(Kokkos::create_mirror_view(std::declval<CCField>()));
58static HostVec setupAndSolve(CutcellMG& mg, C3 ext, C3 og, int nx, int ny, int nz, bool collective,
59 int* iters) {
60 const double hx = LBOX / nx, hy = LBOX / ny, hz = LBOX / nz, dref = hx;
61 const double hp[3] = {hx / dref, hy / dref, hz / dref};
62 const double wx = 1.0 / (hp[0] * hp[0]), wy = 1.0 / (hp[1] * hp[1]), wz = 1.0 / (hp[2] * hp[2]);
63 const std::size_t n = (std::size_t)ext.x * ext.y * ext.z;
64 CCField sdf("sdf", n), ox("ox", n), oy("oy", n), oz("oz", n);
65 {
66 auto m = Kokkos::create_mirror_view(sdf);
67 for (int z = 0; z < ext.z; ++z)
68 for (int y = 0; y < ext.y; ++y)
69 for (int x = 0; x < ext.x; ++x)
70 m((long)x + (long)y * ext.x + (long)z * (long)ext.x * ext.y) =
71 sdfAt(x - G + og.x, y - G + og.y, z - G + og.z, nx, ny, nz, dref);
72 Kokkos::deep_copy(sdf, m);
73 }
74 buildOpenness(ox, oy, oz, CCConst(sdf), ext, hp[0], hp[1], hp[2], 1);
75 mg.setOpenness(CCConst(ox), CCConst(oy), CCConst(oz), wx, wy, wz);
76
77 // The RHS of the single-rank gate: the smooth periodic mode on the rows that carry an open
78 // face, mean-removed over exactly those rows. The mean is GLOBAL, so it is an Allreduce here
79 // and a local sum single-rank — the same number either way (a plain sum of the same set).
80 std::vector<double> hb(n, 0.0);
81 {
82 auto hAC = Kokkos::create_mirror_view(mg.level(0).AC);
83 Kokkos::deep_copy(hAC, mg.level(0).AC);
84 const double k = 2.0 * M_PI / LBOX;
85 double loc[2] = {0.0, 0.0};
86 for (int z = G; z < ext.z - G; ++z)
87 for (int y = G; y < ext.y - G; ++y)
88 for (int x = G; x < ext.x - G; ++x) {
89 const std::size_t i =
90 (std::size_t)x + (std::size_t)y * ext.x + (std::size_t)z * (std::size_t)ext.x * ext.y;
91 if (!(hAC(i) > 0.0f))
92 continue;
93 const double px = (x - G + og.x + 0.5) * (LBOX / nx),
94 py = (y - G + og.y + 0.5) * (LBOX / ny),
95 pz = (z - G + og.z + 0.5) * (LBOX / nz);
96 hb[i] = std::cos(k * px) * std::cos(k * py) * std::cos(k * pz);
97 loc[0] += hb[i];
98 loc[1] += 1.0;
99 }
100 double glob[2] = {loc[0], loc[1]};
101 if (collective) // the distributed build owns one block per rank; the reference owns them all
102 MPI_Allreduce(loc, glob, 2, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);
103 const double mean = glob[1] > 0.0 ? glob[0] / glob[1] : 0.0;
104 for (int z = G; z < ext.z - G; ++z)
105 for (int y = G; y < ext.y - G; ++y)
106 for (int x = G; x < ext.x - G; ++x) {
107 const std::size_t i =
108 (std::size_t)x + (std::size_t)y * ext.x + (std::size_t)z * (std::size_t)ext.x * ext.y;
109 if (hAC(i) > 0.0f)
110 hb[i] -= mean;
111 }
112 }
113 CCField b("b", n), x("x", n), r("r", n), p("p", n), z("z", n), Ap("Ap", n);
114 {
115 auto m = Kokkos::create_mirror_view(b);
116 for (std::size_t i = 0; i < n; ++i)
117 m(i) = hb[i];
118 Kokkos::deep_copy(b, m);
119 }
120 *iters = mg.solvePCG(b, x, r, p, z, Ap, /*maxit=*/500, /*rtol=*/1e-10, 2, 2, 12);
121 auto hx2 = Kokkos::create_mirror_view(x);
122 Kokkos::deep_copy(hx2, x);
123 return hx2;
124}
125
126int main(int argc, char** argv) {
127 MPI_Init(&argc, &argv);
128 Kokkos::initialize(argc, argv);
129 int fail = 0, size = 1, rank = 0;
130 {
131 MPI_Comm_rank(MPI_COMM_WORLD, &rank);
132 MPI_Comm_size(MPI_COMM_WORLD, &size);
133 const int nx = N, ny = 2 * N, nz = N / 2;
134 const double hp[3] = {1.0, 0.5, 2.0};
135
136 // ---- the single-rank reference hierarchy (every rank builds it redundantly) ----------------
137 CutcellMG ref;
138 ref.setMetric(hp);
139 ref.init(nx, ny, nz, NLEV);
140 const C3 re{nx + 2 * G, ny + 2 * G, nz + 2 * G}, ro{0, 0, 0};
141 int refIt = 0;
142 auto hr = setupAndSolve(ref, re, ro, nx, ny, nz, /*collective=*/false, &refIt);
143 const std::vector<C3> refTab = ref.levelRatios();
144
145 // ---- PASS 1: the distributed hierarchy, telescoping ON, economic trigger OFF ---------------
146 // so a merge can only be triggered by a genuinely BLOCKED axis (trap 6).
147 CutcellMG mg;
148 mg.setMetric(hp);
149 mg.setTelescope(true);
151 mg.initMpi(nx, ny, nz, NLEV, MPI_COMM_WORLD);
152 CutcellMG::Level& l0 = mg.level(0);
153 int it = 0;
154 auto hx = setupAndSolve(mg, l0.ext, l0.og, nx, ny, nz, /*collective=*/true, &it);
155 const std::vector<C3> tab = mg.levelRatios();
156
157 // 1. the level table is the same on every rank (a pure function of replicated data).
158 {
159 int nl = (int)tab.size(), nlMin = 0, nlMax = 0;
160 MPI_Allreduce(&nl, &nlMin, 1, MPI_INT, MPI_MIN, MPI_COMM_WORLD);
161 MPI_Allreduce(&nl, &nlMax, 1, MPI_INT, MPI_MAX, MPI_COMM_WORLD);
162 if (nlMin != nlMax) {
163 ++fail;
164 std::fprintf(stderr, "[rank %d] level COUNT differs across ranks: %d..%d\n", rank, nlMin,
165 nlMax);
166 } else {
167 std::vector<int> mine(3 * (std::size_t)nl), lo(3 * (std::size_t)nl),
168 hi(3 * (std::size_t)nl);
169 for (int L = 0; L < nl; ++L) {
170 mine[3 * (std::size_t)L] = tab[(std::size_t)L].x;
171 mine[3 * (std::size_t)L + 1] = tab[(std::size_t)L].y;
172 mine[3 * (std::size_t)L + 2] = tab[(std::size_t)L].z;
173 }
174 MPI_Allreduce(mine.data(), lo.data(), 3 * nl, MPI_INT, MPI_MIN, MPI_COMM_WORLD);
175 MPI_Allreduce(mine.data(), hi.data(), 3 * nl, MPI_INT, MPI_MAX, MPI_COMM_WORLD);
176 for (int i = 0; i < 3 * nl; ++i)
177 if (lo[(std::size_t)i] != hi[(std::size_t)i]) {
178 ++fail;
179 std::fprintf(stderr, "[rank %d] level table entry %d differs across ranks\n", rank, i);
180 break;
181 }
182 }
183 if (rank == 0) {
184 std::printf(" level table (np=%d):", size);
185 for (const C3& rr : tab)
186 std::printf(" (%d,%d,%d)", rr.x, rr.y, rr.z);
187 std::printf("\n single-rank reference: ");
188 for (const C3& rr : refTab)
189 std::printf(" (%d,%d,%d)", rr.x, rr.y, rr.z);
190 std::printf("\n");
191 }
192 // The aspect rule's own signature, whatever the even-block gate does below it.
193 if (!(tab[0].x == 1 && tab[0].y == 2 && tab[0].z == 1)) {
194 ++fail;
195 std::fprintf(stderr, "[rank %d] level 0 ratio (%d,%d,%d), expected (1,2,1)\n", rank,
196 tab[0].x, tab[0].y, tab[0].z);
197 }
198 }
199
200 // 3. TRAP 6: no level telescoped — deferral is not blocking.
201 {
202 int tele = 0;
203 for (int L = 0; L < mg.nLevels(); ++L)
204 if (mg.level(L).tele)
205 ++tele;
206 int teleAll = 0;
207 MPI_Allreduce(&tele, &teleAll, 1, MPI_INT, MPI_MAX, MPI_COMM_WORLD);
208 if (teleAll != 0) {
209 ++fail;
210 std::fprintf(stderr,
211 "[rank %d] %d level(s) telescoped with the economic trigger off — an "
212 "aspect-DEFERRED axis was treated as BLOCKED (trap 6)\n",
213 rank, teleAll);
214 } else if (rank == 0)
215 std::printf(" trap 6: no telescope on the deferred hierarchy (min-extent trigger off)\n");
216 }
217
218 // 2. against the single-rank solve.
219 double xref = 0.0; // the reference solution's scale, for the RELATIVE floor below
220 for (std::size_t i = 0; i < hr.extent(0); ++i)
221 if (!(std::fabs(hr(i)) <= xref))
222 xref = std::fabs(hr(i));
223 auto blockDiff = [&](HostVec h, C3 ext, C3 og) {
224 double m = 0.0;
225 for (int z = G; z < ext.z - G; ++z)
226 for (int y = G; y < ext.y - G; ++y)
227 for (int x = G; x < ext.x - G; ++x) {
228 const int gx = x - G + og.x, gy = y - G + og.y, gz = z - G + og.z;
229 const double a = h((long)x + (long)y * ext.x + (long)z * (long)ext.x * ext.y);
230 const double rr =
231 hr((long)(gx + G) + (long)(gy + G) * re.x + (long)(gz + G) * (long)re.x * re.y);
232 const double d = std::fabs(a - rr);
233 if (!(d <= m))
234 m = d;
235 }
236 return m;
237 };
238 // np = 1 is BIT-EXACT (the distributed gating is byte-identical there). np > 1 is compared
239 // RELATIVE to the solution scale, at the MG-PCG's own floor: the driver stops on a RESIDUAL
240 // (rtol 1e-10 of |b|inf), and on a CUT-CELL operator a small-aperture row has a tiny effective
241 // eigenvalue, so the same residual leaves a much larger solution spread than the all-fluid
242 // hierarchy of test_cutcellmg_mpi does. Measured here (max|phi| = 14.67): 1.01e-07 /
243 // 2.97e-07 relative at np = 2 / 4, and a -DPECLET_FLOW_OPERATOR_DOUBLE build of the identical
244 // source moves it only 4x (to 2.46e-08 / 1.21e-07), so it is the stopping rule and NOT the
245 // float operator storage.
246 // The iteration count -- the decomposition-independence statement that actually has teeth --
247 // is gated to within 2 of the single-rank one.
248 const double tol = (size == 1) ? 0.0 : 1e-6 * xref;
249 const double d1 = blockDiff(hx, l0.ext, l0.og);
250 if (!(d1 <= tol)) {
251 ++fail;
252 std::fprintf(stderr,
253 "[rank %d] pass 1 max|distributed - single-rank| = %.3e (rel %.3e, tol %.3e)\n",
254 rank, d1, d1 / xref, tol);
255 } else if (rank == 0)
256 std::printf(
257 " pass 1: %d iters (single-rank %d), max|dist - single| = %.3e = %.3e relative "
258 "of max|phi| = %.4f (np=%d%s)\n",
259 it, refIt, d1, d1 / xref, xref, size, size == 1 ? ", BIT-EXACT" : "");
260 if (std::abs(it - refIt) > 2) {
261 ++fail;
262 std::fprintf(stderr, "[rank %d] pass 1 iterations %d vs single-rank %d (allowed +/-2)\n",
263 rank, it, refIt);
264 }
265 if (it >= 500 || refIt >= 500) {
266 ++fail;
267 std::fprintf(stderr, "[rank %d] pass 1 CAPPED (%d / %d iters)\n", rank, it, refIt);
268 }
269
270 // ---- PASS 2: a FORCED telescope at level 1 on the deferred hierarchy -----------------------
271 if (size > 1) {
272 CutcellMG mg2;
273 mg2.setMetric(hp);
274 mg2.setTelescope(true);
277 mg2.initMpi(nx, ny, nz, NLEV, MPI_COMM_WORLD);
278 CutcellMG::Level& m0 = mg2.level(0);
279 int it2 = 0;
280 auto hx2 = setupAndSolve(mg2, m0.ext, m0.og, nx, ny, nz, /*collective=*/true, &it2);
281 int forced = mg2.level(1).tele ? 1 : 0, forcedAll = 0;
282 MPI_Allreduce(&forced, &forcedAll, 1, MPI_INT, MPI_MAX, MPI_COMM_WORLD);
283 const double d2 = blockDiff(hx2, m0.ext, m0.og);
284 if (forcedAll != 1) {
285 ++fail;
286 std::fprintf(stderr, "[rank %d] pass 2: setTelescopeForceLevel(1) did not merge\n", rank);
287 }
288 if (!(d2 <= 1e-6 * xref)) {
289 ++fail;
290 std::fprintf(stderr, "[rank %d] pass 2 max|telescoped - single-rank| = %.3e (rel %.3e)\n",
291 rank, d2, d2 / xref);
292 } else if (rank == 0)
293 std::printf(
294 " pass 2 (telescope forced at level 1): %d iters, max|dist - single| = %.3e "
295 "= %.3e relative\n",
296 it2, d2, d2 / xref);
297 if (rank == 0 && mg2.level(0).ratio.x == 1 && mg2.level(0).ratio.y == 2)
298 std::printf(" pass 2: the aspect rule still owns level 0 across the merge\n");
299 }
300 }
301 int totalFail = 0;
302 MPI_Allreduce(&fail, &totalFail, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD);
303 if (rank == 0) {
304 if (totalFail == 0)
305 std::printf("OK (np=%d): the anisotropic CutcellMG hierarchy, distributed\n", size);
306 else
307 std::fprintf(stderr, "FAILED (np=%d): %d rank(s) failed\n", size, totalFail);
308 }
309 Kokkos::finalize();
310 MPI_Finalize();
311 return totalFail ? 1 : 0;
312}
int solvePCG(CCField b, CCField x, CCField r, CCField p, CCField z, CCField Ap, int maxit, double rtol, int pre, int post, int bottom, const StarOverlay *star=nullptr, int nStar=0, C3 nnStar=C3{0, 0, 0})
void setMetric(const double hp[3])
Per-axis cell spacings h_a' = h_a / hRef of the physical domain (doc/anisotropic_metric....
std::vector< C3 > levelRatios() const
The per-level coarsening ratio actually chosen (the level table of §5, gate §8.5).
void setOpenness(CCConst ox, CCConst oy, CCConst oz, double idx2, double idy2, double idz2)
int main()
Definition dbg_scene.cpp:13
flow — portable (Kokkos) cut-cell pressure-operator face openness from an SDF.
flow — portable (Kokkos) geometric multigrid for the cut-cell (variable-openness) pressure Poisson.
Kokkos::View< double *, CCMem > CCField
void buildOpenness(CCField ox, CCField oy, CCField oz, CCConst sdf, C3 ext, double dx, double dy, double dz, int order=1)
Kokkos::View< const double *, CCMem > CCConst
static constexpr double RSPH
static constexpr double LBOX
static HostVec setupAndSolve(CutcellMG &mg, C3 ext, C3 og, int nx, int ny, int nz, bool collective, int *iters)
static constexpr int N
static constexpr int G
static constexpr int NLEV
decltype(Kokkos::create_mirror_view(std::declval< CCField >())) HostVec
static double sdfAt(int gx, int gy, int gz, int nx, int ny, int nz, double dref)
static double mean(const std::vector< double > &a)
static double blockDiff(const std::vector< double > &local, const std::vector< double > &global, int ox, int oy, int oz, int lnx, int lny, int lnz)