Every number laya-apple ships was measured on one machine, an Apple M4 Max
(compatibility.md). This page collects results from other Macs,
each one produced by the same script and submitted as a pull request, so the matrix
fills in from measurements rather than expectations.
These Macs are the current priority, and each has a good first issue with
copy-and-paste steps from git clone to the pull request. You can add a result in
10–25 minutes without changing any code. The matrix below records what has
been measured; any Mac without a result is wanted, with or without an issue.
| Your Mac | Contribute | What you run |
|---|---|---|
| M1 or M2, any variant | #1: M1/M2 MLX-only result | MLX only, about 10 minutes |
| M3 Max | #2: M3 Max result | MLX, plus ANE if you have 15 more minutes |
| Any other Apple silicon Mac, including M5 and newer | No issue needed: follow Add your Mac | Same steps |
| SoC | MLX | ANE | Auto uses ANE | Heterogeneous | Evidence |
|---|---|---|---|---|---|
| M1 | – | – | – | – | Wanted — submit a benchmark |
| M1 Pro | – | – | – | – | Wanted — submit a benchmark |
| M1 Max | – | – | – | – | Wanted — submit a benchmark |
| M1 Ultra | – | – | – | – | Wanted — submit a benchmark |
| M2 | – | – | – | – | Wanted — submit a benchmark |
| M2 Pro (16 GB, macOS 26.6.2) | ✓ | untested | no | untested | bundle (#47; --quick, laya-typed-decisions only; MLX-only run without coremltools, so auto stays on MLX (ane_runtime_unavailable) and the ANE and heterogeneous checks do not run) |
| M2 Max | – | – | – | – | Wanted — submit a benchmark |
| M2 Ultra | – | – | – | – | Wanted — submit a benchmark |
| M3 | – | – | – | – | Wanted — submit a benchmark |
| M3 Pro | – | – | – | – | Wanted — submit a benchmark |
| M3 Max | – | – | – | – | Wanted — submit a benchmark |
| M3 Ultra | – | – | – | – | Wanted — submit a benchmark |
| M4 (32 GB, macOS 26.2) | ✓ | ✓ | no | untested | bundle (#41; --quick, laya-typed-decisions only; not calibrated, so auto stays on MLX and the heterogeneous check does not run) |
| M4 Pro (48 GB, macOS 27.0) | ✓ | ✓ | yes, local calibrated profile | ✓ | bundle (#32; --quick, laya-typed-decisions only) |
| M4 Max (macOS 26.6.2) | ✓ | ✓ | yes | ✓ | benchmarks/v1.0.md, bundle |
| M5 | – | – | – | – | Wanted — submit a benchmark |
| M5 Pro | – | – | – | – | Wanted — submit a benchmark |
| M5 Max | – | – | – | – | Wanted — submit a benchmark |
| M5 Ultra | – | – | – | – | Wanted — submit a benchmark |
| Any other or newer Apple silicon | – | – | – | – | Wanted — submit a benchmark |
What each column means:
- MLX: MLX FP16 passes the parity gate against the shipped PyTorch FP32 goldens for every model measured.
- ANE: the ANE artifacts built on that machine pass the same gate.
untestedmeans no artifacts were built there. - Auto uses ANE:
device="auto"sends at least one request to the ANE. On a profile that is not shipped and not calibrated, the answer isnowithrouting_reasonplatform_not_validated; in an MLX-only run without coremltools it isane_runtime_unavailable. That is the runtime working as designed, not a failure.yes, local calibrated profilemeans the submitter ranlaya-apple calibrateon that Mac andautoused the resulting local profile. The shipped routing table does not include that profile, so on another Mac with the same SoCautostays on MLX until that Mac builds its own ANE artifacts and runslaya-apple calibrate(compatibility.md, "What happens on an untested profile"). - Heterogeneous: a short closed-loop mix of short and long requests has zero answer mismatches and more aggregate throughput with GPU + ANE than GPU alone.
– with Wanted means nobody has submitted a result yet. It says nothing about
whether that Mac works.
A filled row records one submitted run on one machine, with the memory, macOS version and scope shown in the row. It is not a claim about every configuration of that SoC.
You need a Mac with Apple silicon, git, uv
(brew install uv) and about 2 GB of free disk, or 5 GB with the optional ANE step. An
MLX-only result takes about 10 minutes.
-
Clone the repository, install it and download the pinned checkpoint (800 MB, hash verified):
git clone https://github.com/tc3oliver/laya-apple cd laya-apple uv sync uv run laya-apple download laya-typed-decisions -
Optional: build the ANE artifacts on your machine.
uv sync --extra ane --extra convert uv run laya-apple artifacts build laya-typed-decisions
This converts the model for each ANE bucket and registers an artifact only if it passes the placement and parity gates on your Mac. It takes about 5–10 minutes. You can skip it: results for MLX alone are useful too, and the report then records the ANE column as
untestedtogether with this command. -
Run the report:
uv run python scripts/hardware_report.py --quick
The script records your SoC, memory, macOS version and build, the package versions and the laya-apple revision on its own. Then it runs MLX parity, ANE parity when artifacts exist, warm latency, the routing decisions of
device="auto", and a short heterogeneous mix whenautoroutes to the ANE.--quickmeasureslaya-typed-decisionswith fewer iterations. Without--quickit measures all three models with more iterations, so download them all first (uv run laya-apple download) or pass--models laya-typed-decisions. -
Check the new directory before you share it:
NEW=$(git ls-files --others --exclude-standard hardware-results/ | xargs -n1 dirname | sort -u) echo "$NEW" # exactly one new directory head -20 "$NEW/summary.md" # the matrix row and your environment grep -rn -e "$USER" -e "$(hostname -s)" "$NEW" || echo "OK: no user name or host name in the bundle"
The script records no host name or serial number, and it replaces your home directory with
~and the checkout path with.. -
Open a pull request that adds the directory the script prints,
hardware-results/<soc>-macos<major>/, unchanged. With the GitHub CLI:gh repo fork --remote # your fork becomes origin, this repo becomes upstream git switch -c bench/add-my-mac git add hardware-results/ git commit -m "bench(hardware): add <SoC> benchmark result" git push -u origin HEAD gh pr create --fill
Replace
<SoC>with your chip, for exampleM1 ProorM2 Max.gh pr create --filluses the commit subject as the pull request title, for examplebench(hardware): add M3 Max benchmark result. A different title is fine; the maintainer can adjust it when merging.Paste the matrix row the script printed into the description, and name the issue it closes if there is one (#1, #2). Any other Mac needs no issue.
- The bundle is complete. It has
bundle.jsonandsummary.md, every requested model has a section, and any step that did not run says why (unavailablewith the build command, orskippedwith a reason). Errors stay in the bundle; they are results too. - Parity. The MLX and ANE parity summaries are the verdicts of the same gate
laya-apple parityapplies, with the per-row maxima recorded. A ✓ needspassed: truefor every model measured. - No manual edits.
summary.mdmust render frombundle.json, the latency records must keep their raw samples, and the environment must match the directory name. Reviewers regenerate it withuv run python scripts/hardware_report.py --render hardware-results/<dir>/bundle.jsonand compare. Do not edit either file. If something looks wrong, say so in the pull request instead. - Provenance. The laya-apple revision is a published commit, and the dirty flag is false or explained.
A merged bundle fills in that SoC's row from its summary.md. If two bundles for the
same SoC disagree, the row shows both until the reason is understood.
A result does not change what device="auto" does on anyone's machine. The shipped
routing table (laya_apple/data/routing.json) applies only to the profiles it was
derived on, and a validated ANE result on another profile does not add that profile to
it. On your own machine, laya-apple calibrate writes a local profile that lets auto
use the ANE there (compatibility.md, "What happens on an untested
profile"). Adding a new shipped profile is a separate change that commits its own calibration
measurements (support-matrix.md). It is
not done through this matrix.
The bundle format is described in hardware-results/README.md.
--json PATH also writes a compact summary of the run, for a dashboard or a comparison
across hardware-results/*/ that should not have to parse bundle.json. It is derived from
the bundle and every number in it is the bundle's own. It is optional and not part of the
directory you submit: reviewers check bundle.json and summary.md.
uv run python scripts/hardware_report.py --quick --json summary.json
# or, for a bundle you already have, without measuring anything (summary.md is still printed):
uv run python scripts/hardware_report.py --render hardware-results/<dir>/bundle.json --json summary.jsonPATH must be a writable file in an existing directory, and with --render it must not be
the bundle being read. This is checked before the run starts, and an existing file is
overwritten. This is the summary of
hardware-results/apple-m2-pro-macos26/bundle.json:
{
"format": "laya-apple-hardware-summary",
"schema_version": 1,
"laya_apple": "1.0.2",
"started_at": "2026-09-24T05:55:15.663454+00:00",
"quick": true,
"platform": {
"soc": "Apple M2 Pro",
"macos": "26.6.2",
"macos_build": "25G83",
"coremltools": null
},
"hardware": {
"hw_model": "Mac14,12",
"memory_gb": 16
},
"models": {
"laya-typed-decisions": {
"revision": "f9ab0b228f0fc0f14d873dbc99038f135c2da1b2",
"parity": {
"mlx": true,
"ane": null
},
"forward_p50_ms": [
{
"device": "gpu",
"length": 64,
"questions": 1,
"p50_ms": 16.046146163716912
},
{
"device": "gpu",
"length": 128,
"questions": 1,
"p50_ms": 28.06797926314175
},
{
"device": "gpu",
"length": 512,
"questions": 1,
"p50_ms": 96.29687503911555
},
{
"device": "gpu",
"length": 128,
"questions": 4,
"p50_ms": 90.46510397456586
}
]
}
}
}Schema, version 1:
| Key | Contents |
|---|---|
format, schema_version |
laya-apple-hardware-summary and the integer 1. Any change to a key or to what it means bumps schema_version; a test pins the exact output |
laya_apple |
the laya-apple version that ran |
started_at |
UTC start time of the run, as in the bundle |
quick |
true for a --quick run (fewer iterations), so compare like with like |
platform |
soc, macos, macos_build and coremltools: exactly the keys and values of laya_apple.artifacts.platform_profile(), the profile routing uses. coremltools is null when it is not installed |
hardware |
hw_model (for example Mac14,12) and memory_gb |
models.<name>.revision |
the pinned checkpoint revision that was measured |
models.<name>.parity.mlx, .ane |
the parity gate's verdict: true passed, false failed or errored, null did not run (ANE unavailable on this machine, or the weights step failed; see the bundle) |
models.<name>.forward_p50_ms |
one entry per measured configuration, in measurement order: device (gpu or ane), length (tokens), questions and p50_ms, the median warm forward time in milliseconds |
forward_p50_ms is a list, not a map by length, because the same length is measured with
different question counts (128 tokens with 1 and with 4 questions above). The forward time
is the backend call alone, as in laya-apple benchmark; the end-to-end predict times and the
raw samples stay in the bundle. A configuration that was skipped or failed has no entry
there, and the reason is in the bundle.