flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_vof_redistribute_mpi.cpp
Go to the documentation of this file.
1// flow — dynamic load balancing on a VoF run: `rebalance_by_weights` / `Solver::redistribute` with
2// a partition that actually MOVES.
3//
4// The defect this gates (found by WO-V9 item 4, fixed on branch `vof-rebalance`): everything the
5// solver grows AFTER construction — the FieldSet's own storage (`add`: the VoF colour mirror "C",
6// "kappa"/"kappa_branch", every closure target, every transported scalar), the member handles that
7// ALIAS those records, the lazily-allocated per-block scratch, and the block container's per-rank
8// box table — did not follow `allocateBlock` onto the new block. `redistribute` then memcpy'd the
9// NEW padded extent into the OLD allocation: an AddressSanitizer heap-buffer-overflow (a
10// 184320-byte write into a 115328-byte region) surfacing as `free(): invalid pointer` /
11// `malloc(): unsorted double linked list corrupted` on whichever ranks GREW.
12//
13// What is gated here, in order of what each can only catch:
14//
15// R1 THE MOVE IS EXACT DATA MOVEMENT. The global C, u, v, w, P and the transported scalar T are
16// gathered immediately before and immediately after the rebalance and must be BITWISE equal
17// (`!=`, not a tolerance) at every np — `redistributeGridFields` is pure data movement and
18// step 5's `setSolid` re-derives the geometry from the migrated SDF, so nothing about the
19// state may change. A stale view surviving the move shows up here as a differing cell long
20// before it corrupts the heap. The test FAILS if the partition did not actually move, so the
21// gate can never pass vacuously.
22//
23// R2 STEPPING AFTER THE MOVE IS STILL THE SAME SOLVER. The run continues to STEPS on the new
24// partition and is compared with a full-grid single-rank reference that never redistributed.
25// np = 1 is bitwise (one rank owns everything either way); np > 1 carries the pressure
26// driver's documented allreduce reduction-order floor.
27//
28// R3 THE BLOCK CONTAINER'S EXCHANGE TABLE FOLLOWS THE NEW DECOMPOSITION. Configuration 1 turns
29// on the per-bubble block container (rung W0). Its gather/scatter pieces are cut against the
30// owned box of EVERY rank; those boxes come from the `BlockDecomposer`, so a partition that
31// moves invalidates them. Gated by the same R1/R2 pair with the union colour and the
32// per-marker volumes carried through the move.
33//
34// Grid 24x24x48, two bubbles stacked along the long z axis (the aligned ORB's cut axis), all
35// fluid — the block container is all-fluid at rung W0.
36#include <mpi.h>
37
38#include <cmath>
39#include <cstdio>
40#include <cstdlib>
41#include <cstring>
42#include <Kokkos_Core.hpp>
43#include <vector>
44
45#include "flow_ibm.hpp"
46#include "peclet/core/common/types.hpp"
47#include "peclet/core/decomp/block_decomposer.hpp"
48
49using peclet::core::IVec;
50using peclet::core::decomp::BlockDecomposer;
53
54static constexpr int NX = 24, NY = 24, NZ = 48;
55static constexpr std::size_t GCELLS = (std::size_t)NX * NY * NZ;
56static constexpr double RHO_L = 10.0, RHO_G = 1.0, MU_L = 0.5, MU_G = 0.05, SIGMA = 4.0;
57static constexpr double RB = 4.5, GRAV = 2.0e-3;
58static const double CX[2] = {NX / 2.0 + 0.13, NX / 2.0 + 0.13};
59static const double CY[2] = {NY / 2.0 - 0.21, NY / 2.0 - 0.21};
60static const double CZ[2] = {14.0 + 0.31, 30.0 + 0.31};
61static const int ZSPLIT = 22; // clips the seed boxes so each marker gets exactly one sphere
62
63static double bubbleAt(int b, int x, int y, int z) {
64 const int NS = 12;
65 double acc = 0.0;
66 for (int j = 0; j < NS; ++j)
67 for (int i = 0; i < NS; ++i) {
68 const double px = x + (i + 0.5) / NS - CX[b], py = y + (j + 0.5) / NS - CY[b];
69 const double r2 = RB * RB - px * px - py * py;
70 if (r2 <= 0.0)
71 continue;
72 const double h = std::sqrt(r2);
73 const double lo = std::fmax(CZ[b] - h, (double)z), hi = std::fmin(CZ[b] + h, (double)z + 1);
74 if (hi > lo)
75 acc += hi - lo;
76 }
77 return acc / (NS * NS);
78}
79
80// The whole feature surface the defect lived on: VoF ("C" + the curvature mirrors), three property
81// closures (registry targets "rho"/"mu"/"force_z" resolved by NAME), a transported scalar (its own
82// registry buffer + its own per-block operator), and optionally the block container.
83// cfg 0: VoF + closures + scalar; 1: + the block container; 2: VoF + scalar + PHASE CHANGE with
84// the consistent-energy transport — the configuration that owns the g = 3 area drivers
85// (`pcAreaC_`/`pcAreaMc_`) and `vofEnergy_`, whose own `ready()`/`initialized()` predicates ask
86// "has init run", not "is it the right size".
87static void configure(IbmSolver& s, int ox, int oy, int oz, int lnx, int lny, int lnz, int cfg) {
88 const bool blocks = (cfg == 1), pc = (cfg == 2);
89 s.setRho(RHO_L);
90 s.setMu(MU_L);
91 s.setDomainBc(4, 1, 0, 0, 0);
92 s.setDomainBc(5, 1, 0, 0, 0); // walls +-z (the CUT axis)
93 s.setPressureGeometry(std::vector<double>((std::size_t)lnx * lny * lnz, 10.0));
94 // Pure RB-GS / Chebyshev pressure: decomposition-agnostic, so a WEIGHTED partition is admissible
95 // (the geometric coarse levels assume clean coarsening — see test_redistribute_mpi).
97 s.setPressureChebyshev(true, 400, 1e-14);
98 s.enableVof();
99 std::vector<double> c((std::size_t)lnx * lny * lnz, 0.0), t(c.size(), 0.0);
100 for (int z = 0; z < lnz; ++z)
101 for (int y = 0; y < lny; ++y)
102 for (int x = 0; x < lnx; ++x) {
103 const double v =
104 std::fmax(bubbleAt(0, x + ox, y + oy, z + oz), bubbleAt(1, x + ox, y + oy, z + oz));
105 const std::size_t i = (std::size_t)x + (std::size_t)y * lnx + (std::size_t)z * lnx * lny;
106 c[i] = v;
107 t[i] = 0.5 + 0.25 * std::sin(0.3 * (x + ox)) * std::cos(0.2 * (z + oz));
108 }
109 s.setVof(c);
110 s.setPropertyModel("rho", ClosureKind::LinearMix, "C", "", {RHO_L, RHO_G - RHO_L});
111 s.setPropertyModel("mu", ClosureKind::LinearMix, "C", "", {MU_L, MU_G - MU_L});
112 s.setPropertyModel("force_z", ClosureKind::LinearMix, "C", "",
113 {-RHO_L * GRAV, -(RHO_G - RHO_L) * GRAV});
114 if (!pc)
116 s.addScalar("T", 0.02, 1, 30);
117 s.setField("T", t);
118 if (pc) {
119 s.enablePhaseChange(RHO_G, RHO_L, 10.0);
120 s.setPhaseChangeThermal("T", 0.5, 0.02, 0.02, 0.0);
121 s.setPhaseChangeEnergy(1.0, 2.0);
122 }
123 if (blocks) {
124 std::vector<std::array<int, 6>> boxes;
125 for (int b = 0; b < 2; ++b) {
126 std::array<int, 6> q;
127 const double c3[3] = {CX[b], CY[b], CZ[b]};
128 for (int d = 0; d < 3; ++d) {
129 q[d] = (int)std::floor(c3[d] - RB) - 1;
130 q[3 + d] = (int)std::ceil(c3[d] + RB) + 1;
131 }
132 if (b == 0)
133 q[5] = ZSPLIT;
134 else
135 q[2] = ZSPLIT;
136 boxes.push_back(q);
137 }
139 }
140 // cfg 2 has no surface tension, so `capillaryDt()` is infinite there: a fixed dt well inside the
141 // Weymouth-Yue cap instead.
142 s.setDt(pc ? 0.02 : 0.25 * s.capillaryDt());
143}
144
145static std::vector<double> gatherGlobal(const std::vector<double>& local, int ox, int oy, int oz,
146 int lnx, int lny, int lnz, int rank, int size) {
147 std::vector<double> global;
148 if (rank == 0)
149 global.assign(GCELLS, 0.0);
150 for (int r = 0; r < size; ++r) {
151 int meta[6] = {ox, oy, oz, lnx, lny, lnz};
152 if (r == 0) {
153 if (rank == 0)
154 for (int z = 0; z < lnz; ++z)
155 for (int y = 0; y < lny; ++y)
156 std::memcpy(&global[(std::size_t)ox + (std::size_t)(y + oy) * NX +
157 (std::size_t)(z + oz) * NX * NY],
158 &local[(std::size_t)y * lnx + (std::size_t)z * lnx * lny],
159 (std::size_t)lnx * sizeof(double));
160 continue;
161 }
162 if (rank == r) {
163 MPI_Send(meta, 6, MPI_INT, 0, 100 + r, MPI_COMM_WORLD);
164 MPI_Send(local.data(), (int)local.size(), MPI_DOUBLE, 0, 200 + r, MPI_COMM_WORLD);
165 } else if (rank == 0) {
167 std::vector<double> buf((std::size_t)meta[3] * meta[4] * meta[5]);
168 MPI_Recv(buf.data(), (int)buf.size(), MPI_DOUBLE, r, 200 + r, MPI_COMM_WORLD,
170 for (int z = 0; z < meta[5]; ++z)
171 for (int y = 0; y < meta[4]; ++y)
172 std::memcpy(&global[(std::size_t)meta[0] + (std::size_t)(y + meta[1]) * NX +
173 (std::size_t)(z + meta[2]) * NX * NY],
174 &buf[(std::size_t)y * meta[3] + (std::size_t)z * meta[3] * meta[4]],
175 (std::size_t)meta[3] * sizeof(double));
176 }
177 }
178 return global;
179}
180
181// NaN-propagating (WO-R2): `std::fmax(m, NaN) == m`, so the obvious loop reports 0 for a field
182// that has gone entirely NaN.
183static double maxDiff(const std::vector<double>& a, const std::vector<double>& b) {
184 double m = 0;
185 for (std::size_t i = 0; i < b.size(); ++i) {
186 const double d = std::fabs(a[i] - b[i]);
187 if (!(d <= m))
188 m = d;
189 }
190 return m;
191}
192static long countBitDiff(const std::vector<double>& a, const std::vector<double>& b) {
193 long n = 0;
194 for (std::size_t i = 0; i < b.size(); ++i)
195 if (a[i] != b[i])
196 ++n;
197 return n;
198}
199
200int main(int argc, char** argv) {
201 MPI_Init(&argc, &argv);
202 Kokkos::initialize(argc, argv);
203 int fail = 0;
204 {
205 int rank = 0, size = 1;
208 const int STEPS = (argc > 1) ? std::atoi(argv[1]) : 12;
209 const int MOVE_AT = STEPS / 2;
210
211 // D1 = the solver's own default partition; D2 = the WEIGHTED ORB the rebalance produces.
212 // The weight field is the interface-weighted one WO-V9 item 4 uses (1 + W*[mixed]), condensed
213 // to "1 + W where the two bubbles are" so the test does not depend on the transported state.
214 auto D1 = peclet::flow::CutcellMG::decomposition(static_cast<std::size_t>(size), NX, NY, NZ);
215 std::vector<peclet::core::Real> w(GCELLS, 1.0);
216 for (int z = 0; z < NZ; ++z)
217 for (int y = 0; y < NY; ++y)
218 for (int x = 0; x < NX; ++x)
219 if (std::fmax(bubbleAt(0, x, y, z), bubbleAt(1, x, y, z)) > 1e-8)
220 w[(std::size_t)x + (std::size_t)y * NX + (std::size_t)z * NX * NY] = 24.0;
221 BlockDecomposer<3> D2((std::size_t)size, IVec<3>{NX, NY, NZ}, w);
222
223 auto blk = D1.block(rank);
224 const int ox = (int)blk.origin[0], oy = (int)blk.origin[1], oz = (int)blk.origin[2];
225 const int lnx = (int)blk.size[0], lny = (int)blk.size[1], lnz = (int)blk.size[2];
226 // The gate is vacuous unless the partition MOVES: the defect only fires on a rank whose block
227 // changed size (a growing rank overflows its old allocation).
228 int moved = 0;
229 for (int r = 0; r < size; ++r) {
230 const auto a = D1.block(r), b = D2.block(r);
231 for (int d = 0; d < 3; ++d)
232 if (a.origin[d] != b.origin[d] || a.size[d] != b.size[d])
233 moved = 1;
234 }
235 if (rank == 0) {
236 std::printf(
237 "VOF REDISTRIBUTE MPI np=%d grid %dx%dx%d block %dx%dx%d partition moves: %s\n", size,
238 NX, NY, NZ, lnx, lny, lnz, moved ? "YES" : "no");
239 for (int r = 0; r < size && size > 1; ++r) {
240 const auto a = D1.block(r), b = D2.block(r);
241 std::printf(" rank %d: %ldx%ldx%ld @ (%ld,%ld,%ld) -> %ldx%ldx%ld @ (%ld,%ld,%ld)\n",
242 r, (long)a.size[0], (long)a.size[1], (long)a.size[2], (long)a.origin[0],
243 (long)a.origin[1], (long)a.origin[2], (long)b.size[0], (long)b.size[1],
244 (long)b.size[2], (long)b.origin[0], (long)b.origin[1], (long)b.origin[2]);
245 }
246 }
247 if (size > 1 && !moved) {
248 if (rank == 0)
249 std::printf(" FAIL - the weighted ORB coincides with the default one; nothing is gated\n");
250 fail = 1;
251 }
252
253 static const char* CFGNAME[3] = {"VoF + closures + scalar",
254 "VoF + closures + scalar + the BLOCK container",
255 "VoF + scalar + PHASE CHANGE (consistent energy)"};
256 for (int cfg = 0; cfg < 3; ++cfg) {
257 const bool blocks = (cfg == 1);
258 if (rank == 0)
259 std::printf(" --- configuration: %s\n", CFGNAME[cfg]);
261 sd.initMpi(D1, MPI_COMM_WORLD);
262 configure(sd, ox, oy, oz, lnx, lny, lnz, cfg);
263 for (int k = 0; k < MOVE_AT; ++k)
264 sd.step();
265
266 // ---- R1: the move is exact data movement -------------------------------------------
267 const char* fn[6] = {"C", "u", "v", "w", "P", "T"};
268 std::vector<double> pre[6], post[6];
269 auto snap = [&](std::vector<double>* out, int px, int py, int pz, int qx, int qy, int qz) {
270 out[0] = gatherGlobal(sd.getVof(), px, py, pz, qx, qy, qz, rank, size);
271 for (int c = 0; c < 3; ++c)
272 out[1 + c] = gatherGlobal(sd.getVelocity(c), px, py, pz, qx, qy, qz, rank, size);
273 out[4] = gatherGlobal(sd.getPressure(), px, py, pz, qx, qy, qz, rank, size);
274 out[5] = gatherGlobal(sd.getField("T"), px, py, pz, qx, qy, qz, rank, size);
275 };
276 snap(pre, ox, oy, oz, lnx, lny, lnz);
277 std::vector<double> volPre;
278 if (blocks) {
279 const auto st = sd.vofBlockStats();
280 std::vector<double> l(st.size(), 0.0);
281 volPre.assign(st.size(), 0.0);
282 for (std::size_t i = 0; i < st.size(); ++i)
283 l[i] = st[i].volume;
284 MPI_Allreduce(l.data(), volPre.data(), (int)l.size(), MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);
285 }
286
287 sd.rebalanceByWeights(w); // <-- the call that corrupted the heap
288
289 const auto nb = D2.block(rank);
290 const int nox = (int)nb.origin[0], noy = (int)nb.origin[1], noz = (int)nb.origin[2];
291 const int nlx = (int)nb.size[0], nly = (int)nb.size[1], nlz = (int)nb.size[2];
292 snap(post, nox, noy, noz, nlx, nly, nlz);
293 if (rank == 0) {
294 std::printf(" R1 across the move (must be bitwise):");
295 for (int i = 0; i < 6; ++i) {
296 const long nd = countBitDiff(pre[i], post[i]);
297 std::printf(" %s %ld cells / %.2e", fn[i], nd, maxDiff(pre[i], post[i]));
298 if (nd != 0)
299 fail = 1;
300 }
301 std::printf("\n");
302 }
303 if (blocks) {
304 const auto st = sd.vofBlockStats();
305 std::vector<double> l(st.size(), 0.0), volPost(st.size(), 0.0);
306 for (std::size_t i = 0; i < st.size(); ++i)
307 l[i] = st[i].volume;
308 MPI_Allreduce(l.data(), volPost.data(), (int)l.size(), MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);
309 if (rank == 0) {
310 double dm = 0.0;
311 for (std::size_t i = 0; i < volPost.size(); ++i)
312 dm = std::fmax(dm, std::fabs(volPost[i] - volPre[i]));
313 std::printf(" R3 marker volumes across the move: %zu markers, max|dV| = %.3e\n",
314 volPost.size(), dm);
315 if (!(dm <= 1e-12 * std::fmax(1.0, volPre.empty() ? 1.0 : volPre[0])))
316 fail = 1;
317 }
318 }
319
320 // ---- R2: the run continues on the new partition ------------------------------------
321 for (int k = MOVE_AT; k < STEPS; ++k)
322 sd.step();
323 std::vector<double> g[6];
324 snap(g, nox, noy, noz, nlx, nly, nlz);
325
326 // The CONTROL: the same distributed run that never rebalanced. It is what calibrates R2 —
327 // a fixed-sweep-count RB-GS (the scalar's implicit diffusion) is decomposition-dependent by
328 // construction, so an absolute tolerance here would either be vacuous or would measure the
329 // decomposition and blame the redistribute. Measured on this scene at np = 2: the scalar's
330 // own decomposition floor is 4.17e-05 with no rebalance at all and 4.20e-05 with one.
331 IbmSolver sc(lnx, lny, lnz);
332 sc.initMpi(D1, MPI_COMM_WORLD);
333 configure(sc, ox, oy, oz, lnx, lny, lnz, cfg);
334 for (int k = 0; k < STEPS; ++k)
335 sc.step();
336 std::vector<double> gc[6];
337 gc[0] = gatherGlobal(sc.getVof(), ox, oy, oz, lnx, lny, lnz, rank, size);
338 for (int c = 0; c < 3; ++c)
339 gc[1 + c] = gatherGlobal(sc.getVelocity(c), ox, oy, oz, lnx, lny, lnz, rank, size);
340 gc[4] = gatherGlobal(sc.getPressure(), ox, oy, oz, lnx, lny, lnz, rank, size);
341 gc[5] = gatherGlobal(sc.getField("T"), ox, oy, oz, lnx, lny, lnz, rank, size);
342
343 if (rank == 0) {
344 IbmSolver ref(NX, NY, NZ);
345 configure(ref, 0, 0, 0, NX, NY, NZ, cfg);
346 for (int k = 0; k < STEPS; ++k)
347 ref.step();
348 std::vector<double> r[6] = {ref.getVof(), ref.getVelocity(0), ref.getVelocity(1),
349 ref.getVelocity(2), ref.getPressure(), ref.getField("T")};
350 std::printf(
351 " R2 vs the single-rank reference after %d steps"
352 " [rebalanced | never-rebalanced control]:\n ",
353 STEPS);
354 for (int i = 0; i < 6; ++i) {
355 const double d = maxDiff(g[i], r[i]), dc = maxDiff(gc[i], r[i]);
356 // np = 1: the control IS the reference (one rank owns everything), so `bound` collapses
357 // to 1e-13 and the gate is bit-exactness. np > 1: the rebalanced run must sit at the
358 // control's own floor, not merely under some absolute number.
359 const double bound = std::fmax(1e-13, 4.0 * dc);
360 std::printf(" d%s %.2e|%.2e", fn[i], d, dc);
361 if (!(d <= bound))
362 fail = 1;
363 }
364 std::printf("\n");
365 }
366 }
367 }
368 int totalFail = 0;
370 int rank = 0;
372 int size = 1;
374 if (rank == 0)
375 std::printf(totalFail == 0
376 ? "OK (np=%d): a VoF (+block) run survives a MOVING rebalance bit-exactly\n"
377 : "FAILED (np=%d)\n",
378 size);
379 Kokkos::finalize();
380 MPI_Finalize();
381 return totalFail == 0 ? 0 : 1;
382}
void enableVofBlocksFromField(const std::vector< std::array< int, 6 > > &boxes)
void setDomainBc(int face, int type, double vx, double vy, double vz)
std::vector< double > getField(const std::string &name)
void addScalar(const std::string &name, double D, int scheme, int iters, bool cutcell=false)
std::vector< double > getVof()
void setPropertyModel(const std::string &target, ClosureKind kind, const std::string &in0, const std::string &in1, const std::vector< double > &params)
void setSurfaceTension(double sigma)
void setPressureChebyshev(bool on, int maxit, double rtol)
double capillaryDt()
The Brackbill capillary limit in the CALLER's time unit.
void setVof(const std::vector< double > &c)
void setPhaseChangeThermal(const std::string &tname, double Tsat, double kg, double kl, double Rint)
P1: compute mdot each step from the registered scalar tname by the one-sided pure-cell weighted least...
void setPressureLevels(int levels)
void setPressureGeometry(const std::vector< double > &sdfInner)
void setField(const std::string &name, const std::vector< double > &v)
std::vector< double > getVelocity(int c)
void enablePhaseChange(double rhoG, double rhoL, double hlv)
Turn on phase change.
void setMu(double m)
Dynamic viscosity, in the caller's units.
void setPhaseChangeEnergy(double rcpGas, double rcpLiquid)
Turn on the CONSISTENT energy transport (VOF_PLAN §9 item 6) for the scalar set_phase_change_thermal ...
void setRho(double r)
Fluid density, in the caller's units.
std::vector< vof::VofBlockStats > vofBlockStats() const
std::vector< double > getPressure()
int main()
Definition dbg_scene.cpp:13
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
static constexpr int STEPS
static constexpr int NY
static constexpr double GRAV
static constexpr double MU_G
static constexpr double SIGMA
static const int ZSPLIT
static double maxDiff(const std::vector< double > &a, const std::vector< double > &b)
static std::vector< double > gatherGlobal(const std::vector< double > &local, int ox, int oy, int oz, int lnx, int lny, int lnz, int rank, int size)
static const double CY[2]
static constexpr double RHO_G
static constexpr double RB
static const double CZ[2]
static void configure(IbmSolver &s, int ox, int oy, int oz, int lnx, int lny, int lnz, int cfg)
static double bubbleAt(int b, int x, int y, int z)
static constexpr int NX
static constexpr double RHO_L
static constexpr double MU_L
static constexpr std::size_t GCELLS
static const double CX[2]
static constexpr int NZ
static long countBitDiff(const std::vector< double > &a, const std::vector< double > &b)