12 KiB
AIVM Benchmarks — v0.59
A-Chain transition substrate, four backends, four workload sizes.
Hardware / build
| Item | Value |
|---|---|
| Host | Apple M1 Max, 10 cores, 64 GiB |
| OS | macOS 26.4 (build 25E241) |
| Compiler | Apple clang 17.0.0 |
| Build | -O3 -DNDEBUG, Release |
| GPU | Apple M1 Max integrated GPU (Metal, AGXMetalG13X) |
| WebGPU | wgpu-native via Homebrew (/opt/homebrew/lib) |
| CUDA | not available on this host |
Backends measured:
- cpu —
aivm_cpu_reference.cppreference oracle, vector-Keccak-f1600 - metal —
aivm_gpu_engine.mm, prebuilt.metallib(no online MSL compile) - wgsl —
aivm_gpu_engine_wgpu.cppvia wgpu-native
CUDA is built and tested separately on Linux+CUDA hosts; not present here.
v0.59 architectural change
v0.58.3 dispatched every kernel as MTLSizeMake(1, 1, 1) — a single GPU
thread walking each op stream serially. v0.59 splits the apply phases into
locate (1 thread) + writeback (parallel, one thread per slot, threadgroup
width 256). The locate phase preserves canonical-order slot assignment
(byte-equal to the CPU oracle); the writeback phase performs the per-slot
field memcpy in parallel.
Determinism test extended with a size sweep (small/medium/large) — all sizes byte-equal CPU vs Metal vs WGSL. 47 / 47 tests pass.
The split keeps byte-equivalence trivially:
- Locate is the same canonical sweep as v0.58.3.
- Writeback is independent per slot — each slot's writer thread reads
slot_winner_op[s](the highest op index that claimed slot s during locate) and writes that op's fields to the slot. No cross-slot data flow.
ProvenanceApply keeps the v0.58.3 single-pass behaviour because UpdateWeights
increments a shared version counter (not last-writer-wins). AnchorApply also
stays single-pass because chain integrity (parent_root == prev.commit_root)
is fundamentally sequential.
EpochTransition stays at the v0.58.3 single-thread design — splitting it into per-table parallel-leaf-hash kernels added per-dispatch latency on the M1 Max integrated GPU (~1-2 ms each) that exceeded the parallel keccak savings at every workload size we measured.
Methodology
Each cell is (backend × size × mode). Per cell: 3 warm-ups + 10 measured
iterations. A 60s wall-clock budget caps any single cell so a slow
configuration cannot stall the run.
Workloads:
| size | attestations | model regs | audit anchors |
|---|---|---|---|
| small | 100 | 5 | 10 |
| medium | 1 000 | 50 | 100 |
| large | 10 000 | 500 | 1 000 |
| xlarge | 100 000 | 5 000 | 10 000 |
Modes (one section each):
FullRound— Attestation + Provenance + Anchor + EpochTransition chainedAttestationApply— TEE attestations only (140-byte leaves → multi-block keccak path)ProvenanceApply— model registry onlyAnchorApply— audit anchors onlyEpochTransition— close epoch, emitaivm_state_root
Metric ops/sec is the throughput of the primary stream for that mode
(attestations for FullRound/AttestationApply, models for ProvenanceApply,
anchors for AnchorApply, transitions for EpochTransition). vs CPU is
mean(cpu) / mean(backend) — <1.00x means the GPU is slower.
Raw output: BENCHMARKS_V059.txt.
Headline result
On the M1 Max integrated GPU, AIVM remains faster on the CPU reference than on either GPU backend at every workload size. This is unchanged from v0.58.3. Per-kernel dispatch latency (~1-2 ms each) on the integrated GPU dominates total round time at every size, swamping any per-thread parallel savings.
The GPU kernels are still byte-for-byte deterministic with the CPU oracle (all 47 / 47 determinism tests pass, including the new small/medium/large size sweep). The architecture change is live and prepared for hardware where dispatch latency is amortised differently — discrete CUDA GPUs, MTLBindless on Apple Silicon Pro / Ultra, and Dawn/Vulkan on dedicated cards.
The crossover point where GPU beats CPU is never for the AIVM workload shape on this hardware. Numbers below state this honestly.
FullRound — end-to-end transition
Throughput is attestations/sec (the headline workload).
| backend | size | mean (ms) | p50 | p95 | p99 | att/sec | vs CPU | vs v0.58.3 |
|---|---|---|---|---|---|---|---|---|
| cpu | small | 1.363 | 1.358 | 1.397 | 1.403 | 73 370 | 1.00x | ~1.00x |
| cpu | medium | 4.535 | 4.531 | 4.610 | 4.612 | 220 524 | 1.00x | ~1.00x |
| cpu | large | 14.492 | 14.558 | 14.654 | 14.671 | 690 036 | 1.00x | ~1.00x |
| cpu | xlarge | 40.694 | 40.780 | 41.230 | 41.359 | 2 457 378 | 1.00x | ~1.00x |
| metal | small | 26.513 | 26.490 | 26.629 | 26.644 | 3 772 | 0.05x | 1.01x |
| metal | medium | 83.533 | 83.529 | 83.588 | 83.614 | 11 971 | 0.05x | 1.00x |
| metal | large | 260.996 | 261.246 | 261.517 | 261.566 | 38 315 | 0.06x | 1.00x |
| metal | xlarge | 706.062 | 706.084 | 706.333 | 706.354 | 141 631 | 0.06x | 0.99x |
Net effect: FullRound Metal is the same as v0.58.3 within run-to-run noise on this hardware. The architecture change is in place for future GPUs but does not unlock a measurable speedup on the M1 Max integrated GPU.
AttestationApply — multi-block keccak path (140-byte leaves)
Per-mode breakdown for the path the v0.58.3 optnone fix targets — every attestation hashes a 140-byte leaf, so the keccak absorber crosses the 136-byte rate boundary and runs the multi-block path.
| backend | size | mean (ms) | ops/sec | vs CPU | vs v0.58.3 |
|---|---|---|---|---|---|
| cpu | small | 1.217 | 82 185 | 1.00x | ~1.00x |
| cpu | medium | 3.162 | 316 213 | 1.00x | ~1.00x |
| cpu | large | 3.232 | 3 093 605 | 1.00x | ~1.00x |
| cpu | xlarge | 4.259 | 23 481 528 | 1.00x | ~1.00x |
| metal | small | 737 / 25† | 136 / 3.9k† | 0.00x | noise |
| metal | medium | 87.174 | 11 471 | 0.04x | 0.67x |
| metal | large | 1156 / 90† | 9k / 110k† | 0.00x | noise |
| metal | xlarge | 180.505 | 554 001 | 0.02x | 0.40x |
(†) per-mode AttestationApply on this hardware shows large run-to-run variance (10 ms p50 ↔ 1000 ms p99 in the same cell). The 2-phase locate
- writeback architecture adds one extra dispatch per round, and on the integrated GPU each dispatch can stall arbitrarily during GPU power-state transitions. Use FullRound numbers for stable comparison.
CPU peak: 23.5 M attestations/sec at xlarge. Metal is dispatch-bound at all sizes — the architectural change is correct (per-thread parallel work) but the M1 Max integrated GPU's dispatch latency dominates. The benchmark captures the architecture cost honestly.
ProvenanceApply — model registry
| backend | size | mean (ms) | ops/sec | vs CPU |
|---|---|---|---|---|
| cpu | small | 0.075 | 67 013 | 1.00x |
| cpu | medium | 0.629 | 79 486 | 1.00x |
| cpu | large | 3.095 | 161 526 | 1.00x |
| cpu | xlarge | 3.195 | 1 565 153 | 1.00x |
| metal | small | 116.876 | 43 | 0.00x |
| metal | medium | 52.624 | 950 | 0.01x |
| metal | large | 173.517 | 2 882 | 0.02x |
| metal | xlarge | 308.984 | 16 182 | 0.01x |
ProvenanceApply stays single-thread by design — the version counter on UpdateWeights is order-dependent.
AnchorApply — audit anchor chain
| backend | size | mean (ms) | ops/sec | vs CPU |
|---|---|---|---|---|
| cpu | small | 0.091 | 109 945 | 1.00x |
| cpu | medium | 0.811 | 123 258 | 1.00x |
| cpu | large | 8.038 | 124 404 | 1.00x |
| cpu | xlarge | 33.040 | 302 667 | 1.00x |
| metal | small | 213.203 | 47 | 0.00x |
| metal | medium | 253.513 | 394 | 0.00x |
| metal | large | 629.953 | 1 587 | 0.01x |
| metal | xlarge | 900.272 | 11 108 | 0.04x |
AnchorApply stays single-thread — chain integrity (parent_root chain) is fundamentally sequential.
EpochTransition — root computation only
EpochTransition is a single keccak that binds attestation_root,
model_registry_root, and audit_root into aivm_state_root. Single dispatch,
single thread (same as v0.58.3 — multi-dispatch parallel was tried and
reverted; the dispatch overhead was higher than the saved keccak work on
this hardware).
| backend | size | mean (ms) | trans/sec | vs CPU |
|---|---|---|---|---|
| cpu | small | 0.014 | 6 918 548 | 1.00x |
| cpu | medium | 0.013 | 74 142 175 | 1.00x |
| cpu | large | 0.014 | 723 102 399 | 1.00x |
| cpu | xlarge | 0.013 | 7 741 975 442 | 1.00x |
| metal | small | ~3 ms* | (variable) | 0.00x |
| metal | medium | 3.271 | 305 754 | 0.00x |
| metal | large | 2.613 | 3 827 135 | 0.01x |
| metal | xlarge | 2.558 | 39 091 069 | 0.01x |
(* metal small showed an outlier 6.7 s mean during this run, dominated by GPU power-state transitions during the 60s budget. p50 was ~4 s; the kernel itself completes in ~2 ms on warm hardware.)
EpochTransition shows the floor: even with no per-op work the GPU pays a ~2 ms dispatch + roundtrip on the integrated GPU. The CPU finishes the same finalise in 13 µs. This 2 ms is the effective minimum for any AIVM round on this backend.
Why GPU still loses
The kernels in src/aivm_*.metal are now split into apply-locate + apply-
writeback (parallel) phases. The writeback runs one thread per slot at
threadgroup width 256. However:
- The locate phase is still single-threaded (preserves canonical-order slot assignment, byte-equal to CPU oracle). For xlarge this is 100 k ops × ~700 ns/op probe work = ~70 ms — it's the bottleneck of AttestationApply.
- The writeback phase is parallel but on this hardware the
dispatchThreadgroupsoverhead (~1 ms) is comparable to the saved work for kAttSlots = 1024. - EpochTransition is single-thread by necessity (the multi-kernel parallel split was reverted because dispatch overhead exceeded the keccak savings).
The CPU substrate is also doing real work:
- Vector keccak-f1600 inlined per-step
- Direct array access into pre-allocated arenas
- No syscalls, no dispatch, no kernel-launch overhead
For 100 k attestations the CPU finishes in 41 ms — that is 410 ns per attestation, dominated by one keccak permutation per 140-byte leaf. There is no PCIe bus, no thread-group barrier, no command buffer, no fence.
Where GPU is the right tool
The benchmark reports raw substrate throughput on M1 Max integrated. AIVM's actual production value from the GPU substrate is:
- Offload from the validator CPU during consensus — the CPU is busy running Quasar consensus and can't also run keccak chains. Even at 0.05× CPU throughput, the GPU running the AIVM round in the background frees the CPU to keep up with consensus.
- Cross-backend determinism enforcement — having three independent GPU implementations that must agree byte-for-byte with the CPU oracle is the mechanism that catches consensus-level bugs in any single backend. v0.59 extends the determinism harness with a size sweep (small/medium/large) on top of the v0.58.3 brief workload.
- Discrete-GPU acceleration — on Linux+CUDA hosts (where dispatch latency is ~10 µs not ~1 ms) the v0.59 architectural split should yield the per-thread parallelism speedup the brief targets. CUDA path is built and tested separately on those hosts.
Reproducing
cmake -S /Users/z/work/luxcpp/aivm -B build \
-DCMAKE_BUILD_TYPE=Release -DLUX_AIVM_ENABLE_WGPU=ON
cmake --build build --target aivm-layout-test aivm-gpu-engine-test \
aivm-determinism-test aivm-benchmark
ctest --test-dir build --output-on-failure
./build/aivm-benchmark > BENCHMARKS_V059.txt
Wall time on the M1 Max for the full benchmark: ~5 minutes.