Skip to content

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

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

MauroToscano wants to merge 1352 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.

…aces, with a mutation control

`the_compiled_kernels_prove_the_same_bytes` proved add.elf three times through
`prove_with_options`, and its control (two interpreter proofs) failed on the
card: two builds of one program's traces differ. The LT, BRANCH, MUL, DVRM, EQ
and BYTEWISE builders deduplicate through a std HashMap and lay rows out in its
iteration order. On the laptop, two builds of add.elf's traces give different
LT rows and different proof bytes.

The test now builds the traces once and proves copies of them (`FixedTraces`,
`prove_with_options`' prove step on a clone), at grinding 0:
- the control: two interpreter proofs, byte-equal;
- the compiled proof: byte-equal to them, verified, with compiled compositions
  counted;
- the mutation control: BITWISE's kernel swapped for a mutant whose last root
  is off by one. The proof must change and fail to verify, or the prover must
  refuse it, and the mutant must have run.

`one_set_of_traces_proves_the_same_bytes` runs the control alone on the path
the build proves on (ignored: two proves at blowup 4).

Supporting changes:
- `Traces` derives Clone in this crate's tests only.
- codegen: `mutant_composition_kernel` (the kernel plus one wrong statement)
  and `mutant_kernel_name`.
- The generated source carries BITWISE's mutant last. It is not in the key
  table, so no program runs it.
- gpu_interp: `substitute_compiled_kernel` and a call counter, compiled only
  under stark's `test-utils`, launch a named kernel in another's place.

The 36 production kernels and the key table are unchanged.
LAMBDA_VM_GPU_COMPILED_CONSTRAINTS unset, empty or 1 now evaluates every
composition that has a compiled kernel with it; 0 keeps the interpreter for
every program. The banner names the setting either way.

FAST job 260 (block 25368371, A B B A at 422c4b2): whole run 61.05 s with
the interpreter, 60.25 s compiled (-0.80 s; base -0.70 s), identities equal
in all four arms; the kernels run 2-3.8x faster than the interpreter per
program at 2^20 rows. The proofs are byte-identical: the device parity test
and the proof-bytes test with its mutation control passed on the card.
…easurement arm)

ResidencyMode::RecomputeLdeDevice lets one monolithic proof cover a whole
block on the card. Round 1 commits every table on the device and keeps only
the root: the device LDE, the device tree and the trace snapshot are freed
inside the admitted region, and no host LDE is downloaded. At the top of each
table's fused task the trace is committed on the device again, with the same
function and the same device-only gate as Retain's Round 1, and the prover
refuses the proof with RecomputedCommitmentMismatch unless the new root (and
the precomputed root) equals the absorbed one. From there the task is the
Retain task, so the proof is byte-identical. Tables the device declines in
Round 1 keep their host tree and recompute on the host, as RecomputeLde does.
Retain and RecomputeLde behave as before. The PROVE SPLIT line gains a
recommit[Σ] field only when a recommit ran.

prover::block::prove_block executes the whole block (DECODE's root derived on
the device beside the execution), builds every trace at uniform 2^21 caps and
proves it with the monolithic statement under the new mode. The existing
monolithic verifier checks the result. It prints the phase walls and a per-type
instance census.

Tests: the stark residency tests cover the new mode (byte identity, aux
release; on the card: every table recommitted, and a perturbed trace refused
for each table). The prover tests (ignored, GPU box) cover Retain against
RecomputeLdeDevice byte identity from one build of the traces on add,
test_keccak_multi and all_instructions_64 (2^5 caps), refusal of a moved trace
on CPU[0] and BITWISE, and the block harness noepoch_block_prove_and_verify.
@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.6 ± 0.1 2.5 2.7 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head hashmap 111.4 ± 7.1 105.7 130.5 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head keccak 140.6 ± 4.3 135.9 147.5 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head syscall_commit 88.4 ± 3.4 84.6 94.0 1.00

…able commits on the device

add's CPU table has 8 rows and commits on the host, so the device recommit
never ran for it and the test's own guard failed (FAST2 210: "CPU[0]: the
perturbation never fired"). At 160k cycles CPU[0] is 2^18 rows.
…both behind knobs

LAMBDA_VM_GATE_PACKING=1: run_admitted's drivers claim the first table in walk
order whose estimate fits beside what is admitted, instead of taking the next
table and blocking on it (which blocks every driver behind it while a smaller
table further down the walk would fit). An empty gate admits anything, as
before. Off by default: the walk-order admission is unchanged.

LAMBDA_VM_TABLE_TIMELINE=1: one TABLE TL line per table per admitted phase
(claimed, admitted, finished; unix seconds, the PROVE SPLIT clock) and the
bytes it was admitted for. Off by default.

For the no-epoch block, whose fused phase ran at 2.2 tables of parallelism
against the epoch path's 3.8 (FAST 351).
@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 56.2 ± 1.3 55.0 59.0 1.00
head binary_search 58.1 ± 2.8 56.2 65.7 1.03 ± 0.06
Command Mean [ms] Min [ms] Max [ms] Relative
base bitwise_ops 56.2 ± 1.3 55.0 58.4 1.00
head bitwise_ops 56.9 ± 1.4 55.3 58.8 1.01 ± 0.03
Command Mean [ms] Min [ms] Max [ms] Relative
base fibonacci_26 58.9 ± 1.3 57.6 60.7 1.00
head fibonacci_26 60.2 ± 1.3 59.1 62.6 1.02 ± 0.03
Command Mean [ms] Min [ms] Max [ms] Relative
base matrix_multiply 60.1 ± 0.6 59.3 61.4 1.00
head matrix_multiply 61.2 ± 1.3 60.1 63.7 1.02 ± 0.02
Command Mean [ms] Min [ms] Max [ms] Relative
base modular_exp 56.9 ± 1.3 55.3 58.5 1.00
head modular_exp 57.4 ± 1.4 55.9 59.5 1.01 ± 0.03
Command Mean [ms] Min [ms] Max [ms] Relative
base quicksort 59.4 ± 1.0 58.5 61.4 1.00
head quicksort 62.6 ± 2.7 60.3 69.7 1.05 ± 0.05
Command Mean [ms] Min [ms] Max [ms] Relative
base sieve 61.6 ± 1.0 60.3 62.8 1.00
head sieve 62.2 ± 2.7 60.5 69.2 1.01 ± 0.05
Command Mean [ms] Min [ms] Max [ms] Relative
base sum_array 69.8 ± 0.4 69.3 70.5 1.14 ± 0.01
head sum_array 61.2 ± 0.6 60.3 61.9 1.00

…erifier, build stamps

MaxRowsConfig gains keccak_rnd, the rows per KECCAK_RND table. Every existing
constructor sets KECCAK_RND_UNCHUNKED (one table, today's shape), so existing
proofs do not change. The block driver sets 2^16 (knob
LAMBDA_VM_BLOCK_KECCAK_RND_LOG2: off | 5..=26). A chunk holds whole
permutations (24 rows each). KECCAK_RND's constraints read only the current
row, and its rounds chain through the bus.

TableCounts::validate_for(AcceleratorShape) lifts the one-table bound for
KECCAK_RND under AcceleratorShape::KeccakRndChunked only. validate() and every
existing verifier keep Single. block::verify_block is the one verifier using
the chunked shape; the shape is the verifier's constant, never read from the
proof.

Why chunking matters for speed as well as soundness: on FAST 352 the single
2^18-row KECCAK_RND (30.3 GiB estimate, over the 23.4 GiB gate) ran alone for
6.1 s at the head of the fused phase.

Also:
- the block driver builds the traces step by step (DECODE artifacts and the
  initial image beside the execution) and prints collect vs generate;
- the recommit fault hook perturbs the last column (a committed one on every
  table; a preprocessed table's leading columns come from the cached tree), which
  is why FAST 352's gate saw BITWISE proved;
- tests: the shape rule (host), and a chunked KECCAK_RND proving and verifying
  under the block verifier and refused by the single shape (box).
…ocessed commitments beside Round 1

stark: IsStarkProver::precommit_main makes exactly the Round-1 main commit that
multi_prove would make for one table (the same domain, leaf layout, device-only
gate and residency). multi_prove_precommitted takes such commits in place of
its own and absorbs every root in AIR order as before; a precommitted table
costs nothing at the gate. multi_prove is multi_prove_precommitted with none.
The R1 per-table body is now r1_commit_table, shared by both.

prover: before Round 1 the block driver derives every preprocessed table's
commitment for its leaf layout on a helper thread, running beside Round 1. On
FAST 353 the 46 PAGE tables sat at the tail of Round 1 at about 1.7 s each; most
of their commitments are derived on the host on first use.
LAMBDA_VM_BLOCK_WARM_PRECOMPUTED=0 turns it off (the A arm).
…uring the block's execution

FAST 354: warming the preprocessed commitments beside Round 1 shortened Round 1
by 1.3 s and lengthened its prepass by as much, because the host derivation
competed with the prove's own host work (MECHANISM-ONLY). The only costly ones
are the ELF data pages' roots: zero-init and private pages have static roots.

page::data_page_commitment returns a root recorded for the same INIT column,
options and layout (page::record_data_page_commitment), and recomputes on the
host otherwise. The data-page lazy commitments now go through it. The block
driver derives both layouts' roots for every ELF data page with
commit_group_device_or_host_with while the executor runs and the card is idle.
The verifier recomputes every root, so a wrong record gives a rejected proof,
never an accepted one. The beside-Round-1 warm is removed.

Tests: the record rule (host), and device page roots equal to the host's for
all_instructions_64 and the ethrex guest (box).
…d each CPU instance committed as its window arrives

The executor runs on its own thread in windows of one CPU instance (max_rows.cpu
cycles). A WindowedCollector walks each window as it arrives, carrying the
memory and register state. The walk (collect_ops_from_cpu) now appends to a
WalkOutputs accumulator; the one-call wrapper is unchanged, so its outputs are
the same. For each window, one of two builder threads generates that CPU
instance's trace from the window's logs (generate_cpu_window, with the run's
timestamps) and makes its Round-1 commit on the device with
precommit_main. All of this happens while the run is still being executed and
collected. The other tables are built after the last window as before; the build
leaves Traces::cpus to the caller (CollectedOps::cpu_generated_outside).
prove_block_traces hands the CPU precommits to multi_prove_precommitted by
AIR name. LAMBDA_VM_BLOCK_STREAM=0 is the serial producer (the A arm).

Test (host): the windowed build equals Traces::from_elf_and_logs for
all_instructions_64 and test_keccak_multi at 2^5 caps and fib_iterative_160k
at 2^14. Tables laid out in op order are compared chunk by chunk; the six
HashMap-ordered ones by row multiset.
… LDE, not the hash

LAMBDA_VM_RECOMMIT_TOP_LEVELS=k (off by default). Under RecomputeLdeDevice, a
plain table's Round 1 copies its device tree's levels from the root down to
depth - k off the device (math_cuda::lde::download_tree_prefix, about 1/2^k of
the tree) before freeing it, into TableCommit::top_tree. Its fused task then
recomputes the device LDE alone (coset_lde_row_major_keep_no_tree, the keep
path's LDE and trace snapshot without leaf hashing or tree; its D2H/transpose
tail is now shared with the keep path as drain_and_transpose). The main-trace
openings are rebuilt in top_tree_proofs: each queried 2^k-leaf subtree is
hashed from the device LDE's rows with the host twins of the device kernels,
its root is checked against the kept node (RecomputedCommitmentMismatch
otherwise), and the path is the in-subtree siblings followed by the kept
levels'. The Merkle caps come from the kept levels. Preprocessed tables, and
any table whose LDE recompute the device declines, take the full recommit.

Tests (box, threshold 2, knob 2): byte identity with Retain plus verification;
a trace that moved is refused.
…ne line for each recommit

top_tree_proofs rebuilds each queried subtree on the rayon pool. Under
LAMBDA_VM_TABLE_TIMELINE=1 every table's second device commit (full recommit
or LDE-only) prints a TABLE TL recommit line, so a trace can place it.
… and keeps top levels by default

The shared windowed trace builder from noepoch/windowed-builder @ 1f60adc is
brought in unchanged: tables::trace_builder::WindowedTraceBuilder, route_ops /
RoutedSegments, StreamSkip, Traces::insert_streamed, and its tests
windowed_builder_tests and trace_digest_tests. It is now the single windowed
implementation (the lead's ruling), replacing this branch's CPU-only
WindowedCollector and cpu_generated_outside. trace_builder.rs goes back to
cc411aa plus that commit, plus KECCAK_RND chunking in build_traces, which is
re-applied unchanged.

block::build_streamed feeds each executor window to push() (a window is pushed
once the next one arrives; the last goes to finish()). The full chunks of the
eight streamed tables (CPU, MEMW_R, MEMW_A, MEMW, LOAD, LT, SHIFT, STORE) go to
three committer threads, which precommit each under its AIR (named as
VmAirs::new names it). insert_streamed puts the chunks back, and
prove_block_traces places the precommits by AIR name.

Kept top levels: stark::prover::set_default_recommit_top_levels sets the policy
RecomputeLdeDevice uses when LAMBDA_VM_RECOMMIT_TOP_LEVELS is unset (still off
for every other caller), and prove_block sets k = 3 (FAST 358: -4.89 s, gates
green on FAST 359).

Test (host): the windowed build equals Traces::from_elf_and_logs under the
block's own caps, with KECCAK_RND chunked; it replaces the CPU-only test.
…he leaf, the node

The block proof is one monolithic VmProof, so its recursion splits the
instances across leaves (D-NOEPOCH §12):

- block_replay: the Monolithic statement and Phase A over every root of the
  block, then z, α and the transcript's state digest. Each leaf replays this
  front itself instead of receiving a transcript state from another proof.
- block_leaf: a leaf verifies a fixed list of instances (the fork appends each
  index as a program constant, so the leaf's program id commits to its list)
  and publishes the attestation id, the state digest, the public output and
  its share of the bus; exactly one leaf subtracts the COMMIT-bus target.
  BlockPartition refuses a list set that misses or repeats an instance;
  partition_by_rule is §12.2's rule.
- block_node: verifies its children as an epoch node does (emit_leg), asserts
  the id, the state and the output equal across them and sums their shares;
  the top node asserts the sum is zero, the monolithic verifier's bus check.

Nothing here is reached by an existing program, so every proof and program id
of the epoch pipeline is unchanged.
block_tree_tests:
- the block front against the host transcript (z, α and the state word), on a
  synthetic statement and roots;
- the partition's refusals (a gap, an overlap, a repeat, an empty leaf, an
  index out of range) and the rule's seeding and fill;
- every node binding refuses its tamper and is load-bearing: with that check
  removed (BlockBindings::without) the same tampered words execute;
- box tier: leaves over a real small block proof execute and publish the
  host's values with shares that close the bus; a proved fixture tree whose
  top claims the block, a node refusing another block's leaf (equal output,
  different roots) unless the state check is removed, and a top refusing
  shares that do not close unless the bus assert is removed;
- box tier, production scale: the whole block, base to top node, timed, with
  the epoch tree's WHOLE RUN line.

The tree harness's sibling and sampling helpers become pub(super) for reuse.
…m the partition

The partition is bound only through program identity, so the block's verifier
must check the top proof against the top program it derives itself (from the
trusted ELF, the block's shape and the partition constant), as the epoch
tree's root check does (SOUNDNESS.md §6.9). verify_block_top is that check;
compose_block_tree now returns the top proof for it.

- The fixture tree adds the negative: a tree over another partition of the
  same block (one instance moved between leaves), every proof honest and its
  top claiming the same block, is refused against the derived top program and
  verifies against its own: the derived program is what refuses it.
- The production harness uses D-NOEPOCH §12.2's lists as the partition
  constant on the block they were written for (NOEPOCH_PARTITION=rule forces
  the rule), prints whether the rule reproduces them, and runs the final check.
The harvest's production verify of the base proof is a harness assert (5.16 s
on FAST 384), not work a driver does, and it sat on the recursion's critical
path before the first leaf. It now runs on a helper thread beside level 0 and
is joined after the top, before anything is reported: a refused block still
fails the run, the joined harvest must read the leaves' transcript state, and
any wait at the join is counted in the harvest. NOEPOCH_HARVEST_VERIFY=inline
keeps the old order as the control arm.
FAST 385 found e679e43's block proofs refused by production's verifier in 3
of 7 runs, at random across postures, and the test said only verified=false.
The two whole-block tests now initialise env_logger, so the verifier's own
error! lines (the failing table's index and which check failed) reach the
log.
rayon is optional in lambda-vm-prover (the `parallel` feature), but the shared
WindowedTraceBuilder imported it unconditionally, so every build without
default features failed — among them `make compile-recursion-elfs`, whose
guests take the prover without `parallel` (FAST 388's guest step: E0433 on
windowed.rs:47). The chunk jobs now run on rayon under `parallel` and in
order otherwise, as the rest of trace_builder does.
…ance

0dff9a6 showed a moved instance refused at the final check. The dangerous
trees are a skipped instance (its constraints never verified) and an instance
verified twice; BlockPartition::new refuses both list sets, but nothing binds a
prover to it. The fixture tree now builds both over an instance with a zero bus
contribution, so the bus still closes and every in-circuit check passes: each
tree proves and claims the same block, is refused against the top program
derived from the verifier's partition, and verifies against its own program
(the mutation). BlockPartition::unvalidated (test-only) emits such lists.
…ote its main snapshot

The kept-top-levels recompute (coset_lde_row_major_keep_no_tree) returns
with the trace-domain snapshot still queued on its stream: unlike the
tree-building commits it downloads no root, so nothing host-blocks behind
the snapshot. The LogUp aux build then reads the snapshot on a stream of
its own with event tracking off, and can read rows not yet written; the
aux columns come out of stale rows and the table's proof is refused
("Composition Polynomial verification failed") at random.

ResidentMainTrace now carries the producer's ready event and the aux
build waits on it device-side. LAMBDA_VM_GPU_AUX_WAIT=0 drops the wait
for the A/B and the reproduction; under LAMBDA_VM_GPU_XCHECK=1 a build
queued before its producer finished is rebuilt after it and reports
whether the first read was stale. A GPU test holds the producer stream
and checks the build waits (and, as its control, reads the zeroed
snapshot without the event).
TableChallengeShape::derive and TableVerifyShape::derive build a
sub-proof's challenge and legs shapes from (AIR, trace length, options):
the part count from the AIR's degree bound, the OOD blocks from its
layout, the aux-root and bus-input presence from has_aux_trace and
has_trace_interaction. host_table_forked and build_table_legs now call
them and assert the proof's blocks agree, instead of reading the shapes
out of the proof.

DerivedChild::from_artifacts gives a node its child's shapes from the
child's artifacts (AIR set and heights), with no child proof, so a
verifier can derive a tree's node programs itself.
record_data_page_commitment claimed a wrong recorded root is always
rejected because the verifier recomputes it. That holds only in another
process: inside the proving process data_page_commitment returns the
recorded root first, so the in-process verifier and AIR set read it
back. Say so, and name the device-equals-host gate as what keeps a wrong
root out.
…e shape

BlockTreePlan::derive(elf, opts, &BlockShape) runs verify_proof_parts'
pre-checks on the claimed shape (check_shape: accelerator shape,
private-page bound, page configs, instance count, trace lengths), builds
the host verifier's AIR set and takes every instance's shapes from the
AIR. It fixes the partition (the rule over the closed-form costs, one
more leaf while the heaviest is over the cap) and the carrier (leaf 0).

The leaf emitter takes the plan: the carrier bit is leaf == carrier()
and the published attestation id is computed from the preprocessed roots
the leaf absorbs. The node emitter and bind_and_publish no longer take a
binding set; the weakened sets are test-only entry points.

derive_top emits every leaf and node and builds their artifacts with no
proof; verify_block_tree verifies the top proof against that program and
checks the published id and output. The harness harvests over the plan,
emits leaves and nodes through it, and checks the derived top against
the proved one.

New tests: the plan refuses each shape the host refuses (laptop); leaf
arena census, tampered L, non-canonical output halves, two carriers,
part-count and contribution mutants, and the block verifier end to end
(box).
…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.
test_commit_3 commits three bytes, so the statement's last output half
carries one pad byte. The new fixture test executes the honest leaf and
shows the same leaf refuses that half with the pad byte set: the
m5 negative poc_rodata_commit could not exercise (its output fills its
last half).
…hape

ElfConstants holds DECODE's root, recomputed on the host from the ELF,
tied to the ELF digest and the options it was computed under;
BlockTreePlan::derive_with takes it and refuses another ELF's or other
options'. derive is derive_with over constants computed inline, so its
plans are unchanged.

The production tree test computes them beside the base under
NOEPOCH_ELF_BESIDE=<threads> (off by default) on a pool of their own,
and the harvest joins them; the harvest prints its plan / bus target /
verify split.
LAMBDA_VM_ALLOC_PURGE defaults to auto in the library and the suite env
leaves it unset, so a posture word for it made the harness's own BLOCK
POSTURE line read it as off-posture. The posture is now exactly the
behaviour knobs the suite env sets (LFM_EXEC_PARALLEL=1 stays: the suite
names it, at its library default).
HostSampler::stop expected its thread to be there and not to have
panicked, and the node builder expected a slot it had just filled: both
are on the production path now that the whole-block driver runs in the
CLI. A sampler that could not run reports no peak; a node whose program
never arrived is the builder's error.
A generator can now write its trace straight into the packed columns
NarrowMain holds, with no 64-bit table at all: the writer stores each
word's low bytes at a width the caller guesses per column and keeps the OR
of every word written to each column. finish() narrows the columns given
more bytes than they needed in place and gives NarrowMain::pack's bytes for
the same words; a word wider than its column makes it return the widths the
trace needs instead, for the caller to write it again.

TraceTable::try_from_narrow_main gives the packed trace back where a trace
cannot be held packed, so a caller never loses it. Test-only faults (one
column a width too wide, one column shifted) let the prover's gates show
they catch a wrong width or offset.
Under narrow storage every streamed chunk and every table the finish builds
was generated at 8 bytes a cell, then packed (NarrowMain::pack). Generation
plus packing is most of the producer's CPU, and the 64-bit table is most of
its memory traffic and page faults.

Each generator's fill is now generic over VmTable and runs through
generate_main!, which takes a TraceForm. Wide is today's table and every
build outside the block's narrow storage keeps it. Narrow writes the packed
columns directly (stark::narrow::NarrowWriter) at the widths the table's
earlier traces needed in this process (a WidthHint per table kind): the
packed bytes are NarrowMain::pack's of the same words whatever the hint, so
the hint changes time, never a byte. A kind's first trace, with no hint
yet, is built wide and packed as before (KECCAK_RND and LT by their packed
block-at-a-time builds), and its widths become the hint.

Converted: the eight streamed tables (CPU, MEMW_R, MEMW_A, MEMW, LOAD, LT,
SHIFT, STORE) and the finish's chunked tables (MUL, DVRM, BRANCH, EQ,
BYTEWISE, CPU32, COMMIT, KECCAK, KECCAK_RND, ECSM, ECDAS, HINT). The block's
generators write chunks packed through ChunkJob::generate_as, the finish
through WindowedTraceBuilder::generate_packed. LAMBDA_VM_BLOCK_GPACK=0 is the
old path (built wide, then packed); BLOCK NARROW reports how each trace was
built.

Tests: every path of the writer gives the wide table packed; every table of
a windowed build in the block's configuration, on four programs, has the
packed bytes and the words of the whole-run 64-bit build; one column a width
too wide or shifted fails that comparison; the phase-A stream builds the
same packed traces and precommits the same instances with G-pack on or off.
A box-only test builds a real block (NOEPOCH_ELF, NOEPOCH_INPUT) through the
windowed builder at the block's caps, once without G-pack (every table built
at 8 bytes a cell, then packed) and twice with it (the second build writing
every kind at the widths the first learned), and compares every table's
packed digest. The same builds and digests are checked on two small
programs on every run.
Under the shared VRAM gate each artifact commit asked the gate for its
device set from inside the build's parallel map, so the wait ran on a
rayon worker. A worker waiting in a prove's join runs other queued jobs
on its own stack, injected ones included: one that took a commit job
there could park on bytes held by the prove whose frame sits beneath
it, which can then never release them. Not observed in a run; reasoned
from the code (I-SCHED 9.30).

The walk now takes each window's device bytes (the sum of its commits'
device sets that reach the card) on the thread that forks it, before
the fork, and holds them until the window's last commit; the commits
in the fork are told the bytes are held and ask for nothing. The
derivation's own gate keeps admitting per dispatch. Every wait of the
shared gate (acquire, the packing arm, claims, proof places) now
refuses a rayon worker under debug assertions.

Scheduling only: the same commits, the same roots.
MauroToscano added a commit that referenced this pull request Oct 5, 2026
The same writer as #1013's G-pack: a generator writes its trace straight
into packed columns at guessed widths, the writer keeps the OR of every
word per column, narrows the columns given more bytes than they needed in
place, and names the widths the trace needs when a word did not fit. Its
bytes are NarrowMain::pack's for the same words.

Here a trace holds its packed columns as multilinear's NarrowColumns (the
same layout), so NarrowMain::into_parts is public and
TraceTable::try_from_narrow_main takes the columns, giving them back where
a trace cannot be held packed.
MauroToscano added a commit that referenced this pull request Oct 5, 2026
…ctly

Each generator's fill runs through generate_main! (tables::gpack), as on
#1013: TraceForm::Wide is today's 64-bit table; TraceForm::Narrow writes the
packed columns directly at the widths the table's earlier traces needed,
narrows or rewrites on a miss, and builds a kind's first trace wide then
packs it to learn them. The bytes are NarrowColumns::pack_row_major's of the
same words whatever the hint.

On the block (BlockOptions::gpack, LAMBDA_VM_BLOCK_GPACK=0 the old path):
- each streamed chunk is written packed by its job and laid out narrow from
  its packed columns (table_of_narrow), with no 64-bit table and no
  transposition, when the groups are held narrow;
- with pack_finished, the finish writes each table it packs packed
  (WindowedTraceBuilder::generate_packed), KECCAK_RND's chunks included;
- BLOCK GPACK reports how each trace was built.

Tests: every path of the writer gives the wide table packed; every table of
a windowed build in the block's configuration, on four programs, has the
packed bytes and words of the same build without G-pack; one column a width
too wide or shifted fails that comparison; the block proves with the same
partition and tables (the same bytes under the deterministic grind) with
G-pack on or off; a box-only test compares every table of a real block.
MauroToscano added a commit that referenced this pull request Oct 5, 2026
… checks are debug assertions

The shared VRAM gate's waits (an admission, the whole gate, a claim) refused
a rayon caller with a release assert!, as did the card permit (a hold on a
rayon worker, a second hold on one thread, two holders at once) and the
proof place under the armed gate. The callers make each of these
unreachable: every wait runs on a plain thread (the admission driver's, an
artifact build's before its fork, a tree node's) and one hold per thread.
Under the no-production-panic rule they are now debug assertions, as on
#1013 (60bbf53): the gate's three share refuse_rayon_wait.

The tests that rely on the panics (the gate's rayon refusal, the permit's
re-entry and rayon-worker refusals) compile only with debug assertions;
without them the re-entry and rayon tests would park instead of failing.
MauroToscano added a commit that referenced this pull request Oct 5, 2026
…'s hold never refused, the whole gate with one worker

Three edges i-sched2's review of the shared VRAM gate named (I-SCHED 9.33):

- An artifact window that reaches no device (host commits, empty shapes)
  still asked the gate for 0 bytes, a blocking call that refuses a rayon
  caller. admit_window now returns None at 0 bytes, as on #1013 (60bbf53).
- Under the armed gate hold_gated refused a rayon caller for every phase,
  though only a multi_prove waits there (its latch, then a proof place); an
  artifact build's hold never waits. The refusal is now the prove's only.
- With one worker an ungated hold (a W-LFM build or prove) returned before
  taking the card, and so before taking the whole gate, leaving it beside
  admitted bytes. It now takes the whole gate before those early returns;
  with more workers, still after the card, so a holder of the whole gate
  never waits for the card.

A test for each: a one-worker ungated hold waits for admitted bytes; a
window of empty shapes on a rayon worker asks nothing; under the armed gate
a build's hold on a rayon worker passes while a prove's is refused.
LAMBDA_VM_BLOCK_REGEN_PROBE=1 prints two lines and changes nothing else:
the first-touch census of the windows the walker walks (the memory bytes
whose carried state each window reads, the size of a per-window entry
state), and the main traces phase B starts from split by how they could
be rebuilt: the streamed chunks, the ordered class (KECCAK_RND, ECDAS,
CPU32, EQ, BYTEWISE, BRANCH) and whole-run tables. build_streamed now
returns how many chunks of each streamed table it handed out.
LAMBDA_VM_BLOCK_REGEN=shadow: phase A records a recipe per streamed
instance (its chunk, the windows it spans, the digest of its packed trace
taken where it was packed) and phase B runs the sequential regenerator
beside the prove on threads of its own: a fresh execution, the
regeneration walk (the walk's one body without the lists only the finish
reads), a slicer that cuts each table's list as the hand-out does, and
generators that generate, pack and digest-check every recorded instance
and drop it. Nothing it builds reaches the proof. It reports mismatches,
failures, its CPU, and the would-wait of a plan that proves the
regenerable instances last in completion order. Any other value of the
knob is an error, a regenerated mismatch is a report entry and never a
panic, and LAMBDA_VM_BLOCK_REGEN_GENERATORS sets the generator count
(default 3).
The box readouts parse these lines; printing them from a real regeneration
lets a readout be dry-run on the code's own output.
…nless a caller drops one

A packed main trace can be dropped after its Round-1 commit
(TraceTable::drop_main_for_regen): its shape and digest go into a slot of a
RegenWindow and its bytes are freed; a caller's regenerator deposits it
again in phase B. A deposit is checked against the dropped trace's shape
and digest; the window bounds the bytes deposited ahead of their takers.

Phase B: the fused walk takes the dropped tables last, in rank order; each
driver waits for its slot before its VRAM permit (FusedReady composes the
spill read-back and the slots); the fused task takes the bytes before any
device work. A wrong trace is RegeneratedTraceMismatch, a regenerator that
failed, stopped or never started is RegeneratedTraceFailed, as is a trace
dropped before its Round-1 commit. Nothing hangs: the window's last
producer dropping fails every slot not deposited, a panicked task closes
the windows, and the prove's end closes them. With no dropped trace the
walk, the readiness and every read are what they were.
…that never blocks

From R-REGEN (r-regen's review of R2):
- R1: the window reserves by rank from its frontier (the lowest rank not
  yet taken or failed) and always admits the frontier. The prefetch's byte
  rule deadlocked the walk-order arm when generators outnumbered drivers
  (out-of-order deposits filled the window while the rank a driver waited
  on could not deposit); a regenerator depositing in rank order still
  stays inside the window.
- R5: a fused task's take never blocks (it holds its VRAM permit); a slot
  not deposited is an error and settles failed, so a late deposit is
  refused instead of parked for nobody. Readers outside the prover wait
  first.
- A4: readiness is read without the window's lock (the gate's packing scan
  polls it under the gate lock).
- A7: the shape is always checked at deposit; a test-only switch turns the
  digest off so the checks behind it can be tested.
- A1: PrecommittedMain::recommits_on_device, for a caller to drop only
  traces with a device recommit behind the digest.
The review's pacing model runs on the real slots and driver loop, and the
box tests gain the digest-off negatives (never accepted; on the card,
refused by the kept-top check).
… lock; the second-layer card test

From r-regen's review of aa01315: N1, a slot's digest is computed before
the window's lock, so concurrent drops do not queue behind it; N4, a card
test where every regenerated trace is faithful and one packed bit is
flipped before the device widen, refused by the kept-top check. The
pacing test now runs the review's live configurations too (k = 3, G = 3;
k = 8, G = 3), and the dropped-trace readers say they belong beside the
fused phase.
… order; the card twin requires the kept-top refusal

From r-regen's review of e59fb09: the window reserves by (rank, id), so
the walk sorts the dropped tables by the same key and equal ranks cannot
cross (the liveness argument's premise that walk order is the window's
order). The digest-off card test now requires RecomputedCommitmentMismatch
from the kept-top check instead of also accepting a verifier rejection, so
it fails if that layer is removed.
LAMBDA_VM_BLOCK_REGEN=auto|always (default off; the default path is
unchanged). Phase A records each streamed chunk's recipe; once the spill
policy would move an instance off the host, a chunk whose fused task
recommits on the device is dropped instead of spilled (its packed bytes
freed after their digest, a RegenSlot left in their place), and the
resident ones are dropped back in rank order until their accounted bytes
reach host + spill reserve + regen reserve - (target - 2 GiB), computed
once at arming. Drops run on a thread of their own, off the committers'
lock, joined before phase A collects its traces. What cannot be
regenerated still spills; with no spill store the policy still decides
and the rest stays resident. The regen reserve counts only once armed.

Phase B starts a regenerator only when something was dropped: it walks
the run on phase A's windows (the ranks are phase A's hand-out order),
hands the dropped chunks out in rank order, fails a chunk not cut before
a higher rank (R10), fails every undelivered slot on each exit, keeps
draining after the window closes, and a guard closes the window on every
exit of the prove. An AIR set that disagrees with the traces is now an
error on the block path (try_air_trace_pairs), not an assert.

Laptop tests: drops and byte-identical regeneration under always and
auto, unarmed auto drops nothing, the drop-back stopping rule, the
slicer skip, every regenerator exit, the prove's end, windows other
than phase A's refused, and the AIR count refusal.
The regenerator generates each dropped chunk in phase A's form
(generate_as(TraceForm::Narrow) under G-pack, else 64-bit then packed)
and moves the packed trace out instead of copying it. The regen reserve
default goes from 6 to 10 GiB: BIG 636's phase-B peak VmRSS with the
shadow regenerator less without it (+5.90 GiB at p90) plus the 4 GiB
deposit window. The always test regenerates in both forms.
S1: a drop the drop thread finds no longer droppable stays counted
resident (the spill refuses a shared packed copy too, so there is no
spill to fall back on). S2: install_regen_main also checks the column
count (RegenSlot::cols). S3: phase A's window length travels in the
plan, and a regenerator on other windows refuses the whole plan before
anything runs. S4: drop-back counts bytes only once queued. S5: auto
never arms with the spill policy off (documented). S7: the laptop
matrices trimmed and the regeneration tests run one at a time
(block_regen:: 57 s, 3.6 GiB peak).
With LAMBDA_VM_BLOCK_SPILL=off and LAMBDA_VM_BLOCK_REGEN=auto, the auto
policy still decides, with no store, and nothing is written to disk.
Before arming it decides as today; on arming, drop-back takes every
resident regenerable instance (not only the need) and every later
regenerable instance is dropped; what cannot be regenerated stays on
the host and counts resident. The BLOCK REGEN dropped line says
"(no disk)" and the spill line "no store". Every other combination is
unchanged.

Laptop tests: unarmed no-disk drops nothing and builds the resident
traces; armed, every streamed chunk is dropped (drop-back took them
all), nothing spills, and the regenerator brings each back.
MauroToscano added a commit that referenced this pull request Oct 5, 2026
The same writer as #1013's G-pack: a generator writes its trace straight
into packed columns at guessed widths, the writer keeps the OR of every
word per column, narrows the columns given more bytes than they needed in
place, and names the widths the trace needs when a word did not fit. Its
bytes are NarrowMain::pack's for the same words.

Here a trace holds its packed columns as multilinear's NarrowColumns (the
same layout), so NarrowMain::into_parts is public and
TraceTable::try_from_narrow_main takes the columns, giving them back where
a trace cannot be held packed.
MauroToscano added a commit that referenced this pull request Oct 5, 2026
…ctly

Each generator's fill runs through generate_main! (tables::gpack), as on
#1013: TraceForm::Wide is today's 64-bit table; TraceForm::Narrow writes the
packed columns directly at the widths the table's earlier traces needed,
narrows or rewrites on a miss, and builds a kind's first trace wide then
packs it to learn them. The bytes are NarrowColumns::pack_row_major's of the
same words whatever the hint.

On the block (BlockOptions::gpack, LAMBDA_VM_BLOCK_GPACK=0 the old path):
- each streamed chunk is written packed by its job and laid out narrow from
  its packed columns (table_of_narrow), with no 64-bit table and no
  transposition, when the groups are held narrow;
- with pack_finished, the finish writes each table it packs packed
  (WindowedTraceBuilder::generate_packed), KECCAK_RND's chunks included;
- BLOCK GPACK reports how each trace was built.

Tests: every path of the writer gives the wide table packed; every table of
a windowed build in the block's configuration, on four programs, has the
packed bytes and words of the same build without G-pack; one column a width
too wide or shifted fails that comparison; the block proves with the same
partition and tables (the same bytes under the deterministic grind) with
G-pack on or off; a box-only test compares every table of a real block.
The unarmed half of no_disk_auto_drops_every_regenerable_and_spills_nothing
read the real host under auto, so on a Linux host with min(cgroup,
MemTotal) of about 16 GiB or less (CI's ubuntu-latest) it armed at the
first decision and failed. It now reads the host as 0 against a 64 GiB
target, as the armed half's fake does.

Before: LAMBDA_VM_BLOCK_SPILL_TARGET_GIB=5.6 failed at live_tests.rs:564
(Auto (no disk), 6 instances, armed at 0.0 s). After: it passes.
…s back (R-REGEN P1 A1)

Spill::consider's no-disk short-circuit (once armed, every droppable
instance is Fate::Drop) had no test that fails without it: the stream
tests' fake host stays at the target after arming, so baseline auto
drops the same instances. The new unit test arms on a host read as the
target, then reads 0 (under the target) and asserts a regenerable
instance still drops under no disk, is kept without it, and a
non-regenerable one stays resident and counted.
… P1 A2)

SpillPolicy::Off said "keep every trace", live.rs's module doc and
Spill::consider's doc said drop-back stops at the target: neither holds
in no-disk mode. The docs now say that with LAMBDA_VM_BLOCK_REGEN=auto
the off policy decides as auto with no store and drops every
regenerable instance once armed, and that an unparsable
LAMBDA_VM_BLOCK_SPILL value, which reads as off, enters it too.
With LAMBDA_VM_BLOCK_SPILL and LAMBDA_VM_BLOCK_REGEN both unset, the
block now runs no disk: spill off, regeneration auto. Auto still decides
with no store; once it arms, every regenerable instance is dropped and
rebuilt in phase B, and nothing is written to disk. The P1 gates
measured it at p90 (BIG 655: +2.13 s whole, peak 105.86 -> 98.36 GiB,
0 bytes written) and at full gas (ULTRA 072: +0.84 s, 105.73 -> 101.16
GiB, 0 bytes).

A knob that is set keeps the meaning it had: when either is set, an
unset spill is auto and an unset regen is off, as before. So
SPILL=auto alone is the old spill tier and REGEN=off alone falls back
to it, rather than to no relief at all.

The BLOCK POSTURE line now ends with the spill policy and regen mode
the knobs come to ("= no disk" at the default) and the knobs as set.
The parser tests pin both defaults and that set values parse alike
either way. No proof byte depends on the policy.
MauroToscano added a commit that referenced this pull request Oct 5, 2026
prover and stark gained a libc dependency on #1013 (f7ec661, c8c81f0)
without the recursion guest workspace's lockfile, so every
make compile-recursion-elfs rewrote bench_vs/lambda/recursion/Cargo.lock
and left a tracked file modified. This is the lockfile cargo writes; no
dependency version moves.
prover and stark gained a libc dependency on #1013 (f7ec661, c8c81f0)
without the recursion guest workspace's lockfile, so every
make compile-recursion-elfs rewrote bench_vs/lambda/recursion/Cargo.lock
and left a tracked file modified. This is the lockfile cargo writes; no
dependency version moves.
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