Skip to content

Commit b31a598

Browse files
committed
Closes #5185: Benchmark for Multidim Binop Performance
1 parent f641b85 commit b31a598

File tree

1 file changed

+172
-0
lines changed

1 file changed

+172
-0
lines changed
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import math
2+
import operator
3+
4+
import pytest
5+
6+
import arkouda as ak
7+
8+
from benchmark_v2.benchmark_utils import calc_num_bytes
9+
10+
11+
DTYPES = ("uint64", "bigint")
12+
NDIMS = (1, 2, 3)
13+
OPS = ("+", "-", "*", "/", "//", "&", "|", "^")
14+
15+
16+
def choose_shape(n: int, ndim: int) -> tuple[int, ...]:
17+
"""
18+
Choose an ndim-dimensional shape whose product is <= n, while:
19+
1) keeping dimensions as even as possible (minimize max-min)
20+
2) keeping product close to n (minimize overshoot)
21+
22+
Examples
23+
--------
24+
n=36, ndim=3 -> (3, 3, 4)
25+
"""
26+
if ndim == 1:
27+
return (max(1, int(n)),)
28+
29+
if ndim == 2:
30+
root = int(math.isqrt(max(1, n)))
31+
best = None
32+
# search around sqrt(n)
33+
for a in range(max(1, root - 64), root + 65):
34+
b = n // a
35+
dims = tuple(sorted((a, b)))
36+
prod = dims[0] * dims[1]
37+
spread = dims[1] - dims[0]
38+
overshoot = prod - n
39+
score = spread * 1_000_000 + overshoot
40+
cand = (score, dims)
41+
if best is None or cand < best:
42+
best = cand
43+
return best[1]
44+
45+
if ndim == 3:
46+
root = int(round(max(1, n) ** (1 / 3)))
47+
best = None
48+
# search a,b around cube-root; compute c as ceil(n/(a*b))
49+
for a in range(max(1, root - 64), root + 65):
50+
for b in range(max(1, root - 64), root + 65):
51+
ab = a * b
52+
if ab <= 0:
53+
continue
54+
c = n // ab
55+
dims = tuple(sorted((a, b, max(1, c))))
56+
prod = dims[0] * dims[1] * dims[2]
57+
spread = dims[2] - dims[0]
58+
overshoot = prod - n
59+
score = spread * 1_000_000 + overshoot
60+
cand = (score, dims)
61+
if best is None or cand < best:
62+
best = cand
63+
return best[1]
64+
65+
raise ValueError(f"Unsupported ndim={ndim}")
66+
67+
68+
def _make_uint64(shape: tuple[int, ...], seed: int):
69+
size = 1
70+
for d in shape:
71+
size *= d
72+
a = ak.randint(0, 2**64, size=size, dtype=ak.uint64, seed=seed)
73+
if len(shape) > 1:
74+
a = a.reshape(*shape)
75+
return a
76+
77+
78+
def _make_bigint_2limb(shape: tuple[int, ...], seed: int):
79+
"""Make a bigint array using exactly two uint64 limbs (hi, lo)."""
80+
size = 1
81+
for d in shape:
82+
size *= d
83+
84+
hi = ak.randint(0, 2**64, size=size, dtype=ak.uint64, seed=seed)
85+
lo = ak.randint(0, 2**64, size=size, dtype=ak.uint64, seed=seed + 1)
86+
87+
bi = ak.bigint_from_uint_arrays([hi, lo])
88+
if len(shape) > 1:
89+
bi = bi.reshape(*shape)
90+
return bi
91+
92+
93+
def _make_arrays(shape: tuple[int, ...], dtype: str, seed: int):
94+
if dtype == "uint64":
95+
a = _make_uint64(shape, seed)
96+
b = _make_uint64(shape, seed + 10_000)
97+
return a, b
98+
elif dtype == "bigint":
99+
a = _make_bigint_2limb(shape, seed)
100+
b = _make_bigint_2limb(shape, seed + 10_000)
101+
return a, b
102+
else:
103+
raise ValueError(f"Unsupported dtype={dtype}")
104+
105+
106+
def _get_binop(op: str):
107+
# Use Python operators so this works naturally on arkouda pdarrays.
108+
if op == "+":
109+
return operator.add
110+
if op == "-":
111+
return operator.sub
112+
if op == "*":
113+
return operator.mul
114+
if op == "/":
115+
return operator.truediv
116+
if op == "//":
117+
return operator.floordiv
118+
if op == "&":
119+
return operator.and_
120+
if op == "|":
121+
return operator.or_
122+
if op == "^":
123+
return operator.xor
124+
raise ValueError(f"Unknown op={op}")
125+
126+
127+
@pytest.mark.skip_numpy(True)
128+
@pytest.mark.benchmark(group="AK_binop_ops")
129+
@pytest.mark.parametrize("dtype", DTYPES)
130+
@pytest.mark.parametrize("ndim", NDIMS)
131+
@pytest.mark.parametrize("op", OPS)
132+
def bench_binop_ops(benchmark, dtype, ndim, op):
133+
"""
134+
Benchmark binary operations on uint64 and bigint across 1D/2D/3D shapes.
135+
136+
- Total element target is ~ pytest.prob_size * cfg["numLocales"]
137+
- Shapes are chosen to be as even as possible while keeping product close to N.
138+
- Bigint arrays are built from exactly two uint64 limbs via ak.bigint_from_uint_arrays.
139+
"""
140+
cfg = ak.get_config()
141+
N = pytest.prob_size * cfg["numLocales"]
142+
seed = pytest.seed or 0
143+
144+
shape = choose_shape(N, ndim)
145+
a, b = _make_arrays(shape, dtype, seed)
146+
147+
fn = _get_binop(op)
148+
149+
bytes_a = calc_num_bytes(a)
150+
bytes_b = calc_num_bytes(b)
151+
num_bytes = bytes_a + bytes_b
152+
153+
benchmark.pedantic(
154+
fn,
155+
args=[a, b],
156+
rounds=pytest.trials,
157+
)
158+
159+
# metadata
160+
benchmark.extra_info["description"] = (
161+
f"Binary op '{op}' on dtype={dtype} with shape={shape} (target N={N}, "
162+
f"actual elements={math.prod(shape)})."
163+
)
164+
benchmark.extra_info["problem_size"] = N
165+
benchmark.extra_info["shape"] = shape
166+
benchmark.extra_info["ndim"] = ndim
167+
benchmark.extra_info["dtype"] = dtype
168+
benchmark.extra_info["op"] = op
169+
benchmark.extra_info["num_bytes"] = num_bytes
170+
benchmark.extra_info["transfer_rate"] = "{:.4f} GiB/sec".format(
171+
(num_bytes / benchmark.stats["mean"]) / 2**30
172+
)

0 commit comments

Comments
 (0)