flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_vof_blocks_mpi.cpp
Go to the documentation of this file.
1// VoF Part III rung W0 (WO-W0), gate G4 — the DISTRIBUTED per-bubble block container.
2//
3// The reference is not a stored baseline: every rank additionally runs the SAME problem on the
4// whole global grid as one `WyAdvector` (the np = 1 path), and compares its own sub-block of the
5// UNION field bitwise. Under np = 1 that is a self-consistency check of the two gather routes;
6// under np = 2 / 4 it is the decomposition-independence gate proper.
7//
8// Two things this gate is built to break:
9// * the masters are assigned ROUND ROBIN by block id, deliberately independent of where the
10// bubble's cells live. With more blocks than ranks (and the seeds placed away from each other)
11// at least one block's master owns none of its own cells, so its whole state arrives by message.
12// * every scene's decomposition CUTS a bubble: the ORB of 2 / 4 ranks splits the grid across the
13// seeds, so a block's gather draws from several owners and its scatter lands on several.
14//
15// Why bitwise is reachable: the runs of `vofBuildPieces` partition the block's box, so every
16// block-local cell is written by exactly one owner's piece; the union combines with `max`, which is
17// exact and commutative; and the WY update of a cell then consumes the identical doubles the
18// whole-grid run consumes. A last-bit difference here means the plan double-covers or misses a
19// cell, or the block ghost policy is decomposition-dependent.
20#include <mpi.h>
21
22#include <array>
23#include <cmath>
24#include <cstdio>
25#include <cstring>
26#include <Kokkos_Core.hpp>
27#include <vector>
28
29#include "peclet/core/common/types.hpp"
30#include "peclet/core/decomp/block_decomposer.hpp"
33#include "vof_advect_scenes.hpp"
34
35namespace {
36using peclet::core::IVec;
37using peclet::core::decomp::BlockDecomposer;
46
47constexpr int kDim = 3;
48constexpr int G = 3;
49
50int failures = 0;
51#define CHECK(cond) \
52 do { \
53 if (!(cond)) { \
54 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
55 ++failures; \
56 } \
57 } while (0)
58
59struct Seed {
60 double x, y, z, r;
61};
62
63enum class Flow { Uniform, LeVeque, Cellular };
64
65KOKKOS_INLINE_FUNCTION double cellPsi(double x, double y, double A, double phase) {
66 const double PI = 3.14159265358979323846;
67 return -(A / (2.0 * PI)) * Kokkos::sin(2.0 * PI * x) * Kokkos::sin(2.0 * PI * y) * phase;
68}
69void fillCellular(WyAdvector& a, vofscene::Block b, double h, double A, double phase) {
70 SField u = a.faceU(), v = a.faceV(), w = a.faceW();
71 const double invh = 1.0 / h;
73 b, KOKKOS_LAMBDA(long i, int gx, int gy, int) {
74 const double xn = gx * h, yn = gy * h, xp = (gx + 1) * h, yp = (gy + 1) * h;
75 u(i) = (cellPsi(xp, yp, A, phase) - cellPsi(xp, yn, A, phase)) * invh;
76 v(i) = -(cellPsi(xp, yp, A, phase) - cellPsi(xn, yp, A, phase)) * invh;
77 w(i) = 0.0;
78 });
79}
80
81void fillFlow(Flow f, WyAdvector& a, vofscene::Block b, double h, double phase) {
82 if (f == Flow::Uniform)
83 vofscene::fillUniform(a, b, 1.0, 0.7, -0.4);
84 else if (f == Flow::LeVeque)
85 vofscene::fillLeVeque(a, b, h, phase);
86 else
87 fillCellular(a, b, h, 1.0, phase);
88}
89
90struct SceneSpec {
91 Flow flow;
92 int gn;
93 double dt, T;
94 long steps;
95 std::vector<Seed> seeds;
96 const char* name;
100 bool orphanMaster = false;
105 int assign = 0;
106 long reassign = 0;
107};
108
111std::vector<Seed> lattice(int n, double rmin, double rmax) {
112 std::vector<Seed> v;
113 int k = 0;
114 for (int i = 0; i < n; ++i)
115 for (int j = 0; j < n; ++j)
116 for (int m = 0; m < n; ++m, ++k)
117 v.push_back(Seed{(i + 0.5) / n, (j + 0.5) / n, (m + 0.5) / n,
118 rmin + (rmax - rmin) * ((k % 8) / 7.0)});
119 return v;
120}
121
122void runScene(const SceneSpec& sp, int rank, int size) {
123 const int gn = sp.gn;
124 const double h = 1.0 / gn;
125 const std::array<bool, 3> per{true, true, true};
126 const I3 gs{gn, gn, gn};
127
128 // --------------- the flow decomposition (the SAME ORB the solver would build) ---------------
129 BlockDecomposer<kDim> dec(static_cast<std::size_t>(size), IVec<kDim>{gn, gn, gn});
130 std::vector<VofBox> rankBox(static_cast<std::size_t>(size));
131 for (int r = 0; r < size; ++r) {
132 const auto b = dec.block(static_cast<std::size_t>(r));
133 for (int d = 0; d < 3; ++d) {
134 rankBox[r].lo[d] = static_cast<int>(b.origin[d]);
135 rankBox[r].hi[d] = static_cast<int>(b.origin[d] + b.size[d]);
136 }
137 }
138 const VofBox& me = rankBox[rank];
139 const I3 ln{me.n(0), me.n(1), me.n(2)}, lo{me.lo[0], me.lo[1], me.lo[2]};
140
141 // --------------- this rank's patch: it owns the face velocity of its own cells --------------
142 WyAdvector patch;
143 patch.init(ln.x, ln.y, ln.z, h, G);
144 const vofscene::Block pblk = vofscene::blockOf(patch, lo);
145
146 VofBlockSet set;
147 set.init(gs, per, rank, size, h);
148 auto ex = std::make_shared<VofBlockExchange>();
149 ex->init(gs, per, rankBox, rank);
150 VofBlockExchange::Patch pp;
151 pp.e = patch.extent();
152 pp.n = patch.inner();
153 pp.o = lo;
154 pp.g = G;
155 ex->setPatch(pp, patch.faceU(), patch.faceV(), patch.faceW());
156 ex->setComm(MPI_COMM_WORLD);
157 set.setExchange(ex);
158 set.assignMode = static_cast<peclet::flow::vof::VofMasterAssign>(sp.assign);
159 set.reassignEvery = sp.reassign;
160 for (const auto& s : sp.seeds)
161 set.seedSphere(s.x, s.y, s.z, s.r);
162 set.assignMasters();
163 set.scatter(patch.colour());
164
165 // --------------- the whole-grid reference, on every rank --------------------------------
166 WyAdvector ref;
167 ref.init(gn, gn, gn, h, G);
168 const vofscene::Block rblk = vofscene::blockOf(ref, I3{0, 0, 0});
169 const I3 re = ref.extent();
170 ref.exchange = [re](SField f) { vofscene::periodicFill(f, re, G, true, true, true); };
171 VofBlockSet rset; // the SAME container on one rank: this is what the union must equal
172 rset.init(gs, per, 0, 1, h);
173 WyAdvector rpatch;
174 rpatch.init(gn, gn, gn, h, G);
175 const vofscene::Block rpblk = vofscene::blockOf(rpatch, I3{0, 0, 0});
176 {
177 auto rex = std::make_shared<VofBlockExchange>();
178 std::vector<VofBox> one(1);
179 one[0].lo[0] = one[0].lo[1] = one[0].lo[2] = 0;
180 one[0].hi[0] = one[0].hi[1] = one[0].hi[2] = gn;
181 rex->init(gs, per, one, 0);
182 VofBlockExchange::Patch rp;
183 rp.e = rpatch.extent();
184 rp.n = rpatch.inner();
185 rp.o = I3{0, 0, 0};
186 rp.g = G;
187 rex->setPatch(rp, rpatch.faceU(), rpatch.faceV(), rpatch.faceW());
188 rset.setExchange(rex);
189 }
190 rset.assignMode = static_cast<peclet::flow::vof::VofMasterAssign>(sp.assign);
191 rset.reassignEvery = sp.reassign;
192 for (const auto& s : sp.seeds)
193 rset.seedSphere(s.x, s.y, s.z, s.r);
194 rset.assignMasters();
195 rset.scatter(rpatch.colour());
196
197 auto compare = [&](const char* when) {
198 auto hLoc = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), patch.colour());
199 auto hRef = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), rpatch.colour());
200 long diff = 0;
201 double worst = 0.0;
202 for (int z = 0; z < ln.z; ++z)
203 for (int y = 0; y < ln.y; ++y)
204 for (int x = 0; x < ln.x; ++x) {
205 const double a = hLoc(L3(x + G, y + G, z + G, patch.extent()));
206 const double b = hRef(L3(x + lo.x + G, y + lo.y + G, z + lo.z + G, re));
207 if (std::memcmp(&a, &b, sizeof(double)) != 0) {
208 ++diff;
209 worst = std::fmax(worst, std::fabs(a - b));
210 }
211 }
212 long all = 0;
213 double wall = 0.0;
214 MPI_Allreduce(&diff, &all, 1, MPI_LONG, MPI_SUM, MPI_COMM_WORLD);
215 MPI_Allreduce(&worst, &wall, 1, MPI_DOUBLE, MPI_MAX, MPI_COMM_WORLD);
216 if (all != 0 && rank == 0)
217 std::printf(" %s: %ld inner cells differ (max|d| %.3e)\n", when, all, wall);
218 return all;
219 };
220 long bad = compare("seed");
221 CHECK(bad == 0);
222
223 long firstBad = -1;
224 long nMigrated = 0;
225 for (long s = 0; s < sp.steps; ++s) {
226 const double phase = std::cos(M_PI * (s + 0.5) * sp.dt / sp.T);
227 fillFlow(sp.flow, patch, pblk, h, phase);
228 fillFlow(sp.flow, rpatch, rpblk, h, phase);
229 set.advect(sp.dt, patch.colour());
230 rset.advect(sp.dt, rpatch.colour());
231 nMigrated += set.lastReassigned();
232 if (compare("step") != 0) {
233 firstBad = s;
234 break;
235 }
236 }
237 CHECK(firstBad < 0);
238
239 // per-marker volume, reduced from the masters (each block has exactly one)
240 const std::size_t nb = set.count();
241 std::vector<double> volLoc(nb, 0.0), volAll(nb, 0.0), volRef(nb, 0.0);
242 for (std::size_t i = 0; i < nb; ++i)
243 if (set.blocks()[i].mine())
244 volLoc[i] = set.blocks()[i].stats().volume;
245 MPI_Allreduce(volLoc.data(), volAll.data(), static_cast<int>(nb), MPI_DOUBLE, MPI_SUM,
246 MPI_COMM_WORLD);
247 for (std::size_t i = 0; i < nb; ++i)
248 volRef[i] = rset.blocks()[i].stats().volume;
249 double volDiff = 0.0;
250 for (std::size_t i = 0; i < nb; ++i)
251 volDiff = std::fmax(volDiff, std::fabs(volAll[i] - volRef[i]));
252
253 // ownership census: how many of its OWN cells does each block's master hold?
254 long ownedByMaster = 0, blocksWithNoLocalCells = 0;
255 for (const auto& b : set.blocks()) {
256 const VofBox ov = VofBox::intersect(b.box, rankBox[b.master]);
257 if (ov.empty())
258 ++blocksWithNoLocalCells;
259 else
260 ownedByMaster += ov.cells();
261 }
262 std::vector<long> mc, cc;
263 set.masterCensus(mc, cc);
264
265 if (rank == 0) {
266 std::printf(" [%s] %d^3, %ld steps, np=%d, %zu blocks, local block %dx%dx%d\n", sp.name, gn,
267 sp.steps, size, nb, ln.x, ln.y, ln.z);
268 std::printf(" bitwise: first differing step %ld (-1 = none) |dV| vs np=1 %.3e\n",
269 firstBad, volDiff);
270 std::printf(" masters per rank:");
271 for (int r = 0; r < size; ++r)
272 std::printf(" %ld", mc[r]);
273 std::printf(" block cells per rank:");
274 for (int r = 0; r < size; ++r)
275 std::printf(" %ld", cc[r]);
276 std::printf(" cell imbalance %.3f\n", set.cellImbalance());
277 std::printf(" blocks whose master owns NONE of their cells: %ld / %zu\n",
278 blocksWithNoLocalCells, nb);
279 if (sp.reassign > 0)
280 std::printf(
281 " assignment mode %d, re-assigned every %ld steps: %ld master CHANGES migrated "
282 "(%ld B in %ld msgs on rank 0 at the last one)\n",
283 sp.assign, sp.reassign, nMigrated, ex->migrateBytes(), ex->migrateMessages());
284 std::printf(" exchange: gather %ld B in %ld msgs, scatter %ld B in %ld msgs (rank 0)\n",
285 ex->gatherBytes(), ex->gatherMessages(), ex->scatterBytes(), ex->scatterMessages());
286 }
287 CHECK(volDiff < 1e-15);
288 if (size > 1 && sp.orphanMaster)
289 CHECK(blocksWithNoLocalCells > 0);
290}
291
292} // namespace
293
294int main(int argc, char** argv) {
295 MPI_Init(&argc, &argv);
296 Kokkos::initialize(argc, argv);
297 {
298 int rank = 0, size = 1;
299 MPI_Comm_rank(MPI_COMM_WORLD, &rank);
300 MPI_Comm_size(MPI_COMM_WORLD, &size);
301 if (rank == 0)
302 std::printf("VoF rung W0 (WO-W0) - distributed block container, backend: %s, np = %d\n",
303 SExec::name(), size);
304
305 // G4 / G1: one bubble in the LeVeque deformation, the decomposition cutting it
306 runScene(SceneSpec{Flow::LeVeque,
307 32,
308 3.0 / 768, // CFL 0.25, the V1 gate-D schedule at 32^3
309 3.0,
310 48,
311 {{0.35, 0.35, 0.35, 0.15}},
312 "G1 one bubble / LeVeque"},
313 rank, size);
314
315 // G4 / G2: two bubbles pushed together; masters round-robin, so at np >= 2 block 1's master
316 // is a different rank from block 0's and neither need own the cells
317 runScene(SceneSpec{Flow::Cellular,
318 48,
319 0.9 / 216, // CFL 0.2
320 0.9,
321 120,
322 {{0.38, 0.5, 0.5, 0.10}, {0.62, 0.5, 0.5, 0.10}},
323 "G2 two bubbles / cellular"},
324 rank, size);
325
326 // G4 / G3: three bubbles in a uniform translation -- the blocks MOVE (re-centre) while
327 // distributed, and with 3 blocks over 1/2/4 ranks the round-robin leaves an idle rank at np=4
328 runScene(SceneSpec{Flow::Uniform,
329 32,
330 0.25 / 32,
331 1.0,
332 40,
333 {{0.30, 0.30, 0.30, 0.12}, {0.70, 0.65, 0.35, 0.10}, {0.5, 0.2, 0.8, 0.09}},
334 "G3 three bubbles / translation",
335 true},
336 rank, size);
337
338 // W1 (WO-W12): 64 bubbles of MIXED size in the LeVeque field, LPT masters re-assigned every
339 // 8 steps. The single-rank reference container never migrates (size 1), so a bitwise match
340 // here is simultaneously (a) decomposition independence, (b) assignment independence and
341 // (c) exactness of the migration -- the colour and the previous centroid are the only state a
342 // block carries, and they travel as a contiguous message.
343 runScene(SceneSpec{Flow::LeVeque, 48,
344 0.08 / 48, // the LeVeque field peaks near |uf| = 2, so CFL ~ 0.16
345 1.5, 24, lattice(4, 2.0 / 48, 8.0 / 48),
346 "W1 64 bubbles / LeVeque / LPT + re-assignment", true,
347 /*assign=*/1,
348 /*reassign=*/8},
349 rank, size);
350
351 int total = 0;
352 MPI_Allreduce(&failures, &total, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD);
353 failures = total;
354 if (rank == 0)
355 std::printf("%s (%d failure%s)\n", failures ? "FAILED" : "PASSED", failures,
356 failures == 1 ? "" : "s");
357 }
358 Kokkos::finalize();
359 MPI_Finalize();
360 return failures ? 1 : 0;
361}
flow — VoF Part III rung W0: the per-bubble VoF BLOCK, a third container over the same L1 kernels (su...
flow — VoF Part III rung W0: the block gather/scatter (VOF_PLAN.md §10 design items 2 and 4).
The gather / scatter / table replication of the block container.
The container: a replicated table of blocks, the per-step orchestration (gather → advect → re-centre ...
Weymouth-Yue split advection of a colour field on an extended (inner + ghost) block.
int main()
Definition dbg_scene.cpp:13
float worst
Definition cmp.py:18
x
Definition cmp.py:24
y
Definition cmp.py:24
VofMasterAssign
How a block's MASTER rank is chosen.
long L3(int x, int y, int z, I3 e)
Kokkos::DefaultExecutionSpace SExec
Kokkos::View< double *, SMem > SField
wall
Definition prof.py:146
blocks
Definition prof.py:61
one(timed)
Definition prof.py:109
void fillLeVeque(WyAdvector &a, Block b, double h, double phase)
void forEachExtended(Block b, F fn)
Visit every cell of the extended block with (local linear index, global cell index).
void periodicFill(SField f, I3 e, int g, bool px, bool py, bool pz)
Periodic wrap of a single-block extended field (serial reference path; the MPI path uses core's GridH...
void fillUniform(WyAdvector &a, Block b, double ux, double uy, double uz)
Uniform translation. Discrete divergence is EXACTLY zero (identical values differenced).
Block blockOf(const WyAdvector &a, I3 origin)
Half-open global cell box [lo, hi).
Where this block sits in the global grid: o is the global index of inner cell (0,0,...
static constexpr int G
static constexpr int kDim
static bool compare(const char *what, const std::vector< Row > &built, const std::vector< Row > &predicted, int rank)
#define CHECK(cond)