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

IVC step and wrap chain speed, with its review — 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.

note (2026-10-27): the .stands/ checkouts named below were removed in the cleanup of 2026-10-16; the revisions they held are named here, the directories are gone.

Owner directive (2026-10-14): proving ≤ 1 s. User-device proofs take the fast path (profile 1, another branch); this round owns the recursive path — the IVC step per segment and the profile-6 wrap chain. Targets: IVC step ≤ 1 s per segment on Metal, wrap chain ≤ 5 s on Metal, then lower.

Branches: zheng feat/ivc-speed off release/0.4 (f35e13b), lens feat/ivc-speed off release/0.2 (5796e50). Stand .stands/IVC (siblings: nox release/0.4, hemera/strata/tade/honeycrisp main). Baseline = the same fixtures built from the base commits (.stands/IVC/base). Apple M4 Max (12P + 4E), 40-core GPU, 48 GB, macOS 26.4, rustc 1.95.0, --release. Quiet machine for the tables (1-minute load 3–7 at the start of each block; the GPU idles at ~30 % from the desktop).

1. results

IVC (profile 5/6 recursion), hash.tri — one step of 2^15 rows and the decider (ivc_bench)

base Metal Metal base WebGPU WebGPU base CPU CPU
step (pre-commit + circuit + prove + self-check) 3.45 s 1.19–1.21 s 5.31 s 2.26–2.32 s 5.72 s 2.77–2.81 s
decider (once per run) 1.23 s 0.67 s 1.80 s 0.99 s 1.56 s 0.94 s
IVC prove 4.68 s 1.86–1.90 s 7.10 s 3.24–3.26 s 7.29 s 3.71–3.76 s
process CPU 9.8 CPU-s 7.2 CPU-s 10.3 7.7 87 42.5
proof 347,197 B 518,905 B (4-ary paths, 1/8) same bytes same bytes

New step on Metal, phase by phase (ms, wall): pre-commit 97 · circuit 72 · trace 8 · word b 93 · word c 102 · zerocheck 352 · deferred folds 18 · shift 6 · accumulation sumcheck 123 · accumulator commit 301 · openings and grinding 12 · self-check (included) — 1,188.

wrap chain (profile 6), add.tri (wrap_bench, keys derived in-process)

base Metal Metal base WebGPU WebGPU base CPU CPU
IVC prove 4.59 s 1.92 s 6.93 s 3.36 s 7.92 s 4.31 s
wrap 0 / 1 / 2 6.20 / 15.53 / 19.59 s 3.60 / 7.63 / 12.47 s 6.95 / 24.86 / 34.50 s 6.30 / 24.01 / 27.01 s 7.63 / 31.32 / 44.76 s 7.24 / 19.19 / 36.11 s
wrap chain 41.3 s 23.7 s 66.3 s 57.3 s 83.7 s 62.5 s
key derivation (once per chain: IVC + 3 levels) 0.6 + 1.7 + 4.3 + 0.2 s 0.3 + 0.9 + 3.2 + 0.2 s 1.1 + 2.5 + 8.1 + 0.2 s 0.4 + 2.3 + 7.6 + 0.2 s 1.1 + 2.7 + 8.4 + 0.2 s 0.6 + 2.6 + 9.2 + 0.2 s
whole process 53.4 s, 103 CPU-s 31.0 s, 53 CPU-s 85.3 s 71.5 s 104.2 s, 1,385 CPU-s 79.5 s, 1,095 CPU-s
peak RSS 13.0 GB 10.5 GB 12.7 GB 13.4 GB 12.6 GB 11.4 GB
final proof 62,965 B 63,029 B same bytes same bytes
final proof bound (every transcript) 64,213 B 64,213 B
verify (statement prepared) 1.9 ms 1.9–2.2 ms

The base run of this table measured 41.3 s on Metal; the quiet base run of the morning 37.1 s (audit/prover-profile-2026-10.md §4d: 34.5 s).

Same bytes: IVC proof 01cfef5e…, wrap 0 2931bd82…, wrap 1 d3a1e805…, wrap 2 c7bff6d2…, final 7580817e… — SHA-1 equal on Metal, WebGPU and CPU; hash.tri IVC 59bfc9c5… on all three. Every engineering-only commit was checked against the base bytes before the format changed (hash.tri 6b158176…, add.tri dbb617f9… equal to release/0.4 on all three backends).

2. what changed

commit change kind effect (Metal)
lens b21e1a2 Backend::coset_leaves_kept: a held codeword committed in one device pass (coset NTTs, leaf sponges, values copied out); Metal, WebGPU, checked/spot/record wrappers engineering the held commit's interleave, lane layout and copies leave the CPU
lens b21e1a2 grinding dispatches start at 2^bits and double to 2^22 (were 2^16, then 2^22) engineering a 16-bit grind cost 4 M nonces in a third of the calls: IVC grinding 396 → 46 ms
lens b21e1a2 prof-* backends: wall time per device operation (LENS_PROFILE_EACH=1 per call) tooling the measurements below
zheng 68f779c held words through stream::commit (device takes every coset group; CPU only for declined groups — a CPU worker's coset was ~30× a device group's share); codeword kept coset-major; pre-committed words a kept for their steps (2 GiB budget, ZHENG_KEEP_WORDS_BYTES); circuit trace without the key, permutation rows and phase 2 in threads; line folds in threads engineering step 3.45 → 2.3 s
zheng fb96ee7 every IVC tree 4-ary; profile 6's IVC at rate 1/8 (ivc::SHIPPED_RATE) soundness-relevant (format) step circuit 29,628 → 24,259 rows at 1/8 (20,348 at 1/16); every step word half the 1/16 codeword; step 2.3 → 1.55 s
zheng 18d63c6 field-native WHIR schedules rebalanced (recursion::whir::schedule) soundness-relevant (schedule) final level grinding work 2^28.3 → 2^27.7, level 1 2^26.7 → 2^25.0
zheng f6ee2a4 accumulation sumcheck over borrowed base/Fp3 tables (mixed first round); base spot points' eq tables in mixed arithmetic; the accumulator's combination and every Möbius transform in threads engineering step 1.55 → 1.39 s
lens 8f94ce8 / zheng bd1c5f1 LineSession::columns; zerocheck rounds of ≤ 256 pairs on the CPU (the device round's ~11 ms latency floor); transposes, column packing, successor columns and the shift combination in threads engineering step 1.39 → 1.17 s

Formats: IVC key layout ZHKIVC02, RECURSIVE_FORMAT 3, WRAPPED_FORMAT 6. Re-derived on the CPU backend: KEY_ROOTS (the three IVC sets; unchanged by the schedule), INNER_ROOTS, RECURSIVE_DIGEST, WRAPPED_DIGEST (examples/wrap_keys: "roots match the pins").

structural options measured, and the choice

option measured / planned decision
4-ary IVC trees path permutations halve: step circuit −9,280 rows at 1/16; prover nodes a third; IVC proof +27 % (multi-opening siblings) taken
IVC rate 1/8 (with 4-ary) words half the 1/16 codeword; 77 spot checks; circuit 24,259 rows; wrap 0 48,453 rows (2^16) taken (profile 6)
IVC rate 1/4, 20 grinding bits circuit 32,677 of 32,768; combination grinding 2^23 per step (~0.25 s); wrap 0 63,671 rows rejected: grinding eats the saving, no headroom
smaller segments (2^14) step circuit ≥ 18,535 rows at any rate/grinding (ivc_plan) > 16,384 infeasible
final circuit 2^13 (2-level or smaller final) final circuit ≥ 13,778 rows over any inner rate tried (wrap_chain: 1/128…1/1024 inner, 3 and 4 levels) infeasible without a new final circuit
per-round grinding budget (audit wrap-fastverify §10) generalised: per round the regime and both grindings for the least work at lens's queries taken (whir::schedule)
final level 4-ary siblings ×1.5 → over the 64 KiB gate rejected
parallel step proving (accumulation tree) not built: the step circuit would verify two proofs (≈ 2× rows: 48 K at 1/8, over 2^15) — needs n = 16 steps or a smaller verifier open

3. where the time goes now (Metal, prof-metal)

The chain is hemera-bound. Wrap 2 (12.2–12.5 s): coset leaves 5.5 s (217 M leaf permutations: one base word of 2^29 symbols, Fp3 rounds of 2^28 and 2^27), Merkle levels 2.0 s (59 M nodes), grinding 3.8 s (145 M nonces this run; 218 M expected), zerocheck 0.14 s, CPU ≈ 0.8 s. Device permutations run at 35–40 M/s (gpu_bench: grind 45 M/s); the chain is ≈ 93 % device time. The 1/512 final codeword and its size gate fix the permutation count: ≈ 500 M permutations ≈ 12 s at this kernel.

The step (1.19 s): device ≈ 0.75 s — three base words (pre-commit, b, c: 2^24 symbols, 2.1 M leaf + 0.35 M node permutations each, ≈ 70 ms), the Fp3 accumulator (6.3 M + 0.35 M permutations ≈ 200 ms), zerocheck line sums 227 ms (the interpreter: round 0 105 ms for 114,688 evaluations of 12,311 operations); CPU on the critical path ≈ 0.45 s — circuit build 72 ms, accumulation sumcheck 123 ms (eq tables of 88 claims over 2^21), zerocheck column assembly ~100 ms, the rest in threads.

4. review (soundness-relevant changes: 4-ary IVC trees, rate 1/8, rebalanced schedules)

attacks

attack result test
a spot check's 4-ary siblings permuted within a node group (all four words) refused ivc_speed_review::four_ary_paths_bind_their_leaves_and_positions
a sibling taken from another level refused same
another leaf's opening at a query's position refused (word 0 of a one-step proof is the zero accumulator: every leaf opens alike, the substitution is the honest opening — checked, and the two-step proof (tree-12) attacks a committed accumulator) same
a path one node short / long refused natively; through the wire the multi-opening carries each distinct sibling once, so a length-only tamper re-encodes to the honest bytes (accepted only then) same
the decider's 4-ary openings (accumulator tree, key-word tree, final queries): siblings swapped, a sibling limb, a symbol refused same
zero accumulator: ZeroWord::new(layout, 4-ary) vs the committed all-zero word root and every opening equal ivc_speed_review::the_zero_accumulator_is_the_committed_zero_word
rebalanced schedules: queries vs lens's, every term ≥ 128 at every shipped level and the IVC decider equal queries; all ≥ 128 ivc_speed_review::rebalanced_schedules_keep_lens_queries_and_reach_128_bits, whir::schedule::tests
existing suites under the new shapes: recursion tamper set, recursion::review (forged state, unbound public input), proof_stack_review, wrap_review, wrap_fastverify_review, envelopes see §4 runs —

ledger, recomputed independently

audit/ivc-speed/ledger.py (Python, from the WHIR/BCGM formulas and the printed round specs only, no zheng code) reproduces the Rust rows:

level rounds: regime · queries · query/fold grinding comb. weakest (priced) interactive weakest
IVC decider 1/8, pow 16, ℓ 21, 3 words J13·77·17/12, J9·38·17/11, J7·26·14/11, J7·19·16/13 15 shift_2 128.04 111.04 (decider shift_2)
IVC accumulation 1/8 t 77, query 16, comb 17 spot 128.08 combine 111.19
level 0, 1/16, pow 16, 5 words J7·58·18/11, J7·33·16/14, J4·23·17/11, J3·18·16/11 15 shift_3 128.09 110.23
level 1, 1/128, pow 22, 5 words J5·31·24/17, J4·22·22/17, J3·17·22/17, J3·14·20/20 21 shift_1 128.24 104.24
final, 1/512, pow 26 J5·24·24/22, J4·18·24/22, J3·14·27/22 — fold_1 128.40 101.89 (fin after 27 bits)

Every priced row ≥ 128 (Python and Rust agree to 0.01). The tree fan-in changes no IOP row; Merkle binding is the words row (4-ary nodes as in the inner wrap levels since #56). Final proof bound 64,213 B (wrap::max_proof_bytes, unchanged: queries unchanged).

bit flips

proof flips decoded accepted
final (add.tri, shipped chain, wrap_flip) 504,232 (every bit) 503,437 0 (336 s)
IVC (add.tri, rate 1/8, 4-ary, ivc_flip, stride 4) 1,033,458 1,032,390 0 (1,955 s)

runs (final HEAD of feat/ivc-speed)

run result
cargo test --release --workspace 367 passed, 0 failed, 17 ignored
ivc_speed_review (new) 3 passed
keys -- --ignored (the prover's chain against INNER_ROOTS, the bundle against WRAPPED_DIGEST) passed
proof_stack_review --include-ignored (stored proofs of this chain) 5 passed (split-key limbs, substituted program at every level, bundles, size gate, ledgers)
wrap_fastverify_review --ignored 2 passed
proof_stack_backend --ignored (Metal; recording and corrupting backends) passed: no backend moves a verdict
wrapped_envelope --ignored (fresh profile-6 prove of hash.tri, inner roots checked) 4 passed, bit-flip sample 0 accepted; 97 s, 10.7 GB
recursive_envelope --include-ignored 5 passed
lens differential (LENS_GPU_REQUIRE=metal,webgpu) / rspcs 13 / 39 passed
joy release/0.4 over this zheng: cargo test --release --workspace 220 passed, 0 failed
joy wrapped_execution --ignored (e2e profile 6, Metal) passed, 79 s, 10.1 GB

5. what is left to 1 s

  • IVC step: 1.19 s on Metal (target ≤ 1 s: missed by ~0.2 s). Floor of this design at the current hemera kernel ≈ 0.75 s device + the serial CPU between them. Next: JIT-compiled zerocheck program on the device (the interpreter's round 0 is ~5× its arithmetic), the circuit build (72 ms, serial) and the accumulation eq tables (123 ms) on the device, overlap of CPU and device work across steps of a long run.
  • Wrap chain: 23.7 s on Metal (target ≤ 5 s: missed by ~5×). It is hemera-bound: ≈ 1 G permutations at 35–40 M/s; the final level alone ≈ 500 M (its 1/512 codewords and the grinding the 64 KiB gate requires). No parameter change measured here gets below ~20 s: the chain shape is pinned by circuit sizes (§2). Reaching seconds needs a faster hemera kernel (the partial rounds' inversions are 60 % of a permutation; batching inversions across states was measured earlier without gain) or a smaller final circuit / a different final-level commitment.
  • Keys: derivation stays once per chain (0.3 + 0.9 + 3.2 + 0.2 s on Metal); loading committed prover keys from disk is not built.
  • WebGPU: the chain is 57 s (Metal 24 s): its permutation kernel is half Metal's (gpu_bench 10 vs 19 M/s with copies).

Graph