flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
weighted_dec0_telescope_probe.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""SCALING_ISSUES #2 TRAP probe — does a WEIGHTED level-0 decomposition collapse the pressure
3multigrid's telescope to one rank?
4
5The claim (read from `src/mac_cutcell_mg.hpp`, not measured when it was written): after
6`diagnostics.rebalance_by_weights(w)` the level-0 blocks are no longer even on the coarsenable
7axes, the telescope fires at L = 0, and the agglomeration-depth search falls through to d = 0 —
8the whole level-0 residual gathered onto ONE rank every V-cycle.
9
10This measures it, flow-only (the coupling's `CfdDem.rebalance()` reaches the same call):
11
12 1. build a distributed Solver on the default equal-weight aligned ORB, run a few steps;
13 2. record the per-rank blocks (origin, size, parity) and the pressure iterations / step time;
14 3. `rebalance_by_weights(w)` with a particle-bed-like weight (1 + Poisson counts in the bottom
15 `--bed` fraction of z), record the same again.
16
17The MG ladder (levels, ranks per level, which transitions TELESCOPE) is printed by flow itself on
18rank 0 at every hierarchy build when PECLET_FLOW_MG_DEBUG=1 (a trace-only env var; it changes no
19result). The ranks column of the level after a telescope point is the size of the roots'
20sub-communicator, i.e. 2^d for the depth d the search selected (np a power of two).
21
22 PECLET_FLOW_MG_DEBUG=1 OMP_NUM_THREADS=2 OMP_PROC_BIND=false \
23 /usr/bin/mpirun -np 8 --map-by slot:PE=2 --bind-to core python tests/study/weighted_dec0_telescope_probe.py --n 96 --levels 4
24"""
25import argparse
26import sys
27import time
28
29import numpy as np
30from mpi4py import MPI
31
32import peclet.flow as flow
33
34
35def sphere_sdf(o, sz, n, per_axis=2, rfrac=0.35):
36 """Periodic simple-cubic array of spheres (per_axis^3 of them), sampled at cell centres of this
37 rank's block (cell units, origin 0). Negative inside the solid."""
38 a = n / per_axis
39 r = rfrac * a
40 x = o[0] + np.arange(sz[0]) + 0.5
41 y = o[1] + np.arange(sz[1]) + 0.5
42 z = o[2] + np.arange(sz[2]) + 0.5
43 X, Y, Z = np.meshgrid(x, y, z, indexing="ij")
44 dx = (X % a) - 0.5 * a
45 dy = (Y % a) - 0.5 * a
46 dz = (Z % a) - 0.5 * a
47 return np.asfortranarray(np.sqrt(dx * dx + dy * dy + dz * dz) - r)
48
49
50def bed_weights(n, bed, lam, tilt=0.0, seed=7):
51 """1 + Poisson(lam) particle counts below a bed surface, 1 above — a settled bed, as
52 `CfdDem.rebalance(gamma=1)` would build it (1 + gamma * count). tilt = 0 is a FLAT bed at
53 z = bed*n (uniform in x and y, so the x/y ORB splits stay at the equal-cell positions); tilt > 0
54 is a HEAP whose surface falls linearly in x and y, z_s = bed*n*(1 + tilt*(1/2 - x/n) +
55 tilt*(1/2 - y/n)), which moves every split. Global, x-fastest (Fortran ravel), identical on
56 every rank (same seed)."""
57 rng = np.random.default_rng(seed)
58 c = (np.arange(n) + 0.5) / n
59 X, Y, Z = np.meshgrid(c, c, c, indexing="ij")
60 zs = bed * (1.0 + tilt * (0.5 - X) + tilt * (0.5 - Y))
61 counts = np.where(Z < zs, rng.poisson(lam, size=(n, n, n)), 0)
62 return (1.0 + counts).ravel(order="F")
63
64
65def blocks(comm, s):
66 o = list(s.block_origin())
67 sz = list(np.asarray(s.get_p()).shape)
68 return comm.allgather((o, sz))
69
70
71def describe(tag, comm, blks, w, n):
72 if comm.rank != 0:
73 return
74 print(f"\n[probe] {tag}: per-rank blocks (origin / size / odd axes)")
75 W = None if w is None else w.reshape((n, n, n), order="F")
76 loads, cells = [], []
77 for r, (o, sz) in enumerate(blks):
78 odd = [ax for ax in range(3) if (o[ax] % 2) or (sz[ax] % 2)]
79 ld = sz[0] * sz[1] * sz[2] if W is None else float(
80 W[o[0]:o[0] + sz[0], o[1]:o[1] + sz[1], o[2]:o[2] + sz[2]].sum())
81 loads.append(ld)
82 cells.append(sz[0] * sz[1] * sz[2])
83 print(f"[probe] rank {r}: origin {o} size {sz} odd-axes {''.join('xyz'[a] for a in odd) or '-'}"
84 f" weight {ld:.0f}")
85 loads = np.asarray(loads, dtype=float)
86 cells = np.asarray(cells, dtype=float)
87 print(f"[probe] weight imbalance max/mean = {loads.max() / loads.mean():.3f}"
88 f" cell imbalance max/mean = {cells.max() / cells.mean():.3f}")
89 sys.stdout.flush()
90
91
92def run_steps(comm, s, k):
93 """Per step: pressure iterations, wall time (barrier to barrier), and the max over ranks of the
94 solver's own projection and momentum timers (`last_step_timers`)."""
95 its, ts, tp, tm = [], [], [], []
96 for _ in range(k):
97 comm.Barrier()
98 t0 = time.perf_counter()
99 s.step()
100 comm.Barrier()
101 ts.append(time.perf_counter() - t0)
102 its.append(s.diagnostics.last_pressure_iterations())
103 t = s.diagnostics.last_step_timers()
104 tp.append(comm.allreduce(float(t["projection"]), op=MPI.MAX))
105 tm.append(comm.allreduce(float(t["momentum"]), op=MPI.MAX))
106 return its, ts, tp, tm
107
108
109def main():
110 ap = argparse.ArgumentParser()
111 ap.add_argument("--n", type=int, default=96)
112 ap.add_argument("--levels", type=int, default=4, help="set_pressure_multigrid levels")
113 ap.add_argument("--graph-amg", action="store_true", help="set_pressure_graph_amg(True)")
114 ap.add_argument("--bottom", choices=("auto", "smoother", "agglomerated"), default="auto",
115 help="set_pressure_bottom (auto = the default)")
116 ap.add_argument("--warm", type=int, default=2)
117 ap.add_argument("--steps", type=int, default=5)
118 ap.add_argument("--bed", type=float, default=0.35, help="bed height fraction of z")
119 ap.add_argument("--lam", type=float, default=4.0, help="mean particles per bed cell")
120 ap.add_argument("--tilt", type=float, default=0.0, help="0 = flat bed, >0 = heap")
121 ap.add_argument("--seed", type=int, default=7)
122 a = ap.parse_args()
123 comm = MPI.COMM_WORLD
124 n = a.n
125
126 o, sz = flow.mpi_block(n, n, n)
127 s = flow.Solver(tuple(sz))
128 s.init_mpi(n, n, n)
129 s.set_pressure_multigrid(True, a.levels)
130 if a.graph_amg:
131 s.diagnostics.set_pressure_graph_amg(True)
132 s.set_pressure_bottom(a.bottom)
133 s.set_body_force((1e-4, 0.0, 0.0))
134 s.set_dt(1.0)
135 s.set_solid(sphere_sdf(o, sz, n), True)
136 if comm.rank == 0:
137 print(f"[probe] n={n}^3 np={comm.size} levels={a.levels} graph_amg={a.graph_amg} "
138 f"bottom={a.bottom} telescope={s.pressure_telescope()}")
139 sys.stdout.flush()
140
141 describe("BEFORE (equal-weight aligned ORB)", comm, blocks(comm, s), None, n)
142 run_steps(comm, s, a.warm)
143 its0, ts0, tp0, tm0 = run_steps(comm, s, a.steps)
144
145 w = bed_weights(n, a.bed, a.lam, a.tilt, a.seed)
146 if comm.rank == 0:
147 print("\n[probe] ---- rebalance_by_weights ----")
148 sys.stdout.flush()
149 comm.Barrier()
150 t0 = time.perf_counter()
151 s.diagnostics.rebalance_by_weights(w.tolist())
152 comm.Barrier()
153 trb = time.perf_counter() - t0
154 describe("AFTER (weighted ORB)", comm, blocks(comm, s), w, n)
155 its1, ts1, tp1, tm1 = run_steps(comm, s, a.steps)
156
157 if comm.rank == 0:
158 print(f"\n[probe] rebalance took {trb:.2f} s")
159 print(f"[probe] BEFORE pressure iters/step {its0} step s {[round(t, 3) for t in ts0]}"
160 f" median {np.median(ts0):.3f} projection(max rank) {np.median(tp0):.3f}"
161 f" momentum(max rank) {np.median(tm0):.3f}")
162 print(f"[probe] AFTER pressure iters/step {its1} step s {[round(t, 3) for t in ts1]}"
163 f" median {np.median(ts1):.3f} projection(max rank) {np.median(tp1):.3f}"
164 f" momentum(max rank) {np.median(tm1):.3f}")
165 print(f"[probe] SUMMARY n={n} np={comm.size} levels={a.levels} graph_amg={a.graph_amg}"
166 f" bottom={a.bottom} tilt={a.tilt} seed={a.seed}"
167 f" iters {np.mean(its0):.1f} -> {np.mean(its1):.1f}"
168 f" step {np.median(ts0):.3f} -> {np.median(ts1):.3f} s"
169 f" projection {np.median(tp0):.3f} -> {np.median(tp1):.3f} s"
170 f" momentum {np.median(tm0):.3f} -> {np.median(tm1):.3f} s")
171 sys.stdout.flush()
172
173
174if __name__ == "__main__":
175 main()
sphere_sdf(o, sz, n, per_axis=2, rfrac=0.35)
bed_weights(n, bed, lam, tilt=0.0, seed=7)