Skip to content

STARK no-epoch (prove-and-retire) prover: block 25368371 — work in progress - #1013

Draft
MauroToscano wants to merge 1440 commits into
mainfrom
noepoch/stark
Draft

MauroToscano wants to merge 1440 commits into
mainfrom
noepoch/stark

Conversation

@MauroToscano

@MauroToscano MauroToscano commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

One STARK proof per block, with no epochs (the prove-and-retire / VADCOP shape). This is a second prover next to #1009's epoch-based one. #1009 stays the reference and this branch does not touch it. The branch starts from #1009's head cc411aa2c, so the diff against main includes #1009. Compare against cc411aa2c to see only this work.

Whole block: 31.78 s (FAST 456 at the head 08ecc4310, mean of 3 default arms), against the epoch tree's 60.15 s (#1009; FAST 389's same-binary reference, not re-run since). Base 21.28 s, recursion 10.25 s (level 0 5.18 · interior 4.88).

Head d8a422962 (10-05): the generators write the block's packed traces directly (G-pack). Every streamed chunk and every table the finish packs is written straight into its packed columns, with no table at 8 bytes a cell and no separate pack pass; LAMBDA_VM_BLOCK_GPACK=0 restores the old path. Prover-only, the same bytes: under the fixed trace hash and the deterministic grind the base digest is 0224203e… with G-pack on and off, and every table of the 1× block has the old path's packed bytes (BIG 634). Median block 25475471 on BIG, one binary, 3 + 3 (BIG 635): generator thread-seconds 399.8 → 105.7 (−73.6 %), phase A −7.47 s (t −10.2), base −7.35 s (t −10.6), phase B +0.10 s, VmRSS −3.28 GiB. See G-pack.

Head 58a58be95 (10-05): the production CLI proves and verifies a whole block. cli prove-block <ELF> --input <block> -o <proof> runs the whole-block harness's own driver (lfm::block_tree::prove_block_tree; the harness is now a wrapper around it, its output unchanged but for a first BLOCK POSTURE line), and cli verify-block <proof> <ELF> runs the block verifier over the proof file. The binary installs its jemalloc as the prover's allocator hooks, so the auto purge and the late leaf emission act in the shipped CLI, and it sets the measurement posture for every knob the environment leaves unset. Prover and CLI only: no proof byte moves. ULTRA 016, 8 + 8 against the harness after a warm-up: CLI − harness whole −0.161 s (t −2.4), base digest 0224203e… and top 2ef0e0d2… equal the harness's. BIG 632: the p90 25481021 proves through the CLI at 112.33 GiB with both purges (whole 553.77 s) and verify-block passes in 72.86 s. See Production CLI.

Head 4fc4b7b12 (10-05): LogUp k4 is the default on the base tables (Mauro, 10-05; LAMBDA_VM_ZF_LOGUP=pair restores the previous bytes). 1× tree on ULTRA, 8 + 8: base −0.89 s (t −15.6), whole −0.92 s (t −11.7); the p90 proves and verifies under k4 at the defaults (BIG 626: whole 555.29 s, 113.50 GiB, against 590.95 s and 115.79 GiB under pair in BIG 591).

Head 035aef5d6 (10-03): the p90 and full-gas blocks prove end to end at the defaults on BIG (128 GiB host, 120.69 GiB cgroup), and the block verifier passes on both. p90 25481021 (52.0 M gas, 612.3 M cycles, 20.1× the bench block): whole 590.95 s at 115.79 GiB (BIG 591). Full gas 25431071 (60.0 M gas, 602.9 M cycles): whole 541.51 s at 110.62 GiB (BIG 125). Before this head, auto alone ran out of memory in level 0 on the p90 (BIG 120). Prover and harness only: digest ac406fc8… is unchanged (FAST 515). At 1× nothing arms or purges (default against spill off, base −0.065 s, t −0.6, 8 + 8; FAST 515 + 516). Four changes:

  • auto counts the cgroup's working set (the charge less its inactive file pages), not its page cache.
  • The spill's queue budgets arm only once a spill is plausible: host + reserve ≥ 85 % of the target. A block that fits never pays them; the p75's phase-A cost under auto fell from +3.6 to +1.12 s (BIG 118 → BIG 122).
  • The allocator purge returns jemalloc's retained pages at phase A's end and at the base's end, only under memory pressure (auto, once the budgets arm). Blocks that fit pay nothing. LAMBDA_VM_ALLOC_PURGE=off|all|<points>. It acts in the lib's test builds (the harness's jemalloc); the production CLI has no purge yet. (Since 58a58be95 the CLI purges too, through its allocator hooks.)
  • The block verifier's derivation is bounded on the card by bytes, through the shared VRAM gate. Without the bound, the p90's verifier ran out of device memory after a verified proof (BIG 123); with it, the pool's high-water was 17.46 GiB.

Head 00871b913 (10-03): under the shared VRAM gate, a carrying proof now claims its resident bytes plus one headroom shared with the claims in force. These tight claims are on by default; LAMBDA_VM_SHARED_GATE_CLAIMS=whole restores the first form. This is scheduling only: digest ac406fc8… and top 93aa7097… are unchanged. Median block (BIG 474, 5 + 5): recursion −1.25 s (t −5.4), with level-0 claim waits falling from 8–10 s a run to 0. Whole −0.05 s: the base moved +1.18 s, which is scatter. At 1× there is no regression (FAST landing gate: recursion −0.01 s). See Recursion: sibling proofs share the card.

Head edddc6873 (10-03): the trace spill store is on by default as auto. A block that fits spills nothing. A block above the host's limit spills the committed traces that would not fit, instead of running out of memory. LAMBDA_VM_BLOCK_SPILL=off keeps every trace. This is prover-only: digest ac406fc8… is unchanged. BIG 117 at the median block: the default spilled 0 B, with phase-A end −0.97 s against off; forced to a 70 GiB target it spilled 30.1 GiB, cost +1.43 s and verified. The same head adds the spill writer's step timings and splits the AIRs out of BLOCK MEM's unnamed heap; both are diagnostics only. See Disk spill: auto by default.

Head a93018974 (10-03): the block tree's sibling proofs now share the card through one VRAM gate, on by default (LAMBDA_VM_SHARED_VRAM_GATE=0 restores the exclusive card permit). This is scheduling only: digest ac406fc8… and top 93aa7097… are unchanged. Against 564dd02bb (FAST 871): 1× recursion −0.68 s, whole −0.48 s. Median recursion −4.72 s (BIG 471, measured before the arming drained the device); at this head −4.24 s (BIG 472). See Recursion: sibling proofs share the card.

Head 564dd02bb (10-03): adds LogUp k4 as an opt-in (LAMBDA_VM_ZF_LOGUP=k4; default pair = the same bytes; under k4, median phase B −9.56 s and 1× whole −1.04 s).

Head 8f2ce57d5 (10-03): three block-tree changes, all with the same proof bytes and program ids (FAST 673 merge gate green: 1× top 93aa7097…, pinned ids unchanged; median top 6d85454d…, BIG 586).

  • Late leaf-program emission by default (NOEPOCH_TREE_EMIT_LATE=auto): when leaf 0 puts the other leaves' programs at ≥ 8 GiB, they are emitted in phase B onto pages the retired traces freed; smaller trees emit at the shape as before. Median, spill off (BIG 585): base-phase VmRSS −10.37 GiB, whole-run peak 98.99 → 86.49 GiB, recursion −0.31 s, whole −2.15 s; inert with spill on. NOEPOCH_TREE_EMIT_LATE=off restores the old behaviour, and A/Bs against it must set that.
  • The block verifier streams its derivation (production verify_block_tree): each program and its artifacts are dropped once its child is derived. Verifier alone in a fresh process at the median (BIG 587): peak 32.33 → 22.89 GiB, derive −1.48 s. LAMBDA_VM_BLOCK_DERIVE_HOLD=level restores the old hold.
  • Compact leaf programs on the host: the instruction vector is held at its length (−21 % of a program's held bytes, ids unchanged).

Note on drivers: the whole-block numbers here come from the test harness's tree driver (block_tree_pipeline, cfg(test)). The library has the base prover (block::prove_block), the per-node program emitters (BlockTreePlan::leaf_program / node_program) and the production verifier (verify_block_tree), but no production driver that chains them into a whole-block proof yet; that is a landing item.

Head dacbd5b4e (10-03): KECCAK_RND, ECDAS and KECCAK compose on a bounded-slot interpreter by default (LAMBDA_VM_GPU_INTERP_SI=0 restores the slot-file interpreter). Prover-only: digest ac406fc8… unchanged, FAST 784's landing gate green (1× default − off −0.05 s).

Constraint composition on a bounded-slot interpreter (prover-only; proof bytes unchanged). The three programs too large for the compiled kernels (KECCAK_RND, ECDAS, KECCAK) now compose on a bounded-slot GPU interpreter. The slot-file interpreter it replaces (from OpenVM v1) keeps every value of a row in a per-thread file in global memory: 4,350 words for KECCAK_RND, 2.1 GiB per composition, not counted by the VRAM gate. The new lowering evaluates each constraint's cone on demand in constraint-index order, reads trace cells as operands, and fits a row in 16–128 words of shared memory or a local array, recomputing past the budget; each program gets its measured best shape. On one binary, the three programs compose in 0.58–0.63× the time. At the median block (25475471, 3 + 3) the deciding figure is card work −0.43 s (t −1.9), with the slot-file scratch 80 GiB a run → 0 and the phase-B device peak −1.9 GiB (t −3.4); base −1.03 s is not significant (t −1.0); at 1× base −0.06 s. The digest is unchanged (ac406fc8…); LAMBDA_VM_GPU_INTERP_SI=0 restores the slot-file interpreter. Running all 39 programs on it is slower (1.25–2.1× the compiled kernels, +0.16 s at 1×), so the compiled kernels stay for the other 36.

Head d740eb5d5 (10-03): the block tree emits each node's program as soon as its children land, instead of level by level (NOEPOCH_TREE_NODE_EMIT=level restores the old order). This is scheduling only: the same top program id in all 14 arms.

  • Median recursion −7.32 s (BIG 483: 105.97 → 98.65 s).
  • 1× net −0.15 s (FAST 668).
  • FAST 669 gates are green.
  • Two more knobs ship default off: a leaf-emission window (NOEPOCH_TREE_EMIT_WINDOW; −12.9 GiB median base peak for +6.2 s recursion) and a cap on concurrent derivation builds (LAMBDA_VM_BLOCK_DERIVE_BUILDS).

Head 86e71de77 (10-03). Three landings since f805464f6:

  • aa726ce2b: block fan-in 4 (Mauro approved 10-02; see (f)).
  • 5e4176961: KECCAK and ECSM chunked, and tree partition rule v2, a format change. See the format section below.
  • 7ef261724 + 86e71de77: producer and memory work, all prover-only with identical proof bytes:
    • the compact derived LT ops;
    • smaller LOAD/CPU op records;
    • kept subtrees capped at 8,192 elements per table;
    • the walker levers (hand-out without copy, 2,048-cycle walk batches, in-place routing, sized window lists);
    • eight generator threads instead of six (LAMBDA_VM_BLOCK_GENERATORS=6 restores six);
    • the trace spill store with the S2 policy, off by default (LAMBDA_VM_BLOCK_SPILL=always turns it on).

The median block 25475471 on BIG (job 112, Zen 3 host):

  • Proves and verifies end to end: whole block 303.59 s (base 197.39 + recursion 105.46), against 318.25 s on BIG 480.
  • Whole-run VmRSS 99.07 GiB.
  • 1× digest ac406fc8… unchanged.

Disk spill (BIG 111, base only):

  • It holds the median at 30.0–30.3 GiB against 80–82 GiB without spill, reading back 56.5 GiB over 738 slots with 0 mismatches.
  • It costs +10 s on the base, mostly phase A, because one O_DIRECT writer manages about 1.06 GiB/s on that disk.
  • That is why it is off by default for now.

The walker levers with eight generators end the median's phase A 3.45 s sooner (BIG 468) and cost +0.14 s on the 1× base (FAST 470).

Head f805464f6: two more producer and memory changes, both prover-only with identical proof bytes:

  • 6cb6cd557, smaller per-step records: the CPU op shrinks from 128 to 96 B (arg2, the branch decision and the ECALL kind are derived) and MEMW_A ops become 48-byte aligned rows. FAST 612 at 1×: base −0.38 s, digest equal. BIG 424 at the median: phase-A end −6.0 s.
  • f805464f6: LT ops kept as segments rather than concatenated, and the packed KECCAK_RND / LT finish builds back on by default. BIG 107 picked them over capping wide KECCAK_RND chunks: at the median they save about 14 GiB and end phase A 2.6 s sooner.

BIG 108 at this head: 1× verified at 15.1–16.2 GiB with digest ac406fc8…. The median block verifies at 81.8 / 82.4 GiB with base 212.4 / 216.0 s. Phase B scatters run to run by about 2.7 s (sd) at the median.

Head 330057f2a: the packed KECCAK_RND / LT finish builds are now off by default (LAMBDA_VM_BLOCK_PACKED_BUILD=1 turns them on), and the memory knobs that had no effect are removed. Two one-binary A/Bs at the median (BIG 104, 105): packed builds save 13–15 GiB of host peak but cost phase B ≈ 3–6 s. BIG 105 at this head: 1× verified at 18.32 GiB, digest ac406fc8…; median block 96.2 GiB, base 218–224 s on BIG.

Head 161b82415: six generator threads by default (LAMBDA_VM_BLOCK_GENERATORS=0 keeps the committers generating). With the faster walk the three committers, which also generated every chunk, fell behind: at the median block 32.8 GiB of op lists waited and phase A ended 19 s after the finish. Generators build and pack each chunk; the committers only commit. KECCAK_RND and LT in the finish are built packed a block at a time (LAMBDA_VM_BLOCK_PACKED_BUILD=0 builds them wide). BIG 103/104 on the median block 25475471: base 231.8 → 222–226 s, host peak 96.1 → 82.7–83.0 GiB; 1× base flat; digest ac406fc8… unchanged. Within one binary, packed builds cost phase B ≈ 6 s at the median against wide builds and save 14.75 GiB; see the head above for the current default.

Head 3be24abab: narrow storage on by default (merged from a2cd24207). Main-trace cells are stored at about 2 B each instead of 8 and widened on the card; LAMBDA_VM_BLOCK_NARROW=0 keeps them wide. BIG 100 (A/B on one binary, the 128 GiB box): proof digests packed = wide = ac406fc8…; 1× host peak 31.2 → 17.7–19.0 GiB; 1× base −1.29 s on that box. A median mainnet block (25475471, 9.78× this one, 941 sub-proofs) proves and verifies within 128 GiB: host peak 106.87 GiB, base 259 s on BIG's Zen 3 host (BIG 100; that binary predates the E1+E4 producer changes below). BIG 102 re-gated the merged head: 1× verified, digest ac406fc8…, peak 19.29 GiB.

Head 2c440b8dc: base 19.61 s (FAST 602, mean of 2). It adds two producer changes to 19fe5e9e5, whose memory changes are in Memory: a lean walk (the walker builds only what the tables need) and the executor's guest memory in 64 KiB pages instead of a hash map of words. A B B A on one binary: base 20.38 → 19.61 s (Δ −0.77, no overlap); proof bytes equal under LAMBDA_VM_FIXED_TRACE_HASH=1 + LAMBDA_VM_DETERMINISTIC_GRIND (digest ac406fc8… in both arms); host peak flat. At a median-size block (25475471, harness, windows only) the producer's windows phase falls from 42.4 to 25.2 s and the executor from 108 to 11 ns per cycle. LAMBDA_VM_WALK_LEAN=0 and LAMBDA_VM_EXEC_MEMORY=words restore the old paths. The whole tree has not been re-run at this head.

Review: SOUND-FOR-LANDING at host parity (§F2); open: (d) host attestation posture; parked: (e) the declared-height bound (Mauro 09-25).

⛔ Not mergeable. Open before this can land:

  • (a) A race in the GPU prover: root-cause the R2 H-corruption race so the global serialize lock can become a targeted drain #929 class. CLOSED. It was not GPU prover: root-cause the R2 H-corruption race so the global serialize lock can become a targeted drain #929. The kept-top-levels recompute returned with its trace snapshot still queued, and the LogUp aux build read it on another stream with no wait, so some proofs carried stale aux rows (7 of 47 whole-block proofs were refused, every one caught by the verifier). The fix e80674f6d makes the resident aux build wait on the main snapshot's ready event: 52/52 whole-block proofs verified with it (FAST 432 40/40, FAST 433 12/12), cost −0.05 s, so kept top levels' −4.89 s stands. Note: I-R2RACE.md. FAST 392 at this head: 2 of 2 arms green on the first attempt.
  • (b) Review M1: the tree's programs are not derived on the verifier side. CLOSED (follow-up review R-NOEPOCH-S3.md §F1: "M1 is closed structurally"; its minors are done at a1a3a2c22, gate FAST 395, which this PR takes with its next fast-forward).
    • Per-table shapes now come from the AIR, the trace length and the options (TableChallengeShape::derive, TableVerifyShape::derive).
    • lfm::block_plan::BlockTreePlan::derive runs verify_proof_parts' pre-checks on the claimed shape and builds the host verifier's AIR set. It fixes the partition and the carrier, and derives every leaf, node and top program with no proof.
    • verify_block_tree(elf, shape, output, top) takes no options: it derives under the block presets, so a caller cannot hand it fewer queries (F1-m-a). Both presets stamp the process's ZfFormat, so the verifier's environment enters the derived id; a mismatch refuses, which costs completeness, not soundness.
    • The plan's DECODE and ELF data-page roots are the host's recompute from the ELF (ElfConstants), never the process-wide record the prover's device wrote (F1-m-b; m4 is now fully closed).
    • The partition's cost model is versioned (PARTITION_COST_MODEL = 1, pinned by a test).
    • The epoch tree keeps the gap. The derivation functions are reusable for it.
  • (c) Review minors m1–m5. CLOSED (§F1 closes m1, m2 and m3; m4 is closed by F1-m-b; m5 by the pad-byte negative, FAST 393 and 395).
    • The output halves' canonicity is pinned by the carrier leaf's COMMIT-bus target (emit_output_bytes), carried to every leaf by the nodes' out binding, and checked exactly by top_claims. The block front absorbs only each half's live bytes and does not refuse a non-canonical half; a laptop test shows both of these behaviours.
  • (d) Host attestation posture only. The ELF-derived roots ("A") are program text in the leaves, not supplied-roots cells bound by the attestation.
  • (f) For Mauro: block fan-in 4, a tree-format change. It is −1.0 s on the recursion (FAST 399; fan-in 3 is +1.15 s against 4, FAST 450). It changes every node program and the top. BLOCK_FAN_IN is a verifier-side constant for the block tree only; STARK recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 60.18 s #1009's FAN_IN = 2 is untouched. Approved by Mauro 10-02 and landed at aa726ce2b (FAST 661: tree negatives refused, derived top = proved top).
  • (e) Parked (Mauro 09-25): declared-height bound / k-bit PoW before γ, the same as the host verifier. The review's F1-M1: check_shape caps declared trace lengths only at the field's two-adicity, so a prover-chosen height can push a free-height table's DEEP-batching phase below 128 proven bits. This is the host verifier's own exposure, parked on 09-25. The block verifier is at parity with the host.

Review: thoughts/zf/gap2/fix2/R-NOEPOCH-S3.md. The first round read SOUND-FOR-MEASUREMENT (0 blockers, 1 major, 5 minors). §F1 closed M1 and m1–m5. §F2 reads SOUND-FOR-LANDING at host parity on a1a3a2c22, and it confirms the pad-byte correction: the refusal is emit_output_bytes in the carrier leaf, and out is not redundant.

The whole block, epoch tree vs no-epoch tree (FAST 389, one binary at e4ff3f8fb, means of 2 arms)

Block 25368371. Both trees run on one test binary, and its md5 is checked after every arm. The epoch arm is #1009's record harness, unchanged.

no-epoch (this PR) epoch (#1009) Δ
base 24.91 s 26.9 s
through the last interior node 39.01 s 60.15 s −21.14 s
top node (closes the bus) / block-artifact root, on a line of its own 1.67 s 1.70 s (+ 0.3 s verify)
complete 40.67 s 61.85 s −21.18 s
  • The epoch WHOLE of 60.15 s reproduces STARK recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 60.18 s #1009's 60.18 s, so this binary's epoch tree is STARK recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 60.18 s #1009's. If the root replaced the top interior node (option A; an estimate, not measured), the epoch would complete at about 60.35 s, a Δ of −19.68 s. Pre-registered row: no-epoch whole − epoch WHOLE = −19.48 s, band [−24.5, −17.5], IN.
  • The no-epoch recursion is 15.51 s (pre-registered band [12.5, 16.5], IN): harvest 2.03 s, then level 0 with 8 leaves at 6.25 s, then the interior at 7.22 s (3.41 + 2.15 + 1.67).
  • Each harness verifies on the host what it reads. That verify is not work a production driver does, so both trees overlap it with proving: STARK recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 60.18 s #1009's per-epoch verifies run beside its wraps, and the block's 6.8 s verify of the base runs beside level 0. The join waited 0.00 s.
  • Levers measured in that run: harvest verify beside level 0 −3.21 s (EFFECTIVE); level-0 siblings 8 −0.13 s (no effect).
  • The recursion: one leaf per instance list, each leaf replaying the statement and Phase A over all 137 roots; nodes that check one state digest, one claim and summed bus shares; a top node that asserts the bus closes. Since a0954fd1b the lists are the plan's: D-NOEPOCH §12.2's rule over the closed-form costs, a pure function of the block's shape. On this block the rule gives other lists than §12.2's, which came from legmodel.py's costs, but the leaf tiers are the same. FAST 392 at cda620461 (means of 2): level 0 6.21 s, interior 7.19 s, harvest 2.03 s, whole 40.38 s, which is 389's numbers. The verifier's own derivation of the top program takes 8.35 s; that is outside the whole, and the derived top equals the proved top. A tree over another partition, a skipped instance or a duplicated instance is refused at the final check (fixture gate).

The plan's gate and the harvest lever (block 25368371, means of 2 arms, 389's posture)

FAST 389 B2 (§12.2's lists) FAST 392 cda620461 (the plan) FAST 394 a684f4f3f (+ ELF constants beside the base) FAST 395 a1a3a2c22 (+ F1 follow-ups) FAST 396 e7406d6b5 (+ rebuilt windowed builder) FAST 397 6b86432a2 (+ verifier derive)
base 24.91 s 24.70 s 25.02 s 25.05 s 21.77 s 21.86 s
harvest 2.03 s 2.03 s 0.19 s 0.19 s 0.18 s 0.19 s
level 0 (8 leaves) 6.25 s 6.21 s 6.24 s 6.23 s 6.31 s 6.25 s
interior 7.22 s 7.19 s 7.21 s 7.20 s 7.22 s 7.23 s
whole 40.67 s 40.38 s 38.89 s 38.91 s 35.73 s 35.78 s
verifier derivation (verify_block_tree, no proof read, outside the whole) n/a 8.35 s 8.28 s 10.5 s (the data-page roots are now host recomputes) 10.55 s 8.59 s cold, 5.50 s warm (ELF constants cached)
  • Harvest lever (FAST 393, A/B on one binary): EFFECTIVE, −1.80 s on the whole. The plan's ELF-only constants are computed on 4 threads while the base proves, and the harvest joins them. The harvest's plan step fell from 2.02 to 0.16 s, and the base moved +0.02 s. At 395 the constants, including the data pages, take 10.9 s beside a 25 s base, and the harvest waits 0.00 s for them.
  • FAST 396's e7406d6b5 and FAST 397's 6b86432a2 are both on this branch; 6b86432a2 is the head.
  • Rebuilt windowed builder (FAST 396, A/B on one binary): EFFECTIVE, base 21.55 s. The WHIR lane's builder (walker and accumulator split, segmented windows, BITWISE counted per window), driven the way the WHIR block drives it: a walker thread only walks, the producer routes, the committers generate each chunk before its Round-1 commit. Interleaved means: new 21.60 s, the previous schedule over the same builder 23.97 s, serial phase A 25.71 s. The 10 default runs average 21.55 s, against 24.52–25.17 s at FAST 395. The whole tree is 35.73 s, against 38.91 s. The builder came in as 7 cherry-picks (-x) of noepoch/windowed-builder 6e99c74..1d24a2c, not a merge of that branch (90 WHIR commits, 6 conflicting files), plus our parallel gating, which compiles the recursion ELFs (5fe8316, now byte-identical to the builder branch's bcccdef).
  • Verifier derive (FAST 397): cold 10.55 → 8.59 s, warm 5.50 s. verify_block_tree_with(elf, &ElfConstants, …) lets a consumer compute the per-ELF constants once (3.05 s cold), and each node level is now emitted and built in parallel (level 1: 1.31 s wall for 5.0 s of work). Split at 397: constants 3.05 · plan 0.16 · leaves 2.17 · L1 1.31 · L2 0.93 · L3 0.72 · check 0.24.
  • Recursion levers (on noepoch/stark-s3 at 3992a56f9, gated by FAST 399; A/B on one binary each):
    • Child proofs verified beside the timed path: −0.62 s, now the default. Every verify is still joined, and a refusal fails the run before anything is reported.
    • Block fan-in 4: −1.02 s, see (f).
    • The tree's programs derived beside the base with host-built artifacts: a regression, because host builds are about 100× slower than the card's. With everything ready, the mechanism is −3.8 s.
    • The pipeline instead (FAST 451: −2.56 s on the recursion, whole block 32.30 s), the default since f6445c8b6 (FAST 452, gated: −2.48 s against NOEPOCH_TREE_AHEAD=0, P8 10/10). The leaf programs are emitted beside the base. Leaf and node artifacts are built on the card during level 0, and each node program is emitted as soon as its children's artifacts exist. Base +0.01 s; every child proof verified.
    • Builder first at the card permit (FAST 453, c9c29cb5b, NOEPOCH_BUILDER_FIRST, off): recursion −0.60 s in its A/B, but not by the mechanism pre-registered. It cannot pre-empt a leaf's multi_prove, so the level-1 artifacts are still late; what it did was remove level 0's slow mode (≈ −0.4 s pooled over 451–453). Not the default.
    • Node programs emitted on the builder's own pool (FAST 455, 7da2a45d0; the default since 08ecc4310, gated by FAST 456 and 457): recursion −0.29 s, level 0 −0.40 s. Level 0 was bimodal (5.2 or 5.8 s). In the slow runs, one leaf's multi_prove held the card for 1.2 s at about a third busy, starting at the instant the builder began emitting the level-1 programs on the global rayon pool (FAST 454's card trace). With a 4-thread host-only pool of its own, the stalled hold is gone in every run. NOEPOCH_EMIT_POOL=0 restores the global pool.
    • The shared windowed builder's parallel window concatenation (i-noepoch-w's 9d15e7b, cherry-picked as e029731f7; FAST 456): base −0.57 s (P8, 5 against 5 on one binary, no overlap), replicating WHIR's FAST 417 (−0.52 s). LAMBDA_VM_BUILDER_CONCAT=serial restores the old copy.
    • Not landed (recorded on the lane's branches): programs handed out before their artifacts with per-node emission (FAST 457, NOEPOCH_TREE_EAGER): recursion −0.06 s, because level 1 is limited by the card, not by waiting. The card trace (FAST 454) puts the idle inside the recursion's card holds at 1.2–1.8 s, 13–19 %.
    • Whole block: 31.78 s at the default (FAST 456); 32.55 s at f6445c8b6 (FAST 452); 35.03 s with NOEPOCH_TREE_AHEAD=0. Fan-in 4 was measured on the inline path (33.82–33.95 s there).
  • Whole-block verifies: FAST 452, 455, 456 and 457 each ran 10 of 10 P8 base proofs through production's block verifier, all verified (456 alternating the parallel and serial concatenation), and every child proof of every tree run was verified. FAST 396 and 397 each ran 10 of 10 P8 base proofs at their shas through production's block verifier, all verified (396 also verified its 10 A/B arm proofs). FAST 395 ran 10 of 10 P8 base proofs at a1a3a2c22 through production's block verifier, all verified (bases 24.52–25.17 s), so the race fix (a) is shown on the plan's code.
  • Gates are green at every sha, every count exact: the host gates, 25 epoch-reader tests, and the fixture leaves, tree, verifier and pad-byte tests.
  • The verifier's derivation is a consumer cost, paid once per (ELF, block shape) rather than once per ELF: the top program depends on the block's table counts, page ranges and trace lengths. The per-ELF part (ElfConstants) and the per-(AIR, length) shapes can be reused across blocks.

Memory: toward typical blocks (head 19fe5e9e5)

A median mainnet block (25475471) is 9.78× this one, and the host peak grows about linearly with the block. The head carries three changes for that; none changes a proof byte.

change gate 1× base Δ 1× host peak larger block
Each streamed chunk's op lists are freed once the chunk is committed (drop_streamed_ops, default on; LAMBDA_VM_BLOCK_DROP_OPS=0 keeps them) FAST 502, EFFECTIVE −1.07 s 41.9 → 33.0 GiB 1.20× verifies at 39.7 GiB
Kept top levels 3 → 6 (BLOCK_RECOMMIT_TOP_LEVELS) FAST 503, EFFECTIVE −0.05 s 32.8 → 31.7 GiB 1.57× proves at 47.2 GiB
One trace-hash state per process for the six hash-ordered tables (LAMBDA_VM_FIXED_TRACE_HASH=1) FAST 504, EFFECTIVE — 31.2 / 31.6 GiB —
  • Freeing the op lists builds the whole-run build's tables, table for table: 138 tables, 55 streamed chunks, 0 differ (2 of 2 runs). Proof size is unchanged (82,562,392 B).
  • With LAMBDA_VM_FIXED_TRACE_HASH=1 and LAMBDA_VM_DETERMINISTIC_GRIND, two processes produce the same block proof (digest ac406fc8…); a random-key control differs. Byte-identity gates on the block use this.
  • Next, on its own branch until its default-flip gate: narrow column storage (about 2 B per main cell instead of 8). FAST 508: 1× peak 19.1 GiB at +0.28 s base; a 4.13× block proves and verifies at 55.5 GiB.

What it is

  • The whole block is proven as one monolithic VmProof. Every table is cut into instances of 2^21 rows, and KECCAK_RND into 2^16-row instances. There is no L2G table and no global proof; memory uses the monolithic PAGE argument.
  • ResidencyMode::RecomputeLdeDevice: after Round 1 only each instance's root is kept. Each instance's fused task commits its trace on the device again, and the prover refuses the proof unless the new root equals the absorbed one, so the proof bytes are unchanged. Retain and RecomputeLde behave as before.
  • prover::block::prove_block / verify_block. The block verifier is the only one that accepts a chunked KECCAK_RND (AcceleratorShape::KeccakRndChunked). Every other verifier keeps KECCAK_RND to one table.
  • Knobs, all off by default: LAMBDA_VM_GATE_PACKING (VRAM-gate packing admission), LAMBDA_VM_TABLE_TIMELINE (per-table timeline), LAMBDA_VM_RECOMMIT_TOP_LEVELS (kept top levels).
  • Phase A is streamed by default. The executor runs in 2^21-cycle windows that feed the shared WindowedTraceBuilder, and every full chunk of CPU, MEMW_R, MEMW_A, MEMW, LOAD, LT, SHIFT and STORE is committed as soon as it exists. Host tests show it equals the serial build.
  • Kept top levels (k = 6 since 0fe8e4f2f; k = 3 before) are prove_block's default. The ELF data pages' preprocessed roots are computed on the device during execution.

Base, block 25368371 on FAST (A/B on one binary; the epoch base is #1009's prove_continuation on the same binary)

step base Δ
epoch base (#1009 reference) 26.05 s
S1 block proof, first measurement 38.59 s
+ packing admission (k = 4) 37.17 s −0.64 (no effect)
+ k = 8 35.6 s (exploratory)
+ KECCAK_RND chunked at 2^16 32.40 s −3.23
+ ELF data-page roots on the device during execution 30.88 s −1.43
+ streamed phase A (windowed execution, CPU instances committed per window) 27.98 s −2.76
+ kept top levels instead of the second hash (k = 3) 23.14 s −4.89
+ shared windowed builder streaming 8 tables 24.89 s +1.75 against the row above (another run); −0.84 against the serial producer on one binary, where −2.0 or better was pre-registered
+ the rebuilt builder, its walk on a thread of its own, the committers generating (e7406d6b5, FAST 396) 21.55 s −2.37 against the previous schedule over the same builder on one binary; −4.11 against the serial producer

At e7406d6b5 the block base is about 4.5 s below the epoch base (21.55 against 26.05). The first shared builder cut Round 1's span from 6.3 to 2.4 s, but its window builds sat on the executor's path (execute 1.35 → 6.9 s). With the rebuilt builder and the walk on its own thread, execute is 3.35 s and phase A 7.13 s, and the prove is 14.4 s against the serial build's 18.4 s. Nsight on FAST shows the fused phase is 98.6 % card-busy, so the block is card-bound. Most of that card time was the second hash, which kept top levels remove: phase B recomputes the LDE alone, and the openings rebuild each queried 8-leaf subtree and check it against the kept node. The architecture's main gain is in the recursion (above).

S0 census: main 3.301 G elements, aux 1.036 G.

Format change: ECDAS chunked; KECCAK_RND and ECDAS heights capped

Landed at 23b3c8173 (FAST 531 gates GREEN; FAST 533 A/B on 25512221: base −0.17 s, no effect on time, as expected; ECDAS cells −25 %, host peak −0.40 GiB).
ECDAS is one row per double/add step (≈ 382 per ECSM call, ≈ 4.2 calls per transaction), so a median mainnet block
makes ≈ 420 k rows: one table at 2^19 proves 127.91 bits at DEEP batching on a ≈ 22.5 GiB device set, and a p90 block's
2^20 (126.91 bits, 44.7 GiB) no longer fits a 32 GiB card. The block now cuts ECDAS into instances of at most 2^17 rows;
a scalar multiplication may straddle two instances, its steps chaining only through the Ecdas bus, keyed by the call's
timestamp and the step's (round, op). AcceleratorShape::KeccakRndChunked is renamed BlockChunked and lifts the
one-table bound for ECDAS as for KECCAK_RND (count bounded by the sub-proof cross-check). New verifier constants, checked
in verify_block and in the tree's check_shape: every KECCAK_RND instance ≤ 2^16 rows (129.43 bits) and every ECDAS
instance ≤ 2^17 (129.91 bits). This closes G1 (REV-JUDGE item 13, R-NOEPOCH-S3 F1-M1) for KECCAK_RND and ECDAS
only
; every other table's height is still bounded by two-adicity alone, and the rest of G1's per-type list (ECSM,
KECCAK, the CPU family, the fixed tables) stays parked. LAMBDA_VM_BLOCK_KECCAK_RND_LOG2 takes 5..=16 (no off).
A block whose ECDAS fits one 2^17 table (25368371: 2^16) builds the same ECDAS table as before (one chunk of every step is the table generate_optional built; by construction, not byte-compared on a block); 25512221 (2^18 today, 128.91 bits, 0.04
under the minimum of record) now proves 2^17 + 2^16. The epoch and recursion verifiers (Single) are unchanged.

Format change: KECCAK and ECSM chunked; tree partition rule v2 (5e4176961)

Landed with the any-block target. Approved by the lead; Mauro to confirm. It is listed for the cryptography review as S-6 and S-9.

The caps. KECCAK is cut into instances of at most 2^18 rows (129.213 bits) and ECSM into instances of at most 2^17 (129.488 bits), as ECDAS is.

  • A row of either is one whole call (a permutation, a scalar multiplication), so a cut falls between calls.
  • A call reaches its rounds, steps and memory only through buses keyed by its timestamp.
  • The block verifier and the tree check every instance against these caps.
  • LAMBDA_VM_BLOCK_KECCAK_LOG2 (2..=18) and LAMBDA_VM_BLOCK_ECSM_LOG2 (2..=17) lower the caps, for tests.

Partition rule v2 (PARTITION_COST_MODEL = 2). Rule v1 pinned every chunk of a table to one leaf. Now the first instance keeps its seeded leaf and later chunks of KECCAK, ECSM and ECDAS fill by load. Without this, a p99 block's ≈ 15 ECDAS chunks overflowed the leaf cap and the plan was refused. The verifier derives the partition by the rule from the shape alone.

Evidence:

  • FAST 723: 1× digest and tree top unchanged; a forced 3 KECCAK + 4 ECSM chunks verified host and tree; 5 mutations caught.
  • BIG 520: the median base and tree verified, derived top = proved top.
  • Every block measured so far keeps one KECCAK and one ECSM table, and its proof bytes.

Known cost (deferred): a table just over a power of two is cut after padding, so for example 2^20 + 1 KECCAK calls make 8 instances, about half of them padding. This matters only on keccak-heavy blocks.

G-pack: generators write the packed traces directly

Default since d8a422962. LAMBDA_VM_BLOCK_GPACK=0 builds each table at 8 bytes a cell and packs it afterwards, as before. Under narrow storage the producer used to generate every main trace as a 64-bit table and then pack it (NarrowMain::pack). At 1× the generators spent more time packing than generating (ULTRA u004a: generate 6.25 thread-s, host pack 9.21).

  • The writer. stark::narrow::NarrowWriter stores each word's low bytes at a per-column width chosen up front, and keeps the OR of every word written to each column. finish() narrows in place the columns that were given more bytes than their largest word needs. When a word did not fit, it names the widths the trace needs instead. Either way the bytes are NarrowMain::pack's of the same words, whatever the guess.
  • The generators. Each generator's fill is generic over VmTable and runs through generate_main! (tables::gpack) in a TraceForm:
    • Wide is the old table, used by every build outside the block's narrow storage.
    • Narrow writes the packed columns at the widths the table kind needed so far in the process (one WidthHint per kind) and writes the trace again on a miss. A kind's first trace is built wide and packed, to learn the widths (KECCAK_RND and LT through their packed block builds).
    • Converted: the 8 streamed tables and MUL, DVRM, BRANCH, EQ, BYTEWISE, CPU32, COMMIT, KECCAK, KECCAK_RND, ECSM, ECDAS and HINT.
  • Wiring. The block's generator threads call ChunkJob::generate_as(TraceForm::Narrow), and the finish runs under WindowedTraceBuilder::generate_packed(). BLOCK NARROW reports how each trace was built: written packed, narrowed after, written again, or built wide then packed. At the median about 600 traces are written packed, 105 narrowed after, 4 written again and 23 built wide then packed.
  • Bytes.
    • BIG 634 at this head: the 1× base under LAMBDA_VM_FIXED_TRACE_HASH=1 + LAMBDA_VM_DETERMINISTIC_GRIND=1 gives digest 0224203e… with G-pack on and off. Every table of the 1× block built through the windowed builder has the old path's packed bytes (138 tables, twice: before the widths are learned and after).
    • Laptop: every table of four programs equals the whole-run 64-bit build packed, and its words are equal. A debug-only scan checks each written column's width against its stored bytes.
    • A column written one width too wide, or shifted by a byte, fails that comparison, so the gate is load-bearing.
    • The phase-A stream builds the same packed traces and precommits the same instances with G-pack on and off.
  • Measured.
    • BIG 635, median 25475471, one binary, 3 + 3 after a warm-up:
      • generator thread-seconds (generate + host pack): 399.8 → 105.7 (−73.6 %);
      • phase A: 73.14 → 65.67 s (−7.47, t −10.2);
      • base: 184.11 → 176.75 s (−7.35, t −10.6);
      • phase B: +0.10 s (t +0.4);
      • VmRSS: 79.24 → 75.96 GiB.
    • BIG 629 ran the same A/B before the rebase onto 58a58be95: −73.5 % and phase A −7.75 s.
    • The 1× timing pair on ULTRA runs after the bench (061/062).

Production CLI: prove-block and verify-block

Since 58a58be95. Until this head every whole-block number came from the test harness; the shipped CLI could not prove a block as a tree, had no CUDA build and never purged.

cargo build --release -p cli --features cuda
cli prove-block guest.elf --input block.bin -o block.proof [--time] [--digest]
cli verify-block block.proof guest.elf [--time]
  • One driver. The harness's body moved to lfm::block_tree::prove_block_tree: the base, the harvest, the leaves, the interior and the top, with every in-run check (the base and every child verified beside the run, each leaf's published words, the top's claim, the final check) returning an error instead of panicking. Every knob is read once, by BlockTreeConfig::from_env, under the harness's names and defaults. The harness test is a wrapper that keeps its post-run block verifier; the proof readers it used (HostTable, TableLegs, the child harvest) live in lfm::harvest, re-exported under their old names.
  • The allocator. The binary installs its jemalloc as alloc_purge::AllocatorHooks (statistics and arena.<all>.purge), so the purge points under auto, the BLOCK MEM columns and the late leaf emission run as in the harness. It still compiles in the never-purge posture; a CLI unit test now reads that setting back, and another shows the installed purge returns freed pages (each fails under its mutation).
  • The posture. For prove-block and verify-block, every knob in lfm::block_tree::POSTURE that the environment leaves unset is set before any thread starts: TABLE_PARALLELISM=8, LAMBDA_VM_VRAM_BUDGET_MB=24000 (only when nvidia-smi reports a card of at least 31 GiB), LAMBDA_VM_GATE_PACKING=1, LAMBDA_VM_MAX_ROWS_LOG2=21, LFM_PRECOMPUTED_TREE_CACHE_CAP=64, LFM_EXEC_PARALLEL=1, LFM_TREE_SIBLINGS_L0=8, LFM_TREE_SIBLINGS=4. A BLOCK POSTURE line on stderr says what was set; the harness prints how its own environment compares.
  • The proof file. BlockTreeProof: the claimed shape, the public output and the top node's proof, behind a magic, a version and a pipeline tag. verify-block is block_plan::verify_block_tree over those claims, unchanged: the plan and the top program are derived from the trusted ELF under the block presets, and nothing about the format is read from the file.
  • Evidence.
    • ULTRA 016 (harness H and CLI C interleaved, 8 + 8 after a warm-up): every run verifies and prints the same posture; C − H whole −0.161 s (t −2.4), recursion −0.06 s, VmRSS −0.94 GiB; with --digest under the fixed trace hash and the deterministic grind, the CLI's base digest equals the harness's digest test at the same sha (0224203e…, the k4 reference), top 2ef0e0d2…; a small asm guest proves and verifies through the CLI.
    • BIG 632: the p90 25481021 through cli prove-block with only the instrumentation knobs set: whole 553.77 s, VmRSS 112.33 GiB, purges phase-a 121.39 → 78.73 GiB and base 109.30 → 52.96 GiB, 54.56 GiB spilled; cli verify-block in its own process passes in 72.86 s at 37.47 GiB (BIG 626, the harness at the same base: 555.29 s, 113.50 GiB).

LogUp: four interactions per aux column (the default since 4fc4b7b12)

Default k4 (Mauro, 10-05); LAMBDA_VM_ZF_LOGUP=pair restores the previous bytes. Each base table commits four bus interactions per LogUp aux column instead of two, where that commits fewer extension columns: groups of four have degree 5, which blowup 4 admits, at the price of four composition parts instead of two. The rule is per table and verifier-side (⌈N/k⌉ aux columns + parts, ties keep pairs): KECCAK_RND 516 + 2 → 258 + 4, ECSM 290 → 145, ECDAS 194 → 97, CPU 10 + 2 → 5 + 4, MEMW_A 10 → 5; LT, STORE, MEMW_R, LOAD, PAGE and the small tables keep pairs. The LFM chips keep pairs; #1014 is untouched.

  • Code: ProofFormat.logup (stark), the group/accumulator emitters for k ≥ 3 (one body for the prover folder, the verifier folder and the IR capture), the host and device aux builds grouped by arity, the four-part composition split on the card (radix-2 twice) and its host mirror, compiled kernels for the k4 table programs, the knob at block_base_options, and one new verifier refusal (parts > blowup).
  • Bytes: with =pair the 1× digest is ac406fc8 and every program id, kernel key and golden is as before (the small-tree id pin derives under pair and passes; the 44-program golden holds); with the knob unset the base tables prove k4 (1× tree top 2ef0e0d2).
  • Measured (one binary each): median block 25475471 on BIG: phase B −9.56 s (t −27, 3 + 3), host peak −1.06 GiB, proof −4.6 %; about a third is stage work (aux commit −11.5 worker-s against composition commit +7.1) and two thirds packing under the 24,000 MB VRAM gate (smaller per-table device sets let three large tables run where two did). 1× block on FAST: whole −1.04 s, base −0.97 s (t −15), almost all packing.
  • Correctness: the card's split equals the host mirror (2^8…2^21) and a device-only k4 proof equals the host proof byte for byte; k4 verifies on the host and through the block tree; six tampered-proof negatives refused.
  • Security: CRYPTO-REVIEW S-11. LogUp soundness is unchanged (it counts interactions × rows); every FRI phase stays ≥ 128.946 bits (batching rises as L falls); DEEP 161.18 bits per table at degree 5.
  • Landed at 564dd02 (landing gate FAST 863: pair bytes and tree top unchanged, k4 verifies, six negatives refused).
  • Default since 4fc4b7b12 (Mauro 10-05; ULTRA 018: both arms verify, base −0.89 s, whole −0.92 s; BIG 626: the p90 verifies under k4 at 113.50 GiB).

Recursion: sibling proofs share the card through one VRAM gate (on by default)

Before: the recursion's card permit was a mutex, so one proof at a time ran inside multi_prove. The card then sat idle while the holder ran its host stages: uploads, absorbs, queries. At the median block that was 24.6 s of card idle inside the holds (BIG 469).

Now: every multi_prove in the tree admits its tables through one process-wide byte gate, and the artifact commit takes its bytes from the same gate. Sibling proofs overlap wherever their bytes fit. Three parts keep the gate's account matching the card:

  • Resident bytes stay counted. A Retain prove keeps each table's main LDE, trace snapshot and tree on the card from its Round-1 commit until its fused task ends. Those bytes stay in the gate the whole time: the Round-1 task carries them past its own permit, and the fused task takes them over and is admitted only for the rest of its set.
  • Claims make the carry deadlock-free. Each prove first claims room for its residents plus a headroom (see "Tight claims" below). The claim settles after Round 1 and shrinks as tables finish. A prove whose claim alone exceeds the budget runs alone.
  • The pool posture and the calibration.
    • The device pool releases freed blocks at each sync only while the gate is armed; the base keeps its retained pool.
    • Arming drains the device, trims the pool, and sets the budget to the card's free memory minus 3.5 GiB, capped at the configured 23.44 GiB.
    • At most three proofs are inside multi_prove at once.

LAMBDA_VM_SHARED_VRAM_GATE=0 restores the exclusive permit. LAMBDA_VM_SHARED_GATE_TRACE=1 prints the gate's account (SGATE lines). The gate acts only while the tree arms it for concurrent proofs, so the base and every single-proof path are unchanged.

Bytes: unchanged, since the gate only reorders admission. The digest is ac406fc8…, and every A/B arm has the same top program id.

Measured:

  • Landing gate, FAST 871 (two binaries, a93018974 against 564dd02bb, 4 + 4):
    • recursion −0.68 s (t −9.6); whole −0.48 s (t −2.9); base +0.09 s (t +0.8);
    • VRAM peak 22.3–26.4 GiB against 24.1–27.3; level-0 budget 23.44 GiB in every gate-on run.
  • One binary, gate on against off, FAST 479 (8 + 8): recursion −0.59 s (t −17.2); whole −0.51 s (t −6.9); base −0.02 s.
  • Median block 25475471, BIG 471 (3 + 3, measured before the arming drained the device):
    • recursion −4.72 s (t −7.9); whole −4.29 s; level 0 −3.42 s; interior −1.30 s;
    • VRAM 25.0–26.1 GiB against 26.5–26.7; host peak at level 0 ≤ 97.9 GiB.
    • It missed the pre-registered −5 s. Without the drain, the level-0 budget (16.9–19.5 GiB) sometimes fit only one ≈ 9.7 GiB leaf claim at a time; the drain lifts it to the 23.44 GiB cap.
    • At this head (BIG 472, drain in, 3 + 3): recursion −4.24 s (t −9.4), with the level-0 budget at the cap in every run. Level 0 turned out to be bound by the serial artifact builder, not the budget. Running two builders in parallel only displaced the proofs on the card (BIG 473, +0.98 s), so that is parked.
  • Interior node claims (23–24 GiB) run one at a time at any budget up to the cap. Pairing them needs tighter device-set estimates, which over-count by a median of 2–14 GiB.

Tight claims (head 00871b913, on by default). A claim is a held part R plus a headroom H.

  • R: the proof's tables' resident bounds. After Round 1 it settles to the bytes actually carried, and it shrinks as tables finish.
  • H: the largest request any table can still make beyond its own resident bound, max over tables of max(fused set − resident, commit scratch). Each carry is at most its bound, so H covers every Round-1 commit and every fused top-up, including a table that falls back to the host and carries nothing.
  • Admission: a claim enters while Σ R + max H ≤ budget over the claims in force. When every proof waits, the gate holds at most Σ R and each request is at most max H, so every waiting proof fits. A finished table, a settle or a departure only lowers the left side.
  • Why the largest H: keeping the smallest headroom instead is not safe. Once the proof that owns it leaves, the rest can wedge, and a test shows this.
  • Size: a leaf's claim falls from 9.4–10.0 GiB to ≈ 7.4. Three leaves share one headroom: 18.7 GiB of the 23.44 budget, against 29.1 before.
  • Off switch: LAMBDA_VM_SHARED_GATE_CLAIMS=whole restores the first form (residents plus the largest table's whole set).
  • Measured, median (BIG 474, one binary, 5 + 5):
    • recursion −1.25 s (t −5.4); level-0 claim waits Σ 7.9–10.4 s a run → 0.00 in every run; VRAM ≤ 28.7 GiB.
    • Whole −0.05 s (t −0.1), because the base moved the other way: +1.18 s (t +1.9; A 191.9–193.5 s, B 192.8–195.9 s).
    • Claims never act in the base, and the arms' environments are equal, so that is the median base's scatter (phase B sd ≈ 2.7 s).
  • Measured, 1×: recursion +0.00 s on one binary (FAST 479-p12) and −0.01 s at the landing gate (FAST 479-land2, against 278e6a8c6); no regression.

Readout note: FAST 871's "claims in force" row read OUT because it counted from the optional trace, which that gate ran without. From each claim's own log line, every gate-on run peaked at 3 claims and the off runs had none.

Tests:

  • the gate's exact account at every fused start, with one driver;
  • the claim arithmetic;
  • two carrying proves that deadlock without claims and finish with them (each test fails under its mutation);
  • tight claims: two top-ups that would wedge without the headroom; the largest headroom against the smallest; a randomized run of 4 threads × 6 proofs × 4 rounds with host fallbacks that never wedges (admission without the headroom, or with the smallest one, fails them);
  • the permit tests pin the exclusive card where they test it.

Disk spill: auto by default

What it does. Once Round 1 has committed an instance, its packed main trace can go to a spill file that phase B reads back ahead of its walks. The words that come back are the words that went out, so no proof byte depends on the policy.

  • The file is unnamed (O_TMPFILE) and refuses a tmpfs directory. It uses O_DIRECT where a probe write allows it.
  • One writer thread works behind a 2 GiB queue. The writer takes each slot's digest, and every read checks it.
  • A store that fails to write keeps its traces resident.
  • LAMBDA_VM_BLOCK_SPILL = auto (default) | off | always | <GiB> (a resident budget for committed packed traces).

How auto decides (prover/src/block.rs, spill_target_bytes / spill_decision). A committed instance is spilled when the host's bytes, plus the reserve, plus the instance's own bytes would pass the target.

  • The host's bytes are the larger of the process's VmHWM and its cgroup's working set: the charge (v2 memory.current, v1 memory.usage_in_bytes) less its inactive file pages from memory.stat, which the kernel reclaims first. Since cf253d235; the charge alone counted the page cache, and at 1× on FAST it read 25.57 GiB against a working set of 17.09.
  • The reserve is 0.18 GiB per G committed cells plus 6 GiB, for the finish's transients and phase B's bump.
  • The target comes from the machine, in this order:
    1. LAMBDA_VM_BLOCK_SPILL_TARGET_GIB, if set;
    2. else the smaller of the cgroup's limit and MemTotal, less 10 GiB. The limit is v2 memory.max, or v1 memory.limit_in_bytes, read at the process's cgroup path and then at the hierarchy root, which is what a container without a cgroup namespace sees. Since 278e6a8c6: FAST 509 read 47.5 GiB on FAST's v1 limit, where the v2-only rule read 49.9;
    3. else no spill (no /proc).
  • On BIG that is 120.69 − 10 = 110.69 GiB. A 57.5 GiB host gets ≈ 47.5.
  • While a store is open, the stream's queues have byte budgets (ops 4 GiB, generated chunks 2 GiB). They keep work from piling up behind a slow writer. Since fb0dc0057, under auto they arm only once a spill is plausible (the host's bytes plus the reserve reach 85 % of the target) and then stay armed; always and a fixed budget arm them from the start. At the median they never arm (waits 0.01 / 0.18 s, BIG 122). At the p75 they armed at 59.7 s, for a phase-A cost of +1.12 s against off; armed from the start, the same block cost +3.6 s (BIG 118).

Measured, median block 25475471 on BIG (3 runs per arm, one binary per job):

job arm spilled phase-A end vs no spill host peak
BIG 117 auto (default) 0 B in every run −0.97 s 81.9–82.8 GiB (off 80.8–82.7)
BIG 117 auto, 70 GiB target 30.1 GiB, 427–429 slots +1.43 s 60.0–62.4 GiB
BIG 116 always 56.5 GiB, 738 slots +7.07 s 31.8–32.5 GiB
BIG 115 always 56.5 GiB +6.03 s 31.6–32.7 GiB
BIG 111 always (first measurement) 56.5 GiB +6.8 s (base +10 s) 30.0–30.3 GiB
  • Every spilled slot was read back with 0 mismatches.
  • The 1× digest ac406fc8… holds under the default (BIG 117) and under always (BIG 114–116).
  • Phase B moved within ±1 s.

Blocks up to full gas on BIG (128 GiB host, 120.69 GiB cgroup; one run each):

block gas · cycles job, posture whole · base · recursion whole-run VmRSS spilled
p75 25440371 39.2 M · 447.6 M (14.67×) BIG 118, defaults at edddc6873 421.0 · 285.5 · 133.3 s 108.0 GiB 29.3 GiB
p90 25481021 52.0 M · 612.3 M (20.08×) BIG 591, defaults at 035aef5d6 590.95 · 400.30 · 183.38 s 115.79 GiB 53.04 GiB, 765 slots
full gas 25431071 60.0 M · 602.9 M (19.77×) BIG 125, defaults at 035aef5d6 541.51 · 372.37 · 161.82 s 110.62 GiB 46.18 GiB, 672 slots
  • Every block verifies, and every spilled slot reads back with 0 mismatches.
  • The p75's peak is phase B's end, with the late-emitted leaf programs. With the spill off its base still fit, at 116.1 GiB, by squeezing the page cache from 12 to 4 GiB (BIG 118).
  • At the defaults the full-gas block's budgets armed at 59.6 s, so auto purged at phase-a (121.19 → 82.32 GiB) and at the base (116.53 → 48.45). Its verifier passed cold and warm (66.59 / 61.61 s, pool high-water 17.39 GiB; BIG 125). With both purges forced on the pre-gate train it read 549.32 s at 111.30 GiB (BIG 1215).
  • Base = execute + build + prove. The whole wall also covers what runs between base and recursion (the harvest, the base purge).
  • The largest normal block is the largest by cycles, not gas: new storage slots cost gas but few cycles, and no July block found reaches 650 M cycles.

The allocator purge. The harness's jemalloc never purges (dirty_decay_ms:-1), and a phase rarely reuses the pages the phase before it freed, so the host ratchets from phase to phase. One arena.<all>.purge hands every arena's dirty pages back.

  • At the p90 (BIG 591), the phase-a purge (2.5 s) took jemalloc's resident bytes from 120.76 to 80.54 GiB, and the base purge (3.9 s) from 120.58 to 54.86. Level 0 then starts at ≈ 55 GiB of RSS (BIG 1215: 55.63) instead of ≈ 115, and peaks at 93.9 GiB.
  • The base purge carries level 0: without it, level 0 was OOM-killed at the p90 (BIG 120). The phase-a purge buys phase B 3–9 GiB of margin: p90 119.79 → 116.60, full gas 120.22 → 111.06 GiB (BIG 120 against BIG 1215).
  • auto purges only once the spill's budgets arm, so a block that fits prints ALLOC PURGE <point>: skipped (auto, no memory pressure) and pays nothing (FAST 515 at 1×, BIG 123 at the median).

What the cost follows. While the writer runs, the generators slow by ≈ 16 %. The writer's two passes over every spilled byte (digest, then the aligned copy) and the frees of the spilled buffers both contribute; BIG 115 and 116 could not split them further. At the median, phase A is bound by the generators during the walk, so the walk waits on them.

  • The 70 GiB target started spilling at t ≈ 33 s. It handed ≈ 13–15 GiB to the writer before the walk ended and the rest after, for +1.43 s.
  • always hands ≈ 39 GiB to the writer during the walk, for +6–7 s.
  • Rule of thumb (inferred): ≈ 0.1–0.18 s of phase A per GiB spilled during the walk, ≈ 0 for what is spilled after it.
  • The writer manages 0.85–1.06 GiB/s. Its seconds by step under always: digest 22, aligned copy 28, pwrite 14. The pwrite runs at the disk's own rate (3.7 GiB/s raw). The BLOCK SPILL line prints the split.

Measured and not landed (each has a FAILED-LEVERS row):

  • two writer threads (BIG 113: no effect);
  • queue budgets that bind only while the writer is behind (BIG 114: phase A +9.4 s, peak 37 GiB);
  • F1, trace buffers in page-aligned memory written straight from their pages with the digest fused per chunk (BIG 116). The copy was gone, but phase A moved only −0.9 s against the heap writer, at the price of an munmap per slot.

Tests:

  • a spilled trace round-trips bit for bit, O_DIRECT and buffered;
  • every trace spilled proves the resident bytes under every residency;
  • a flipped byte on disk, or a perturbed slot, is refused, on the card before any device work;
  • a failed store proves the resident bytes;
  • the read-back keeps to its window;
  • the stream under always, a zero budget and auto builds the resident traces;
  • the policy and decision tests; the writer's step seconds add up;
  • auto reads the host's working set, and the target reads v2 and v1 cgroup limits;
  • the budgets arm where a spill is plausible, and an unarmed budget binds nothing;
  • the purge knob names its boundaries, and auto purges only under memory pressure.

@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Benchmark Results for modified programs 🚀

Command Mean [ms] Min [ms] Max [ms] Relative
head ecsm 2.8 ± 0.4 2.3 3.3 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head hashmap 85.2 ± 2.5 81.4 90.3 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head keccak 114.3 ± 1.5 112.9 117.6 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head syscall_commit 75.1 ± 1.4 72.3 76.3 1.00

@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Benchmark Results for unmodified programs 🚀

Command Mean [ms] Min [ms] Max [ms] Relative
base binary_search 50.4 ± 0.5 49.8 51.1 1.00
head binary_search 52.2 ± 0.2 51.8 52.5 1.04 ± 0.01
Command Mean [ms] Min [ms] Max [ms] Relative
base bitwise_ops 50.6 ± 0.5 50.0 51.4 1.00
head bitwise_ops 51.8 ± 1.4 50.3 54.3 1.02 ± 0.03
Command Mean [ms] Min [ms] Max [ms] Relative
base fibonacci_26 53.1 ± 0.6 51.9 53.9 1.00
head fibonacci_26 56.5 ± 1.5 53.1 58.2 1.06 ± 0.03
Command Mean [ms] Min [ms] Max [ms] Relative
base matrix_multiply 52.7 ± 0.7 51.7 53.7 1.00
head matrix_multiply 56.7 ± 3.2 54.6 65.4 1.07 ± 0.06
Command Mean [ms] Min [ms] Max [ms] Relative
base modular_exp 50.6 ± 0.6 49.8 51.7 1.00
head modular_exp 51.2 ± 0.6 50.2 51.7 1.01 ± 0.02
Command Mean [ms] Min [ms] Max [ms] Relative
base quicksort 53.6 ± 0.6 52.6 54.1 1.00
head quicksort 55.5 ± 0.5 54.5 56.1 1.03 ± 0.01
Command Mean [ms] Min [ms] Max [ms] Relative
base sieve 54.2 ± 0.6 53.6 55.0 1.00
head sieve 55.0 ± 0.8 53.3 56.3 1.01 ± 0.02
Command Mean [ms] Min [ms] Max [ms] Relative
base sum_array 62.2 ± 0.6 61.6 63.2 1.14 ± 0.02
head sum_array 54.6 ± 0.5 53.9 55.4 1.00

MauroToscano added a commit that referenced this pull request Oct 1, 2026
…ch/stark-s3

Brings e80674f, the R2 race fix #1013 now carries, under the block
tree plan, so the plan's gate and its re-measure run on the fixed
prover.
MauroToscano added a commit that referenced this pull request Oct 2, 2026
On block 25368371, 10 of 11 LT tables come from finish's phase 3 (LT ops
derived from MEMW and MEMW_A), about one group of cells in phase A's
tail. Each MEMW op's LT ops depend on that op alone, so
WindowedTraceBuilder::stream_memw_lt derives each window's as the window
arrives, into the window's LT list, and LT's full chunks stream with them.
StreamSkip counts the MEMW/MEMW_A ops already converted, and phase 3
derives LT only from the ops past them.

Block path only, under the lead's ruling: the LT ops are the whole-run
build's in another order (a window's MEMW-derived ops follow its walk's),
so a chunk holds other ops than the whole-run chunk at that index. LT
deduplicates per chunk, but every op keeps its whole-run multiplicity
summed over the chunks, which is what LT's constraints and the bus see.
The whole-run order, and so every epoch proof, is untouched. For a given
window length the chunks are a function of the run. Off unless called.

Test: with it on, every table but LT is the whole-run build's; LT has as
many chunks and the same op multiplicities summed over them; dropping one
row of one chunk breaks that (the mutation); and more LT chunks stream.

Cherry-picked from noepoch/windowed-builder 074b772 onto #1013's builder, without the KECCAK_RND streaming it sat on (f5a3503): #1013 chunks KECCAK_RND through MaxRowsConfig::keccak_rnd instead.
MauroToscano added a commit that referenced this pull request Oct 2, 2026
… leaves

The builder kept every walked window until finish, so at the run's end
all of its op lists were resident, about 300 B per cycle (0.28 GiB per M
cycles: 8.6 GiB on block 25368371 and about 100 GiB on a median mainnet
block). WindowedTraceBuilder::drop_streamed_ops keeps only the ops not yet
in a handed-out chunk for each of the 8 streamed tables (Tail: the windows'
lists moved in whole and freed once used up). As a chunk leaves, the
builder takes what the table phase would have read from its ops. It
counts BITWISE's lookups from LT, SHIFT, STORE, MEMW_A and MEMW_R, plus
DECODE's lookups and the last ECALL from CPU. It derives and keeps the
phase-3 LT ops of MEMW and MEMW_A, unless stream_memw_lt already put them
in the windows' LT lists. The other walk lists are appended window by
window instead of being concatenated at finish. The in-walk BITWISE
lookups are counted and dropped. finish runs the table phase over the
tails (StreamSkip::tails), padding the dropped CPU chunks and placing the
kept LT ops where phase 3 would have derived them.

The tables are the same. The new tests compare every table with the
whole-run build, and with stream_memw_lt every table with the builder
that keeps its windows, plus LT's multiplicities. Both go through push and
the split, at window lengths 1 to the whole run, and with chunks small
enough that all 8 tables drop. The same comparison runs with KECCAK_RND
streamed. Another test checks that after each window each table holds
less than a chunk of ops. Removing any one accounting step (LT, MEMW_R or
DECODE counts, the CPU padding, either LT prefix) fails the first test.
Off by default.

Cherry-picked from mem/m1 74e1df1 onto #1013's builder: no streamed KECCAK_RND here (KECCAK_RND chunks are built by finish from the run's keccak ops, which dropping keeps), so its test checks the MaxRowsConfig::keccak_rnd split against the whole-run build instead.
KECCAK_RND chunks are 2^16 x 1480 cells, 776 MB at 8 bytes a cell, and the
finish generates them all at once in phase 5 (at the median about 37 of them,
the finish's long pole). With StreamSkip::pack each chunk is now built 128
permutations (36 MB) at a time into stark::narrow::NarrowBuilder, which packs
each block into its columns and re-encodes a column that a later block needs
wider: the result is NarrowMain::pack of the whole table, byte for byte
(narrow and keccak_rnd identity tests), and TraceTable::from_narrow_main holds
it with no 64-bit copy ever made. KECCAK_RND then takes no wide-chunk permit,
so its parallelism is kept. Other builds, debug-checks and disk-spill keep the
64-bit path.
LAMBDA_VM_BLOCK_GENERATORS=n (1..=16): n generator threads take each streamed
chunk's job, generate it and (under narrow storage) pack it on the host, and
hand it to the committers through a second queue, so the committers only
commit and a chunk waits packed (about 2 B/cell) rather than as its ops.
LAMBDA_VM_BLOCK_READY_MIB=n bounds that queue by bytes (QueueRoom, as the ops
queue). Unset or 0: the committers generate, as before. A committer that stops
on an error frees both queues. BLOCK QUEUE reports both queues; BLOCK MEM the
chunks ready. build_streamed takes a StreamConfig (read from the environment by
the block) so a host test can drive it: the stream builds the same traces and
precommits the same instances with 3 generators and every queue held to one
chunk, or 1 and 1, as with the committers generating. A measurement knob until
its gate.
The finish generates LT from the MEMW-derived ops kept to its end (10 of the
record block's 11 LT tables), every chunk at once in phase 5: 2^21 x 17 cells,
285 MB each at 8 bytes a cell. As KECCAK_RND, a packing build now
deduplicates each chunk as before and writes its rows 4096 at a time into a
NarrowBuilder, so no 64-bit LT table is ever held and no permit is taken. The
rows keep their order (one map, one hash state per process): the packed build
is the 64-bit build packed, byte for byte (lt_tests).
…m's threads

The executor's log windows on their way to the walker and the walked windows on
their way to the accumulator (two bounded channels plus the windows each
thread holds): at 4.13x on BIG about 7.5 GiB of phase A's live heap was not
named by the ledger, and these are its first candidates. Measurement only.
…ore the finish

On BIG at 4.13x the posture's never-purge allocator left the finish's working
set on top of the pages the windows phase had freed (resident 55.4 GiB at the
finish against 50.7 live); a run with decay 0 throughout peaked 10.3 GiB lower
but walked 1.8x slower. This purges once instead: when the run is walked,
every arena's decay is set to 0 (which purges at once) and back. A BLOCK PURGE
line reports the time and VmRSS before and after. In the lib's tests, which
install jemalloc; a measurement knob, off by default.
Phase 4 round-robins its collectors into at most eight 80 MiB histograms, but
LT, KECCAK and the other per-op sources were one collector each: at the median
block LT (2.6 G cells of its ops kept to the finish) and KECCAK were single
long poles, and each built one list of every lookup it sends (LT 8 lookups per
op, KECCAK about 5,000 per permutation) before counting it. Every source that
is a sum over its ops is now cut into slices of whole ops (LT, SHIFT, BRANCH,
BYTEWISE, EQ, STORE 2^20; MEMW_A 2^22; KECCAK 2^11; ECDAS 2^16), and MUL and
DVRM, which deduplicate per instance, into one slice per instance. The
histogram is a commutative sum, so BITWISE does not move: the 187 whole-run
trace digests (four programs at small and default caps) are equal before and
after.
…rn freed pages during the finish

On BIG at 7.42x the finish ends with 40 GiB of jemalloc's resident pages not
live: among them the streamed chunks' ops, allocated by the walker and the
accumulator and freed by the committers while the finish runs, which no other
thread's arena reuses. This sets those two arenas' dirty and muzzy decay to 0
(purge at once, and on every later free) from the walk's end until the
committers are done, then puts back what they had; every other arena keeps the
posture. BLOCK DRAIN PURGE reports the arenas. In the lib's tests, which
install jemalloc; a measurement knob, off by default.
mem/stark-flip (narrow storage: the M3a/M3c/M3b chain, on by default, with
the BLOCK MEM measurement knob) on top of E1's lean walk and E4's paged
executor memory. One conflict, in executor/src/vm/memory.rs: Memory::heap_bytes
(BLOCK MEM's executor term) now reads E4's store, the pages and their
directories, or the word map under LAMBDA_VM_EXEC_MEMORY=words.
…rge 3be24ab

The packed KECCAK_RND / LT builds, the queue, finish, generator, purge and
drain-purge knobs, the in-flight BLOCK MEM terms and phase 4 in slices, on
#1013's head with E1 and E4. One conflict: both sides appended a test to
windowed_builder_tests.rs (E1's lean-walk identity test and the stream's
generator test); both kept.
With LAMBDA_VM_BLOCK_MEMLOG=1 the block installs a finish-marks hook
(trace_builder::set_finish_marks) for phase A: build_traces calls it after
phase 3 (LT), phase 4 (BITWISE), as each phase-5 generator finishes and when
phase 5 is done, and each call prints a BLOCK MEM line on the ledger's clock,
so the finish's memory (+23 GiB live at the median) can be read per phase and
per table. Unset, nothing is called; no table changes.
…sed walk

At the median block (BR-MED, BIG 440b) the heaviest-first fused walk puts the
36 KECCAK_RND instances ahead of CPU, and with packing every slot freed in the
first ~28 s goes to the next KECCAK_RND, LT or PAGE table: the card runs at
~68 % there and at 100 % for the rest, with the gate full throughout.

LAMBDA_VM_FUSED_WALK=mix keys the j-th of a type's n tables (j + 1/2)/n and
walks the keys ascending, so every prefix of the walk holds each type in its
share of the whole. Unset or `heaviest` keeps today's walk; anything else stops
the run. The walk only orders the drivers' claims: each table proves on its own
transcript fork and the proofs are drained in index order, so no proof byte
depends on it.

With LAMBDA_VM_TABLE_TIMELINE=1 and LFM_PROVE_SPLIT=1 each TABLE TL line now
also carries that table's own stage seconds (recommit, aux build and commit,
rounds 2-4), from a per-driver-thread copy of the per-table slots: the stages
run on the driver thread that claimed the table. Both off by default.
… committers

The interface for committing the tables the block's finish builds while the
finish still runs (I-SCHED lever 1), instead of in phase B's main commit:

- FinishSink: the finish hands each plain table it generates, by value, as
  soon as it exists; hand_or_keep is the one call per table, leaving the
  streamed chunks' placeholder in the slot when the sink takes the table and
  the table itself when the sink declines (its committers stopped).
- FinishedKind: the plain tables Traces holds in a Vec. The preprocessed
  tables and the singletons stay with phase B.
- air_for: the AIR VmAirs::new builds for an instance (same constructor, same
  name); precommit_finished makes the Round-1 commit under it; insert_finished
  puts the tables back into their placeholders, refusing any other slot.
- ChannelSink sends into a committer queue and declines once it is closed.

The prove still places each precommit on its AIR by name and absorbs every root
in AIR order, so no proof byte depends on when a table was committed. Nothing
calls the sink yet: the finish's calls come with the bounded finish, the
committer pool's with the phase-A gate.

Tests: the AIRs match VmAirs::new's by name, layout and constraints for every
kind (a swapped constructor fails it); a finish double hands every table off
from rayon workers and the build comes back whole; a table only lands on a
placeholder; the channel sink declines after its receiver is gone. A GPU-box
test proves one build with every plain table precommitted through the module
against phase-B commits: the same bytes, verified, and a precommit of another
trace is not accepted.
…_VM_BLOCK_GENERATORS=0 keeps the committers generating)

Each streamed chunk is generated, and packed on the host, by one of six
generator threads before a committer takes it, so the committers only commit
and a chunk waits packed rather than as its ops. On BIG at the median block
(25475471) with E1 and E4 the ops waiting for a committer fell from 32.8 to 1.4
GiB, the peak from 96.1 to 82.3 GiB and the base from 231.8 to 219.6 s; at
7.42x 75.4 to 66.8 GiB and 178.5 to 172.7 s; at 1x the base moved -0.05 s
(BIG 103, D g6 g6 D). The digest under fixed+dgrind was ac406fc8... with
generators on (BIG 101). No trace changes.
…ish marks and six generators, the base for levers 1 and 3
… then packs them

The A arm of the packed builds (badec90, ab807a2): with narrow storage the
finish builds KECCAK_RND and LT at 8 bytes a cell and packs each afterwards,
as every other table, instead of a block at a time
(WindowedTraceBuilder::build_wide_then_pack, StreamSkip::wide_builds).
i-median measured the median's phase B 7 s slower on a head with the packed
builds than on one without; this knob is the in-run control. The words are the
same either way (the finish-pack builder test also runs the wide arm).
… gate

The committer side of the finish's hand-off (I-SCHED levers 1 and 3), on the
generators' stream:

- The committers' unit is a FinishedTable: a streamed chunk or a table the
  finish handed off. BlockFinishSink puts a handed table into the committers'
  queue behind the streamed chunks without waiting (no ops budget: it is built
  already); a generator passes it through ungenerated, and the committer makes
  its Round-1 commit under finish_sink::air_for, which replaces stream_air.
  Every committed table goes back with finish_sink::insert_finished.
- A finished table the device packed after its commit drops its 64-bit copy
  there, as phase B's Round 1 does after the same commit.
- LAMBDA_VM_BLOCK_FINISH_COMMIT=after hands every plain table the finish built
  to the committers once it returns (finish_sink::hand_built): the finish does
  not hand its tables off while it builds them yet, so this is a measurement
  arm and the committer side's gate, not the overlap itself. Unset: phase B
  commits them, as before.
- Phase A's card gate: each commit is admitted by its device set (the estimate
  phase B's Round 1 uses) under the card's admission budget, so more
  committers cannot over-fill the card; before, each commit was admitted alone
  against the whole budget. LAMBDA_VM_BLOCK_CARD_GATE=0 drops it. A BLOCK CARD
  GATE line reports the most it held, the committers' wait and the finished
  tables committed.

The roots are still absorbed in AIR order and each precommit is placed by
name, so no proof byte moves. Test: the stream with the finish's tables
committed in phase A, the committers generating or generators ahead of them,
under a gate that admits one commit at a time, builds the same traces,
precommits every plain table, and leaves the ledger empty (a sink that
declines everything fails it).
…, finish permits, queue budgets)

BIG 101 and BIG 103 read no effect for the purge before the finish
(LAMBDA_VM_BLOCK_PURGE), the drain purge of the producer's arenas
(LAMBDA_VM_BLOCK_DRAIN_PURGE) and the finish's wide-chunk permits
(LAMBDA_VM_BLOCK_FINISH_WIDE, StreamSkip::wide_chunks, WidePermits); the
queue budgets (LAMBDA_VM_BLOCK_QUEUE_MIB, LAMBDA_VM_BLOCK_READY_MIB) are not
needed once generators keep the queues short. Their records stay in
FAILED-LEVERS.md and I-MEM.md. QueueRoom keeps counting what waits, for
BLOCK QUEUE; the stream test runs 2, 1 and 6 generators. No trace changes.
…LOCK_PACKED_BUILD=1 turns them on)

BIG 104's in-run A/B at the median (one binary, P W W P): with the packed
builds phase B was 6.32 s slower (P 145.75 / 141.34, W 138.29 / 136.16 s) and
the base 3.80 s slower, against a peak 14.75 GiB lower (82.9 vs 97.6 GiB).
That is over the lead's 3 s line for a default, so the finish builds every
table at 8 bytes a cell and packs it afterwards unless the knob is set. The
words are the same either way.
…g them

Phase 3 extended the kept LT tail with the MEMW and MEMW_A prefixes (the LT
ops of the MEMW chunks already handed out), the ops it derives from the MEMW
tails and the HINT range checks: at the median block several GiB of LtOperation
copied by Vec reallocation (BIG 103's finish marks: jemalloc resident +5.9 GiB
across phase 3 with live flat). The segments are now kept apart
(Segmented): phase 4 counts them slice by slice, and LT's chunks are cut from
them by chunk_and_generate_segmented, each chunk borrowed from one segment or
copied only when it straddles two, when its generation starts.
generate_chunks_with is generate_chunks over such views. The chunks are the
concatenation's: a test checks every split, chunk size, streamed-chunk mode
and the empty list against chunk_and_generate_skipping (a straddling chunk
that is not copied fails it), and the 187 whole-run trace digests are equal
before and after.
…the segments' A arm)

StreamSkip::concat_lt / WindowedTraceBuilder::concat_lt: phase 3 extends one
LT list with every segment, as it did before 5d4cca3f8, for an in-run A/B of
the segments' memory and time at the median. The tables are the same (the
finish-pack builder test also runs this arm).
Each BLOCK MEM line now carries /proc/self/stat's minor faults (millions): a
fresh page's first touch. BIG 104 read phase B 6.3 s slower at the median when
the finish leaves 15 GiB fewer freed pages resident; this counts the faults
per phase to confirm or refute that mechanism. Measurement only.
… committers

The interface for committing the tables the block's finish builds while the
finish still runs (I-SCHED lever 1), instead of in phase B's main commit:

- FinishSink: the finish hands each plain table it generates, by value, as
  soon as it exists; hand_or_keep is the one call per table, leaving the
  streamed chunks' placeholder in the slot when the sink takes the table and
  the table itself when the sink declines (its committers stopped).
- FinishedKind: the plain tables Traces holds in a Vec. The preprocessed
  tables and the singletons stay with phase B.
- air_for: the AIR VmAirs::new builds for an instance (same constructor, same
  name); precommit_finished makes the Round-1 commit under it; insert_finished
  puts the tables back into their placeholders, refusing any other slot.
- ChannelSink sends into a committer queue and declines once it is closed.

The prove still places each precommit on its AIR by name and absorbs every root
in AIR order, so no proof byte depends on when a table was committed. Nothing
calls the sink yet: the finish's calls come with the bounded finish, the
committer pool's with the phase-A gate.

Tests: the AIRs match VmAirs::new's by name, layout and constraints for every
kind (a swapped constructor fails it); a finish double hands every table off
from rayon workers and the build comes back whole; a table only lands on a
placeholder; the channel sink declines after its receiver is gone. A GPU-box
test proves one build with every plain table precommitted through the module
against phase-B commits: the same bytes, verified, and a precommit of another
trace is not accepted.
… generated

WindowedTraceBuilder::finish_handing(logs, sink) builds what finish builds,
but each of the 20 plain kinds (finish_sink::FinishedKind) goes to the sink
through finish_sink::hand_or_keep as soon as its chunk is generated and
packed, on the rayon worker that made it: a table the sink takes leaves the
streamed chunks' placeholder in its slot, one it declines stays. finish is
finish_handing with no sink, so every existing build is unchanged.

The instance a table is handed as is its slot in Traces: the chunk's place
among those generated, past the streamed chunks' placeholders (skipping and
segmented builds). BITWISE, DECODE, KECCAK_RC, REGISTER, PAGE, HALT and BLAKE3
stay with the build, and disk mode keeps every table (it spills them).

Packing carries the hand-off with its packing flag, since both say what the
build does with each table it generates.

Test: a windowed build with streamed ops dropped, packed or not, chunks of 4
rows streamed ahead, keeps no plain table when the sink takes them, and the
handed tables put back with insert_finished plus the streamed chunks give the
whole-run build; a declining sink leaves the build as finish makes it.
Mutations each fail it: the skip offset dropped (MEMW_R lands on a streamed
slot), the segmented offset dropped (LT), KECCAK_RND not handed.
At the median block the builder holds 9.5 GiB once the run is walked, and
the finish frees about 13.4 GiB when it returns, but nothing says which lists
those are. Under LAMBDA_VM_BLOCK_MEMLOG=1 the "windows walked" mark is now
followed by one "BLOCK MEM builder parts" line: the kept tails and lists, the
kept windows, each routed segment, what was counted ahead and the walk's
memory state, largest first (capacities, at least 0.01 GiB). The "p3 lt" mark
also carries the bytes of LT's ops and how many segments hold them.

WindowedTraceBuilder::heap_parts is the breakdown; it changes nothing.
Test: the parts add up to the accumulator's held bytes plus the walker's
memory state after every window, with the streamed ops dropped or kept.
Under LAMBDA_VM_TREE_PROGRAM_BUDGET (auto by default; the pipeline mode
without an emission window), every leaf and node program is admitted in
prove order before it is emitted. Beside phase B the late emission admits
leaves in order while the budget has room; the builder emits the rest on
four streaming threads as the budget admits them; each early node is
admitted before its emission. A permit travels in the slot and is dropped
with its program by the prover. On a host with room nothing waits, so the
schedule is the one before. The programs are the same, so no id moves.
…the cap in the tag (REV-P1-JUDGE)

The adversarial P1 review (REV-P1-JUDGE, SOUND-AFTER-FIXES) requires four
P1-local items before landing; this builds them.

1. Leaf width tag. Every P1 leaf's first-block capacity is
   [len, LEAF_DOMAIN, 0, 0] (LEAF_DOMAIN = poseidon1_w16::DOMAIN_LEAF,
   "P1WL"), mirroring RPX's leaf capacity: poseidon1_stark::linear_hash,
   the P1 backends through it, the production p1s_* device leaf kernels
   (zisk_leaf<V, TAG>; the measurement kernels keep ZisK's untagged leaf)
   and the emitted p1w16_emit::leaf_hash. ZisK's untagged leaf stays as
   zisk_linear_hash for its known-answer vectors. A leaf now binds its
   width: the judge's shape-dual family (w = 12, 18, 24, 30) parts, and
   the node(a,b,c,0) = leaf and in-block zero-padding identities are gone.
   No extra permutation. Every P1 byte and id moves; RPX's do not.
2. F4: at arity 4, cap 0 and odd depth the host requires the top group's
   two padding siblings to be the padding node, as the in-guest walk
   supplies them (cap.rs).
3. F2: the P1 statement tag names the policy (C0 for Off/Fixed(0), C<h>,
   Cauto), and checked_base refuses Fixed(c) above the arity-4 clamp (8).
4. The M8 gap: B's every-cap-node-is-bound test lands.

The reviewers' public tests land, flipped where these change them (A: the
inverse-permutation collision, the F4 pair, the tag, the raw sample; B: the
cap root, cross-verification, the W8 grind, the zero-padded opening with its
RPX control, the tag; the judge's width-tag family and identity tests on the
production leaf). Tests naming the inherited heights premise stay out. The
tagged leaf is pinned in Rust and, through the host shim, in the device
kernels' host KAT.
…H_ELF_CONSTS)

The constants took the four-thread pool beside the base and committed the
block ELF's twelve data pages one at a time. At P1's 12.2 s 1x base that
was 12.9 s, so the harvest waited 3.7 s and every leaf program, which needs
the constants, was emitted after the base (RYZEN 011).

They now compute on a host-only pool of their own, eight threads by
default, DECODE and the pages at once (ElfConstants::compute_parallel); the
pool beside the base keeps its four for the leaf programs.
NOEPOCH_ELF_CONSTS=0 restores the old path. The constants are the same:
tests pin compute_parallel to compute at widths 1, 3 and 8, on synthetic
pages and on an ELF, and the plan still refuses another ELF's or other
options' constants. A box instrument checks the block ELF at 4, 8 and 16.
…nstrument)

p1_one_table_per_leaf_sizing derives the P1 cap-1 plan over a real block's
shape (AIR-name rows reconstructed from a box run's census and table walk)
and, per leaf cap, emits every leaf: the socket tables (one, or split by
HashChunking), the instructions, cells and FAST cost law, and the node legs
a parent pays for its children; and the no-split rule's cells and legs at
the same cap. Emission only, no proof. partition_at_cap re-runs the rule
under another cap (test-only).
…short

LAMBDA_VM_TREE_LEVEL_PURGE (auto by default) purges every arena after a
tree level once the block's memory is short and VmRSS has reached 90 % of
the host target: a level frees its programs and working sets into pages
the next level rarely reuses (the p90 recursion held 12.9-30 GiB over its
live heap on a 74 GiB emulated host, BIG 662). A host with room never
purges there; off never purges there; always purges after every level.
LFM_PRECOMPUTED_TREE_CACHE_CAP, when the environment leaves it unset, is
set by the CLI's posture from the host target (the spill target): 64
entries (about 6.2 GiB once full) from a 64 GiB target up, 16 below it.
A miss only rebuilds a tree whose root is the key, so nothing moves in a
proof. The BLOCK POSTURE line compares the knob against the same rule.
…H table a leaf)

At v1's 162,000 every production P1 leaf held 2^17 to 3/4 * 2^18 socket
rows, so HashChunking split its LFM_HASH table in two and each parent
verified both. At 260,000 the leaves fill one 2^18 table: over the real
block shapes (reconstructed from RYZEN 011/012), 5 leaves at 1x instead
of 8 and 32 at the median instead of 52, socket rows 219,516-227,012 and
249,805-257,341, the parents' legs -41 % and -43 %, the leaves' cost law
+0.31 s and -1.07 s.

A new model id (0x5031_0002) moves only the P1 tree ids; RPX's model, its
partitions and every RPX id are unchanged. no_production_height_p1_leaf_
splits_its_hash_table checks the production-height plan's leaves; the
sizing instrument also prints each leaf's socket rows against its closed
form.
NOEPOCH_LEAVES forced the partition in the harvest only, while the
pipeline's leaf programs were emitted beside the base over the plan's
own partition, so level 0 took programs of another tree than its
arenas. The forced count now partitions the plan beside the base too
(test builds only, as before), so a forced arm proves one tree.
Brings #1013's six commits since 1a98537 (the staged precomputed-tree
download as the default, its per-path harness prints, no disk only where
live regeneration can drop) under the Poseidon1 base, P3a level 0, the
ELF constants' own pool and the P1 cost model v2. The memory default
lands in prove_block_under, the body both hashes share. One conflict, in
the tree harness: it keeps the base-format lines and reads the download
totals after the P1 statics are warmed.
…he GPU suite)

Per PR, the host-KAT job also runs the Poseidon1 width-16 kernels' known
answers (g++, seconds): the permutation, the base's width-tagged leaf
kernels against the host's tagged leaf, ZisK's untagged leaf, the 4-ary
node and the W8 grind. On the merge-queue GPU suite, group 7 proves
add.elf's no-epoch base under RPX and under Poseidon1 and runs the P1
refusals (other format, other dispatch arm, RPX's statement tag, flipped
bytes): about 16 s of proving on an RTX 5090.
A walk's lists, and the kept rest built from them, counted ECSM ops at
their inline size, so the double/add steps every witness holds (one Vec
of ECDAS steps a call) showed up only as unnamed memory. The ECSM list
now counts each op's steps buffer too. Logging only; the tables are
unchanged. (#1014's 02cb536, on this builder.)
… byte-identical)

collect_ecsm_ops cloned each witness's steps into the ECDAS ops and kept
the whole witness, steps included, in the ECSM op for the rest of the
run. Nothing reads an ECSM op's steps: its table and its BITWISE lookups
read the witness's other fields. The steps are now taken out of the
witness and moved into the ECDAS ops, so each call's steps are held
once.

Tables are unchanged: a test builds a call's ops for six scalars and
checks the ECDAS rows against a fresh witness's steps, the ECSM row
against the fresh witness, and that the ECSM op keeps no steps.
(#1014's b09c122, on this builder.)
Unset, NOEPOCH_ELF_CONSTS now decides by the base: a pool of their own of
8 threads under P1, whose 12 s 1x base cannot hide them (RYZEN 022: the
whole -3.40 s), and the pool beside the base under RPX, whose longer base
already hides them and where their own pool was neutral (+0.04 s) and
held the leaf programs 6 s longer (+0.65 GiB of host peak, RYZEN 021).
RPX's default is #1013's in bytes, time and memory. 0 and <threads>
apply to both hashes.
Brings the program budget (the tree's programs emitted under a host budget,
level purges, the CLI's cache-cap posture) and the S0e port (ECSM steps in
the ECDAS ops) under the Poseidon1 base. One conflict: BlockTreeConfig
gains both the base format and the program budget. P1 leaves go through
the budget as RPX's do: the budget takes the plan the pool beside the base
derives (after the harness's forced count), sizes each leaf's estimate
from leaf 0's own program, and the builder builds every program's
artifacts under the program's own hasher.
…d the builder

The ledger's builder term was stored during the walk and never reset, so
every line after the finish (phase B's included) still showed the run's
builder as held. The finish consumes the builder (finish_handing takes
it by value), so the term is set to 0 before the 'finish done' line.
Logging only; nothing reads the term.
…te-identical)

Port of #1014's S3c-BRANCH (5170a9e, 2b4ae41). The BRANCH segment
(32 bytes an op, 2.44 GiB at p90 on #1014) is kept as a DeltaStream: a
flags byte and three zigzag LEB128 varints an op, in 64 MiB blocks never
reallocated, with a sparse index every 4096 ops so any range expands on
its own. Every op takes at most 31 bytes, so it is lossless with no raw
form.

The run's segments take each op as it is routed (route_ops_into pushes
into the stream); a window's routing is appended op for op. p4 counts
BRANCH's lookups range by range, and its tables are chunked off the
stream as a compact segment (chunk_and_generate_segmented with no skip,
optional: the chunks chunk_and_generate_optional makes).

Tests: the codec's (any range across the index and small blocks, chunks
starting past a mark, the worst op at 31 bytes, the table of the
expanded ops); the lean routing tests compare the stream by its ops; the
whole-run and windowed builds, plain and dropping, make the same tables
with an index mark every three ops. The digest probe's 351 table-set
digests are identical against 20f8d3b.
…ical)

Port of #1014's S3c-EQ (c8612fb). The EQ segment (24 bytes an op, 1.20
GiB at p90 on #1014) is a DeltaStream: a flags byte (invert, and how each
operand is coded) and two zigzag varints, `a` as it is or from the
previous op's, `b` as it is, from the op's own `a` or from the previous
op's, each the cheapest. Every op takes at most 21 bytes, so any op is
kept losslessly. The run's segments push each op as it is routed; p4
counts EQ's lookups range by range; its tables are chunked off the
stream as a compact segment.

Tests: the codec's (any range across the index and small blocks, every
chunk size around the index's spacing and at marks every three ops, the
worst op at 21 bytes, the table of the expanded ops); the routing and
dense-mark build tests pass. The digest probe's 351 digests are
identical against 20f8d3b.
…; byte-identical)

Port of #1014's S3c-BYTEWISE (34ab1aa). The BYTEWISE segment (24
bytes an op, 1.03 GiB at p90 on #1014) is a DeltaStream: a flags byte
(how each operand is coded, and the opcode when it is under 8; a larger
one follows in a byte of its own) and two zigzag varints, `a` as it is
or from the previous op's, `b` as it is, from the op's own `a` or from
the previous op's, each the cheapest. Every op takes at most 22 bytes,
so any op is kept losslessly. The run's segments push each op as it is
routed; p4 counts BYTEWISE's lookups range by range; its tables are
chunked off the stream as a compact segment.

Tests: the codec's (any range across the index and small blocks, every
chunk size around the index's spacing and at marks every three ops,
operands at both ends of u64 / i64 and an opcode past the flags byte,
the worst op at 22 bytes, the table of the expanded ops); the routing
and dense-mark build tests pass. The digest probe's 351 digests are
identical against 20f8d3b.
…ort; byte-identical)

Port of #1014's S3c-ECDAS (in 6f0dc78). An ECDAS row is 1.87 KB, 1.5
KB of it the carries c0 / c1 / c2 (three [i64; 64]), which the table
range-checks as offset 16-bit values. The walk now keeps each row as a
CompactEcdasOp, its carries offset to u16 (about 0.72 KB a row); a row
any of whose carries does not fit keeps all three as they are (boxed),
so every row widens back to exactly its op whatever the input, with no
release assert relied on. An ECSM call's rows are converted as the walk
emits them, so the run's ECDAS list never exists wide.

Phase 4 counts each row's lookups from the widened op (the lookups are
split out as ecdas_lookups); the tables are generated by
generate_ecdas_rows_of, which widens each row as it fills it, at the
padded height generate_ecdas_trace_as uses, chunked as before.

Tests: a compact row widens to its op, carries at both ends of their
16-bit range held offset and any carry outside it (by one, or at either
end of i64) held raw; the table of compact rows is the table of the
ops, wide and packed, the raw rows included; real ECSM calls' rows are
all held offset, count the ops' lookups and build the ops' table. The
digest probe's 351 digests are identical against 20f8d3b.
…e (byte-identical)

The table phase's generators borrowed every op list, so every list lived
until the last family's tables were made (LT's, at p90). A generator
that is the only reader of its lists now owns them (a move closure, run
once through `run`, in the rayon scope and in the sequential path
alike), so each list is freed as its own family finishes: MEMW, MEMW_A,
MEMW_R, LOAD, SHIFT, MUL, DVRM, BRANCH, EQ, BYTEWISE, STORE, CPU32,
COMMIT, BLAKE3, ECSM, ECDAS and HINT, plus BITWISE's histogram and the
register map. The lists two generators read (CPU, KECCAK) and LT's
segments stay borrowed. Phase 4's serial path consumes its collectors so
none still borrows a list.

Only when the memory is freed changes; the tables are the same. The
digest probe's 351 digests are identical against 20f8d3b; the
trace_builder, ecdas and windowed-builder suites pass.
One line after the top proof's size: blake3 over every base instance's
main-trace root, in AIR order. Under LAMBDA_VM_FIXED_TRACE_HASH=1 two
runs that build the same traces print the same digest whatever their
grinding, so a box gate can check trace identity at any block size
without the deterministic grind (whose base at 1x takes ~12x longer).
Test harness only.
…estamp

The CPU holds its clock in one column and sends a zero high word on the
Memw bus; MEMW_R receives the timestamp as two 32-bit words. Below 2^32
the messages are one; past it only the two timestamp slots differ. The
block's clock is 4·cycle + 4 over the whole execution, so a block past
2^30 cycles cannot balance the Memw bus (T200-R, 1.83 G cycles, refused
with BusImbalance).
…imestamp)

A cycle's timestamp is 4·cycle + 4 over the whole run, and the CPU sends it
with a zero high word while the tables it talks to take two 32-bit words.
At 2^32 the two encodings part, the buses cannot balance and the proof
cannot verify (T200-R, 1.83 G cycles: BusImbalance). The walk now refuses,
with Error::ClockPastLimit, the window whose last cycle would reach 2^32
(cycle 2^30 − 1), and the finish refuses a final PC token the last
chunk's padding rows would put there. Below the bound nothing changes.

LAMBDA_VM_BLOCK_UNVERIFIABLE_CLOCK=1 proves past the bound anyway, for
memory and timing measurements, with a warning that the proof will not
verify.
…the knob is release-only

The REGISTER table's final PC token, halt_timestamp + 4·padding_rows + 1, is
the largest timestamp a run's trace holds: the last CPU padding row's PC
write. The finish now takes it from clock::final_pc_timestamp, and a test
builds a run just under 2^30 cycles whose padding rows cross 2^32: every
window fits, the CPU trace's last padded row writes the PC at the final PC
token, and that token is refused.

LAMBDA_VM_BLOCK_UNVERIFIABLE_CLOCK=1 proves past the bound only in a release
build: HALT and HINT debug-assert 32-bit timestamps, so a build with debug
assertions refuses instead of proving into them.
LAMBDA_VM_ALLOC_WATERMARK (auto by default) runs one monitor thread for
the block: every 0.5 s it reads VmRSS, and once the block's memory is
short, VmRSS has reached 90 % of the host target and the allocator holds
at least 4 GiB of freed pages, it purges every arena, at most once per
5 s. It reaches the pages a phase frees and does not reuse inside the
phase, which the boundary points cannot (at the p90 block on a 48 GiB
emulated host, phase A's end and level 1's end, BIG 683). A host with
room never fires it. Each fire prints one ALLOC WATERMARK line with the
time, VmRSS before and after and the purge's duration; the driver prints
the count at the end.
…tree cache's live bytes

The watermark monitor now samples every 0.5 s whatever its setting (off
only stops it purging): the highest-VmRSS reading of a window, with
jemalloc's allocated and resident bytes at it. The driver prints one
ALLOC PEAK line for the base and for each tree level, with the
precomputed-tree cache's live node bytes (a new reading in the stark
crate: inserts add, the cap's evictions remove). Measurement only.
auto admitted a tree program by room while VmRSS plus the program stayed
under the host target, so it spent again whatever the allocator's
watermark (at 0.9 of the target) returned: at the p90 block on a 48 GiB
emulated host, level 1's live heap was 42-46 GiB with the watermark
against 30-32 without it (RYZEN 050). The room line is now a share of
the target, 0.75 by default, below the watermark's; above it, programs
come in by the AHEAD rule alone. LAMBDA_VM_TREE_PROGRAM_BUDGET_SHARE sets
the share; 1.0 is the line at the target, the budget before.
MauroToscano added a commit that referenced this pull request Oct 7, 2026
… block)

LFM_PRECOMPUTED_TREE_CACHE_CAP, when the environment leaves it unset, is
set by the CLI's posture from the host target (block_whir's spill target):
64 entries (about 6.2 GiB once full) from a 64 GiB target up, 16 below it,
as #1013's CLI does (49c4bb5). A miss only rebuilds a tree whose root is
the key, so nothing moves in a proof. The BLOCK POSTURE line compares the
knob against the same rule, and a note says when the posture set 16.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant