Apple (Metal GPU / MPSGraph) bridge
Version: Loom v0.83.0 "Apple Bridge" — experimental (same maturity bar as the Intel & Qualcomm NPU bridges)
Status: macOS on Apple silicon · Metal GPU (MPSGraph) + Accelerate/CPU reference · forward-only
Plugin: libloom_accel_apple.dylib (Apple Metal Performance Shaders Graph inside)
Lucy menu: [13] — log: lucy/lucy_testing_output/apple.txt
This is the Apple mirror of the Intel path in
accelerators.mdand the Qualcomm path insnapdragon_npu.md. It shares the identical vendor-neutral C ABI (accel/apple/include/loom_accel.h), the samepoly/accelloader, the sameSyncToAccel→DispatchLayerflow. Only the vendor backend differs: Metal / MPSGraph instead of OpenVINO or QNN.
TL;DR
The Apple plugin is the cleanest of the three vendor bridges to stand up, because there is
no external SDK to vendor — Metal, MetalPerformanceShaders, and MetalPerformanceShadersGraph
ship with macOS. There's no 1.3 GB download, no signed DSP skel, no LD_LIBRARY_PATH dance.
It exposes two devices behind the same C ABI:
CPU— a portable, deterministic C++ reference forward for all 15 layers. This is the parity anchor (it plays the role QnnCpu plays for Qualcomm).GPU— a Metal / MPSGraph backend for the ops MPSGraph handles well (MatMul, MHA, ReLU, Sigmoid, Softmax, Add, Multiply). Anything it doesn't build silently falls back to the CPU reference, so every cell still returns a correct value.
The result: a real Loom → Apple GPU dispatch bridge with byte-perfect run-to-run determinism (180/180 on both devices) and multi-× speedups on large MatMul/MHA on the GPU and on elementwise ops on the CPU. It is not production-complete — Conv and the norms are still CPU-reference-only and slower than Loom's own CPU, and the usual low-bit MatMul drift is present. Treat it exactly like the Intel/Qualcomm bridges: proven plumbing to build on, not a "flip the GPU on for any network" feature.
What actually runs today
Measured from apple.txt (Lucy [13] → [5], the 10-layer DispatchLayer matrix × 6 dtypes ×
3 sizes = 180 cells, SyncToAccel once per device). The raw CABI bench (menu [0])
additionally covers DepthwiseConv / AvgPool / MaxPool for the full 15-layer plugin surface.
| Layer | Metal GPU | Apple CPU ref | Notes |
|---|---|---|---|
| MatMul | ✅ MPSGraph | ✅ | GPU wins on large tiers |
| MHA-MatMul | ✅ MPSGraph | ✅ | GPU wins on large tiers |
| ReLU | ✅ MPSGraph | ✅ | CPU is faster at small/medium |
| Sigmoid | ✅ MPSGraph | ✅ | CPU is faster at small/medium |
| Softmax | ✅ MPSGraph | ✅ | GPU floor-bound; CPU near-instant |
| Add / Multiply | ✅ MPSGraph | ✅ | (raw CABI bench) |
| GELU | ⚠️ CPU fallback | ✅ | not built on MPSGraph yet |
| Conv1D / Conv2D | ⚠️ CPU fallback | ✅ | CPU reference; slower than Loom CPU |
| DepthwiseConv / AvgPool / MaxPool | ⚠️ CPU fallback | ✅ | (raw CABI bench) |
| LayerNorm / RMSNorm | ⚠️ CPU fallback | ✅ | parity broken (no Loom weights baked) |
✅ = compiles + infers on that device. ⚠️ CPU fallback = the GPU device transparently runs the CPU reference (correct result, no GPU speedup).
Every dtype runs on every layer — the matrix is FP32 / FP16 / BF16 / INT16 / INT8 / INT4.
Compute is FP32 internally; the dtype only changes the wire byte layout (FP16/BF16 are 2 bytes,
the rest hand over FP32 values, matching poly/accel_intel.go). BF16 is the Apple-native
addition to the bridge — Apple silicon has native bfloat16.
Numbers that matter (from apple.txt)
Determinism — production-grade
Repeat-forward drift (same input twice) is 💎 EXACT on every cell, both devices:
| Check | Pass | Total |
|---|---|---|
| Apple infer repeat (CPU) | 180 | 180 |
| Apple infer repeat (GPU) | 180 | 180 |
Byte-identical outputs run to run — the single most important property for using either device as a real execution target.
Loom ↔ Apple parity
| Check | ≤ INDUS | Total |
|---|---|---|
| Loom ↔ Apple parity (GPU) | 132 | 180 |
| Loom ↔ Apple parity (CPU) | 78 | 180 |
Full GPU bucket spread: 💎 6 · ✅ 126 · 🟨 36 (LOWBIT) · ❌ 12 (BROKE). Full CPU bucket spread: 💎 6 · ✅ 72 · 🟨 54 · 🟤 27 (H-DRIFT) · ❌ 21.
The ≤ INDUS count is higher on the GPU than the CPU because the GPU target carries a looser
parity tolerance (GPU math legitimately reorders reductions) — the raw drift magnitudes for the
two devices are nearly identical. So "GPU parity looks better" is a tolerance-bucket effect, not
the GPU being more precise than the CPU reference.
Parity highlights:
- Softmax is effectively exact (
~1e-9FP32,~1e-6FP16/BF16) — the standout. - MatMul / MHA / Conv sit at 🟨 LOWBIT → 🟤 H-DRIFT as tiers grow (FP32/FP16/BF16 accumulation order vs Loom's CPU matmul).
- LayerNorm / RMSNorm are ❌ BROKE at ~1.8 for FP32/BF16 — the CPU reference fills its own constant scale/bias instead of Loom's weights (identical root cause to the Intel bridge norms).
- INT8 MatMul/Conv/MHA drift is large (3 → 31) and goes ❌ BROKE on the large tier — the same Loom-dequant-matmul vs plugin-FP32 mismatch seen on Intel/Qualcomm.
Speed — GPU for big MAC, CPU for elementwise
Two clear winners depending on op class (Spd = Loom ÷ Apple, higher = Apple faster):
Metal GPU wins on large MatMul / MHA (the GPU has a ~0.5 ms launch floor, so it only pays off once the tensor is big enough):
| Tier | Layer | DType | Loom ms | Metal GPU ms | Speed |
|---|---|---|---|---|---|
| large | MHA-MatMul | FP16 | 3.079 | 0.571 | 5.4× |
| large | MHA-MatMul | INT8 | 2.096 | 0.399 | 5.3× |
| large | MatMul | FP16 | 2.613 | 0.562 | 4.6× |
| large | MatMul | INT8 | 2.322 | 0.571 | 4.1× |
| large | MatMul | BF16 | 1.603 | 0.598 | 2.7× |
Apple CPU reference wins big on elementwise (tight native loop, no launch cost):
| Tier | Layer | DType | Loom ms | Apple CPU ms | Speed |
|---|---|---|---|---|---|
| medium | ReLU | INT4 | 0.903 | 0.010 | 90× |
| large | ReLU | INT4 | 1.880 | 0.020 | 94× |
| large | GELU | INT4 | 2.395 | 0.029 | 83× |
| large | Sigmoid | INT4 | 2.179 | 0.027 | 81× |
| medium | GELU | INT8 | 0.869 | 0.017 | 51× |
Where both Apple devices lose:
- Small tier, GPU — the ~0.5 ms floor swamps sub-0.01 ms compute (
0.02×on MatMul/ReLU). - Conv1D / Conv2D, every tier — both devices run the CPU reference conv, which is ~0.24–0.27× Loom's own optimized CPU conv. Don't offload Conv yet.
- Norms — tiny ops, CPU-reference-only, ~0.3–0.6× (and parity broken anyway).
One-time compile cost
SyncToAccel compiles each layer once. The CPU reference is ~free; the MPSGraph build is the
cost on the GPU device — and it is cheap compared to Hexagon/OpenVINO:
| Device | Compile (ms) |
|---|---|
| Apple CPU reference | ~0.00 |
| Metal GPU (MPSGraph MatMul/act) | ~0.4 – 5.8 |
Compile is paid once per layer/device; steady inference is the sub-ms numbers above.
How it plugs into Loom (unchanged dispatch model)
reg, err := poly.DiscoverAppleAccel(accel.AccelConfig{
AppleSO: accel.DefaultApplePath(),
})
if err != nil { /* no plugin — stay on Loom CPU */ }
defer reg.Close()
net, _ := poly.BuildNetworkFromJSON(spec)
net.Accel = reg
net.Layers[0].ExecTarget = accel.ExecAppleGPU // or ExecAppleCPU
_ = net.SyncToAccel("medium") // compile once + upload weights
out, _, _ := poly.ForwardPolymorphic(net, input)
ExecTarget values
| Value | Runs on |
|---|---|
accel.ExecLoomCPU |
Default — Go poly CPU |
accel.ExecAppleCPU |
Apple plugin CPU reference (deterministic parity anchor) |
accel.ExecAppleGPU |
Metal / MPSGraph (falls back to the CPU reference per-op when MPSGraph can't build) |
Accelerator devices
| Device | Backend | Data types | Role |
|---|---|---|---|
| GPU | Metal / MPSGraph | FP32/FP16/BF16 + INT* (as fp32 wire) | The fast path for large MatMul / MHA |
| CPU | portable C++ reference | all 6 bench dtypes | Parity anchor + fallback for unbuilt GPU ops |
| ANE (Neural Engine) | — | — | Not wired — reachable only indirectly via Core ML (future) |
Known gaps — what's left
Honestly experimental. What's still missing or weak:
| Gap | Detail |
|---|---|
| Conv1D / Conv2D on GPU | Not built on MPSGraph — both devices run the CPU reference, which is slower than Loom's own CPU conv. Biggest speed opportunity left. |
| GELU on GPU | Falls back to CPU reference; MPSGraph has the ops, just not wired. |
| LayerNorm / RMSNorm parity | ❌ BROKE — CPU reference uses internal constant scale/bias instead of Loom's uploaded weights. Needs weight bake (same fix as Intel). |
| INT8 MAC drift | MatMul/Conv/MHA INT8 drift is large and BROKE on large tiers (Loom dequant matmul vs plugin FP32). |
| FP16/BF16 precision | Compute is FP32 internally; FP16/BF16 only change the wire layout, so no true half-precision GPU math yet. |
| ANE | Apple Neural Engine is not a Metal device; reaching it needs a Core ML path (future plugin mode). |
| Training / backward | Forward-only; backward stays on Loom CPU. |
JSON exec field |
No "exec": "apple-gpu" yet — ExecTarget is set programmatically. |
| Zero-copy I/O | Per-hop tensor → []byte → Metal buffer copies. |
When to offload
| Target | Offload | Skip |
|---|---|---|
| Apple GPU | large MatMul / MHA-MatMul (2.4–5.4×) | small tensors (0.5 ms floor); Conv (CPU fallback); norms |
| Apple CPU | elementwise ReLU / GELU / Sigmoid at medium+ (16–94×); small MatMul | large MatMul/MHA (single-threaded ref, 0.1–0.3×); Conv |
| Either | many steady forwards (compile paid once) | one-shot micro-ops |
Rule of thumb: big matmuls → Metal GPU; heavy elementwise → Apple CPU; convolutions → keep on Loom CPU until the GPU conv path lands.
Build & run
No SDK download — just Xcode command-line tools (for the Metal frameworks). Full notes in
accel/apple/README.md. Short version:
cd accel/apple
./build.sh # CMake → build/libloom_accel_apple.dylib
cd ../../lucy
CGO_ENABLED=1 go run . # → [13] Apple GPU bridge
# [4] medium DispatchLayer suite (fast)
# [5] full 10×6×3 matrix (the apple.txt tables above)
# [0] raw CABI matrix (all 15 layers, direct plugin)
accel.DefaultApplePath() walks up from cwd for accel/apple/build/libloom_accel_apple.dylib,
or set LOOM_ACCEL_APPLE_DYLIB.
See also
accelerators.md— the accel model + Intel NPU bridge + vendor support matrixsnapdragon_npu.md— the Qualcomm/Hexagon mirroraccel/apple/README.md— build / dtypes / layoutnumerical_types.md— Loom's 21 DTypes vs vendor bench dtypesdispatch.md—DispatchLayerrouting