soft3/zheng/audit/ivc-speed-2-2026-10.md

IVC step and wrap chain speed, round 2 — 2026-10-14

superseded by hemera 0.4 (2026-10-16): the hemera figures below (x⁻¹ partial rounds, 8 + 16 rounds, 144 constants, ~736 constraints) describe the 0.3 record; the frozen profile is x⁷ in every round, 6 + 6 + 48 rounds, 240 constants, ~1,440 constraints — hemera/audit/profile-0.4-2026-10.md.

Follow-up to audit/ivc-speed-2026-10.md (zheng #63, lens #24). Items from the coordinator: (1) the hemera device kernels — batched partial inversions, tuned arithmetic — on Metal, WebGPU and the CPU path, target ≥ 3× on the wrap chain; (2) a compiled device program for the zerocheck line sums; (3) prover keys from disk; (4) the step's serial CPU overlapped with device work. Branches: lens feat/kernel-speed off release/0.2 (61c2a8f), zheng feat/ivc-speed-2 off release/0.4 (15b9549). Same machine and stand as round 1; the machine was shared with the fast-path agent's runs during the session — the tables below are runs taken when its jobs were idle (1-minute load 5–7 at the start). Every change is prover-only: no transcript, shape, key or pin changes.

1. results

Metal (round 1 → round 2) WebGPU CPU
hemera grind, 24 bits (62 M nonces; gpu_bench) 1,301 → 905 ms (1.44×) 2,084 → 1,294 ms (1.61×) 4,659 ms (unchanged)
hemera permute 2^20 (with copies) 26.6 → 19.7 ms 79.5 → 83.8 ms 81.9 ms
IVC step, hash.tri 1.19 → 1.12–1.15 s 2.29 → 2.34–2.39 s 2.79 → 2.65–2.70 s
IVC prove (step + decider) 1.88 → 1.77–1.79 s 3.25 → 3.28–3.32 s 3.73 → 3.61–3.66 s
wrap 0 / 1 / 2 3.60 / 7.63 / 12.47 → 3.31 / 6.31 / 9.24 s 6.30 / 24.01 / 27.01 → 6.51 / 17.47 / 25.41 s 7.24 / 19.19 / 36.11 → 7.63 / 18.86 / 33.28 s
wrap chain 23.7 → 18.9 s 57.3 → 49.4 s 62.5 → 59.8 s
key derivation, IVC + 3 levels 0.3 + 0.9 + 3.2 + 0.2 → 0.10 + 0.15 + 0.11 + 0.19 s (cache warm) 0.10 + 0.15 + 0.10 + 0.20 s 0.11 + 0.16 + 0.13 + 0.15 s
whole profile-6 process (keys + IVC + chain + verifies) 31.0 → 21.9 s 71.5 → 53.5 s 79.5 → 65.1 s
peak RSS 10.5 → 11.2 GB 13.4 → 12.0 GB 11.4 GB

Same bytes: hash.tri IVC 59bfc9c5…, add.tri IVC 01cfef5e…, final 7580817e… — SHA-1 equal on Metal, WebGPU and CPU, and equal to round 1's (audit/ivc-speed-2026-10.md §1).

2. what was done

item change result
1 Metal hemera's CPU kernel structure in MSL: representatives below 2^64 between operations (one reduction a product), wide sums reduced once (external layer, Σ_{i≥1} s_i), fused multiply-adds, the partial rounds' next S-box input carried as (d₀+1)·y + (Σ + c); four states a thread, one inversion a partial round (Montgomery's trick, zero ↦ zero by masks); the sponge, coset-leaves and Merkle kernels take four items a thread (gid + k·T, coalesced) compute 1.44×; coset leaves in the step 660 → 463 ms
1 WebGPU two states a thread, one inversion a partial round in the permute, grind, sponge and Merkle kernels grind 1.61× (an inversion is ~16 limb products of 16 bits per multiplication in WGSL); the coset-leaves kernel stays single (paired it read slower: its cost is the values' readback)
1 CPU already in hemera main (#17): 16-lane batches, Montgomery's batch inversion across lanes, aarch64 multiply-reduce in assembly, the partial-round chain interleaved nothing to add
2 compiled line-sum kernels from the recorded program (straight-line MSL, then blocks of 256 operations as functions with live values through a private array) rejected: the 12,307-operation program compiled in 384 s and ran 2.5× slower than the interpreter (275 vs 107 ms round 0); the inline form did not finish compiling in 10 min. A wide-accumulating Fp3 product in the interpreter did not move it either (107 vs 105 ms): the interpreter is bound by its register traffic, not its arithmetic. Kept: the interpreter
3 committed key trees on disk (ZHENG_KEY_CACHE=<dir>, recursion::keycache): stored under a 128-bit digest of the members' coefficients, layout and fan-in; a damaged or foreign file ignored; roots still checked against the pins chain keys 4.8 → 0.55 s; IVC key 0.25 → 0.10 s
4 the zerocheck's columns (local, successor, public, key, public input) assembled and packed for the device on other threads while word c commits on the device step −30–40 ms

why ≥ 3× on the chain is out of reach with the kernel alone

Cross-thread batching does not pay on a GPU: a SIMD group runs one instruction stream, so one inversion shared by 32 lanes costs those lanes the same 72 multiplications as 32 private inversions — the saving needs several states per thread. Measured: states in threadgroup memory (4 a thread) ran 1.8× slower than registers (the state traffic); four states in registers with the old arithmetic gained 13 % (register pressure); removing the inversions entirely (not hemera) gained 1.5× — the ceiling of any inversion trick. With the lazy arithmetic and four states a thread the Metal kernel is 1.44× on pure compute; the chain gained 1.25× (its coset-leaves and Merkle dispatches move memory as well as hash).

3. review

Prover-only changes (no transcript, key, shape or ledger change); the evidence is byte identity and the differential tests:

check result
lens differential (LENS_GPU_REQUIRE=metal,webgpu): permutations (random, zero and p − 1 lanes, all-zero states), grinding across dispatches, sponges, coset leaves (kept and ragged groups), Merkle levels, line sums and sessions 14/14
new: states built backwards through the first four full rounds so the first partial S-box input is zero, at every position of a batch (Metal: four a thread; WebGPU: pairs) equal to the CPU on both devices
new: line sums over runs of edge limbs (p − 1, p − 2^32, 0, 1) equal
proofs: IVC (hash.tri, add.tri) and the add.tri chain's final proof on Metal / WebGPU / CPU SHA-1 equal, and equal to round 1's
key cache: a stored tree loads as the committed one (binary and 4-ary), another key's members never hit it, a truncated file is ignored and recommitted recursion::word::tests::a_cached_key_tree_is_the_committed_one
zheng workspace see §4

4. runs

run result
zheng cargo test --release --workspace 368 passed, 0 failed, 17 ignored
zheng wrapped_envelope --ignored (fresh profile-6 prove on Metal, inner roots checked, bit-flip sample) 4 passed
joy release/0.4 wrapped_execution --ignored (e2e profile 6) over this zheng and lens passed

5. what is left

  • IVC step 1.12 s on Metal (≤ 1 s missed by ~0.12 s). Device ≈ 0.6 s now; the zerocheck interpreter (≈ 0.23 s) and the serial CPU (circuit build 72 ms, the accumulation's eq tables ≈ 120 ms) are the rest. Next: specialised zerocheck kernels per constraint kind instead of one interpreted program (compiled whole programs are not viable on the Metal compiler), the accumulation sumcheck's first round started before γ (the accumulator's claims are known at the step's start).
  • Wrap chain 18.9 s on Metal (≤ 5 s missed by ~4×). Still ≈ 0.9 G hemera permutations, now at ≈ 55–70 M/s; the final level ≈ 9 s. The remaining levers are structural (a smaller final circuit or a cheaper final-level commitment) — no kernel-only path reaches 3×.
  • WebGPU: the kept-codeword readback dominates its commitments (coset leaves 1.6–1.8 s in the step); a device-resident codeword would remove it.

Graph