SPB Git

spb/modelmap Public License

Internal cartography of local LLMs on Apple Silicon — registered, gated, negative-first. Public atlas at modelmap.io.

Python 66.3% JavaScript 24.5% CSS 8.1% Shell 0.7%
11.4 KB · 292 lines python
Raw Blame History
1#!/usr/bin/env python32# =============================================================================3#  Project   : modelmap4#  File      : experiments/micro/expH_capture_cost_frontier/implementation/benchmark.py5#  Purpose   : Run #1 — storage-format throughput on APFS + hook overhead MLX/MPS6#  Author    : Simon-Pierre Boucher7#  Contact   : contact@spboucher.ai8#  Website   : https://modelmap.io9#  Created   : 2026-08-1210#  Modified  : 2026-08-1211#  Platform  : macOS / Apple Silicon (arm64) — MLX / Metal / MPS12#  License   : All rights reserved (research code)13# =============================================================================14"""expH run #1. See hypothesis.md (registered before this run).1516Part A — activation-store formats on APFS (warm cache, declared):17    raw np.memmap | safetensors (mmap) | zarr zstd | zarr uncompressed18    sequential write, sequential scan, random-batch reads (SAE-shuffle).19Part B — capture overhead on a synthetic 12-layer transformer (fp16):20    torch-MPS forward hooks vs MLX retained arrays;21    modes: plain / retain-on-device / retain + CPU copy + mmap write.22"""2324from __future__ import annotations2526import json27import shutil28import subprocess29import sys30import tempfile31import time32from pathlib import Path3334import numpy as np3536ROOT = Path(__file__).resolve().parents[4]37sys.path.insert(0, str(ROOT / "benchmarks"))38from hardware_manifest import manifest3940REPEATS = 341ROWS, DIM = 100_000, 4096          # ~0.82 GB fp16 per format42WRITE_CHUNK = 4_096                 # rows per write43BATCH = 4_096                       # rows per random-batch read44N_BATCHES = 2445SEED = 04647# Part B config48L, D_MODEL, N_HEADS, SEQ, BSZ = 12, 1024, 4, 512, 849N_FWD, N_FWD_WRITE, WARMUP = 20, 10, 3505152def _timed(fn, repeats=REPEATS):53    out = []54    for _ in range(repeats):55        t0 = time.perf_counter()56        fn()57        out.append(time.perf_counter() - t0)58    return out596061# ---------------------------------------------------------------- Part A62def bench_storage(workdir: Path) -> list[dict]:63    rng = np.random.default_rng(SEED)64    data = rng.standard_normal((WRITE_CHUNK, DIM)).astype(np.float16)65    batches = [rng.integers(0, ROWS, BATCH) for _ in range(N_BATCHES)]66    total_bytes = ROWS * DIM * 267    batch_bytes = BATCH * DIM * 2 * N_BATCHES68    results = []6970    def record(fmt, op, times, nbytes):71        results.append({72            "format": fmt, "op": op, "bytes": nbytes,73            "seconds": times, "gb_per_s_mean": nbytes / 2**30 / np.mean(times),74        })75        print(f"  {fmt:18s} {op:12s} {nbytes/2**30/np.mean(times):8.2f} GB/s")7677    # ---- raw np.memmap78    p = workdir / "acts.raw"79    def write_raw():80        m = np.memmap(p, dtype=np.float16, mode="w+", shape=(ROWS, DIM))81        for i in range(0, ROWS, WRITE_CHUNK):82            end = min(i + WRITE_CHUNK, ROWS)83            m[i:end] = data[: end - i]84        m.flush(); del m85    record("raw-mmap", "write", _timed(write_raw), total_bytes)86    m = np.memmap(p, dtype=np.float16, mode="r", shape=(ROWS, DIM))87    record("raw-mmap", "seq-scan", _timed(lambda m=m: float(np.asarray(m).sum(dtype=np.float32))), total_bytes)88    record("raw-mmap", "random-batch", _timed(lambda m=m: [m[b].sum(dtype=np.float32) for b in batches]), batch_bytes)89    del m9091    # ---- safetensors (mmap-backed numpy)92    from safetensors import safe_open93    from safetensors.numpy import save_file94    p = workdir / "acts.safetensors"95    full = np.memmap(workdir / "acts.raw", dtype=np.float16, mode="r", shape=(ROWS, DIM))96    def write_st(full=full, p=p):97        save_file({"acts": np.asarray(full)}, str(p))98    record("safetensors", "write", _timed(write_st), total_bytes)99    def open_st():100        return safe_open(str(p), framework="np")101    f = open_st()102    t = f.get_tensor("acts")   # mmap-backed load103    record("safetensors", "seq-scan", _timed(lambda t=t: float(t.sum(dtype=np.float32))), total_bytes)104    record("safetensors", "random-batch", _timed(lambda t=t: [t[b].sum(dtype=np.float32) for b in batches]), batch_bytes)105    del t, f, full106107    # ---- zarr (zstd default) and uncompressed108    import zarr109    for codec, name in ((None, "zarr-uncompressed"), ("default", "zarr-zstd")):110        p = workdir / f"acts_{name}.zarr"111        kwargs = {} if codec == "default" else {"compressors": None}112        def write_zarr(p=p, kwargs=kwargs):113            if p.exists():114                shutil.rmtree(p)115            z = zarr.create_array(store=str(p), shape=(ROWS, DIM), chunks=(WRITE_CHUNK, DIM),116                                  dtype=np.float16, **kwargs)117            for i in range(0, ROWS, WRITE_CHUNK):118                end = min(i + WRITE_CHUNK, ROWS)119                z[i:end] = data[: end - i]120        record(name, "write", _timed(write_zarr), total_bytes)121        z = zarr.open_array(store=str(p), mode="r")122        record(name, "seq-scan", _timed(lambda z=z: float(z[:].sum(dtype=np.float32))), total_bytes)123        record(name, "random-batch", _timed(lambda z=z: [z[np.sort(b)].sum(dtype=np.float32) for b in batches]), batch_bytes)124125    return results126127128# ---------------------------------------------------------------- Part B129def bench_torch_mps(workdir: Path) -> list[dict]:130    import torch131    from torch import nn132    assert torch.backends.mps.is_available(), "MPS required"133    dev, dt = torch.device("mps"), torch.float16134135    class Block(nn.Module):136        def __init__(self):137            super().__init__()138            self.ln1, self.ln2 = nn.LayerNorm(D_MODEL), nn.LayerNorm(D_MODEL)139            self.attn = nn.MultiheadAttention(D_MODEL, N_HEADS, batch_first=True)140            self.mlp = nn.Sequential(nn.Linear(D_MODEL, 4 * D_MODEL), nn.GELU(),141                                     nn.Linear(4 * D_MODEL, D_MODEL))142        def forward(self, x):143            h = self.ln1(x)144            x = x + self.attn(h, h, h, need_weights=False)[0]145            return x + self.mlp(self.ln2(x))146147    torch.manual_seed(SEED)148    model = nn.Sequential(*[Block() for _ in range(L)]).to(dev, dt).eval()149    x = torch.randn(BSZ, SEQ, D_MODEL, device=dev, dtype=dt)150    store = np.memmap(workdir / "torch_capture.raw", dtype=np.float16, mode="w+",151                      shape=(N_FWD_WRITE * L * BSZ * SEQ, D_MODEL))152153    def run(n, capture, to_disk):154        captured, row = [], 0155        hooks = []156        if capture:157            def hook(_m, _i, out):158                captured.append(out)159            hooks = [b.register_forward_hook(hook) for b in model]160        with torch.no_grad():161            for _ in range(WARMUP):162                model(x)163            torch.mps.synchronize()164            t0 = time.perf_counter()165            for _ in range(n):166                captured.clear()167                model(x)168                if to_disk:169                    for c in captured:170                        a = c.to("cpu").numpy().reshape(-1, D_MODEL)171                        store[row:row + a.shape[0]] = a172                        row += a.shape[0]173            torch.mps.synchronize()174            dt_s = time.perf_counter() - t0175        for h in hooks:176            h.remove()177        return dt_s / n178179    out = []180    for mode, cap, disk, n in (("plain", False, False, N_FWD),181                               ("retain", True, False, N_FWD),182                               ("retain+copy+write", True, True, N_FWD_WRITE)):183        times = [run(n, cap, disk) for _ in range(REPEATS)]184        out.append({"backend": "torch-mps", "mode": mode,185                    "s_per_forward": times,186                    "tokens_per_s_mean": BSZ * SEQ / np.mean(times)})187        print(f"  torch-mps {mode:22s} {np.mean(times)*1000:8.1f} ms/fwd")188    return out189190191def bench_mlx(workdir: Path) -> list[dict]:192    import mlx.core as mx193    import mlx.nn as mnn194195    class Block(mnn.Module):196        def __init__(self):197            super().__init__()198            self.ln1, self.ln2 = mnn.LayerNorm(D_MODEL), mnn.LayerNorm(D_MODEL)199            self.attn = mnn.MultiHeadAttention(D_MODEL, N_HEADS)200            self.fc1, self.fc2 = mnn.Linear(D_MODEL, 4 * D_MODEL), mnn.Linear(4 * D_MODEL, D_MODEL)201        def __call__(self, x):202            h = self.ln1(x)203            x = x + self.attn(h, h, h)204            return x + self.fc2(mnn.gelu(self.fc1(self.ln2(x))))205206    mx.random.seed(SEED)207    blocks = [Block() for _ in range(L)]208    for b in blocks:209        b.set_dtype(mx.float16)210    x = mx.random.normal((BSZ, SEQ, D_MODEL)).astype(mx.float16)211    store = np.memmap(workdir / "mlx_capture.raw", dtype=np.float16, mode="w+",212                      shape=(N_FWD_WRITE * L * BSZ * SEQ, D_MODEL))213214    def fwd(capture):215        captured, h = [], x216        for b in blocks:217            h = b(h)218            if capture:219                captured.append(h)220        return h, captured221222    def run(n, capture, to_disk):223        row = 0224        for _ in range(WARMUP):225            out, cap = fwd(capture)226            mx.eval(out, *cap)227        t0 = time.perf_counter()228        for _ in range(n):229            out, cap = fwd(capture)230            mx.eval(out, *cap)231            if to_disk:232                for c in cap:233                    a = np.array(c, copy=False).reshape(-1, D_MODEL)234                    store[row:row + a.shape[0]] = a235                    row += a.shape[0]236        return (time.perf_counter() - t0) / n237238    out = []239    for mode, cap, disk, n in (("plain", False, False, N_FWD),240                               ("retain", True, False, N_FWD),241                               ("retain+copy+write", True, True, N_FWD_WRITE)):242        times = [run(n, cap, disk) for _ in range(REPEATS)]243        out.append({"backend": "mlx", "mode": mode,244                    "s_per_forward": times,245                    "tokens_per_s_mean": BSZ * SEQ / np.mean(times)})246        print(f"  mlx       {mode:22s} {np.mean(times)*1000:8.1f} ms/fwd")247    return out248249250# ---------------------------------------------------------------- main251def main() -> int:252    workdir = Path(tempfile.mkdtemp(prefix="modelmap_expH_"))253    print(f"expH run #1 — workdir {workdir}")254    try:255        print("Part A — storage formats (warm cache):")256        storage = bench_storage(workdir)257        print("Part B — capture overhead:")258        compute = bench_torch_mps(workdir) + bench_mlx(workdir)259    finally:260        shutil.rmtree(workdir, ignore_errors=True)261262    commit = subprocess.run(["git", "rev-parse", "HEAD"], cwd=ROOT,263                            capture_output=True, text=True, check=False).stdout.strip()264    ts = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())265    outdir = ROOT / "results" / "expH_capture_cost_frontier" / ts266    outdir.mkdir(parents=True)267    doc = {268        "experiment": "expH_capture_cost_frontier",269        "run": 1,270        "scope": "storage formats (warm cache) + capture overhead, synthetic model",271        "commit": commit,272        "config": {273            "storage": {"rows": ROWS, "dim": DIM, "write_chunk": WRITE_CHUNK,274                        "batch": BATCH, "n_batches": N_BATCHES, "repeats": REPEATS,275                        "cache": "warm (declared limitation; cold pass = run #2)"},276            "compute": {"layers": L, "d_model": D_MODEL, "heads": N_HEADS,277                        "seq": SEQ, "batch": BSZ, "dtype": "float16",278                        "n_forwards": N_FWD, "warmup": WARMUP, "repeats": REPEATS},279            "seed": SEED,280        },281        "manifest": manifest(),282        "storage": storage,283        "compute": compute,284    }285    (outdir / "results.json").write_text(json.dumps(doc, indent=2) + "\n")286    print(f"results -> {outdir / 'results.json'}")287    return 0288289290if __name__ == "__main__":291    sys.exit(main())292