v0.16.3
Loading...
Searching...
No Matches
profile_auxiliary_logarithmic_stress.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Profile auxiliary explicit/condensed and direct condensed Cook solves.
3
4Collect wall timings, standard PETSc events and convergence while other builds
5and tests are idle. The supplied environment must already support ep, including
6its Python modules. Fixture and examples paths default to the ep directory.
7"""
8
9import argparse
10import copy
11import hashlib
12import json
13import math
14import os
15from pathlib import Path
16import re
17import shutil
18import statistics
19import subprocess
20import time
21
22
24 parser = argparse.ArgumentParser(description=__doc__)
25 parser.add_argument("--ep", type=Path, required=True)
26 parser.add_argument("--mpi", default="mpiexec")
27 parser.add_argument("--mpi-arg", action="append", default=[],
28 help="launcher argument; repeat, e.g. --mpi-arg=--oversubscribe")
29 parser.add_argument("--ranks", type=int, choices=(1, 2), default=1)
30 parser.add_argument("--repetitions", type=int, default=3,
31 help="measured repetitions after one warm-up (default: 3)")
32 parser.add_argument("--output", type=Path, required=True,
33 help="new or empty directory for all run artifacts")
34 parser.add_argument("--fixture", type=Path)
35 parser.add_argument("--examples", type=Path)
36 parser.add_argument("--param-file", type=Path)
37 args = parser.parse_args()
38 if args.repetitions < 1:
39 parser.error("--repetitions must be positive")
40 args.ep = args.ep.expanduser().resolve()
41 args.fixture = (args.fixture or args.ep.parent /
42 "ep_cook_auxiliary_logarithmic_stress.json").expanduser().resolve()
43 args.examples = (args.examples or args.ep.parent / "examples").expanduser().resolve()
44 args.param_file = (args.param_file or args.ep.parent /
45 "param_file.petsc").expanduser().resolve()
46 for path in (args.ep, args.fixture, args.param_file):
47 if not path.is_file():
48 parser.error(f"required file does not exist: {path}")
49 if not os.access(args.ep, os.X_OK):
50 parser.error(f"ep is not executable: {args.ep}")
51 if not args.examples.is_dir():
52 parser.error(f"examples directory does not exist: {args.examples}")
53 launcher = shutil.which(args.mpi)
54 if launcher is None:
55 parser.error(f"MPI launcher is not executable: {args.mpi}")
56 args.mpi = str(Path(launcher).resolve())
57 args.output = args.output.expanduser().resolve()
58 if args.output.exists() and (not args.output.is_dir() or any(args.output.iterdir())):
59 parser.error("--output must be a new or empty directory")
60 return args
61
62
63def file_hash(path):
64 digest = hashlib.sha256()
65 with path.open("rb") as stream:
66 for block in iter(lambda: stream.read(1024 * 1024), b""):
67 digest.update(block)
68 return digest.hexdigest()
69
70
71def write_json(path, value):
72 path.write_text(json.dumps(value, indent=2) + "\n")
73
74
75def configure_case(configuration, case, ranks):
76 configuration = copy.deepcopy(configuration)
77 mesh = "cook.cub" if ranks == 1 else "cook_2p.h5m"
78 configuration["meshes"][0]["file_name"] = f"examples/cook_cantilever/{mesh}"
79 configuration["mofem"]["auxiliary_logarithmic_stress"] = case != "direct_condensed"
80 elastic = configuration["petsc"]["elastic"]
81 elastic["snes"]["converged_reason"] = True
82 elastic["mat_mumps_icntl_20"] = 0
83 elastic["mat_mumps_icntl_14"] = 800
84 elastic["ksp"] = {"type": "preonly", "converged_reason": True}
85 elastic["pc"] = {"type": "lu", "factor_mat_solver_type": "mumps"}
86 if case.endswith("_condensed"):
87 elastic["ksp"].update(type="fgmres", rtol=1e-12, atol=1e-12)
88 elastic["pc"] = {"type": "fieldsplit", "fieldsplit_type": "schur"}
89 elastic["fieldsplit_0"] = {"ksp": {"type": "preonly"}}
90 elastic["fieldsplit_1"] = {
91 "pc": {"type": "ksp"},
92 "ksp": {
93 "type": "preonly",
94 "pc": {"type": "lu", "factor_mat_solver_type": "mumps"},
95 "mat_mumps_icntl_20": 0,
96 "mat_mumps_icntl_14": 800,
97 },
98 }
99 return configuration
100
101
103 steps = re.findall(r"Step (\d+) time\s+(\S+) strain energy (\S+)", log)
104 return {
105 "accepted_steps": [
106 {"step": int(step), "time": float(t), "physical_energy": float(energy)}
107 for step, t, energy in steps
108 ],
109 "nonlinear_solves": [
110 {"reason": reason, "iterations": int(iterations)}
111 for reason, iterations in re.findall(
112 r"Nonlinear (?:elastic_ )?solve converged due to (\S+) iterations (\d+)", log)
113 ],
114 "linear_iterations": [int(value) for value in re.findall(
115 r"Linear (?:elastic_ )?solve converged due to \S+ iterations (\d+)", log)],
116 }
117
118
119def validate_convergence(record, configuration):
120 steps = [step for step in record["accepted_steps"] if step["step"] > 0]
121 solves = record["nonlinear_solves"]
122 ts = configuration["petsc"]["elastic"]["ts"]
123 expected_time = min(ts["max_time"], ts["dt"] * ts["max_steps"])
124 if (not steps or not solves or len(steps) != len(solves)
125 or not math.isclose(steps[-1]["time"], expected_time,
126 rel_tol=0., abs_tol=1e-12)
127 or any(not math.isfinite(step["physical_energy"]) for step in steps)
128 or any(not solve["reason"].startswith("CONVERGED_") for solve in solves)):
129 raise RuntimeError(f"Incomplete or invalid solve; see {record['directory']}/run.log")
130
131
132def run_case(args, case, configuration, environment, summary):
133 case_directory = args.output / case
134 case_directory.mkdir()
135 write_json(case_directory / "input.json", configuration)
136 mesh = args.examples / Path(configuration["meshes"][0]["file_name"]).relative_to("examples")
137 case_record = {
138 "case": case,
139 "configuration_sha256": file_hash(case_directory / "input.json"),
140 "mesh": str(mesh),
141 "mesh_sha256": file_hash(mesh),
142 "runs": [],
143 }
144 summary["cases"].append(case_record)
145 for repetition in range(args.repetitions + 1):
146 run_name = "warmup" if repetition == 0 else f"run_{repetition}"
147 directory = case_directory / run_name
148 directory.mkdir()
149 (directory / "examples").symlink_to(args.examples, target_is_directory=True)
150 shutil.copy2(case_directory / "input.json", directory / "input.json")
151 shutil.copy2(args.output / "param_file.petsc", directory / "param_file.petsc")
152 command = [
153 args.mpi, *args.mpi_arg, "-np", str(args.ranks), str(args.output / "ep"),
154 "-json_config", "input.json", "-log_view", ":petsc_profile.txt",
155 "-elastic_snes_view_solution", "binary:accepted.vec",
156 ]
157 record = {"directory": str(directory), "warmup": repetition == 0,
158 "command": command}
159 case_record["runs"].append(record)
160 start = time.monotonic()
161 with (directory / "run.log").open("w") as output:
162 result = subprocess.run(command, cwd=directory, env=environment,
163 stdout=output, stderr=subprocess.STDOUT)
164 record.update(exit_code=result.returncode, elapsed_seconds=time.monotonic() - start)
165 record.update(log_diagnostics((directory / "run.log").read_text(errors="replace")))
166 for artifact in ("run.log", "petsc_profile.txt", "accepted.vec"):
167 path = directory / artifact
168 if path.is_file():
169 record.setdefault("artifact_sha256", {})[artifact] = file_hash(path)
170 write_json(directory / "result.json", record)
171 write_json(args.output / "profile.json", summary)
172 if result.returncode:
173 raise RuntimeError(f"{case}/{run_name} failed ({result.returncode}); see {directory / 'run.log'}")
174 if not all((directory / name).is_file() for name in ("petsc_profile.txt", "accepted.vec")):
175 raise RuntimeError(f"{case}/{run_name} did not write its PETSc profile and final vector")
176 validate_convergence(record, configuration)
177 print(f"{case} {run_name}: {record['elapsed_seconds']:.3f} s", flush=True)
178 case_record["median_elapsed_seconds"] = statistics.median(
179 run["elapsed_seconds"] for run in case_record["runs"] if not run["warmup"])
180 write_json(args.output / "profile.json", summary)
181
182
183def main():
184 args = parse_arguments()
185 configuration = json.loads(args.fixture.read_text())
186 args.output.mkdir(parents=True, exist_ok=True)
187 shutil.copy2(args.ep, args.output / "ep")
188 shutil.copy2(args.fixture, args.output / "fixture.json")
189 shutil.copy2(args.param_file, args.output / "param_file.petsc")
190 environment = dict(os.environ)
191 # Keep PYTHONPATH and runtime library paths supplied by the build environment.
192 thread_options = {name: "1" for name in (
193 "OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS",
194 "BLIS_NUM_THREADS", "VECLIB_MAXIMUM_THREADS")}
195 environment.update(thread_options)
196 summary = {
197 "ranks": args.ranks, "measured_repetitions": args.repetitions,
198 "source_executable": str(args.ep), "thread_environment": thread_options,
199 "input_sha256": {name: file_hash(args.output / name)
200 for name in ("ep", "fixture.json", "param_file.petsc")},
201 "vector_contents": "One final accepted solution per run; PETSc overwrites earlier steps.",
202 "cases": [],
203 }
204 write_json(args.output / "profile.json", summary)
205 for case in ("auxiliary_explicit", "auxiliary_condensed", "direct_condensed"):
206 run_case(args, case, configure_case(configuration, case, args.ranks),
207 environment, summary)
208 print(f"Profiles and median timings: {args.output / 'profile.json'}")
209
210
211if __name__ == "__main__":
212 try:
213 main()
214 except (OSError, ValueError, KeyError, RuntimeError) as error:
215 raise SystemExit(str(error)) from error
run_case(args, case, configuration, environment, summary)