-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathbenchmark_interleave.py
More file actions
126 lines (100 loc) · 4.27 KB
/
Copy pathbenchmark_interleave.py
File metadata and controls
126 lines (100 loc) · 4.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
"""Benchmark the lockstep trajectory interleave against adversarial inputs.
The RK2 tracer processes K=3 particles per worker in lockstep
(``trace_particle_block``), so the out-of-order core can overlap the
independent trajectories' dependent chains. Two input families stress it:
- ``stripes``: lane-divergence worst case. Zero flow everywhere except every
fourth x-column, which carries an in-plane swirl of magnitude
``2*tol/dt`` — those particles never converge and never leave the mask,
while their group neighbours converge on the first step, so a lockstep
group idles most of its lanes for all 50 iterations.
- ``random``: irregular trajectories with unpredictable per-lane branching
(same generator as ``benchmark_midpoint_reuse.py``).
Compare kernels by running this script under two installed builds (swap the
prebuilt ``_core*.so`` between runs — rebuild rounds drift thermally).
Run::
python development/flow/benchmark_interleave.py --repeats 3
"""
from __future__ import annotations
import argparse
import sys
from statistics import median
from time import perf_counter
import numpy as np
import bioimage_cpp as bic
from bioimage_cpp._data import load_flow_data
SHAPE_3D = (32, 192, 192)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Benchmark the lockstep trajectory interleave."
)
parser.add_argument("--repeats", type=int, default=3)
parser.add_argument("--threads", type=int, default=1)
parser.add_argument("--timeout", type=float, default=60.0)
parser.add_argument(
"--skip-fixture",
action="store_true",
help="Only run the synthetic cases (no registered-fixture download).",
)
return parser.parse_args()
def _time(flow: np.ndarray, mask: np.ndarray, repeats: int, threads: int) -> tuple[np.ndarray, list[float]]:
timings = []
result = None
for _ in range(repeats):
start = perf_counter()
result = bic.flow.compute_flow_density(
flow, mask, sigma=None, number_of_threads=threads
)
timings.append(perf_counter() - start)
assert result is not None
return result, timings
def _report(label: str, result: np.ndarray, timings: list[float]) -> None:
print(
f"{label}: median={median(timings):.4f}s min={min(timings):.4f}s "
f"particles={int(result.sum())}"
)
def _stripe_flow(n_orbiter_columns_of_4: int) -> np.ndarray:
"""Swirling flow on every n-of-4 x-columns, zero elsewhere."""
zz, yy, xx = np.indices(SHAPE_3D, dtype=np.float32)
cy = (SHAPE_3D[1] - 1) / 2.0
cx = (SHAPE_3D[2] - 1) / 2.0
dy, dx = yy - cy, xx - cx
radius = np.hypot(dy, dx)
radius[radius == 0] = 1.0
# |dt * step| = 0.01 >= tol = 0.005 at the defaults: orbiters never
# converge; the rotation keeps them inside the mask for all iterations.
speed = 0.05
flow = np.stack(
[np.zeros(SHAPE_3D, np.float32), -dx / radius * speed, dy / radius * speed]
).astype(np.float32)
orbiter = (xx.astype(np.int64) % 4) < n_orbiter_columns_of_4
flow *= orbiter[None]
return flow
def main() -> int:
args = parse_args()
if args.repeats < 1:
print("--repeats must be >= 1", file=sys.stderr)
return 2
if not args.skip_fixture:
for ndim in (2, 3):
dist, fg, _ = load_flow_data(ndim, timeout=args.timeout)
flow = np.ascontiguousarray(-dist, dtype=np.float32)
mask = np.ascontiguousarray(fg > 0.5)
result, timings = _time(flow, mask, args.repeats, args.threads)
_report(f"fixture {ndim}D threads={args.threads}", result, timings)
mask = np.ones(SHAPE_3D, dtype=bool)
for n_orbiters in (1, 3):
flow = _stripe_flow(n_orbiters)
result, timings = _time(flow, mask, args.repeats, args.threads)
_report(
f"stripes {n_orbiters}/4 long-lived threads={args.threads}",
result,
timings,
)
rng = np.random.default_rng(0)
for scale in (1.0, 10.0):
flow = rng.normal(scale=scale, size=(3,) + SHAPE_3D).astype(np.float32)
result, timings = _time(flow, mask, args.repeats, args.threads)
_report(f"random scale={scale:g} threads={args.threads}", result, timings)
return 0
if __name__ == "__main__":
sys.exit(main())