flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
check_decomposition.py
Go to the documentation of this file.
1#!/usr/bin/env python
2"""Inspect how a grid + rank count decomposes, and how deep the pressure multigrid gets on it.
3
4Answers, without spending any GPU time, the questions that decide distributed multigrid performance:
5
6 * how balanced is the ORB partition (max block / min block)?
7 * is a direction split that must not be (e.g. the wall-normal one in a channel)?
8 * how many multigrid levels does the hierarchy ACTUALLY reach, and where does each axis stop?
9 * does the coarse-first decomposition help here, and at what depth?
10
11Run it before committing a grid or a rank count to a large job. It needs only an MPI build of
12`flow` (the OpenMP one is fine, no GPU), and `mpirun --oversubscribe` happily runs more ranks than
13there are cores because nothing here is timed.
14
15 # one grid, several rank counts, both decomposition modes:
16 python check_decomposition.py --grid 480,80,160 --levels 6 --np 1,2,4,8,12,16,24 --mode 0,coarse
17
18 # a whole weak-scaling ladder, ORB only (no solver allocation -> any size, instantly):
19 python check_decomposition.py --grid 960,160,320 --np 1 --orb-only
20 python check_decomposition.py --grid 3072,512,1024 --np 32 --orb-only
21
22 # what the hierarchy looks like level by level:
23 python check_decomposition.py --grid 1508,240,503 --levels 5 --np 4 --verbose
24
25 # the ladder after diagnostics.rebalance_by_weights(w), and the alignment it picks:
26 python check_decomposition.py --grid 96,96,96 --levels 8 --np 4,8 --predict --weights w.npy
27
28`--orb-only` skips constructing a Solver, so it reports the partition (balance, splits) for grids far
29too large to allocate on a host — but it cannot report the achieved level count, which needs the real
30MG init. Without it, the grid must fit in host memory (a few hundred M cells at most).
31
32Requires PYTHONPATH to point at an MPI-enabled flow build, e.g.
33 PYTHONPATH=$PWD/build_mpi_omp python scripts/check_decomposition.py ...
34"""
35import argparse
36import os
37import re
38import subprocess
39import sys
40
41HERE = os.path.abspath(__file__)
42
43
44def halvings(d):
45 """How many times an axis can be halved: the multigrid's per-axis depth budget."""
46 n = 0
47 while d % 2 == 0 and d // 2 >= 2:
48 d //= 2
49 n += 1
50 return n
51
52
53# ---- child: runs under mpirun, prints one RESULT line ------------------------------------------
54def child(args):
55 from mpi4py import MPI
56
57 w = MPI.COMM_WORLD
58 import numpy as np
59
60 from peclet import flow
61
62 gx, gy, gz = args.grid
63 origin, size = flow.mpi_block(gx, gy, gz, levels=args.decomp_levels)
64 lnx, lny, lnz = size
65 cells = lnx * lny * lnz
66 hi = w.allreduce(cells, op=MPI.MAX)
67 lo = w.allreduce(cells, op=MPI.MIN)
68 split = [w.allreduce(1 if size[k] < (gx, gy, gz)[k] else 0, op=MPI.SUM) for k in range(3)]
69
70 if not args.orb_only:
71 # Constructing the Solver is what builds the MG hierarchy; with PECLET_FLOW_MG_DEBUG=1 it
72 # prints the level table, which the parent parses for the achieved depth.
73 s = flow.Solver(lnx, lny, lnz)
74 s.set_decomposition(args.decomp_levels) # must match the mpi_block() call above
75 s.init_mpi(gx, gy, gz)
76 s.set_rho(1.0)
77 s.set_mu(0.1)
78 s.set_dt(0.02)
79 s.set_pressure_multigrid(True, args.levels)
80 s.set_pressure_pcg(True, 200, 1e-6)
81 if args.walls:
82 s.set_domain_bc("-y", "wall")
83 s.set_domain_bc("+y", "wall")
84 s.set_pressure_geometry(np.asfortranarray(np.full((lnx, lny, lnz), 1e30)))
85
86 if w.rank == 0:
87 print(
88 f"RESULT np={w.size} block={lnx}x{lny}x{lnz} imbalance={hi / lo:.3f} "
89 f"split=({split[0]},{split[1]},{split[2]})",
90 flush=True,
91 )
92 MPI.Finalize()
93 os._exit(0)
94
95
96# ---- parent: spawn one mpirun per (mode, np) and tabulate ---------------------------------------
97MODES = {"0": ("aligned", "0"), "aligned": ("aligned", "0"), "legacy": ("aligned", "0")}
98
99
100def mode_spec(tok, levels):
101 """'0'/'aligned' -> the legacy aligned ORB; 'coarse'/'coarse-first'/<int> -> coarse-first."""
102 if tok in MODES:
103 return MODES[tok]
104 if tok in ("coarse", "coarse-first", "cf"):
105 return ("coarse-first", str(levels))
106 return (f"coarse-first({tok})", tok)
107
108
109def predict(args):
110 """Pure-function pre-flight: the hierarchy init_mpi would build, per rank count, with and
111 without telescoping. No mpirun -- this is what makes np=1536 answerable on a laptop."""
112 from peclet import flow
113
114 gx, gy, gz = args.grid
115 for np_ in args.np:
116 for tele in (False, True):
117 if args.weights is not None:
118 # The hierarchy after diagnostics.rebalance_by_weights(w): its aligned weighted
119 # ORB replaces the set_decomposition partition, and the call returns the alignment.
120 rows, align = flow.predict_hierarchy(gx, gy, gz, np_, args.levels, tele,
121 weights=args.weights)
122 if not tele: # the partition, hence the alignment, is the same either way
123 print(f"\n{gx}x{gy}x{gz} np={np_}: rebalance_by_weights -> aligned weighted "
124 f"ORB, align {align} (a = {align.bit_length() - 1})")
125 else:
126 # --decomp-levels must reach predict_hierarchy: it is a pure function with no
127 # process state to read since flow ad917b1, so omitting it predicts the ALIGNED
128 # hierarchy.
129 rows = flow.predict_hierarchy(gx, gy, gz, np_, args.levels, tele,
130 decomposition_levels=args.decomp_levels)
131 cg = rows[-1][0]
132 print(f"\n{gx}x{gy}x{gz} np={np_} levels={args.levels} telescope={'ON' if tele else 'off'}"
133 f" -> {len(rows)} levels, coarsest {cg[0]}x{cg[1]}x{cg[2]}, "
134 f"max extent {max(cg)}, bottom on {rows[-1][1]} rank(s)")
135 for L, (g, ranks, blk, rat, t) in enumerate(rows):
136 print(f" L{L} global {g[0]:5d}x{g[1]:5d}x{g[2]:5d} ranks {ranks:5d} block0 "
137 f"{blk[0]:4d}x{blk[1]:4d}x{blk[2]:4d} ratio({rat[0]},{rat[1]},{rat[2]})"
138 f"{' -> TELESCOPE' if t else ''}")
139
140
141def main():
142 ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
143 ap.add_argument("--grid", required=True, help="GNX,GNY,GNZ")
144 ap.add_argument("--np", default="1", help="comma-separated rank counts")
145 ap.add_argument("--levels", type=int, default=5, help="multigrid levels requested (default 5)")
146 ap.add_argument("--mode", default="0", help="comma-separated: 0/aligned, coarse, or a depth")
147 ap.add_argument("--walls", action="store_true", help="no-slip -y/+y (the channel's domain-BC MG path)")
148 ap.add_argument("--orb-only", action="store_true", help="partition only; no Solver, so any size")
149 ap.add_argument("--verbose", action="store_true", help="also print the per-level dims/ratios")
150 ap.add_argument("--mpirun", default="mpirun")
151 ap.add_argument("--predict", action="store_true",
152 help="use flow.predict_hierarchy (pure function, no mpirun, any np): print the "
153 "level ladder with and without coarse-level telescoping")
154 ap.add_argument("--weights", default=None,
155 help="with --predict: a .npy of per-cell weights (what "
156 "diagnostics.rebalance_by_weights takes: flat x-fastest, or an (nx,ny,nz) "
157 "array) -- predict the hierarchy AFTER that rebalance and log the "
158 "alignment its aligned weighted ORB picks")
159 ap.add_argument("--decomp-levels", type=int, default=0,
160 help="Solver.set_decomposition(levels) / flow.mpi_block(levels=): 0 = the "
161 "aligned ORB, >= 2 = coarse-first with that depth")
162 ap.add_argument("--child", action="store_true", help=argparse.SUPPRESS)
163 args = ap.parse_args()
164 if args.weights is not None and not args.predict:
165 ap.error("--weights needs --predict")
166 if getattr(args, "predict", False):
167 if isinstance(args.grid, str):
168 args.grid = [int(v) for v in args.grid.split(",")]
169 if isinstance(args.np, str):
170 args.np = [int(v) for v in args.np.split(",")]
171 if args.weights is not None:
172 import numpy as np
173
174 w = np.load(args.weights)
175 if w.ndim == 3:
176 if tuple(w.shape) != tuple(args.grid):
177 sys.exit(f"--weights: shape {w.shape} is not the grid {tuple(args.grid)}")
178 w = w.ravel(order="F") # x-fastest, as rebalance_by_weights takes it
179 args.weights = np.asarray(w, dtype=np.float64).ravel().tolist()
180 # The weighted ORB replaces the set_decomposition partition, so --mode does not apply.
181 print("=== weighted: diagnostics.rebalance_by_weights(w) (--mode / --decomp-levels "
182 "ignored) ===")
183 predict(args)
184 return
185 # In --predict, --mode selects the depth (it can list several). --decomp-levels is the
186 # run-mode flag; honour it when the user gave it and left --mode at its default, rather
187 # than silently overwriting it below.
188 if args.decomp_levels and str(args.mode) == "0":
189 args.mode = str(args.decomp_levels)
190 for spec in (args.mode if isinstance(args.mode, list) else str(args.mode).split(",")):
191 label, lv = mode_spec(spec, args.levels)
192 args.decomp_levels = int(lv)
193 print(f"=== decomposition mode {label} (set_decomposition({int(lv)})) ===")
194 predict(args)
195 return
196 args.grid = [int(v) for v in args.grid.split(",")]
197 if args.child:
198 return child(args)
199
200 gx, gy, gz = args.grid
201 print(f"grid {gx}x{gy}x{gz} = {gx * gy * gz / 1e6:.1f} Mcells "
202 f"halvings x/y/z = {halvings(gx)}/{halvings(gy)}/{halvings(gz)}"
203 f"{' <-- an axis with 0 halvings NEVER coarsens' if 0 in (halvings(gx), halvings(gy), halvings(gz)) else ''}")
204 print(f"multigrid levels requested: {args.levels}\n")
205 print(f"{'mode':<20} {'np':>4} {'block':>18} {'imbalance':>10} {'split x,y,z':>12} {'levels':>7}")
206
207 for tok in args.mode.split(","):
208 label, decomp = mode_spec(tok.strip(), args.levels)
209 for np_ in [int(v) for v in args.np.split(",")]:
210 env = dict(os.environ, PECLET_FLOW_MG_DEBUG="1")
211 cmd = [args.mpirun, "--oversubscribe", "-np", str(np_), sys.executable, HERE, "--child",
212 "--grid", ",".join(map(str, args.grid)), "--levels", str(args.levels),
213 "--decomp-levels", str(decomp)]
214 if args.walls:
215 cmd.append("--walls")
216 if args.orb_only:
217 cmd.append("--orb-only")
218 try:
219 out = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=900).stdout
220 except subprocess.TimeoutExpired:
221 print(f"{label:<20} {np_:>4} TIMED OUT")
222 continue
223 res = re.search(r"RESULT np=(\d+) block=(\S+) imbalance=(\S+) split=\‍((\S+)\‍)", out)
224 if not res:
225 print(f"{label:<20} {np_:>4} FAILED (rerun by hand for the error)")
226 continue
227 lev = re.search(r"-> (\d+) levels", out)
228 cf = re.search(r"coarse-first depth (\d+) \‍(refine (\S+),", out)
229 note = f" [coarse-first depth {cf.group(1)}, refine {cf.group(2)}]" if cf else ""
230 print(f"{label:<20} {res.group(1):>4} {res.group(2):>18} {res.group(3):>10} "
231 f"{res.group(4):>12} {lev.group(1) if lev else '-':>7}{note}")
232 if args.verbose:
233 for line in out.splitlines():
234 if line.startswith("[mg] L"):
235 print(" " + line)
236 print("\nreminders: an ODD axis never coarsens (measured 3.2x slower on one GPU);")
237 print(" in MPI a level coarsens an axis only if EVERY rank's block is even on it,")
238 print(" so the achievable depth is set by the per-rank block, not the global grid.")
239
240
241if __name__ == "__main__":
242 main()