mlex 0.1.3

Safe, idiomatic Rust runtime for Apple MLX: quantized LLM inference (Qwen, Gemma4, NemotronH, DharaAR, ...) with multi-modal support
# 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()