from __future__ import annotations
import argparse
import csv
import os
import statistics
import sys
import time
from collections.abc import Callable
DEFAULT_WARMUPS = 3
DEFAULT_RUNS = 15
ELEMENTWISE = "elementwise"
OUTER_PRODUCT = "outer_product"
BATCHED_OUTER_COMPACT = "batched_outer_compact"
BATCHED_OUTER_NONCOMPACT = "batched_outer_noncompact"
BATCHED_OUTER_NONCOMPACT_TORCHLIKE_OUTPUT = "batched_outer_noncompact_torchlike_output"
BATCHED_OUTER_NONCOMPACT_LHS_SCALAR = "batched_outer_noncompact_lhs_scalar"
BATCHED_OUTER_NONCOMPACT_SINGLE_OUTER_GROUP = "batched_outer_noncompact_single_outer_group"
PERMUTED_ELEMENTWISE = "permuted_elementwise"
def positive_int(value: str) -> int:
parsed = int(value)
if parsed <= 0:
raise argparse.ArgumentTypeError("value must be positive")
return parsed
def parse_dtypes(value: str) -> list[str]:
dtypes = []
for item in value.split(","):
item = item.strip()
if item in {"f64", "c64", "c128"}:
dtypes.append(item)
elif item:
raise argparse.ArgumentTypeError(f"unknown dtype: {item}")
return dtypes or ["f64"]
def configure_thread_env(num_threads: int) -> None:
value = str(num_threads)
xla_multi_thread = "true" if num_threads > 1 else "false"
os.environ.update(
{
"OMP_NUM_THREADS": value,
"OMP_THREAD_LIMIT": value,
"OMP_DYNAMIC": "FALSE",
"RAYON_NUM_THREADS": value,
"OPENBLAS_NUM_THREADS": value,
"GOTO_NUM_THREADS": value,
"MKL_NUM_THREADS": value,
"VECLIB_MAXIMUM_THREADS": value,
"VECLIB_NUM_THREADS": value,
"NUMEXPR_NUM_THREADS": value,
"BLIS_NUM_THREADS": value,
"XLA_FLAGS": (
f"--xla_cpu_multi_thread_eigen={xla_multi_thread} "
f"intra_op_parallelism_threads={value}"
),
}
)
def case_specs(profile: str) -> list[tuple[str, tuple[int, ...]]]:
if profile == "smoke":
return [
(ELEMENTWISE, (64,)),
(OUTER_PRODUCT, (64,)),
(BATCHED_OUTER_COMPACT, (4, 4, 8, 8)),
(BATCHED_OUTER_NONCOMPACT, (4, 4, 8, 8)),
(BATCHED_OUTER_NONCOMPACT_TORCHLIKE_OUTPUT, (4, 4, 8, 8)),
(BATCHED_OUTER_NONCOMPACT_LHS_SCALAR, (4, 4, 8, 8)),
(BATCHED_OUTER_NONCOMPACT, (2, 8, 8, 8)),
(BATCHED_OUTER_NONCOMPACT, (8, 2, 8, 8)),
(BATCHED_OUTER_NONCOMPACT_SINGLE_OUTER_GROUP, (32, 32, 1, 1)),
(PERMUTED_ELEMENTWISE, (6, 3)),
]
if profile == "quick":
return [
(ELEMENTWISE, (1024,)),
(OUTER_PRODUCT, (2048,)),
(BATCHED_OUTER_COMPACT, (16, 16, 64, 64)),
(BATCHED_OUTER_NONCOMPACT, (16, 16, 64, 64)),
(BATCHED_OUTER_NONCOMPACT_TORCHLIKE_OUTPUT, (16, 16, 64, 64)),
(BATCHED_OUTER_NONCOMPACT_LHS_SCALAR, (16, 16, 64, 64)),
(BATCHED_OUTER_NONCOMPACT, (8, 32, 64, 64)),
(BATCHED_OUTER_NONCOMPACT, (32, 8, 64, 64)),
(BATCHED_OUTER_NONCOMPACT_SINGLE_OUTER_GROUP, (256, 256, 1, 1)),
(PERMUTED_ELEMENTWISE, (12, 3)),
]
return [
(ELEMENTWISE, (2048,)),
(OUTER_PRODUCT, (4096,)),
(BATCHED_OUTER_COMPACT, (16, 16, 64, 64)),
(BATCHED_OUTER_NONCOMPACT, (16, 16, 64, 64)),
(BATCHED_OUTER_NONCOMPACT_TORCHLIKE_OUTPUT, (16, 16, 64, 64)),
(BATCHED_OUTER_NONCOMPACT_LHS_SCALAR, (16, 16, 64, 64)),
(BATCHED_OUTER_NONCOMPACT, (8, 32, 64, 64)),
(BATCHED_OUTER_NONCOMPACT, (32, 8, 64, 64)),
(BATCHED_OUTER_NONCOMPACT_SINGLE_OUTER_GROUP, (1024, 1024, 1, 1)),
(PERMUTED_ELEMENTWISE, (16, 3)),
]
def benchmark_name(kind: str, dims: tuple[int, ...]) -> str:
if kind == ELEMENTWISE:
n = dims[0]
return f"bin_elementwise_mul_{n}x{n}"
if kind == OUTER_PRODUCT:
return f"bin_outer_product_{dims[0]}"
if kind == PERMUTED_ELEMENTWISE:
rank, extent = dims
return f"bin_permuted_elementwise_mul_rank{rank}_extent{extent}"
j, k, o, t = dims
if kind == BATCHED_OUTER_COMPACT:
return f"bin_batched_outer_product_compact_j{j}_k{k}_o{o}_t{t}"
if kind == BATCHED_OUTER_NONCOMPACT:
return f"bin_batched_outer_product_noncompact_j{j}_k{k}_o{o}_t{t}"
if kind == BATCHED_OUTER_NONCOMPACT_TORCHLIKE_OUTPUT:
return f"bin_batched_outer_product_noncompact_torchlike_output_j{j}_k{k}_o{o}_t{t}"
if kind == BATCHED_OUTER_NONCOMPACT_LHS_SCALAR:
return f"bin_batched_outer_product_noncompact_lhs_scalar_j{j}_k{k}_o{o}_t{t}"
if kind == BATCHED_OUTER_NONCOMPACT_SINGLE_OUTER_GROUP:
return f"bin_batched_outer_product_noncompact_single_outer_group_j{j}_k{k}_o{o}_t{t}"
raise ValueError(f"unknown benchmark kind: {kind}")
def median_iqr(samples: list[float]) -> tuple[float, float]:
ordered = sorted(samples)
return statistics.median(ordered), ordered[(3 * len(ordered)) // 4] - ordered[len(ordered) // 4]
def bench(fn: Callable[[], object], runs: int, warmups: int) -> tuple[float, float]:
for _ in range(warmups):
fn()
samples = []
for _ in range(runs):
start = time.perf_counter()
value = fn()
samples.append((time.perf_counter() - start) * 1000.0)
if hasattr(value, "numel"):
value.numel()
return median_iqr(samples)
def torch_dtype(torch, dtype: str):
if dtype == "f64":
return torch.float64
if dtype == "c64":
return torch.complex64
if dtype == "c128":
return torch.complex128
raise ValueError(f"unknown dtype: {dtype}")
def randn_tensor(torch, shape: tuple[int, ...], dtype: str):
if dtype == "f64":
return torch.randn(shape, dtype=torch_dtype(torch, dtype))
real_dtype = torch.float32 if dtype == "c64" else torch.float64
return torch.randn(shape, dtype=real_dtype) + 1j * torch.randn(shape, dtype=real_dtype)
def empty_tensor(torch, shape: tuple[int, ...], dtype: str):
return torch.empty(shape, dtype=torch_dtype(torch, dtype))
def make_elementwise_case(torch, dtype: str, n: int):
lhs = randn_tensor(torch, (n, n), dtype)
rhs = randn_tensor(torch, (n, n), dtype)
out = empty_tensor(torch, (n, n), dtype)
return f"{n}x{n}", lambda: torch.mul(lhs, rhs, out=out)
def make_outer_product_case(torch, dtype: str, n: int):
lhs = randn_tensor(torch, (n, 1), dtype)
rhs = randn_tensor(torch, (1, n), dtype)
out = empty_tensor(torch, (n, n), dtype)
return f"{n}x{n}", lambda: torch.mul(lhs, rhs, out=out)
def make_batched_outer_compact_case(torch, dtype: str, j: int, k: int, o: int, t: int):
lhs = randn_tensor(torch, (t, k, j), dtype)
rhs = randn_tensor(torch, (t, o), dtype)
out = empty_tensor(torch, (t, o, k, j), dtype)
lhs_view = lhs[:, None, :, :]
rhs_view = rhs[:, :, None, None]
return f"j={j};k={k};o={o};t={t}", lambda: torch.mul(lhs_view, rhs_view, out=out)
def make_batched_outer_noncompact_case(torch, dtype: str, j: int, k: int, o: int, t: int):
lhs = randn_tensor(torch, (t, j, k), dtype)
rhs = randn_tensor(torch, (t, o), dtype)
out = empty_tensor(torch, (t, o, k, j), dtype)
lhs_view = lhs[:, None, :, :].permute(0, 1, 3, 2)
rhs_view = rhs[:, :, None, None]
return f"j={j};k={k};o={o};t={t}", lambda: torch.mul(lhs_view, rhs_view, out=out)
def make_batched_outer_noncompact_torchlike_output_case(torch, dtype: str, j: int, k: int, o: int, t: int):
lhs = randn_tensor(torch, (t, j, k), dtype)
rhs = randn_tensor(torch, (t, o), dtype)
out = torch.empty_strided((t, o, k, j), (o * k * j, k * j, 1, k), dtype=torch_dtype(torch, dtype))
lhs_view = lhs[:, None, :, :].permute(0, 1, 3, 2)
rhs_view = rhs[:, :, None, None]
return f"j={j};k={k};o={o};t={t}", lambda: torch.mul(lhs_view, rhs_view, out=out)
def make_batched_outer_noncompact_lhs_scalar_case(torch, dtype: str, j: int, k: int, o: int, t: int):
lhs = randn_tensor(torch, (t, o), dtype)
rhs = randn_tensor(torch, (t, j, k), dtype)
out = empty_tensor(torch, (t, o, k, j), dtype)
lhs_view = lhs[:, :, None, None]
rhs_view = rhs[:, None, :, :].permute(0, 1, 3, 2)
return f"j={j};k={k};o={o};t={t}", lambda: torch.mul(lhs_view, rhs_view, out=out)
def permuted_elementwise_axes(rank: int) -> tuple[int, ...]:
if rank == 16:
return (7, 4, 10, 5, 12, 2, 9, 13, 1, 3, 6, 15, 14, 11, 8, 0)
return tuple(reversed(range(rank)))
def make_permuted_elementwise_case(torch, dtype: str, rank: int, extent: int):
shape = (extent,) * rank
lhs = randn_tensor(torch, shape, dtype)
rhs_base = randn_tensor(torch, shape, dtype)
rhs = rhs_base.permute(permuted_elementwise_axes(rank))
out = empty_tensor(torch, shape, dtype)
return f"rank={rank};extent={extent}", lambda: torch.mul(lhs, rhs, out=out)
def make_case(torch, dtype: str, kind: str, dims: tuple[int, ...]):
if kind == ELEMENTWISE:
return make_elementwise_case(torch, dtype, dims[0])
if kind == OUTER_PRODUCT:
return make_outer_product_case(torch, dtype, dims[0])
if kind == BATCHED_OUTER_COMPACT:
return make_batched_outer_compact_case(torch, dtype, *dims)
if kind == BATCHED_OUTER_NONCOMPACT:
return make_batched_outer_noncompact_case(torch, dtype, *dims)
if kind == BATCHED_OUTER_NONCOMPACT_TORCHLIKE_OUTPUT:
return make_batched_outer_noncompact_torchlike_output_case(torch, dtype, *dims)
if kind == BATCHED_OUTER_NONCOMPACT_LHS_SCALAR:
return make_batched_outer_noncompact_lhs_scalar_case(torch, dtype, *dims)
if kind == BATCHED_OUTER_NONCOMPACT_SINGLE_OUTER_GROUP:
return make_batched_outer_noncompact_case(torch, dtype, *dims)
if kind == PERMUTED_ELEMENTWISE:
return make_permuted_elementwise_case(torch, dtype, *dims)
raise ValueError(f"unknown benchmark kind: {kind}")
def shape_label(kind: str, dims: tuple[int, ...]) -> str:
if kind in {ELEMENTWISE, OUTER_PRODUCT}:
return f"{dims[0]}x{dims[0]}"
if kind == PERMUTED_ELEMENTWISE:
rank, extent = dims
return f"rank={rank};extent={extent}"
j, k, o, t = dims
return f"j={j};k={k};o={o};t={t}"
def emit_error_rows(writer: csv.DictWriter, args: argparse.Namespace, status: str) -> None:
for dtype in parse_dtypes(args.dtypes):
for kind, dims in case_specs(args.profile):
writer.writerow(
{
"suite": "mul",
"benchmark": benchmark_name(kind, dims),
"dtype": dtype,
"threads": args.num_threads,
"shape": shape_label(kind, dims),
"backend": "pytorch-cpu",
"median_ms": "",
"iqr_ms": "",
"status": status,
}
)
def run(args: argparse.Namespace, writer: csv.DictWriter) -> None:
try:
import torch
except Exception as exc:
emit_error_rows(writer, args, f"error: {exc}")
return
torch.manual_seed(0)
torch.set_num_threads(args.num_threads)
torch.set_num_interop_threads(args.num_threads)
for dtype in parse_dtypes(args.dtypes):
for kind, dims in case_specs(args.profile):
try:
shape, fn = make_case(torch, dtype, kind, dims)
median_ms, iqr_ms = bench(fn, args.runs, args.warmups)
status = "ok"
median = f"{median_ms:.6f}"
iqr = f"{iqr_ms:.6f}"
except Exception as exc:
shape = ""
status = f"error: {exc}"
median = ""
iqr = ""
writer.writerow(
{
"suite": "mul",
"benchmark": benchmark_name(kind, dims),
"dtype": dtype,
"threads": args.num_threads,
"shape": shape,
"backend": "pytorch-cpu",
"median_ms": median,
"iqr_ms": iqr,
"status": status,
}
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--num-threads", type=positive_int, default=int(os.environ.get("OMP_NUM_THREADS", "1")))
parser.add_argument("--profile", choices=["smoke", "quick", "full"], default=os.environ.get("STRIDED_KERNEL_MUL_BENCH_PROFILE", "full"))
parser.add_argument("--runs", type=positive_int, default=int(os.environ.get("STRIDED_KERNEL_MUL_BENCH_RUNS", DEFAULT_RUNS)))
parser.add_argument("--warmups", type=int, default=int(os.environ.get("STRIDED_KERNEL_MUL_BENCH_WARMUPS", DEFAULT_WARMUPS)))
parser.add_argument("--dtypes", default=os.environ.get("STRIDED_KERNEL_MUL_BENCH_DTYPES", "f64"))
parser.add_argument("--output", default="")
return parser.parse_args()
def main() -> int:
args = parse_args()
if args.warmups < 0:
raise SystemExit("--warmups must be non-negative")
configure_thread_env(args.num_threads)
fieldnames = [
"suite",
"benchmark",
"dtype",
"threads",
"shape",
"backend",
"median_ms",
"iqr_ms",
"status",
]
if args.output:
file_exists = os.path.exists(args.output) and os.path.getsize(args.output) > 0
with open(args.output, "a", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames, lineterminator="\n")
if not file_exists:
writer.writeheader()
run(args, writer)
else:
writer = csv.DictWriter(sys.stdout, fieldnames=fieldnames, lineterminator="\n")
writer.writeheader()
run(args, writer)
return 0
if __name__ == "__main__":
raise SystemExit(main())