flow 0.4.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`--orb-only` skips constructing a Solver, so it reports the partition (balance, splits) for grids far
26too large to allocate on a host — but it cannot report the achieved level count, which needs the real
27MG init. Without it, the grid must fit in host memory (a few hundred M cells at most).
28
29Requires PYTHONPATH to point at an MPI-enabled flow build, e.g.
30 PYTHONPATH=$PWD/build_mpi_omp python scripts/check_decomposition.py ...
31"""
32import argparse
33import os
34import re
35import subprocess
36import sys
37
38HERE = os.path.abspath(__file__)
39
40
41def halvings(d):
42 """How many times an axis can be halved: the multigrid's per-axis depth budget."""
43 n = 0
44 while d % 2 == 0 and d // 2 >= 2:
45 d //= 2
46 n += 1
47 return n
48
49
50# ---- child: runs under mpirun, prints one RESULT line ------------------------------------------
51def child(args):
52 from mpi4py import MPI
53
54 w = MPI.COMM_WORLD
55 import numpy as np
56
57 from peclet import flow
58
59 gx, gy, gz = args.grid
60 origin, size = flow.mpi_block(gx, gy, gz)
61 lnx, lny, lnz = size
62 cells = lnx * lny * lnz
63 hi = w.allreduce(cells, op=MPI.MAX)
64 lo = w.allreduce(cells, op=MPI.MIN)
65 split = [w.allreduce(1 if size[k] < (gx, gy, gz)[k] else 0, op=MPI.SUM) for k in range(3)]
66
67 if not args.orb_only:
68 # Constructing the Solver is what builds the MG hierarchy; with PECLET_FLOW_MG_DEBUG=1 it
69 # prints the level table, which the parent parses for the achieved depth.
70 s = flow.Solver(lnx, lny, lnz)
71 s.init_mpi(gx, gy, gz)
72 s.set_rho(1.0)
73 s.set_mu(0.1)
74 s.set_dt(0.02)
75 s.set_pressure_multigrid(True, args.levels)
76 s.set_pressure_pcg(True, 200, 1e-6)
77 if args.walls:
78 s.set_domain_bc(2, 1)
79 s.set_domain_bc(3, 1)
80 s.set_pressure_geometry(np.asfortranarray(np.full((lnx, lny, lnz), 1e30)))
81
82 if w.rank == 0:
83 print(
84 f"RESULT np={w.size} block={lnx}x{lny}x{lnz} imbalance={hi / lo:.3f} "
85 f"split=({split[0]},{split[1]},{split[2]})",
86 flush=True,
87 )
88 MPI.Finalize()
89 os._exit(0)
90
91
92# ---- parent: spawn one mpirun per (mode, np) and tabulate ---------------------------------------
93MODES = {"0": ("aligned", "0"), "aligned": ("aligned", "0"), "legacy": ("aligned", "0")}
94
95
96def mode_spec(tok, levels):
97 """'0'/'aligned' -> the legacy aligned ORB; 'coarse'/'coarse-first'/<int> -> coarse-first."""
98 if tok in MODES:
99 return MODES[tok]
100 if tok in ("coarse", "coarse-first", "cf"):
101 return ("coarse-first", str(levels))
102 return (f"coarse-first({tok})", tok)
103
104
105def main():
106 ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
107 ap.add_argument("--grid", required=True, help="GNX,GNY,GNZ")
108 ap.add_argument("--np", default="1", help="comma-separated rank counts")
109 ap.add_argument("--levels", type=int, default=5, help="multigrid levels requested (default 5)")
110 ap.add_argument("--mode", default="0", help="comma-separated: 0/aligned, coarse, or a depth")
111 ap.add_argument("--walls", action="store_true", help="no-slip -y/+y (the channel's domain-BC MG path)")
112 ap.add_argument("--orb-only", action="store_true", help="partition only; no Solver, so any size")
113 ap.add_argument("--verbose", action="store_true", help="also print the per-level dims/ratios")
114 ap.add_argument("--mpirun", default="mpirun")
115 ap.add_argument("--child", action="store_true", help=argparse.SUPPRESS)
116 args = ap.parse_args()
117 args.grid = [int(v) for v in args.grid.split(",")]
118 if args.child:
119 return child(args)
120
121 gx, gy, gz = args.grid
122 print(f"grid {gx}x{gy}x{gz} = {gx * gy * gz / 1e6:.1f} Mcells "
123 f"halvings x/y/z = {halvings(gx)}/{halvings(gy)}/{halvings(gz)}"
124 f"{' <-- an axis with 0 halvings NEVER coarsens' if 0 in (halvings(gx), halvings(gy), halvings(gz)) else ''}")
125 print(f"multigrid levels requested: {args.levels}\n")
126 print(f"{'mode':<20} {'np':>4} {'block':>18} {'imbalance':>10} {'split x,y,z':>12} {'levels':>7}")
127
128 for tok in args.mode.split(","):
129 label, decomp = mode_spec(tok.strip(), args.levels)
130 for np_ in [int(v) for v in args.np.split(",")]:
131 env = dict(os.environ, PECLET_FLOW_MG_DEBUG="1", PECLET_FLOW_DECOMP_LEVELS=decomp)
132 cmd = [args.mpirun, "--oversubscribe", "-np", str(np_), sys.executable, HERE, "--child",
133 "--grid", ",".join(map(str, args.grid)), "--levels", str(args.levels)]
134 if args.walls:
135 cmd.append("--walls")
136 if args.orb_only:
137 cmd.append("--orb-only")
138 try:
139 out = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=900).stdout
140 except subprocess.TimeoutExpired:
141 print(f"{label:<20} {np_:>4} TIMED OUT")
142 continue
143 res = re.search(r"RESULT np=(\d+) block=(\S+) imbalance=(\S+) split=\‍((\S+)\‍)", out)
144 if not res:
145 print(f"{label:<20} {np_:>4} FAILED (rerun by hand for the error)")
146 continue
147 lev = re.search(r"-> (\d+) levels", out)
148 cf = re.search(r"coarse-first depth (\d+) \‍(refine (\S+),", out)
149 note = f" [coarse-first depth {cf.group(1)}, refine {cf.group(2)}]" if cf else ""
150 print(f"{label:<20} {res.group(1):>4} {res.group(2):>18} {res.group(3):>10} "
151 f"{res.group(4):>12} {lev.group(1) if lev else '-':>7}{note}")
152 if args.verbose:
153 for line in out.splitlines():
154 if line.startswith("[mg] L"):
155 print(" " + line)
156 print("\nreminders: an ODD axis never coarsens (measured 3.2x slower on one GPU);")
157 print(" in MPI a level coarsens an axis only if EVERY rank's block is even on it,")
158 print(" so the achievable depth is set by the per-rank block, not the global grid.")
159
160
161if __name__ == "__main__":
162 main()