210 lines
6.0 KiB
Python
210 lines
6.0 KiB
Python
# Copyright © 2026 Apple Inc.
|
|
|
|
import argparse
|
|
import time
|
|
|
|
import mlx.core as mx
|
|
import numpy as np
|
|
|
|
MLX_DTYPES = {
|
|
"float16": mx.float16,
|
|
"bfloat16": mx.bfloat16,
|
|
"float32": mx.float32,
|
|
}
|
|
|
|
|
|
def parse_cases(cases):
|
|
parsed = []
|
|
for spec in cases.split(","):
|
|
m, n, k, s = [int(x) for x in spec.split("x")]
|
|
parsed.append((m, n, k, s))
|
|
return parsed
|
|
|
|
|
|
def make_segments(k, num_segments, pattern, seed):
|
|
if pattern == "equal":
|
|
cuts = np.linspace(0, k, num_segments + 1, dtype=np.int64)
|
|
else:
|
|
rng = np.random.default_rng(seed)
|
|
cuts = rng.integers(0, k + 1, size=(num_segments - 1,), dtype=np.int64)
|
|
cuts = np.sort(cuts)
|
|
cuts = np.concatenate(([0], cuts, [k]))
|
|
return np.stack([cuts[:-1], cuts[1:]], axis=1).astype(np.uint32)
|
|
|
|
|
|
def numpy_segmented_mm_ref(a, b, segments):
|
|
"""Ground-truth reference in float64."""
|
|
out = []
|
|
for start, end in segments:
|
|
out.append(a[:, start:end] @ b[start:end, :])
|
|
return np.stack(out, axis=0)
|
|
|
|
|
|
def mlx_segmented_mm_loop(a, b, segments):
|
|
"""MLX loop-of-matmuls baseline."""
|
|
segments_list = segments.tolist()
|
|
out = []
|
|
for start, end in segments_list:
|
|
out.append(a[:, start:end] @ b[start:end, :])
|
|
return mx.stack(out, axis=0)
|
|
|
|
|
|
def bench_mlx(a, b, segments, warmup, iters):
|
|
for _ in range(warmup):
|
|
y = mx.segmented_mm(a, b, segments)
|
|
mx.eval(y)
|
|
mx.synchronize()
|
|
|
|
start = time.perf_counter()
|
|
for _ in range(iters):
|
|
y = mx.segmented_mm(a, b, segments)
|
|
mx.eval(y)
|
|
mx.synchronize()
|
|
end = time.perf_counter()
|
|
return (end - start) * 1e3 / iters
|
|
|
|
|
|
def bench_mlx_loop(a, b, segments, warmup, iters):
|
|
for _ in range(warmup):
|
|
y = mlx_segmented_mm_loop(a, b, segments)
|
|
mx.eval(y)
|
|
mx.synchronize()
|
|
|
|
start = time.perf_counter()
|
|
for _ in range(iters):
|
|
y = mlx_segmented_mm_loop(a, b, segments)
|
|
mx.eval(y)
|
|
mx.synchronize()
|
|
end = time.perf_counter()
|
|
return (end - start) * 1e3 / iters
|
|
|
|
|
|
def print_table(headers, rows):
|
|
widths = [len(h) for h in headers]
|
|
for row in rows:
|
|
for i, cell in enumerate(row):
|
|
widths[i] = max(widths[i], len(cell))
|
|
|
|
def fmt_row(row):
|
|
return (
|
|
"| "
|
|
+ " | ".join(f"{cell:<{widths[i]}}" for i, cell in enumerate(row))
|
|
+ " |"
|
|
)
|
|
|
|
sep = "|-" + "-|-".join("-" * w for w in widths) + "-|"
|
|
print(fmt_row(headers))
|
|
print(sep)
|
|
for row in rows:
|
|
print(fmt_row(row))
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
"--cases",
|
|
default=(
|
|
"128x128x1024x16,"
|
|
"128x128x1024x32,"
|
|
"256x256x2048x16,"
|
|
"512x512x4096x32,"
|
|
"1024x1024x4096x32,"
|
|
"1024x1024x8192x64"
|
|
),
|
|
help="Comma-separated MxNxKxS list.",
|
|
)
|
|
parser.add_argument(
|
|
"--dtype",
|
|
default="float32",
|
|
choices=["float16", "bfloat16", "float32"],
|
|
)
|
|
parser.add_argument("--warmup", type=int, default=10)
|
|
parser.add_argument("--iters", type=int, default=50)
|
|
parser.add_argument(
|
|
"--segments",
|
|
choices=["equal", "random"],
|
|
default="random",
|
|
help="Segment generation pattern.",
|
|
)
|
|
parser.add_argument("--seed", type=int, default=0)
|
|
parser.add_argument("--no-check", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
mlx_dtype = MLX_DTYPES[args.dtype]
|
|
|
|
print(
|
|
f"dtype={args.dtype} warmup={args.warmup} iters={args.iters} segments={args.segments}"
|
|
)
|
|
|
|
headers = [
|
|
"Case",
|
|
"MLX ms",
|
|
"Loop ms",
|
|
"Speedup",
|
|
"MLX err",
|
|
"Loop err",
|
|
]
|
|
rows = []
|
|
|
|
cases = parse_cases(args.cases)
|
|
for idx, (m, n, k, s) in enumerate(cases):
|
|
rng = np.random.default_rng(args.seed + idx)
|
|
a_np = rng.standard_normal((m, k)).astype(np.float32)
|
|
b_np = rng.standard_normal((k, n)).astype(np.float32)
|
|
seg_np = make_segments(k, s, args.segments, args.seed + idx)
|
|
|
|
a_mx = mx.array(a_np, dtype=mlx_dtype)
|
|
b_mx = mx.array(b_np, dtype=mlx_dtype)
|
|
seg_mx = mx.array(seg_np, dtype=mx.uint32)
|
|
mx.eval(a_mx, b_mx, seg_mx)
|
|
|
|
mlx_err_str = ""
|
|
loop_err_str = ""
|
|
if not args.no_check:
|
|
y_mlx = mx.segmented_mm(a_mx, b_mx, seg_mx)
|
|
y_loop = mlx_segmented_mm_loop(a_mx, b_mx, seg_mx)
|
|
mx.eval(y_mlx, y_loop)
|
|
|
|
if args.dtype == "float32":
|
|
ref = numpy_segmented_mm_ref(
|
|
a_np.astype(np.float64),
|
|
b_np.astype(np.float64),
|
|
seg_np.tolist(),
|
|
)
|
|
mlx_err = np.max(np.abs(np.array(y_mlx, dtype=np.float64) - ref))
|
|
loop_err = np.max(np.abs(np.array(y_loop, dtype=np.float64) - ref))
|
|
else:
|
|
a_mx_f32 = mx.array(a_np, dtype=mx.float32)
|
|
b_mx_f32 = mx.array(b_np, dtype=mx.float32)
|
|
ref = mx.segmented_mm(a_mx_f32, b_mx_f32, seg_mx)
|
|
mx.eval(ref)
|
|
mlx_err = float(mx.max(mx.abs(ref - y_mlx.astype(mx.float32))).item())
|
|
loop_err = float(mx.max(mx.abs(ref - y_loop.astype(mx.float32))).item())
|
|
mlx_err_str = f"{mlx_err:.2e}"
|
|
loop_err_str = f"{loop_err:.2e}"
|
|
|
|
t_mlx = bench_mlx(a_mx, b_mx, seg_mx, args.warmup, args.iters)
|
|
t_loop = bench_mlx_loop(a_mx, b_mx, seg_mx, args.warmup, args.iters)
|
|
ratio = t_loop / t_mlx if t_mlx > 0 else float("inf")
|
|
rows.append(
|
|
[
|
|
f"{m}x{n}x{k}x{s}",
|
|
f"{t_mlx:.3f}",
|
|
f"{t_loop:.3f}",
|
|
f"{ratio:.2f}x",
|
|
mlx_err_str,
|
|
loop_err_str,
|
|
]
|
|
)
|
|
|
|
print_table(headers, rows)
|
|
if not args.no_check:
|
|
if args.dtype == "float32":
|
|
print("err: max|result - numpy_fp64_ref|")
|
|
else:
|
|
print("err: max|result - own_fp32_result|")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|