0ff1115a46
Co-authored-by: Cheng <git@zcbenz.com>
194 lines
5.6 KiB
Python
194 lines
5.6 KiB
Python
# Copyright © 2025 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(","):
|
|
parts = spec.split("x")
|
|
m, n, k, bs = int(parts[0]), int(parts[1]), int(parts[2]), int(parts[3])
|
|
sparsity = float(parts[4]) if len(parts) > 4 else 0.5
|
|
parsed.append((m, n, k, bs, sparsity))
|
|
return parsed
|
|
|
|
|
|
def make_masks(m, n, k, block_size, sparsity, rng):
|
|
"""Create block masks with given sparsity (fraction of blocks zeroed)."""
|
|
tm = (m + block_size - 1) // block_size
|
|
tn = (n + block_size - 1) // block_size
|
|
tk = (k + block_size - 1) // block_size
|
|
|
|
lhs_mask = (rng.random((tm, tk)) >= sparsity).astype(np.bool_)
|
|
rhs_mask = (rng.random((tk, tn)) >= sparsity).astype(np.bool_)
|
|
out_mask = (rng.random((tm, tn)) >= sparsity).astype(np.bool_)
|
|
return lhs_mask, rhs_mask, out_mask
|
|
|
|
|
|
def mlx_naive_block_masked_mm(a, b, block_size, out_mask, lhs_mask, rhs_mask):
|
|
"""MLX naive: expand masks and use regular matmul."""
|
|
M, K = a.shape[-2], a.shape[-1]
|
|
N = b.shape[-1]
|
|
|
|
def expand(mask, rows, cols):
|
|
e = mx.repeat(mx.repeat(mask, block_size, axis=-2), block_size, axis=-1)
|
|
return e[..., :rows, :cols]
|
|
|
|
a_masked = a * expand(lhs_mask, M, K)
|
|
b_masked = b * expand(rhs_mask, K, N)
|
|
c = a_masked @ b_masked
|
|
c = c * expand(out_mask, M, N)
|
|
return c
|
|
|
|
|
|
def bench_mlx(fn, warmup, iters):
|
|
for _ in range(warmup):
|
|
y = fn()
|
|
mx.eval(y)
|
|
mx.synchronize()
|
|
|
|
start = time.perf_counter()
|
|
for _ in range(iters):
|
|
y = fn()
|
|
mx.eval(y)
|
|
mx.synchronize()
|
|
return (time.perf_counter() - 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(
|
|
description="Benchmark block_masked_mm vs naive expand+matmul"
|
|
)
|
|
parser.add_argument(
|
|
"--cases",
|
|
default=(
|
|
"256x256x256x32x0.5,"
|
|
"512x512x512x32x0.5,"
|
|
"1024x1024x1024x32x0.5,"
|
|
"1024x1024x1024x64x0.5,"
|
|
"2048x2048x2048x64x0.5,"
|
|
"256x256x256x32x0.0,"
|
|
"1024x1024x1024x32x0.0,"
|
|
"1024x1024x1024x32x0.9"
|
|
),
|
|
help="Comma-separated MxNxKxBSxSparsity list. Sparsity=fraction of blocks zeroed.",
|
|
)
|
|
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("--seed", type=int, default=42)
|
|
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}")
|
|
|
|
headers = [
|
|
"Case (MxNxKxBS)",
|
|
"Sparsity",
|
|
"MLX ms",
|
|
"Naive ms",
|
|
"Speedup",
|
|
]
|
|
if not args.no_check:
|
|
headers.append("Max err")
|
|
rows = []
|
|
|
|
cases = parse_cases(args.cases)
|
|
for idx, (m, n, k, bs, sparsity) 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)
|
|
lhs_mask_np, rhs_mask_np, out_mask_np = make_masks(m, n, k, bs, sparsity, rng)
|
|
|
|
a_mx = mx.array(a_np, dtype=mlx_dtype)
|
|
b_mx = mx.array(b_np, dtype=mlx_dtype)
|
|
lhs_mask_mx = mx.array(lhs_mask_np)
|
|
rhs_mask_mx = mx.array(rhs_mask_np)
|
|
out_mask_mx = mx.array(out_mask_np)
|
|
mx.eval(a_mx, b_mx, lhs_mask_mx, rhs_mask_mx, out_mask_mx)
|
|
|
|
# Correctness check: block_masked_mm vs naive expand+matmul
|
|
err_str = ""
|
|
if not args.no_check:
|
|
y_op = mx.block_masked_mm(
|
|
a_mx, b_mx, bs, out_mask_mx, lhs_mask_mx, rhs_mask_mx
|
|
)
|
|
y_naive = mlx_naive_block_masked_mm(
|
|
a_mx, b_mx, bs, out_mask_mx, lhs_mask_mx, rhs_mask_mx
|
|
)
|
|
mx.eval(y_op, y_naive)
|
|
err = float(mx.max(mx.abs(y_op - y_naive)).item())
|
|
err_str = f"{err:.2e}"
|
|
|
|
# Benchmark
|
|
t_mlx = bench_mlx(
|
|
lambda: mx.block_masked_mm(
|
|
a_mx, b_mx, bs, out_mask_mx, lhs_mask_mx, rhs_mask_mx
|
|
),
|
|
args.warmup,
|
|
args.iters,
|
|
)
|
|
t_naive = bench_mlx(
|
|
lambda: mlx_naive_block_masked_mm(
|
|
a_mx, b_mx, bs, out_mask_mx, lhs_mask_mx, rhs_mask_mx
|
|
),
|
|
args.warmup,
|
|
args.iters,
|
|
)
|
|
speedup = f"{t_naive / t_mlx:.2f}x" if t_mlx > 0 else "-"
|
|
|
|
row = [
|
|
f"{m}x{n}x{k}x{bs}",
|
|
f"{sparsity:.0%}",
|
|
f"{t_mlx:.3f}",
|
|
f"{t_naive:.3f}",
|
|
speedup,
|
|
]
|
|
if not args.no_check:
|
|
row.append(err_str)
|
|
rows.append(row)
|
|
|
|
print_table(headers, rows)
|
|
if not args.no_check:
|
|
print("err: max|block_masked_mm - naive_expand_matmul|")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|