Skip to content

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

Draft
MauroToscano wants to merge 1495 commits into
mainfrom
noepoch/whir
Draft

MauroToscano wants to merge 1495 commits into
mainfrom
noepoch/whir

Conversation

@MauroToscano

@MauroToscano MauroToscano commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

One WHIR (multilinear) proof per block, with no epochs (the prove-and-retire / VADCOP shape). This is a second prover next to #1010's epoch-based one. #1010 stays the reference and this branch does not touch it. The branch starts from #1010's head f3d359998; compare against f3d359998 to see only this work.

Status: draft, work in progress. One binary (FAST 416, #1010's head 88b0d31 merged in, ABBA × 2): the block tree 23.20 s whole vs #1010's epoch tree 31.80 s, −8.60 s. Since then: 20.89 s whole at d9d0ac5 (FAST 424: base 16.76 + recursion 4.13), with the batched argue on by default, ops dropped after commit (M1), the lean walk and paged executor memory (E1/E4), and one prepared stack per group. e06dce5 added narrow trace storage (M4: same bytes, time-neutral, max RSS 1× 36.2 → 29.5 GiB, 4.13× now proves). 3cfffe2 added the KECCAK and ECSM split (§6: the same tables and proof bytes on every block measured so far, FAST 830). 01da99f uploads phase A's next group beside the current commit (prover-only, same bytes, base −0.60 s at 1×, FAST 832). 59e9890 lays out the streamed chunks on three threads by default (E3; prover-only, same bytes; whole block −0.48 s to 19.75 s and max RSS −1.51 GiB at 1×, FAST 838). 82f9046 re-hashes phase B's kept-top paths in parallel (prover-only, same bytes; phase B −0.96 s, whole block −0.82 s to 18.90 s at 1×, FAST 843 + 844 pooled). d169edd lets the tree's leaves execute while their artifacts are built (prover-only, same bytes; level 0 −0.24 s, whole block −0.24 s to 18.54 s at 1×, FAST 841 + 842 pooled). 70eee3e lays out the rest in waves and builds KECCAK_RND as its tables (W, lane i-m4b; same bytes; max RSS 88.9 → 79.5 GiB at 4.13×, base −0.14 s at 1×). fac261f lets the tree's first finished leaf execute during phase B (R2-ii; prover-only, same bytes; whole block −0.24 s to 18.39 s at 1×, measured before the merge with W, FAST 845 + 846 pooled). 365e3ab packs the finish's tables as they are built and commits them narrow (b2, lane i-m4b; same bytes; max RSS 78.8 → 52.1 GiB at 4.13×, 28.5 → 22.5 at 1×; base −0.36 s at 1×). 351a773 adds a device readout to the tree test (test-only), and the measurement posture moves to the card memory pool's code default, which retains freed memory: a baseline shift, not a prover change (whole block 17.91 → 17.27 s at 1×, FAST 848 + 849 pooled; every number above was taken under the old posture and stays as measured). 9a18ab5 makes the prover refuse a block over the group maximum as its groups close, with the verifier's own error (lane i-m4b; prover-only, same bytes, FAST 858). 54aae28 builds the recursion tree's node programs beside the tree, each level proving as its own nodes arrive (A; prover-only, same bytes; median whole block 242.69 → 233.80 s, −8.89 s, BIG 611; 2.66× recursion −1.22 s; 1× unchanged), and closes a latent card-permit re-entry that parked BIG 569. d107a91 proves each node of the tree as soon as its own children are proved (C; prover-only, same bytes; median recursion −1.73 s, BIG 612; 2.66× recursion −0.49 s, BIG 614 + 615; 1× unchanged). 0d61081 spills the held tables to disk when memory is short (lane i-m4b; prover-only, same bytes; #1013's store and auto policy, on by default; nothing spills at 1× or 4.13× by default, and the 1× time is unchanged within noise; BIG 616, ULTRA 006). acfd54a counts phase 4's BITWISE lookups in slices (#1013's c369843, ported by lane i-m4b; prover-only, same bytes; median whole-run VmRSS 108 → 89 GiB, phase A −9.5 s, BIG 619; 1× base −0.30 s, ULTRA 010). c55dec4 lets the tree's top execute each child's share as that child is proved, while a child is still unproved (B′; prover-only, same bytes; median recursion −0.47 s, BIG 613 / 620; 1× unchanged, where the top executes whole, ULTRA 009). 806c211 lets each node of the tree execute from its program as soon as it is emitted, with only its prove waiting for its artifacts (b′; prover-only, same bytes; 1× recursion −0.27 s, ULTRA 012; 2.66× −0.41 s, BIG 624; median unchanged). Head cbfa7d7 grinds the block's WHIR proofs at 18 bits instead of 20 and opens 114 queries instead of 112 (§7, a format change approved by Mauro 10-05; proven bits unchanged at 130.393; 1× whole block −0.27 s, ULTRA 019; median whole block −3.42 s, BIG 628). Every format change below but §6 (the port of #1013's S0a, gated FAST 830) is approved by Mauro (10-02; §7 on 10-05); the cryptography team reviews them at the end (CRYPTO-REVIEW.md rows W-0…W-6, W-8, W-9).

What it is

  • prover::block_whir::prove_block_whir / verify_block_whir. The whole block is one MultiProof:
    • every table is cut into instances of at most 2^21 rows, and KECCAK_RND into 2^16-row instances;
    • memory is the monolithic PAGE argument, with no local-to-global bookend and no cross-epoch proof.
  • The tables are packed into groups of at most 3 stacked polynomials (2^27 each).
    • Phase A commits each group and retires it: the codewords go, and only the top of each tree stays on the host (the bottom 4 levels are dropped).
    • The roots block: every root goes into the transcript, then (z, α, β) are drawn once.
    • Phase B proves each group on its own fork S_post ‖ g: upload again, argue (LogUp-GKR), re-encode with the NTT alone, open. The dropped tree levels are rebuilt from the queried cosets and checked against the kept nodes.
    • The bus is checked once, over every table of the block.
  • Streamed phase A. The executor runs in 2^20-cycle windows feeding the shared WindowedTraceBuilder (noepoch/windowed-builder, also used by STARK no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1013). Every full chunk of CPU, MEMW_R, MEMW_A, MEMW, LOAD, LT, SHIFT and STORE is laid out and committed as soon as it exists.
  • The builder is deterministic: on block 25368371 the whole-run build and two windowed builds under host contention give identical per-table digests (FAST 411; 129 tables row for row, the six HashMap-ordered tables as row multisets).

⚠ Format changes (all approved by Mauro, 10-02 (§7 on 10-05), but §6, the port of #1013's S0a; for the cryptography team's end review; ledger rows W-0…W-6, W-8, W-9 in CRYPTO-REVIEW.md)

The block is one WHIR proof (BlockWhirProof, one MultiProof) over all of the block's tables.

  • Phase A commits the tables in groups as the trace is built. Each group is at most 3 stacked polynomials of 2^27, and each group is committed and retired as it closes.
  • The challenges. The transcript then absorbs the statement, the groups' roots and the derived prepared roots, and draws z, α, β.
  • Phase B proves each group on its own fork of the transcript (S_post ‖ g).

Every parameter below is a verifier-side constant. None is read from the proof.

constant value meaning
BLOCK_GROUP_POLYS 3 a group of two or more tables stacks into at most 3 polynomials of 2^27
BLOCK_MAX_GROUPS 64 at most 64 groups
BLOCK_MAX_TABLE_VARS 27 no table stated over 2^27 rows
BLOCK_MAX_KECCAK_RND 2^12 at most 4096 KECCAK_RND tables
BLOCK_KECCAK_RND_MAX_VARS 16 each at most 2^16 rows
BLOCK_MAX_ECDAS 2^12 at most 4096 ECDAS tables
BLOCK_ECDAS_MAX_VARS 17 each at most 2^17 rows
BLOCK_MAX_KECCAK 2^12 at most 4096 KECCAK tables
BLOCK_KECCAK_MAX_VARS 18 each at most 2^18 rows
BLOCK_MAX_ECSM 2^12 at most 4096 ECSM tables
BLOCK_ECSM_MAX_VARS 17 each at most 2^17 rows
ArgueFormat::BATCHED bins of 2^26 input cells change 5 below
PRODUCTION_WHIR_GRIND_BITS 18 (Q 114) each WHIR chain's query grind; change 7 below
LEAF_PERMS_CAP, BLOCK_FAN_IN 279,000 permutations; 3 the recursion tree's leaf cap and fan-in

1. The group partition is in the statement

What. The streamed prover packs tables into groups in the order their chunks complete. It writes the partition (per group, its tables' indices) into the statement. The transcript absorbs it before any root.

Verifier. The verifier checks:

  • that the partition is exact (every table once);
  • that each group of two or more tables fits within 3 stacked polynomials under the verifier's own stack cap;
  • that there are at most 64 groups.

It then rebuilds every group's stack layout itself.

Security. Proven bits are unchanged.

  • Each WHIR chain is 130.393 bits, and the proof minimum under the campaign's min-over-phases accounting is 128.946.
  • The chain heights are capped by BLOCK_MAX_TABLE_VARS (change 3), so no partition can make a chain taller than 2^27.

Proof bytes vary from run to run. Arrival order depends on thread timing, and the hash-ordered tables' row order (LT, EQ, BYTEWISE, BRANCH, MUL, DVRM) follows a hash state that is random per process. Mauro, 10-02: "the proof not having the same bytes it's fine, they never had the same bytes anyways".

  • Byte-identity tests run with LAMBDA_VM_FIXED_TRACE_HASH=1 (fixed hash keys) and LAMBDA_VM_DETERMINISTIC_GRIND=1. Under both, two processes prove the same bytes (FAST 423).
  • Fixed keys let a crafted program steer many operations into one bucket, so they are a test setting, not production's.

2. Prepared openings, one stack per group (approved by Mauro, 10-02: "it's a standard technique, go ahead")

In short. This is #1010's existing prepared DECODE opening, carried into the block format. It is extended to the dense genesis pages and stacked per group.

  • Costs: +0.14 s of base and −0.57 s of recursion (4.71 → 4.14 s).
  • The verifier recomputes every derived root from the ELF.

What. The recursion guest cannot evaluate DECODE's five 2^20 preprocessed columns, nor the dense genesis pages (millions of rows), on its own. So each group's prepared tables (DECODE and the dense genesis pages chosen by genesis_stack_plan) have their leading preprocessed columns stacked into one commitment.

  • Both sides derive this commitment from the ELF and the partition. The verifier recomputes every derived root from the ELF: verify_block_whir, and verify_block_tree's plan. The prover's own roots only shortcut the prover's side.
  • Its roots are absorbed after the groups' roots, before z.
  • It is opened on the group's fork after the group's own opening, each table's block at that table's point.

Negatives, on a guest with two dense genesis pages. Each is refused by the host verifier and by the recursion leaf, and admitted when the openings are skipped (the mutation):

  • two groups' openings swapped;
  • a block opened at another table's point.

Wrongly committed stacks are refused at the roots block: two pages swapped, a page holding another page's columns, another program's DECODE.

Cost. One stack per group costs +0.14 s of base (FAST 419; one commitment per table cost +0.49 s) and saves 0.57 s of recursion, by taking 90 k permutations out of the leaves. BlockFormat::prepared = false turns the openings off, for measurement only.

3. ECDAS split; table heights capped

What.

  • ECDAS is split into tables of at most 2^17 rows, as KECCAK_RND is at 2^16 (split_ecdas).
  • A scalar multiplication may straddle two tables. Its steps chain only through the Ecdas bus, keyed by the call's timestamp and the step's (round, op).
  • The frame (the host verifier and the tree plan) refuses:
    • a KECCAK_RND table over 2^16 rows;
    • an ECDAS table over 2^17 rows;
    • any table over 2^27 rows (REV-JUDGE G2). A 2^30 table would otherwise make a 2^30 chain at 127.39 bits.

Why. At a median block, one ECDAS table (2^19) argues on a 12 GiB tree. At p90 (2^20) it stacks into five polynomials on a 24 GiB tree.

Gates. FAST 534 and 535: the negatives, 3 mutations caught.

4. Recursion leaf: the leaf cap (G3) and the inverse share (W1)

  • G3. The tree plan refuses a group over LEAF_PERMS_CAP. With no fixed leaf count, it adds leaves while the heaviest leaf is over the cap. The block's own partition is unchanged.
  • W1, on by default. Each WHIR leaf publishes its bus share Σ p/q as p · ediv(1, q), which has no satisfying assignment for q = 0 whatever p is. The former ediv(p, q) left the share free at p = q = 0 (≈ 2^-160 under GKR soundness).
    • The leaf gains one XALU op per table, so every WHIR tree id changes. The base proof is untouched.
    • Gate: FAST 536. LFM_WHIR_SHARE_INVERSE=0 keeps the former form.

5. Batched argue, on by default since d9d0ac5 (BlockFormat::argue = ArgueFormat::BATCHED; Mauro, 10-02: "batch the constraints")

What. Each group's tables are argued together on the group's fork:

  • one lockstep LogUp-GKR ladder per bin, the tables packed first-fit-decreasing under 2^26 input cells;
  • one front-loaded constraint sumcheck per group;
  • no claim reduction for unshifted tables (every VM table).

Verifier side. The verifier derives the bins from the statement's shapes and its own cap; the variant is never read from the proof. The proof carries one BatchedArgue per group in BlockWhirProof.argues, and proof.tables is empty under this format. The recursion leaves verify the same format (lane i-batch2, N-4).

Security at the block's measured inputs (D-BATCH §3.2's formulas; re-checked at FAST 424's inputs: |T| ≤ 60 tables a group, ≤ 51 trees a bin, ≤ 2^28.87 input cells, bus messages ≤ 204 elements, N_C ≤ 413, D_max 4, n ≤ 21; L = 300):

term bits
LogUp fractional 147.22
GKR step batching 185.34
GKR sumcheck round 190.42
GKR line 186.33
zerocheck point 187.61
constraint β (N_C − 1 = 412) 183.31
claim batching 184.52
constraint rounds 190.00

Every term is above the WHIR phase, so the proof minimum is unchanged: 128.946 under the campaign accounting (130.393 for the WHIR fold under the calculator of record).

Measured.

  • FAST 348: phase-B argue −0.685 s, whole block −0.385 s (≈ −0.65 s attributable).
  • FAST 424 at the flip (d9d0ac5, A B B A, one binary): base 17.56 → 16.76 s (−0.80), whole block 21.85 → 20.89 s (−0.96), recursion −0.16 s. Every proof verified; determinism within each format 2/2.
  • Peaks: BIG 402/403 found none raised at 1.0 / 1.31 / 1.80×. BIG 395 at 1.80× with ops dropped: per-table 62.39 against batched 63.12 GiB (+0.72).

The knob. ArgueFormat::PerTable (one argument per table, the format before) stays measurable, with BLOCK_WHIR_ARGUE=per-table in the real-block tests. At the per-table format, the MultiProof, the prepared openings and the partition are digest-equal to the head before the batched merge (BIG 393).

6. KECCAK and ECSM split; their heights capped (the port of #1013's S0a; landed at 3cfffe2; approved by the lead under Mauro's any-block target, Mauro to confirm)

What.

  • KECCAK is split into tables of at most 2^18 rows and ECSM into tables of at most 2^17 (split_keccak, split_ecsm), as ECDAS is at 2^17.
  • A row of either is one whole call (a permutation, a scalar multiplication), so a cut falls between calls. A call reaches its rounds (KECCAK_RND), its steps and scalar bits (ECDAS) and its memory only through buses keyed by its timestamp.
  • The frame takes up to 2^12 tables of each and refuses a KECCAK table over 2^18 rows or an ECSM table over 2^17. Every other verifier keeps both to one table.

Why. A keccak-heavy block at the gas limit makes about 2^21 permutation calls. One KECCAK table that tall stacks into eight polynomials of 2^27, against a group budget of three. At its cap each table fits one polynomial.

Bytes. Every block measured so far has KECCAK ≤ 2^17 rows and ECSM ≤ 2^11, so it builds the same tables and the same proof (FAST 830: digest 07d1bd43… unchanged on block 25368371).

Gates. FAST 830: KECCAK and ECSM forced into 4 tables each on block 25368371 prove and verify, base and tree; the count and height negatives; mutation C caught.

7. WHIR grinding 20 → 18 bits, queries 112 → 114 (landed at cbfa7d7; approved by Mauro, 10-05)

What.

  • Each WHIR chain grinds 18 bits instead of 20 before each round's queries, and opens 114 queries instead of 112 to buy the two bits back. At every block height (15 to 27 variables) Q = num_queries(2, rounds, 128, 18).

  • One site, multilinear_prove::chain_config_under. Every side of the block reaches it through BlockFormat::chain_config:

    • the prover's group chains and prepared openings;
    • verify_block_whir;
    • the plan the recursion leaves' in-guest verifier is emitted from.

    The statement absorbs Q and the grind bits.

  • Scope: the block's WHIR proofs only. The recursion tree's own leaf, node and top proofs are STARK proofs (FRI) and keep their own grind.

  • It ports WHIR recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 32.62 s #1010's grind 18 (landed there at 2687e48). Opt-out: LAMBDA_VM_ZF_WHIR_GRIND_BITS=20, which reproduces the previous proofs byte for byte.

Security. The calculator of record: Johnson bound, η = 1/300, the fold proof of work unground.

  • The binding WHIR phase is the unground fold at 27 variables, 130.393 bits at both settings. Neither Q nor the query grind reaches it.
  • The query phases move from 130.926 to 130.907 bits (15 to 27 variables).
  • The argue terms and the tree's FRI phases do not move. The proof minimum stays 128.946 under the campaign accounting.
  • The grind bits are a verifier constant:
    • a block ground at 18 bits is refused by a verifier at 20, and the reverse;
    • a leaf emitted at 20 refuses an 18-bit block;
    • at chain level, the host and the machine refuse fewer grind bits at the same Q.

Measured behind the knob, on c55dec4, one binary per box:

  • 1× (ULTRA 019, 8 + 8 runs after one warm-up):
    • phase B −0.344 s (t −12.9), from the grind falling 0.577 → 0.214 s;
    • whole block −0.27 s (18.71 → 18.44 s);
    • recursion −0.01 s, i.e. unchanged.
  • Median 25475471 at i-m4b's cap-128 exploration head (BIG 628, A B B A):
    • phase B −3.48 s;
    • whole block −3.42 s (226.51 → 223.09 s);
    • recursion −0.12 s;
    • both arms verified.
  • The leaves' cost. The leaves verify two more queries per chain: +1,041 permutations per three-polynomial group. On these two blocks no leaf table crossed a power of two, and the leaf counts did not change (3 at 1×, 23 at the median).

Bytes. The proof bytes change. Under the fixed trace hash and deterministic grind, the 1× top proof's digest is b9b4826d… at the default and 0da6fea9… under =20. Both trees are accepted (ULTRA 015, at this head).

Known limit. The leaf cap (279,000 permutations) sits above 2^18 LFM_HASH rows, and 18 bits moves every leaf about 1.7 % closer to that doubling.

  • On the median, the heaviest leaf's LFM_HASH is 256,549 of 262,144 rows: 2.1 % headroom.
  • A heavier block can cross it. That doubles the table, which costs that leaf's prove; it does not affect correctness.

Gates.

  • Laptop: zf_format::; the chain refusals and the legacy-bytes KAT; the pins the flip moved; the host block refusal.
  • ULTRA 019: the leaf refusal (box tier); the opt-out byte-identical to c55dec4.
  • ULTRA 015: the landing sha's identity, above.

One-binary A/B: the block tree vs #1010's epoch tree (FAST 416, noepoch/whir @ 454dad6)

Eight arms E B B E E B B E on the same binary (md5 checked after every arm):

whole block base last stage (E: root; B: top) whole − last stage harness verifies
E, #1010's epoch tree (n = 4) 31.80 s 22.48 s 1.05 s 30.75 s 0.92 s, in its timed path
B, the block tree (n = 4) 23.20 s 18.50 s 1.35 s 21.86 s 2.97 s, off the clock
B − E −8.60 s −3.98 s −8.89 s

Base, block 25368371 on FAST (each run against #1010's epoch base on the same binary)

run change block base
FAST 360 W1: non-streamed block proof 29.9 s
FAST 364 first streamed version 25.96 s
FAST 365 chunk generation off the walk thread 24.45 s
FAST 366 windowed executor 23.55 s
FAST 367 walker/accumulator split 22.68 s
FAST 368 walked windows kept whole, concatenated at finish 21.98 s
FAST 369 BITWISE counted per window, KECCAK_RND split by rows 20.73 s (epoch base 25.71 s, −4.98 s)
  • Phase B: 9.96 s. Host peak: 43 GiB.
  • Every block proof the windowed builder produced verified: 14 of 14 (FAST 363–369). Non-windowed: 6 of 6 (FAST 360–362).

Recursion (W3): proved and verified on block 25368371 (FAST 413)

  • Leaves are cut along groups: a group's opening covers all of its tables, so a group is the smallest unit a leaf can verify alone. The tree plan (lfm::whir_block::WhirBlockPlan) runs the host verifier's own statement checks, derives every shape from the AIR and every prepared root from the program, and never reads a proof. It prices each group in permutations and partitions the groups over the leaves (heaviest first, onto the least-loaded leaf).
  • A leaf:
  • Nodes are the STARK block's (block_node, shared byte for byte with STARK no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1013). They check the id, the state and the output equal across children, add the shares, and the top asserts zero.
  • Final check: verify_block_tree(elf, statement, top) derives the top program and checks the top proof against it. Every preset is pinned inside: the base's options, the block format, the tree's options, the leaf rule and the fan-in.
  • Tests:
    • every new check has a negative, and each negative a mutation showing that check is the one refusing;
    • the leaf's challenges equal the host's;
    • a tree over another partition is refused at the final check.
block 25368371 FAST 413: first build, serial FAST 414: pipelined (mean of 2 arms)
base (streamed, 9 groups, 27 chains, 12 prepared tables) 21.00 s 20.98 s
plan + leaf emission 0.23 s + 2.38 s, after the base inside the base (done at 12.2 s of 21.0)
leaves (3) 7.04 s, one at a time 3.36 s, three at a time
nodes 2 levels: 3.41 + 1.91 s one top over 3 children: 1.36 s
recursion 12.36 s 4.75 s
whole block 33.37 s 25.72 s
tree verify (derives every program from the ELF and the statement) accepted, 4.97 s accepted, 2.95 s

Every proof was verified by the harness off the clock. The base is unchanged by the emission running beside phase B (20.98 s vs 21.18 s in the arms without the change).

Next:

  • one prepared stack per group (group 8 carries 12 prepared chains);
  • the base levers: build_traces over segmented lists, and KECCAK_RND streamed.

Base levers after the A/B (FAST 417–422, each S/P on one binary)

lever base status
builder: windows concatenated in parallel −0.52 s (417), −0.21 s (419, replication in band) on by default
KECCAK_RND chunks streamed −0.14 s, no effect (this block's keccak work is at its end) off by default
LT's MEMW-derived ops streamed per window (FAST 420) +1.27 s, regression: the layout thread binds (9 more LT chunks, layout busy +2.47 s); LT heights deterministic across runs off by default
streamed chunks laid out on 3 threads, prepared columns after the groups (FAST 421, 422; BIG 390) −0.54 s (421), −0.22 s (422 replication); but the extra host memory scales with the block: +2.2 / +4.2 / +8.7 GiB max RSS at 1.0 / 1.3 / 1.8× (BIG 390) reverted at 11e2de5; on again since 59e9890, bounded (E3)
the rest of the run packed as it is laid out, AIR order (FAST 422) +0.21 s, regression: groups 6–8 still close together off

Memory (D-MEMORY, with i-mem / i-mem2)

stage effect status
M1: the builder drops each streamed chunk's ops as it leaves (drop_streamed_ops) FAST 501: 1× base −0.74 s, peak 47.4 → 36.6 GiB; the 1.20× block 25490321 now proves (49.2 GiB, OOM before). BIG 391 lean gate on the exact head: same statement shape kept vs dropped, both verify, 1.20× at 49.05 GiB on (f4aafea)
E1 lean walk + E4 paged executor memory (D-EXEC, lane i-exec) base −0.45 s (FAST 602); median windows phase 42.4 → 25.2 s (harness, FAST 599) on (5b4e6b4)
E1 v2: smaller per-step records (CPU op 96 B, MEMW_A ops as 48 B rows; D-EXEC, lane i-exec) byte-identical: proof digest equal to d9d0ac5's under the fixed trace hash + deterministic grind (FAST 426) on (773f134)
E3: layout workers bounded to K + 1 chunks unpacked (layout_ahead, D-EXEC) BIG 392: max RSS vs workers 0 +0.23 / −0.06 / +0.16 GiB at 1.0 / 1.3 / 1.8× (flat). FAST 425: no effect on time (whole 20.91 s both), since phase A no longer waits on layout on (3 workers since 59e9890)
one prepared stack per group prepared cost +0.49 → +0.14 s; recursion −0.57 s in
W: the rest laid out in ≤ 2 GiB waves; KECCAK_RND built as its 2^16-row tables at the finish (lane i-m4b) BIG 562: max RSS 88.88 → 79.54 GiB at 4.13×, 29.84 → 27.10 at 1×; FAST 852 + 853: base −0.14 s on (70eee3e)
b2: the finish packs each table as it builds it; phase A commits those tables narrow, never transposing them (lane i-m4b) BIG 563: max RSS 78.84 → 52.10 GiB at 4.13× (89.15 with W and b2 off), 28.53 → 22.45 at 1×; FAST 855 + 856: base −0.36 s on (365e3ab)

Narrow trace storage (M4, lane i-m4)

Prover-only; the proof bytes are the same. The proof digest equals the head's before M4 (07d1bd43…) under the fixed trace hash + deterministic grind (FAST 820).

  • What changes: between phase A's commit and phase B, each table of at least 2^16 cells is kept on the host packed at the width its values need, 1, 2, 4 or 8 bytes a column (BlockOptions::narrow, production Narrowing::CARD).
    • The pack runs on the card from the group's resident store, on a thread of its own, after the group's commit.
    • Phase B uploads the packed table and widens it on the card. A host reader widens on the host.
    • A wrong width map is refused by the kept-tree check (test plus mutation).
  • Time-neutral: whole block −0.04 s at 1×.

Max RSS, wide → narrow (BIG 560; the traces themselves shrink about 4×, e.g. 24.89 → 6.20 GiB at 1×):

block wide narrow
25368371 (1.0×) 36.23 GiB 29.47 GiB
25453112 (1.8×) 63.52 GiB 46.10 GiB
25482821 (2.66×) 80.80 GiB 52.83 GiB
25410821 (4.13×) — 86.93 GiB, proves and verifies

Before M4, #1014 ran out of memory at 3.03× (i-m4); the narrow line puts the 120.7 GiB edge near 5.5–6×.

Phase A uploads the next group beside the commit (3′, lane i-noepoch-w2)

Prover-only; the proof bytes are the same (digest 07d1bd43… at the default, FAST 832; the commits, their order and their bytes are unchanged).

  • What changes: phase A used to upload, commit and retire each group in turn, so the card waited on every group's upload. Now the current group's commit runs on a thread of its own while phase A's thread takes the next group and uploads its columns (BlockOptions::upload_ahead, on by default; BLOCK_WHIR_UPLOAD_AHEAD=0 is the control).
    • The upload starts only after the commit has asked the card for its room, so it takes what the ledger has left; a store the ledger refuses goes up after the commit, as before (no store or commit room was refused in any run).
  • Time (FAST 832, A B B A on one binary): base −0.60 s, phase A −0.66 s, whole block −0.61 s; 46 % of the upload seconds hidden. Groups 4–5 cannot hide theirs: the inline layout thread closes them after the previous commit ends, which is the next term (E3, next section: on since 59e9890).
  • Memory: one more group's columns held at the peak — the previous group's wide columns and its packed bytes wait for its pack's install while the current group commits (FAST 834, BLOCK MEM terms). At most one group (≤ ≈ 3.8 GiB), constant per group, not growing with the block; max RSS +1.8 to +2.3 GiB at 1× (FAST 832, 833, 834).

Streamed chunks laid out on three threads (E3 on by default, lane i-noepoch-w2)

Prover-only; the proof bytes are the same (digest 07d1bd43… at the default, FAST 838 and 835; the groups, their order and the packing are unchanged).

  • What changes: layout_workers 0 → 3 in BlockOptions::production (59e9890). The streamed chunks are laid out on three threads, with at most K + 1 = 3 of them unpacked at a time (layout_ahead, E3), instead of on the builder's one layout thread. BLOCK_WHIR_LAYOUT_WORKERS=0 (the inline layout, 01da99f's) is the control.
  • Why it pays now: with phase A uploading ahead (3′), the inline layout closed groups 4–5 only after the previous commit had ended, so the card waited on them; three workers close them in time.
  • Time (FAST 838, A B B A on two binaries, A = 01da99f, B = 59e9890): base −0.54 s (t −14.7), phase A −0.58 s, whole block −0.48 s (t −12.4; 20.23 → 19.75 s); phase B +0.06 s (one job, below what a single job resolves). FAST 835 (one binary, workers 0 vs 3) read whole −1.00 s.
  • Memory: max RSS −1.51 GiB at 1× (31.99 → 30.48 GiB, FAST 838; 835 read −0.68 GiB). The bound is what the earlier revert lacked: unbounded, the workers' host memory grew with the block (BIG 390); bounded, it stayed flat at 1.0 / 1.3 / 1.8× (BIG 392, measured before M4 and 3′, not re-run on this head).
  • 59e9890 also caps a readout stamp (an upload's "paid" seconds at the upload's own length); no proving change.

Phase B's kept-top paths re-hashed in parallel (lever 1, lane i-noepoch-w2)

Prover-only; the proof bytes are the same (digest 07d1bd43… at the default, FAST 840 at 733557a — 82f9046 adds only the knob; the paths, their order and the refusal are unchanged).

  • What changes: phase B serves each revived commitment's first-round paths from the kept tree top. The leaves under each queried block are gathered from the card, re-hashed on the host, and the block's subtree root is checked against the kept node. That re-hash ran one block at a time on the prover's thread while the card waited: 27 gaps of ≈ 37 ms, 1.01 s at 1× (nsys timeline, FAST 839). The blocks are now re-hashed in parallel and kept in block order; the root check still refuses before any path is returned. BLOCK_WHIR_REHASH_SERIAL=1 restores the serial re-hash (a measurement knob).
  • Time (FAST 843 + 844 pooled, 6 + 6 + 6 runs: one binary with the knob, serial S vs parallel P, plus 59e9890's binary as the reference R): re-hash 1.01 → 0.07 s; phase B −0.96 s (P − R; P − S −0.93 s, t −30.5); whole block −0.82 s (P − R, t −5.5; 19.72 → 18.90 s); phase A unchanged (this build vs R +0.09 s, t +1.0); max RSS unchanged.
  • FAST 840 (two binaries, 2 + 2 runs) read phase A +0.71 s, from one run that stalled in the layout; the code runs only in phase B, and the one-binary check shows no phase-A effect.
  • Also in: BLOCK OPEN SPLIT (with LAMBDA_VM_BASE_SPLIT=1), phase B's openings by host stage and the kept-top gather and re-hash seconds.

The tree's leaves execute while their artifacts are built (R2-i, lane i-noepoch-w2)

Prover-only; the proofs are the same (the top proof's digest equal with and without it, 0da6fea9…, under the fixed trace hash + deterministic grind, FAST 841).

  • What changes: WHIR no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1014's tree is proved by the W3 harness (prove_tree_pipelined, the way a prover would run it; there is no production tree driver yet). Its level 0 built all three leaves' artifacts first (three serial card holds, 0.24 s at 1×) and only then let the leaves execute and fill their traces (0.46 s until the first was ready) while the card sat idle (nsys timeline, FAST 839). Execution and fill do not read the artifacts; only the prove does. lfm_prove is now cut into its host half (lfm_execute_and_fill) and its card half (LfmFilled::prove, which asserts the artifacts' hasher is the one the traces were filled for), in the same order; the harness builds the artifacts on a thread of their own and each leaf executes and fills beside them, waiting for its artifacts only to prove. W3_EXEC_BESIDE_ARTIFACTS=0 is the control (the order before).
  • Time (FAST 841 + 842 pooled, 8 + 8 runs on one binary): level 0 −0.24 s (t −17.8), recursion −0.23 s, whole block −0.24 s (t −5.2; 18.78 → 18.54 s; per job −0.29 / −0.20); base and max RSS unchanged.
  • The first leaf now takes the card 0.48 s after the tree starts, not 0.76 s (one run of each arm, FAST 841's card-hold trace).

A second whole-block field. The W3 readout prints, on the clock between the base and the tree, the block report, a second block frame for the LT heights and the group tables: 0.13–0.14 s at 1× that a prover would not spend. "Whole block" keeps its meaning (every number above includes them); from d169edd on, W3 RECURSION also prints "whole excl. harness readouts" beside it (at d169edd, FAST 841 + 842 pooled: whole 18.54 s, whole excl. harness readouts 18.40 s).

The rest laid out in waves; KECCAK_RND built as its tables (W, lane i-m4b)

Prover-only; the proof bytes are the same (digest 07d1bd43… under the fixed trace hash + deterministic grind: FAST 854 at 0bcc1fc, and FAST 855's W arm on this head's code).

  • What changes:
    • The tables the finish builds (the "rest") were turned into columns all at once, and a table's column copy exists before its rows are freed. So for a few seconds most of the rest was held twice, and WHIR no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1014's memory log put the 1× and 4.13× peaks there (BIG 561). The rest is now laid out in AIR order, in waves of at most 2 GiB of rows, each wave in parallel (BlockOptions::rest_layout_bytes). The tables, their order and the groups are the same (test).
    • The finish built KECCAK_RND as one table of 1,480 columns, which split_keccak_rnd then copied into its 2^16-row tables. At 4.13× that copy was a second peak of the same height. The finish now builds the 2^16-row tables directly (BlockOptions::finish_keccak_rnd_chunks), the same tables as the split's (test), and hands none out during the windows, which would change the groups.
    • BLOCK_WHIR_REST_LAYOUT=all and BLOCK_WHIR_KR_FINISH_CHUNKS=0 restore the old behaviour.
  • Memory (BIG 562, one binary at 0bcc1fc, which 70eee3e merges with WHIR no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1014's later commits; A = both off, W = this; three layout workers; jemalloc never purges, the record posture):
block A W
25368371 (1.0×) 29.84 GiB 27.10 GiB
25482821 (2.66×) — 49.02 GiB
25410821 (4.13×) 88.88 GiB 79.54 GiB
  • The live peak (jemalloc active) at 4.13× fell 72.9 → 63.7 GiB and now sits at the finish's end.
  • Not as pre-registered, on magnitude: the bands were ≤ 76 GiB and a ≥ 10 GiB drop at 4.13×, and ≤ 27 GiB at 1×. The resident-set gain came from the KECCAK_RND copy alone. The waves bound the live column copies, but the resident set still grows ≈ 16 GiB during the layout, because the freed row buffers stay with the allocator while the column copies take new pages. That growth and the finish's 8-byte tables are the next cut's target (finish packing, being measured).
  • On the max-RSS line, the 120.7 GiB edge moves from ≈ 5.9× to ≈ 6.4–6.7× (a linear estimate).
  • Time (FAST 852 + 853 pooled, one binary per job, three layout workers): base −0.14 s against both off, phase A −0.12 s.
    • The time comes from the finish ending without the KECCAK_RND copy.
    • The waves add ≈ 0.3 s to the rest's layout, which phase A does not wait on.
    • Packing the rest as it is laid out (pack_rest_as_laid_out) was re-measured with the waves: no gain (+0.01 s), so it stays off.
  • Also in:
    • the block memory log, LAMBDA_VM_BLOCK_MEMLOG=1 (off by default). It prints the host memory term by term every half second and at each phase mark, with the line at jemalloc's active peak and each arena's bytes (those two in the lib tests, which install jemalloc).
    • the readouts BLOCK REST TABLES (each rest table's layout end and group) and each group's commit start (from@).

The tree's first finished leaf executes during phase B (R2-ii, lane i-noepoch-w2)

Prover-only; the proofs are the same (base digest 07d1bd43… and the top proof's digest equal with and without it, 0da6fea9… — FAST 841's — under the fixed trace hash + deterministic grind: FAST 845, and FAST 847 again on fac261f, the merge with W).

  • What changes: phase B now hands each group's share of the proof (its argue, its opening, its prepared opening) to an observer as that group's opening ends (block_prove_on_forks_observed; the old entry point passes a no-op, and the proof is the same with any observer). A leaf's arena is built from its groups' words alone (group_arena_words + leaf_arena; block_leaf_arena is built from them). The W3 harness turns the groups into words as they arrive; the first leaf whose groups are all opened while another group is still to come (leaf 0 = groups 1, 5, 6 at 1×, done after group 6) executes and fills its traces on a thread of its own, about 1.5 s before the base ends, and the tree picks it up. Off the clock its arena is checked against the finished proof's. W3_LEAF_DURING_PHASE_B=0 is the control.
  • Time (FAST 845 + 846 pooled, 8 + 8 runs on one binary, at c5d9cec before the merge with W): whole block −0.24 s (18.63 → 18.39 s; per job −0.21 / −0.28); phase B +0.02 s; max RSS unchanged. The card's wait before the first leaf's prove fell from 0.23 s to 0.003 s; level 0 gained 0.15 s of it, since the first prove now runs beside the other two leaves' execution and fill.
  • Memory: the host's high-water is set in phase A at every size (1×: 25.5 GiB active at its peak, 7.2 GiB active by group 6's end with 31.0 GiB resident; 4.13×: 63.7 GiB at the finish's end, i-m4b), so one leaf's traces during phase B's tail fit in what phase A already holds.

The finish's tables packed as they are built (b2, lane i-m4b)

Prover-only; the proof bytes are the same (digest 07d1bd43… under the fixed trace hash + deterministic grind at both arms: FAST 855 at 0cebe16, and FAST 857 again on 365e3ab, the merge with R2-ii).

  • What changes:
    • After W, the finish still built its tables 8 bytes per cell, and phase A transposed each one into columns before packing it on the card. At 4.13× that layout took 24.6 GiB of wide tables and grew the resident set by ≈ 16 GiB (BIG 562).
    • The finish now packs each table into the narrow layout as soon as it is built (BlockOptions::pack_finished). KECCAK_RND is packed in waves of four 2^16-row tables. Phase A takes those tables as narrow columns and uploads them as they are, so the card packs only the groups' other tables. Nothing packed is transposed.
    • KECCAK, ECSM and ECDAS stay wide (BLOCK REST PACKED counts the rest: 187 of 191 tables at 4.13×).
    • A packed table's preprocessed columns are checked word for word against the AIR's before it is committed; a wrong one is refused (test, with a mutation).
    • BLOCK_WHIR_PACK_FINISHED=0 restores W.
  • Memory (BIG 563, one binary at 0cebe16; O = W and b2 off, A = W, B = W + b2; the memory log on; jemalloc never purges):
block O A (W) B (W + b2)
25368371 (1.0×) — 28.53 GiB 22.45 GiB
25482821 (2.66×) — — 37.64 GiB
25410821 (4.13×) 89.15 GiB 78.84 GiB 52.10 GiB
  • At 4.13× the live peak (jemalloc active) fell 63.7 → 49.7 GiB. The resident set now sits within 2 GiB of it (retention +2.0 GiB, from +14.7), and the rest's layout adds +1.15 GiB to it instead of +16.1. Every pre-registered row is in but one: the 1× live peak, at 21.0 GiB against a band of ≤ 19, because at 1× it falls in the middle of the finish, where the commit pipeline's in-flight copies bind.
  • On the max-RSS line (52.10 GiB at 4.13×, 12.82 + 9.47 GiB per 1×), the 120.7 GiB edge moves from ≈ 6.4–6.7× to ≈ 11.1–11.4× (a linear estimate for the base alone; the median block is 9.78×).
  • Next in line at the 4.13× peak: the committed groups held narrow for phase B (18.5 GiB, ≈ 5.9 GiB per 1×), then the finish's tables still being built (16.5 GiB).
  • Time (FAST 855 + 856 pooled, 8 + 8 runs on one binary): base −0.36 s (t −6.2; per job −0.40 / −0.32), all of it in phase A: the rest is laid out in 0.13 s instead of 0.57.

Measurement posture: the card's pool now retains freed memory (baseline shift)

Not a prover change; the proofs are the same (the top proof's digest 0da6fea9… under both postures, fixed trace hash + deterministic grind, FAST 848; 351a773 adds only a test readout).

  • What the env set, and why: every WHIR no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1014 box run (the canonical env since FAST 416) set LAMBDA_VM_MEMPOOL_RELEASE_MB=0. The card's stream-ordered memory pool then hands its freed blocks back to the driver at each synchronize, so a VRAM sampler's total − free reads the live working set. The code default is to retain every freed block (DEFAULT_MEMPOOL_RELEASE_THRESHOLD_BYTES = u64::MAX in math-cuda), and that is what a prover runs.
  • What release-0 cost: each synchronize paid for the release, and the next large allocation mapped the memory again. In FAST 839's trace, the time spent inside cuStreamSynchronize and cuMemAllocAsync while the card was idle was ≈ 0.25 s in phase A, 0.42 s in phase B (0.26 s of it in the encode: one 18–33 ms gap per group) and 0.34 s in the tree.
  • Measured (FAST 848 + 849, one binary at 351a773, release-0 vs the default, 8 + 8 runs): whole block 17.91 → 17.27 s, −0.64 s (t −6.0). Phase B −0.44 s (t −28, against the 0.42 s above); phase A −0.07 s (t −0.7; phase A's runs scatter); recursion −0.12 s (level 0 −0.09 s); base −0.51 s; max RSS −0.16 GiB.
  • Gates, every run: the tree verified; the log's posture line matched its arm; no host fallback, device commit error, decline or ledger refusal; the card's peaks 24.15 GiB (the ledger's reservation) and 22.8 GiB (the pool's live high-water), read from the ledger and the pool, not from total − free (the new W3 DEVICE line).
  • From 351a773 on, WHIR no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1014's numbers are taken at the code default. Every earlier number in this description was taken under release-0 and stays as measured. VRAM figures come from the ledger or the pool's high-water, never from total − free.

The prover refuses a partition over the group maximum (lane i-m4b)

Prover-only; the proof bytes are the same (digest 07d1bd43…, FAST 858). The prover now refuses a block over BlockFormat::max_groups as its groups close, with the verifier's own InvalidTableCounts error, instead of proving a block the verifier refuses. The median block (90 groups against the cap of 64) spent its whole ≈ 190 s base on such a proof (BIG 564). A test covers both prover paths, with a mutation for each check.

The tree's nodes built beside it, each level proving as its nodes arrive (A, lane i-noepoch-w2)

Prover-only (the W3 harness); the proofs are the same (the top proof's digest 0da6fea9… — FAST 841's — with and without it, under the fixed trace hash + deterministic grind: FAST 889, and FAST 891 again after the permit fix below).

  • What changes:
    • The W3 tree (prove_tree_pipelined) built every node program and its artifacts on one thread, level after level, and level 0 joined that thread before any node proved, so level 1 waited for the whole tree's builds. At the median block, 32 s of serial node builds after the leaves' artifacts held level 0 open 10.7 s past its last leaf proof, and the card sat idle 11.4 s before level 1 (BIG 565 / 568's card trace). At 2.66× the wait was 1.5–2.1 s (FAST 888).
    • Now a builder runs beside the whole tree and puts each node's program and artifacts in a slot of its own as soon as they exist. A level's programs are emitted together on a pool of W3_EMIT_THREADS threads (default 4, the builder's own: on the global pool a prover's join can steal an emission and leave the card idle, STARK no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1013's FAST 454), and the builder's thread builds each one's artifacts as it arrives. Each node proves once its children are proved and its slot is filled. A builder that stops fails every empty slot. W3_NODE_PIPE=0 is the control.
    • This is STARK no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1013's slot pipeline (i-tree's per-node emission, −7.32 s at the median there) in the W3 harness, with three differences: W3's Published<T> serves as the slot; there is no per-group early emission (W3 builds every leaf's artifacts up front, so a level's programs are emitted together); and there is no host-only thread marking (WHIR no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1014 has none).
    • The level builder is generic (build_levels, an emit on the pool and a finish on the builder's thread). A toy tree at the median's shape puts the serial builder's programs in their slots whatever order a pool finishes them in, and finishes every node off the rayon workers (test; finishing on a worker, or publishing in arrival order, fails it). A failed build fails every slot above it without a hang (test).
  • Time:
    • Median (BIG 611, block 25475471 at i-m4b's cap-128 exploration head with these commits, P O O P on one binary): whole block 242.69 → 233.80 s, −8.89 s, every pipeline run below every control run (the estimate from BIG 565 / 568 was −10.6 s). Recursion 52.85 → 43.89 s; level 1's start gap 12.52 → 1.73 s (card idle 11.4 → 1.5 s). Peak VRAM 24274 → 24242 MiB and VmRSS 112.44 → 111.95 GiB: no growth (the gates).
    • 2.66× (FAST 889 + 890 pooled, one binary, 4 + 4 runs): recursion −1.22 s (t −5.2; whole block −1.21 s). Level 1's start gap (the last leaf proof's end to level 1's first prove on the card) fell from 1.97 to 0.69 s. After the permit fix: −1.19 s (t −6.5, FAST 891).
    • 1× (8 + 8 runs): recursion +0.007 s (t +0.3). At 1× the tree has one node, built in the same 1.8 s either way, so there is nothing to take.
  • What is left: a level still starts only after its children's proofs and its node's own execute + fill (0.45 s at 1×, 0.63 s at 2.66×; at the median the card sits idle 0.9–1.1 s at each later level's start). That start term is the next one; it is not addressed here.
  • Part of this landing: the card permit's latent re-entry, closed (lane i-m4b's diagnosis and repro). W3 built its leaves' artifacts as rayon jobs that each held the card around a build that uses rayon. A holder that waits inside rayon runs queued jobs on its own thread, so a sibling build could take the card a second time on that thread: the permit's assert fired and BIG 569's run parked. Which run trips it was the scheduler's choice (568 ran clean on the same code). Now:
    • the armed permit refuses a hold on a rayon worker before it takes the card, so any hold inside a rayon job fails at once instead of by luck (i-m4b's repro test now asserts the refusal; a plain thread holding the card while rayon builds under it is tested too);
    • W3 builds the leaves' artifacts one after another on their own thread (each held the card throughout anyway);
    • the node pipeline builds artifacts only on the builder's plain thread;
    • the harness disarms the permit before its off-the-clock verify, whose WhirBlockPlan::programs builds as rayon jobs.
  • Also in: W3_NODE_TIMES=1 (off by default). It prints, per program, when it was built, taken and proved; its prove's split (execute, fill, the prove net of the card wait, the wait); and each node level's start against the level below. The stamps are taken either way and cost a handful of Instant reads.
  • Toward a production tree driver: WHIR no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1014 has none, and prove_tree_pipelined is its de-facto driver. With the slots it now has a driver's shape: leaves from phase B, nodes from slots, levels as their nodes and children arrive. Moving it out of the test harness is a landing item for STARK no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1013 and WHIR no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1014 both.

Each node proved as soon as its own children are (C, lane i-noepoch-w2)

Prover-only (the W3 harness); the proofs are the same (the 1× top proof's digest with and without it, under the fixed trace hash + deterministic grind, both trees accepted: 0da6fea9…, BIG 614 — the digest FAST 841 / 889 / 891 printed).

  • What changes:
    • After (A), a level still started only once its whole level below was proved, and then ran its first node's execute + fill with the card idle. At the median the card sat idle 1.4–1.6 s before level 1 and 0.9–1.1 s before each later level (BIG 611).
    • Now one pool of siblings workers proves every program of the tree, leaves and nodes, taking them in topological order (the leaves, then each node level) and publishing each proof into a slot of its own. A node waits only for its own children's slots, so it executes, fills and proves while the rest of the level below still proves. W3_DATAFLOW=0 is the control: each node waits for its whole level below (tested never to start early).
    • Workers that take programs in this order reach a node only after every program before it has been taken, so the earliest unfinished program never waits on one not yet taken: no deadlock at any number of workers (test; taking the nodes first deadlocks it). The siblings bound, and so the memory in flight, is unchanged. A failed or panicking prove fails every program above it without a hang (test).
    • A level's W3 LEVEL wall now runs from the level below's last proof to its own.
  • Time (one binary, arms alternated on one box):
    • Median (BIG 612, block 25475471, F O O F): recursion 43.78 → 42.05 s, −1.73 s, every dataflow run below every control run. The card's idle at level 1's start fell 1.4 → 0.0 s, and at level 2's 0.93 → 0.0 s, in both runs. Whole block −2.60 s (234.15 → 231.55 s), but −0.87 s of that is the base, which the knob cannot reach (it is read after the base): recursion is the number. VRAM flat (24306 → 24274 MiB); the tree phase's VmRSS max 120.05 GiB (one control run) / 112.07 GiB.
    • 2.66× (BIG 614 + 615 pooled, 6 + 6 runs): recursion 12.51 → 12.02 s, −0.49 s (t −4.5). Whole block −0.13 s (t −0.8): the base read +0.37 s (t +2.1), all of it in phase A's host front (executor and trace build), before the knob is read. Phase B was equal within 0.02 s, so nothing moved out of it. At 1× the base moved +0.04 s.
    • 1× (8 + 8 runs): recursion −0.02 s (t −0.5; per-run sd 0.08 s). At 1× the tree has one node, so there is nothing to take.
  • Mechanism, as registered (2.66× level-1 start, card idle with no hold active, ≤ 0.30 s): 0.31 s, OUT by 0.007 s (control 1.11 s).
    • Four of the six dataflow runs read 0.00. In the other two (0.88 / 0.96 s), the first level-1 node ready to prove had its program by 4.0 s, but its artifacts (a 0.14 s build on the card) waited 2.0 s behind level 0's proves and were ready only after level 0 ended. A node's slot holds its program and its artifacts together, so its execute + fill waited too, and ran with the card idle. In the four other runs the same build got the card between two leaf proves.
    • So the residual is the slot waiting for the artifacts before the execute, not the dataflow. At the median, level 1's idle read 0.00 in both runs. (Corrected: the first version of this section blamed the builder's emission; the build stamps include the card wait.)
  • A readout note: under dataflow the levels overlap, so a level's start gap (from the level below's last proof to the first prove hold starting after it) can count the next level's own proves. At the median, level 2's gap read 0.65 s with the card idle 0.00 s (BIG 612). The level starts are therefore read as card idle with no hold active.
  • What is left: the top node's execute + fill after its last child (1.0–1.1 s of card idle at the median, BIG 612). That is (B)'s target, next.
  • Toward a production tree driver: with (C), the driver's prove loop is one topological pool over the slots, with the levels taken from the plan's shape.

The held tables spill to disk, by default only when memory is short (lane i-m4b)

Prover-only; the proof bytes are the same: one shape and one proof digest (07d1bd43…) across the base and all four policies, under the fixed trace hash + deterministic grind (BIG 616). The 1× top proof's digest (0da6fea9…) and the tree verify at the landed merge (ULTRA 006).

  • What changes:
    • In phase A, after each group is installed, the narrow (packed) tables of every committed group past the first two can move to STARK no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1013's spill store. The store (crypto/stark/src/spill.rs) is imported byte-identical; it digests each slot and writes it with O_DIRECT on a writer thread, off the committer's path. A table moves only when the writer's queue has room at that moment, so the committer never waits.
    • Phase B reads the tables back ahead of their upload, within a window of two groups' bytes, and checks every slot's digest. A table whose columns are still out when its group is read is refused (SpillFailed). After a group's opening its packed columns are let go.
    • The policy is STARK no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1013's knob and rule: LAMBDA_VM_BLOCK_SPILL = auto (the default) | off | always | <GiB> (a resident budget). auto spills a table when the host's bytes plus a reserve plus the table would pass the target.
    • BLOCK SPILL prints the policy, the slots and bytes, the writer's split, the read-back, and the largest host reading auto decided on.
    • Tests:
      • the same bytes spilled and held;
      • a byte flipped in a spilled slot is refused, and so is a slot that does not come back;
      • a budget the block fits in spills nothing, and a zero budget spills every table past the first two;
      • unit tests of the knob, the rule, the cgroup v2/v1 readers and the working-set measure, each with a mutation that fails it.
  • Measured:
    • 4.13×, always (BIG 566, single runs): 22.74 GiB spilled, max RSS 52.53 → 40.12 GiB, whole block +1.15 s, drivers waited 0 s.
    • 4.13×, auto (BIG 567): by default nothing spills (target 110.7 GiB). Under a 35 GiB target it spills 21.21 GiB, max RSS 41.04 GiB, +2.91 s.
    • The median, on the cap-128 exploration head (BIG 568; the cap is not part of this landing):
      • auto spilled 26.98 GiB from group 44 of 90, and the tree verified.
      • VmRSS fell only 110.55 → 105.32 GiB. Under the never-purge allocator posture, freed pages stay resident, and the whole-run peak forms in phase A's finish, which the spill does not reach.
    • The landing gate (BIG 616 at 3d99c64, base WHIR no-epoch (prove-and-retire) prover: block 25368371 — work in progress #1014 54aae28, two jobs pooled, arms B, D = auto, O = off, S = always, T = auto at a 47.5 GiB target):
      • the card tests;
      • one shape and one proof digest across all five arms;
      • every auto run spilled 0 in both jobs, at 110.7 GiB and under the 47.5 GiB target (FAST's limit, kept as a real test on BIG's 128 GiB; the host read 25.3–26.8 GiB across those runs);
      • every run's environment the same size;
      • D − B −0.063 s (t −0.6), O − B −0.048 s (t −0.4), both inside the box's ±0.23 s band.
    • Earlier 1× gates on FAST (880 + 881, 882): always and auto at 0 spilled within noise.
    • ULTRA 006 at the landed merge 0d61081: 1× top digest 0da6fea9…, tree accepted, auto spilled 0 (host at most 27.65 GiB).
  • What is left: the median's peak is phase A's own (the finish's transient over the tables still held when it starts), not phase B's. That is the next memory term.

Phase 4 counts the BITWISE lookups in slices (#1013's c369843, lane i-m4b)

Prover-only; the bytes are the same:

  • the 187 whole-run trace digests (four programs at the small and default caps) are identical with the slices on and off;
  • the 1× proof digest is 07d1bd43… at both, under the fixed trace hash + deterministic grind (ULTRA 010).

What changes:

Measured: the median, BIG 619. One binary on the cap-128 exploration head, N P P N, auto spill on. N runs the whole-source path, P the slices.

N P (slices)
phase 4's transient (unnamed peak) 31.9 / 30.9 GiB 11.0 / 11.0
phase 4's length 13.3 / 15.4 s 6.6 / 6.5
active peak 93.1 / 89.2 GiB 83.5 / 83.1
VmRSS − active peak (retention) +13.2 / +18.1 GiB +5.1 / +4.3
base VmRSS, phase A / B 104.1 / 106.3 GiB (N1) 86.1 / 88.6, 85.0 / 87.4
whole-run VmRSS 107.6 / 108.9 GiB 89.1 / 89.9
the tree phase's cgroup peak 115.8 / 117.1 GiB 97.3 / 98.1
phase A 135.4 / 132.3 s 124.1 / 124.6
whole block 238.3 / 235.0 s 227.2 / 227.4
spilled (auto) 25.9 / 25.6 GiB 25.9 / 25.0
  • Phase A −9.5 s (whole block −9.3 s) comes from the finish, not the spill: every run spilled ≈ 25–26 GiB.
    • Phase 4 lost its two long poles (LT, KECCAK), so the finish builds the run 8.5–11 s sooner.
    • Phase A's wait for the rest's groups falls 33.8 / 32.0 → 20.8 / 22.6 s.
  • 1× (ULTRA 010, 8 + 8, one warm-up discarded): base −0.30 s (t −2.0), recursion −0.01 s.
  • The pre-registered bands for phase 4's transient (≤ 9 GiB), the active peak (≤ 83) and phase A (±3 s) were missed, all on the good side. Landed on the lead's judgment; there is a row in FAILED-LEVERS.
  • Proves 25416971 (12.4×) at cap 128, exploration (BIG 621, acfd54a + the cap, defaults): 40.7 G cells, 109 groups, base and tree verified; whole-run VmRSS 98.0 GiB, cgroup 106.2; 40.3 GiB spilled; whole block 278.1 s. On the line through the median and this block (≈ 1.18 GiB per G cells), cap 128 binds first, at ≈ 14.4× (≈ 47.8 G cells); memory binds at ≈ 15.5–16×.
  • What is left: the median's active peak is now the finish's phase 5. It holds the rest's tables until the finish ends (unnamed +24.0 GiB), beside the held narrow tables (33.8 GiB), which auto spills only once memory is short.

The top executes each child's share as that child is proved, while a child is still unproved (B and B′, lane i-noepoch-w2)

Prover-only (the W3 harness and the LFM executor); the proofs are the same. The 1× top digest is 0da6fea9… with the streamed top on and off, under the fixed trace hash + deterministic grind, and both trees are accepted (BIG 617; ULTRA 008 and 009; ULTRA 011 at c55dec4 itself).

  • What changes:
    • The top node executed its whole program after its last child was proved, although most of that program verifies one child at a time.
    • executor::StreamedExecution runs an execution as its arena groups land: each instruction runs in the wave in which the last group it reads lands (each instruction's group set comes from one forward pass), in the level schedule's order, into the slot the schedule gives it. So the witness is execute's, word for word.
    • Tests: the streamed execution is byte-identical in every landing order; a missing group, a group landed twice, or a wrong arena count or length is refused; finish checks that every instruction ran once. Dropping the group propagation (ReadBeforeWrite) or re-running instructions across waves (DoubleWrite) fails them.
    • The W3 top lands each child's arenas as that child is proved (W3_STREAM_TOP, on by default), so only the last child's share and the fill remain after the last child.
    • The readiness gate (B′): the top streams only if a child is still unproved when its program and artifacts arrive; otherwise it executes whole, on the control's path. W3 TOP EXECUTE prints which. A test covers a program ready before the last child (streamed), after it (whole), and a failed child (whole, which then reports the failure). Inverting the predicate fails the test.
  • Why the gate: the first version (B) always streamed. At 1× and 2.66× the top's slot arrives after its last child: its artifacts are a short card hold that queues behind the children's proves. So there was nothing to stream behind, and the streamed path costs more than one whole execution. 1× recursion rose by 0.22 s (t +5.8, BIG 617 + 618), and the top's start idle rose too (1.01 → 1.20 s). (B) was not landed. With the gate, the top executes whole at those sizes.
  • Time:
    • Median (block 25475471, BIG 613, S O O S O S on one binary): recursion −0.47 s (t −3.2); the card idle at the top's start fell 1.07 → 0.69 s. There the top's program is built 6–8 s before its first child, so it streams. BIG 620 (one gated run at the median) streamed, with recursion 41.91 s, inside 613's streamed range.
    • 1× (ULTRA 009, 8 + 8 after a warm-up): recursion +0.019 s (t +0.8), within the registered ±0.05 s; every gated run executed whole. At 2.66× the top's slot also arrives after its last child, so it executes whole there too.
  • What is left: the top's fill and the streamed path's own overhead (≈ 0.7 s of card idle before the median's top). Splitting a node's slot into its program and its artifacts, so that a node executes before its artifacts are built, is the next lever.

A node executes from its program; only its prove waits for its artifacts (b′, lane i-noepoch-w2)

Prover-only (the W3 harness); the proofs are the same: the 1× top digest is 0da6fea9… with W3_EXEC_EARLY on and off, under the fixed trace hash + deterministic grind, and both trees are accepted (ULTRA 012; ULTRA 013 at 806c211 itself).

  • What changes:
    • A node's slot held its program and its artifacts together, and its artifacts are a card hold that queues behind the proves below it. So a node could not execute until its artifacts were built, although executing needs only its program. At 1× and 2.66× the top's slot arrived 0.12–0.15 s after its last child, though its program had been emitted 0.03–2.1 s before it. At 2.66× a level-1 node's artifacts waited up to 2.1 s for the card while its children were already proved (BIG 614 / 617 / 618's card holds).
    • Now the builder publishes each node's program as soon as it is emitted: on the pool thread that emitted it, before the builder's thread builds its artifacts. The node executes and fills from the program once its children are proved, and only its prove waits for its own artifacts (node_flow). The traces are filled under the block hasher, which the node artifacts are built for. The top's streaming choice (B′) is taken when the top can execute. W3_EXEC_EARLY=0 is the control.
    • Tests:
      • A toy at the median's shape runs the real builder, scheduler and node flow at 1 to 6 workers, without a hang. It checks that every node gets its program within 40 ms of its emission or its take, executes before its artifacts where it can, and proves only with its own artifacts, after they are built.
      • Mutations fail it: waiting on another node's artifact slot; waiting for the artifacts before the execute; publishing a program after a sibling's finish.
      • The readiness predicate's test, the deadlock test (1–6 workers) and the failure-propagation test were re-run.
    • Part of this landing: proving filled traces against artifacts built for another hasher is refused (LfmProveError::HasherMismatch, naming both) instead of failing a release assert, and the streamed execution's finish returns an error instead of asserting its public words' capacity. Each has a test and a mutation.
  • The first build published each program from the builder's thread, between finishes that wait for the card. In one of BIG 622's six runs, a level-1 node got its program 2.3 s after it was emitted, so level 1's idle read 0.183 s against the registered ≤ 0.15. Publishing on the pool fixed it (BIG 624).
  • Time (one binary per box, arms alternated):
    • 1× (ULTRA 012, 8 + 8 after a warm-up): recursion −0.274 s (t −15.1; 4.125 → 3.851 s). The card idle at the top's start fell 0.536 → 0.239 s: the top's program now arrives before its last child, so B′ streams it.
    • 2.66× (BIG 624, 6 + 6 after a warm-up): recursion −0.410 s (t −2.8; 12.008 → 11.598 s). Level 1's start idle fell 0.197 → 0.007 s and the top's 0.795 → 0.525 s.
    • Median (BIG 625, N O O N): unchanged. In every run, every node is taken at least 1.27 s after its artifacts exist: the three workers are busy with the 23 leaves. So the split has nothing to act on there. 625 read −0.55 s, but the control arm itself moved +0.52 s from BIG 623 with no change on its path; over both jobs, −0.31 s at t ≈ −1.3. It passed its registered row by chance.
  • What is left: the top's fill and the streamed path's overhead (≈ 0.5–0.7 s of card idle at the top's start), and at the median the worker-bound leaf level.

@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 1.9 ± 0.1 1.8 2.0 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head hashmap 57.4 ± 1.0 56.1 59.2 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head keccak 79.2 ± 1.5 77.0 82.0 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head syscall_commit 59.4 ± 6.0 55.4 74.3 1.00

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Benchmark Results for unmodified programs 🚀

Command Mean [ms] Min [ms] Max [ms] Relative
base binary_search 50.3 ± 0.5 49.8 51.0 1.00
head binary_search 52.0 ± 0.4 51.1 52.5 1.03 ± 0.01
Command Mean [ms] Min [ms] Max [ms] Relative
base bitwise_ops 51.0 ± 0.6 50.2 51.8 1.00
head bitwise_ops 52.6 ± 1.5 50.5 54.3 1.03 ± 0.03
Command Mean [ms] Min [ms] Max [ms] Relative
base fibonacci_26 52.9 ± 0.8 52.2 54.7 1.00
head fibonacci_26 56.8 ± 1.5 54.5 58.9 1.07 ± 0.03
Command Mean [ms] Min [ms] Max [ms] Relative
base matrix_multiply 52.9 ± 0.6 52.0 53.6 1.00
head matrix_multiply 55.8 ± 0.4 55.0 56.2 1.05 ± 0.01
Command Mean [ms] Min [ms] Max [ms] Relative
base modular_exp 50.4 ± 0.6 49.8 51.3 1.00
head modular_exp 51.6 ± 0.5 50.5 52.5 1.02 ± 0.02
Command Mean [ms] Min [ms] Max [ms] Relative
base quicksort 54.9 ± 3.3 53.0 64.1 1.00
head quicksort 55.6 ± 0.4 55.1 56.3 1.01 ± 0.06
Command Mean [ms] Min [ms] Max [ms] Relative
base sieve 54.5 ± 0.6 54.0 55.4 1.00
head sieve 54.7 ± 1.0 53.7 55.8 1.00 ± 0.02
Command Mean [ms] Min [ms] Max [ms] Relative
base sum_array 62.5 ± 0.4 61.8 63.1 1.13 ± 0.02
head sum_array 55.2 ± 0.6 54.5 56.4 1.00

MauroToscano added a commit that referenced this pull request Oct 2, 2026
… leaves

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

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

Cherry-picked from mem/m1 74e1df1 onto #1014, with the streamed finish's build marks kept.
BlockOptions::layout_ahead = Some(k) lets at most k + 1 streamed chunks be
laid out (or in the making) and not yet packed, where the inline layout
holds one: a worker takes a permit before it takes a job, so every job
taken holds one and the next chunk to pack is never starved, and the
packer gives it back once the chunk is packed. None keeps the channel-only
bound that BIG 390 saw grow with the block (+2.2 / +4.2 / +8.7 GiB at
1.0 / 1.3 / 1.8x). Production Some(2); layout_workers stays 0 until E3's
gates.

BLOCK LAYOUT reports the most chunks unpacked at once. Test: with three
workers, k = 0 and k = 2 hold at most k + 1 and pack the inline groups (an
unbounded permit pool fails it: 8 unpacked at k = 0). Harness knob
BLOCK_WHIR_LAYOUT_AHEAD=k|none.
BlockFormat gains `argue` (production PerTable). Under ArgueFormat::Batched
each group's tables argue in one batched argue on the group's fork (a
lockstep GKR per bin, one front-loaded constraint sumcheck), whose reduced
claims feed the group's opening exactly as the per-table points do; the
upload, revive, openings and prepared openings are unchanged. The verifier
reads the format from its own config, needs that format's messages and none
of the other's, verifies each group's argue on its fork, sums the outputs
into the block-wide balance and checks each table's preprocessed columns
past the prepared prefix.

BlockWhirProof gains `argues` (empty under PerTable; MultiProof, the
prepared openings and the partition keep their bytes). BlockFormat::
chain_config builds every side's config. Test hooks: ArgueDeviation (a
group's faults, the host reference) and VerifierChecks (the mutations).
The W3 plan refuses a batched format until its leaves walk it. The real
block test takes BLOCK_WHIR_ARGUE=batched and prints the GKR tree refusals
and fused counts; the block report prints each group's argue high-water.
Round trips at one group and many (keccak chunks, the dense prepared
fixture), the format read from the verifier only (both ways, a foreign bin
cap), a group argued with each prover fault, tampered argues, another
fork, the block-wide balance and the reduction shape with their mutations.
Box only: the per-table default's bytes against noepoch/whir @ 11e2de5
over canonical rows (a block's traces are not reproducible across
processes), and the card proving the host reference's bytes.
…M1 streamed-ops drop) into batch/noepoch-argue

# Conflicts:
#	prover/src/tests/block_whir_tests.rs
Under the batched block format a leaf hints each group's batched argue with
every count from the tables' shapes and the host's argue_plan at the
format's cap, runs the B-4a leg on the group's fork, then each table's
preprocessed leg at the point its columns are claimed at (the host's
check_preprocessed after verify_argue), leaving the per-table walk's
outputs, points and values for the group opening and the prepared openings.
The plan costs a batched group by batched_argue_cost, so the leaf partition
sees the new costs; the arena writes each group's argue in the hint order
and refuses one whose word count is not its shapes'. The per-table walk is
unchanged.
Batched leaves execute and close the bus on four small blocks, each group
costing no more than its per-table walk; a batched leaf refuses a forged bus
output, ladder half, ladder round, constraint round and factor value, and a
tampered prepared opening (admitted with the openings left out); the
batched tree proves to its derived top, accepted under the batched format
and refused under the per-table one. The real-block tree test takes
BLOCK_WHIR_ARGUE=batched.
…'s run

The digests depend on the ELF bytes, which differ between toolchains (the
box's guests are not the laptop's), so the expected digests come in
BLOCK_DEFAULT_DIGESTS from the same body run at the base sha on the same
machine; the laptop's record stays in the doc comment.
…d layout-ahead) into batch/noepoch-argue

One conflict, the real-block banner (both formats' fields kept); the
batched tests' BlockOptions take layout_ahead.
The walk is the producer's serial chain: on block 25368371 it never waits
for the executor and costs 143 ns a cycle (FAST 590). Four of its costs
go, each behind its own part of LAMBDA_VM_WALK_LEAN (unset or 1: all,
0: none, or a comma list of decode, memory, lookups, route), so each can
be measured alone in one binary:

- decode: every pc's DecodeEntry is built once into dense runs of
  consecutive pcs (DecodeTable), instead of a hash lookup and a decode
  of the instruction every cycle. Built from the instruction map, so it
  answers exactly the pcs the map holds.
- memory: a load or store inside one 256 KiB page finds its page once
  (PagedMem::page_index) instead of once per byte, and the last page
  found is remembered. Every page allocation then goes through the
  cache, so a remembered index is never one an allocation moved; an
  access across a page or past u64::MAX takes the per-byte path.
- lookups: the walk no longer lists each cycle's seven CPU lookups and
  each narrow load's MSB8 lookup (a Vec per cycle); the accumulator
  counts them from the CPU and LOAD ops it already holds, with the same
  per-op generator the list was built from.
- route: the accumulator routes a window in one pass over its CPU ops
  instead of one filter pass per segment; each op still goes to every
  segment whose filter it passes, in CPU order.

Only the windowed builder walks lean; the whole-run build keeps the old
walk and is the reference the identity tests compare against. The
tables are the same with any set of parts.
The windowed build with the lean walk, with the old walk and with each
lean part alone, against the whole-run build: six programs (keccak calls,
narrow and odd-offset loads and stores), windows of 1 to the whole run,
with and without dropping the streamed ops. And each part against what it
replaces: the decode table answers exactly the map's pcs (gaps,
misaligned pcs, the top of the address space); lean memory accesses read
and write what the per-byte ones do (an access across a page, a first
touch between two allocated pages, wrapping past u64::MAX, 40k random
accesses); the counted lookups equal the listed ones, and differ without
the LOAD ops' part on programs with narrow loads; the one-pass routing
builds the per-segment routing's segments; the knob's values parse.
The executor kept guest memory in a map of 4-byte words keyed by their
address through the identity hasher. hashbrown takes its 7-bit control tag
from the hash's top bits, so every address below 2^57 gets the same tag and
a probe compares keys one by one; the cost grows with the memory a run
touches: 31.6 ns a cycle on block 25368371, 108.5 on the median block
25475471 (FAST 591), where the executor becomes the producer's serial floor
once the walk is lean.

Memory now keeps 64 KiB pages: page n of the low 8 GiB (code, data, heap,
private input) and of the top 8 GiB (the stack) is an index into a
directory, any other page an entry of a BTreeMap. A page records which of
its 4-byte words were written, so iter_bytes yields exactly the bytes it
yielded before (as a set; the order differs). Every load and store, its
overflow refusals and load_bytes answer as before.

LAMBDA_VM_EXEC_MEMORY=words keeps the word map, as the control the pages
are measured against; the differential test drives both through 200k
random accesses of every width across pages, directories and the top of
the address space.
…cutor memory (E4) (i-exec, FAST 602)

The windowed builder walks lean and the executor's guest memory lives in
64 KiB pages found by directory. FAST 602 (record posture, A B B A): base
17.62 -> 17.17 s, phase-A end 10.01 -> 9.56 s; BLOCK SHAPE DIGEST ffaccd25
in all four runs; verify 4/4; real-block determinism 2/2.
…LL kind derived (D-EXEC E1 v2)

The walk materializes one CpuOperation per cycle, and the windowed producer
is bound by the bytes it moves. arg2 (CPU-A1's multiplex), branch_cond and
the seven ECALL-kind flags are functions of the decode entry and rv1/rv2
(an ECALL reads a7 into rv1), so they become methods; COMMIT's buffer and
count and KECCAK's and BLAKE3's state address share two operand slots
(ecall_args). 128 -> 96 bytes, pinned by a const assertion. Every reader
calls the method of the same name; the values are the same.
An op routed to MEMW_A has one old timestamp for all its bytes, and its eight
value (old) elements fit one u64: bytes for a memory access, the two 32-bit
halves for a register access that missed MEMW_R. The walk's MEMW_A bucket,
the windowed builder's MEMW_A tail and chunk jobs, and the table phase keep
AlignedRow (memw_aligned.rs) instead of MemwOperation: 152 -> 48 bytes,
pinned by a const assertion. The MEMW_A generator, the MEMW_A LT derivation
and its BITWISE lookups read the row; the cells are the same (the 187 trace
digests equal; a round-trip test over every width, memory and register).
LT, EQ, BYTEWISE, BRANCH, MUL and DVRM deduplicate their operations
through a HashMap and lay their rows out in its iteration order. Each map
took fresh random keys (HashMap::new), so no two builds of the same run
laid these tables out alike, even in one process. Regenerating a streamed
chunk in phase B (D-MEMORY A0) needs the chunk built again row for row.

Every such map now hashes with tables::trace_hash's state: random keys
per process, as before, shared by every map, so a table's row order is a
function of its operation list. LAMBDA_VM_FIXED_TRACE_HASH=1 fixes the
keys (SipHash-1-3 with zero keys) so that two processes build the same
traces. The hash function is the one HashMap::new used, so the table
generators cost the same, and the rows are the same multiset as before.

The windowed builder tests and the streamed-collection test now compare
these six tables row for row instead of as row multisets, and a new test
builds one run twice in a process and compares every table, with MUL and
DVRM also built from 199 distinct operations. Giving LT or DVRM its own
fresh keys again fails it.
…the three-argument same_list

The hash-ordered tables now hash with one state per process, so every
table compares row for row and same_list lost its multiset flag.
…oof bytes

Under LAMBDA_VM_FIXED_TRACE_HASH=1 and LAMBDA_VM_DETERMINISTIC_GRIND=1 two
processes prove the same bytes; the digest lets a box gate compare them.
Mauro 10-02: "batch the constraints". BlockFormat::production().argue is
ArgueFormat::BATCHED (one lockstep LogUp-GKR ladder per bin under 2^26
input cells, one front-loaded constraint sumcheck per group); the verifier
takes the variant from its own format, never from the proof, and the
production tree verifier pins it. BLOCK_WHIR_ARGUE=per-table keeps the
per-table format measurable; unset, the real-block tests follow production.
… v2) (i-exec)

The CPU op at 96 bytes (arg2, the branch decision and the ECALL kind
derived) and MEMW_A ops kept as 48-byte aligned rows. Prover-only: the
traces are the same.
…ajor

The narrow kernels and their host entry points come over from the STARK
side as they are there (widen_narrow_row_major, column_max_col_major,
pack_col_major, math_cuda::narrow), with one addition beside the row-major
widen: widen_narrow_col_major writes out[c * rows + r], the layout a
table's run of columns has in the multilinear prover's resident store.
The row-major path is unchanged; the pack now reads a device view, so a
run inside a larger store can be packed in place.

DeviceColumns::upload_tables builds a store from whole tables, each as it
is held: a wide table's columns are copied up as before, a packed one
crosses the bus at its packed bytes and is widened on the card into the
run its columns take, so the store holds the same words at the same
offsets. The packed bytes are promised for the largest table while the
tables widen one at a time in stream order. pack_run packs one table's
run and downloads only the packed bytes; download brings a store back,
for the tests that compare two of them.
…em on the host only when they read them there

NarrowColumns keeps a table's raw words column by column, each column
little-endian at the width of its largest word (1, 2, 4 or 8 bytes) - the
stark crate's NarrowMain layout - so widening gives back the committed
words bit for bit and no proof byte can depend on it. TraceData holds its
columns wide or narrow: install_narrow takes packed columns of the table's
shape (Goldilocks only, whose elements are their raw words) and lets the
field elements go; columns() widens a narrow table once, the first time a
host reader asks, and keeps that copy until drop_widened. host_widens
counts those widenings.

The readers that used to take &[Mle<F>] only to learn the shape, or to hand
the device a host copy it did not read when the columns were resident, now
take HostColumns (a table) or HostColumn (one column, what a stacked
polynomial is made of): the width and height always, the field elements on
demand. gpu::column_factors takes the shape; upload_factors_from_columns,
evaluate_many_base and the reduce's card session read the resident run when
the card holds one and widen only otherwise; the opening builds its host
parts only without a resident store; claim_reduce::prove and the stacked
commit, revive and opening are generic over the column type. Every call
with field columns is unchanged in what it does.

gpu::upload_tables puts whole tables on the card, a narrow one widened
there; gpu::pack_resident packs a table's run of a resident store on the
card.

Tests: the host pack and widen round trip at every width boundary, a wrong
width map widens to other words, only Goldilocks widens, packed parts of
the wrong shape are refused; a trace held narrow proves the same bytes as
held wide and is refused packed columns of another shape or field. On a
card (narrow_device): the column-major widen at every width boundary
against the host pack, the card's pack of a resident run = the host's,
and a store uploaded from packed and wide tables holds the wide words.
…ed on the card after its commit

BlockOptions::narrow (production Narrowing::CARD) says how phase A holds a
group's columns once committed. On the card, every table of at least 2^16
cells is packed from its run of the group's resident store (each column at
the bytes its words need) by a thread of its own, and installed once the
next group's columns are up: the committer is phase A's critical path
(FAST 425: every group after the first waits 0 s for its tables), and the
pack's download overlaps that upload. A table's field elements go when its
packed columns are installed. Narrowing::Host packs every table on the host
(a test's, or a host without a card); Narrowing::Wide keeps today's.

Phase B uploads each group as it is held - a narrow table at its packed
bytes, widened on the card into the store - and hands the revive and the
opening the group's columns as ColumnOf handles, which read a table's field
elements on the host only for a host path. A host widening is dropped when
the group's opening ends. The words are the committed ones bit for bit, so
the proof is the same byte for byte; a widening that went wrong is refused
when the opening reads a path from the kept tree top.

BlockStamps reports a BLOCK NARROW line: the tables and cells packed, the
bytes before and after, the committer's wait for the pack and the packer's
own time, and the narrow tables phase B widened on the host.
Deviations::narrow_width_map breaks the first narrow table's width map
between the phases. The real-block harnesses take BLOCK_WHIR_NARROW=
wide|card|host; the test helpers pack every table on a card
(Card { min_cells: 0 }), so a box run walks the narrow path at a test's
size.
…ap is refused

narrow_storage_moves_no_byte_of_the_proof proves the same traces with
every group held wide and held narrow (packed on the host), under the
per-table and the batched argue: both verify, every table is packed, the
host widens them without a card, and the bytes are equal when the grind
is deterministic. a_streamed_block_held_narrow_proves_and_verifies does
the same for the build streamed into phase A.
a_wrong_narrow_width_map_is_refused_by_the_kept_tree breaks one width map
between the phases: the prover refuses with RecomputedCodewordMismatch.
a_wrong_narrow_width_map_is_refused_on_the_card (box, cuda) does both on
a card: every table packed there, the honest proof verifies, the broken
width map is refused.
…eights

KECCAK and ECSM were one table each in the block format, with no height
bound of their own beyond the 2^27 table cap. A keccak-heavy block at the
gas limit makes about 2^21 permutation calls: one KECCAK table that tall
stacks into eight polynomials of a 2^27 stack, against a group budget of
three. The block prover now cuts KECCAK into tables of at most 2^18 rows
and ECSM into tables of at most 2^17 (BlockOptions::keccak_rows_log2 and
::ecsm_rows_log2), at the same two places it cuts KECCAK_RND and ECDAS; at
those heights each table's columns fit one polynomial of the stack.

A KECCAK or ECSM row is one whole call, so a cut falls between calls. No
constraint of either table reads another row, and a call reaches its
rounds (KECCAK_RND), its double/add steps and scalar bits (ECDAS), and its
memory only through buses keyed by its timestamp.

The block frame now takes up to 2^12 tables of each (validate_block_counts
lifts the one-table bound for both) and refuses a KECCAK table stated over
2^18 rows or an ECSM table over 2^17 (chunked_table_ranges names four
ranges in AIR order: KECCAK, KECCAK_RND, ECSM, ECDAS). Every other verifier
keeps both to one table. A block whose KECCAK fits 2^18 rows and ECSM 2^17
(every block measured so far) builds the same tables and the same proof.

Tests: the count bounds and the four height caps (host), the ranges pinned
against the AIR names, KECCAK and ECSM cut at one and two rows a table in
the whole-run and the streamed builds, whole calls swapped across tables
verify while swapped timestamps are refused, each table stated over its
cap is refused at the frame with the cap's error, and both prove through
the block's tree. The real-block harnesses take BLOCK_WHIR_KECCAK_LOG2 and
BLOCK_WHIR_ECSM_LOG2 to force a split, and print the chunked heights.
…olynomial holds

The height-cap test reads the cap constants, so a lifted KECCAK or ECSM cap
passed it. Each of the four caps is the tallest power of two at which the
table's columns fill at most one polynomial of the format's 2^27 stack
(KECCAK 511 x 2^18, KECCAK_RND 1,480 x 2^16, ECSM 667 x 2^17, ECDAS
521 x 2^17); the new test checks that against the AIRs' widths, so lifting
either new cap fails it.
…NECHAIN B1)

prover/src/tests/block_onechain_tests.rs, host builds only (the card's
grouped round 0 is stage B3):
- REV-B2-JUDGE B1-1 (a): REV-B2-A's frame probe under Indexed. A table
  stated at 2^27 alone in its group stacks into one polynomial per column,
  and the verifier's split cuts them into runs of at most four (38 -> nine
  of 4 and one of 2), each within the index and two-adicity bounds.
- B1-5 at the block: up to 2, 3 and 4 polynomials a group, RPX and P1, an
  Indexed block proves and verifies with grouped chains, its phase-A roots
  are the PerPoly block's, and neither mode verifies under the other.

Also the BaseGrouped arm in the hash-metrics bench test's exhaustive match
and two clone_on_copy lints in whir_group_tests (make lint was red at
435205a under the hash-metrics feature).
D-ONECHAIN stage B2. Under PerPoly (production) every emitted row, its
order and every arena word are unchanged; a run of one polynomial under
Indexed is today's chain row for row.

- ChainShape carries the config's GroupFold and a run's tree count:
  chains() is the host's index_chains (one split for the emitter, the
  arena and the W3 plan), run(P') the run's shape, messages(r) = k0 + m at
  round 0, bound() counts the index.
- emit_verify_grouped (emit_verify_weighted's body, now emit_chain):
  round 0 draws k0 + m challenges, rho last; eq(rho, p) from the sampled
  rho wires (emit_eq_index, never hinted, absent slots have none); at each
  query ONE draw feeds every tree's verify_opening, the successor's leaf and
  slot, and the fold; each tree against its root by position; the blocks
  combined (emit_combine) from exactly the felts each tree's leaf hashed,
  then folded as one 2^k0 block. The weight is sum_p eq(rho, p) w_p at
  alphas[..k0] || alphas[k0+m..].
- emit_stacked_verify_chains / emit_group_walk_chains / stacked_verify_cost
  / hint_group_chains_shaped / push_chains walk the runs; the block leaf
  passes each chain its run's slice of the carried (or derived prepared)
  roots. RoundStorage, round_words and push_round_words lay out a grouped
  round 0: k0 + m messages, the trees' caps in tree order, then per query
  each tree's block and path.
- Closed forms: openings, caps and query rows per tree, the combine and
  eq rows, k0 + m sumcheck rounds.

Tests (lfm/whir_onechain_tests.rs, REV-B2-JUDGE B2-1..B2-6): host-accepted
grouped openings execute (RPX and P1 C2; P' = 2, 3, 4, a [4, 1] split, mixed
heights, R = 1); the threaded schedule is the host's; the closed forms are
the emitted counts; the base-lift pin in every slot (NotBaseWord); every
forgery the host refuses has no execution; each in-guest reading of the
index is load-bearing (test-only emitter faults); the production run of
three opens for 14,845 permutations (design: 14,845). Box tier: the leaves
over an Indexed block (whir_block_tests, host builds) and the median census
by emission (whir_p1_census_tests::whir_onechain_census, B2-7).
ChainShape::run refuses a run whose virtual domain H_{n+m+lb} would pass
Goldilocks' two-adicity, the in-guest side of the host's runtime refusal
(REV-B2-JUDGE B1-1); tested.
The lead's #1013 lesson: a refusal test must fail at its intended check,
not on any error. The stacked layer reported every chain refusal as
ColumnOpeningRejected, and the in-guest negatives only saw the machine
refuse.

Host (multilinear):
- stacked_eval::verify_traced returns the refusal with its cause
  (Refusal::{Before, Chain { chain, poly, cause }}); verify is it mapped
  exactly as before (a shape refusal by name, any other chain refusal as
  the chain's first polynomial's ColumnOpeningRejected), so PerPoly's
  verdicts and errors do not move.
- whir_round::verify_grouped reports a round-0 tree's failed opening as
  the new Error::GroupedOpeningRejected { query, tree } (appended last).
- whir_group_tests assert the cause of every negative: the tree and query
  of each tampered, swapped, moved or combination-preserving block; the
  fold for rho swapped in the combine, the final check for the weight's
  faults; the per-polynomial count for a grouped proof under PerPoly; the
  cap check or the root for a proof read under another cap.

Guest (prover):
- whir_chain::onechain_steps (test only, recording only, nothing
  emitted): marks of each emitted check (chain, sumcheck, out-of-domain
  and grinds, each tree's opening, the successor's, the fold, the final
  check); failed_at names the check whose assert failed.
- whir_onechain_tests name each forgery's refusal on the serial schedule
  (program order, the host's) and require the machine to refuse at the
  check the host does; the faults refuse at round 0's first fold or the
  final check; B1-2's shapes refuse as another arena length. Forgery
  fixtures run eight queries and every forgery applies to every case (no
  silent skip): the one-round case at blowup 2^6, P1's at 2^10.
- block_whir::verify_block_whir_traced carries the multilinear refusal
  (verify_block_whir_with is it, as a bool). The block tests assert the
  other mode's verifier refuses by the statement (the two statements are
  one bit apart, the fold word's 47) before any opening's shape, and the
  Indexed leaf refuses a forged round-0 value at tree 1's opening of that
  chain, as the host does at that chain's opening.
…B1+B2)

Seven tests from the B1+B2 code review (REV-OC-B), renamed; no production
code.

Host (whir_group_tests):
- round_zero_trees_cannot_trade_caps: trees 0 and 1 trading their caps
  (binary cap) or owner paths (Poseidon1's 4-ary cap) is refused at tree
  0's cap, CapRejected.
- successor_blocks_follow_the_schedule_not_the_proof: successor blocks at
  a final round 0, and none at a round with a successor, are each refused
  by name.

Guest (lfm/whir_onechain_tests):
- round_zero_caps_are_bound_per_tree_in_guest: cap words traded in the
  arena have no execution; the failing assert is in the chain's cap
  authentication.
- grouped_chains_open_against_constant_roots: the block leaf's prepared
  wiring at the stacked level (roots as program constants, each chain its
  run's slice): honest arenas execute; exchanged constants and a forged
  value are refused at tree 0's and tree 1's openings.
- emitted_program_digests (ignored instrument): digests of the emitted
  programs, to compare a test build with one whose cfg(test) emitter
  statements are compiled out.

Block tier (lfm/whir_block_tests, ignored, host builds):
- a_grouped_prepared_stack_verifies_and_its_leaf_refuses_a_forgery: at a
  stack of 2^8 a prepared stack is opened by one chain of three.
- a_split_prepared_stack_refuses_a_forgery_in_its_last_chain: at 2^7 it
  splits [4, 1] and the last chain is forged.
  Each forgery is refused by the host at that stack's opening and by the
  leaf at the tree's opening, under RPX and Poseidon1.
Three checks of a grouped chain had no test that fails without them: the
suite stayed green with any one of them removed.

- Each round-0 tree bound to the root at its own position through its
  cap. For every slot in turn, tree 0 included, a stack whose slot
  commits another polynomial is claimed against the stated roots: the
  host refuses at that tree's cap (uncapped: at its first opening), and
  the leaf has no execution. Trading two trees' caps breaks two bindings
  at once and cannot show either alone.
- The grouped round 0's successor bound to the root the round absorbed.
  A successor cap node off its root is refused at the cap by the host,
  and in guest where that cap is authenticated, node by node.
- A one-round grouped chain's folds compared with the final value, on
  the host and in guest.

Also holds the test build to the program a release build emits: the
grouped leg's instruction stream is pinned for four cases (RPX and
Poseidon1, runs [4, 1] and [4, 2]) in the library's unit tests and in an
integration test built against the library without cfg(test).

Test code only.
The in-guest test of the grouped round 0's successor cap gains a first
case with a cap of eight nodes, unground. The successor is opened at
most six times (three as round 0's successor, three as round 1's
current tree), so some cap node is met by no opening whatever the
draw: an arena that moves such a node is refused only where the cap is
authenticated. Under a two-node cap every node is usually met by a
query, and a later opening would refuse the forgery as well.

Test code only.
…N B1+B2)

The block path admits its proof family's base only (Poseidon1 at the
4-ary cap): a block format under RPX is refused before anything is
proved. The four block-tier tests of the grouped chain looped over RPX
and Poseidon1; they now run at hash_pin::BLOCK_BASE alone:

- tests::block_onechain_tests::an_indexed_block_round_trips_and_refuses_the_other_mode
- lfm::whir_block_tests::an_indexed_blocks_leaves_execute_and_refuse_a_grouped_forgery
- lfm::whir_block_tests::a_grouped_prepared_stack_verifies_and_its_leaf_refuses_a_forgery
- lfm::whir_block_tests::a_split_prepared_stack_refuses_a_forgery_in_its_last_chain

The stacked-level tests (lfm::whir_onechain_tests) keep both hashes:
they emit the opening leg directly, below the block path.

Test code only; the rest of the diff is the loop bodies' indentation.
… split (D-ONECHAIN B1)

The round trip asserted every grouped run within 2..=group_polys. A run
is cut by index_chains at four whatever the group budget is, and a table
that alone needs more than the budget is a group of its own, so a run of
four occurs at two and at three polynomials a group. A box run failed
there, at its first turn.

In its place, each printed before it is asserted: per group, the proof's
chain widths are index_chains of the frame's own layout, ones included;
a group's stack exceeds the budget only where the group is one table;
the per-polynomial proof holds one chain a polynomial; and over the
three budgets a grouped run of every width from two to four occurs. The
roots' equality, each mode's own verdict and the statements' differing
bytes are printed before their asserts. Test code only.
The order's weights summed a contiguous slice of the run's shapes, but those
are in AIR order and a group lists its tables' AIR indices, which phase A's
packer does not keep contiguous. Every group was weighed by some other run of
tables: on the median block the rest read as 8 % of the rebuilt cells, where
its groups hold 28 % (and a third of phase B's time), so phase B took 17 of
the 24 rest groups in its first 21 places and waited on the rest's
regenerator (ULTRA 127: 3.1-3.4 s a run). Each group now weighs its own
tables (`group_cells`), and the order readout reads the same weights.

An ordering-only fix: the order is outside the transcript (each group proves
on its own fork S_post || g) and any-order identity is pinned, so proofs are
byte-identical; each class is still taken in group order. Tests: weights of
non-contiguous groups and of contiguous ones with a stray index; with the
slice put back both fail.
…(N4a)

With LAMBDA_VM_BLOCK_REGEN and LAMBDA_VM_BLOCK_SPILL both unset, the
production block now regenerates: whenever auto arms, the streamed chunks
are dropped and rebuilt in phase B (the adaptive N2b window) instead of
written, and the spill tier stays for the rest. A knob that is set keeps
the meaning it had, as #1013's 1a98537: a set regen is read when the
prove starts, and an unset regen beside a set spill is off, so scripts
that pin the spill behave as before. Options a caller builds keep None as
off.

Measured with REGEN=auto: p90 disk 23 against 83 GiB, whole +0.05 s;
T200-R disk 60 against 354 GiB, prove +0.3 %, takers never waited; the
phase-A peak lower at both. Every prove now says the mode and the knobs
(BLOCK REGEN mode). No proof byte depends on the mode.
With both LAMBDA_VM_BLOCK_SPILL and LAMBDA_VM_BLOCK_REGEN unset,
BlockOptions::production() now pairs regeneration `auto` (N4a) with the
spill policy NoDisk: `off` whenever live regeneration runs, so a table
`auto` moves off the host is dropped when phase B can build it again
(the streamed chunks, and KECCAK_RND's and LT's rest tables since N3) and
stays otherwise, with no store opened. Without live regeneration (chunks
not generated packed) NoDisk is `auto`, so no block loses its relief.
Either knob set keeps the meaning it had, as #1013's 1a98537: a set
spill reads as ever, an unset spill beside a set regen is `auto`.

The BLOCK REGEN mode line is printed once the policy is resolved and ends
with `no disk` when it runs with none.

LAMBDA_VM_BLOCK_SPILL_RESERVE_PER_G (GiB per G cells committed, default
1.65 = today) is a measurement knob for the reserve retune D-FINISH S4
names; the default is unchanged until a gate reads the median at defaults.

The proof's bytes are the same under every policy (tests under the
deterministic grind).
…sh's end (N4c)

ULTRA 123 read the no-disk default (N4a + N3 + N4b) at +3.1 % on the
median and +2.1 % on p90. With the rest's KECCAK_RND and LT dropped too,
88 of 90 median groups are rebuilt, so no plain groups head phase B. Both
regenerators started 0.04 s before phase B, so its first rebuilt groups
waited for their start-up: about 2.5 s in each window, the whole phase-B
excess.

When live regeneration has armed (or drops every table) by the finish's
end, both regenerators now start there, on a thread of their own outside
phase A's (regen::EarlyStart), about 3.4 s before phase B at both block
sizes:
- the rest's regenerator starts at once and takes each table's fate as
  phase A decides it: BlockRegen::await_rank(class, rank) gives the slot
  once the drop thread makes it, Kept once the table is held by its group's
  class or gone to the store, or Kept once phase A's end closes the
  decisions; nothing is skipped before its fate is known. Tables it waits
  for are counted on its line. (The rest's last groups commit after the
  finish's end, so a final plan would only exist at phase A's end.)
- the streamed chunks' regenerator starts once every streamed chunk's fate
  is known (their groups commit first), from that final plan; phase B checks
  it against phase A's drops and the layout.
Every deposit still goes through its slot's digest. A waiter returns
Stopped as soon as its class's window closes, and an early start dropped
before phase B (the prove failed) closes every window and joins.

Knob LAMBDA_VM_BLOCK_REGEN_EARLY (on; 0 starts with phase B as before).
New line BLOCK REGEN head start.

Tests: the decision feed (a table dropped after the wait began, a held
table, kept only once phase A ends, Stopped when the window closes);
both regenerators started early with each rest drop held back prove the
bytes of regeneration off, as does the start with phase B; phase A
failing right after the finish returns its error and never hangs.
Mutation: an undecided table taken as kept is refused ("the regenerator
did not bring its columns back"), never proved.
…til phase B (N4c)

The early start fills both windows at the finish's end, where T200's
phase-A peak sits (087: 69.11 GiB). Until phase B starts, each window now
holds only what the host's room allows: N2b's reading (VmHWM less the
resident set net of the windows' deposits, less the 4 GiB margin) with no
floor, split four to one between the streamed chunks' window and the rest's,
each capped at its phase-B size, moved in whole-GiB steps. With no room, or
no /proc reading, each window admits only its frontier. When phase B
starts, an open gate gives each window its phase-B size and the streamed
one its own pacer. So filling ahead of phase B never lifts the run's peak.

The head-start line reports the most each window held before phase B and
the readings. Tests: early_aheads at room, at no room and capped; the
early-start tests prove the same bytes with the windows sized so.
…nish's end (N4c)

With phase B's interleave weighing each group by its own tables, the rest's
groups are spread over phase B. A replay of ULTRA 127's median runs puts the
rest's regenerator about 2.7 times ahead of its takers when it starts with
phase B (its waits 0.0 s at the median and p90, and at +50 % KECCAK_RND
cost), so its early start, fed each table's fate as phase A committed it, is
dropped. The rest's regenerator starts with phase B from phase A's final
drops, as N3 built it.

The streamed chunks' regenerator keeps its start at the finish's end (127:
its takers' waits 2.5 s -> 0.00 s), each chunk's fate through
BlockRegen::await_rank. Its window alone is sized by the host's room until
phase B (VmHWM - (VmRSS - deposits) - 4 GiB, capped at its phase-B size);
the rest's window is not touched before phase B.

Removed: the rest's fed source and its feeder, the recorder's rest tags by
family, BlockRegen::dropped_of, and the early pacer's rest window. The test
deviation that held drops back now holds the streamed ones: the liveness
test (phase A failing right after the finish, the streamed regenerator
waiting on fates) returns its error and never hangs. The head-start line says
which regenerator started early.
one_byte_windows_take_each_class_in_its_order and
a_mixed_boundary_group_drops_one_class_only waited a fixed 600 s for their
regenerating prove. Since the block path proves under the format's base
(RPX) rather than the knob's keccak default, every block prove in the suite
is slower, and on the laptop the one-byte prove ran past 600 s and then
completed (phase B never waited on the regenerator). A hang never returns,
so the guard now waits ten times the plain prove the test timed before it,
and at least ten minutes: it still finds a hang, and no longer fails a
complete prove on a slower host.
…s are blessed constants

Every LFM program's artifacts carry the roots of the two static
preprocessed tables. The legacy pin reads them from blessed constants;
a Poseidon1 configuration computed them once per process: bitwise's
2^20 rows by 11 columns interpolated, extended and committed on the
host, about a second in a prover and, with two threads missing the
cache together, about two in every verify-block.

`static_commitment_p1` holds the two roots at the one key the block's
proof family asks for (blowup 4, coset 3, row pairs), generated by
`compute_static_commitments --hash p1`. Any other key under Poseidon1
is still computed, the legacy pin keeps its own constants, and no proof
byte moves: each constant is the value computed until now.

Drift tests recompute both constants. One more test takes each way off
the key (another blowup, another coset, one row, the legacy pin) and
shows the computation is taken there, and that a configuration's cap
does not enter a root.
The key test covered every off-key case on keccak_rc and said bitwise's
dispatch was the same code. It is a second function with the same
conditions, so one case is now recomputed for bitwise too: at the other
coset the wrapper returns the computation, not the blessed constant.
…s key

The key test took the legacy pin and Poseidon1 at blowup 4, coset 3, and
no configuration that is neither. One now does, for both tables: under
RPO the wrappers return the computation, not either family's constant.
The test first checks that RPO is neither family's hash, so a change of
pin asks for another third here instead of passing silently.
…purges

A block that proves with no disk notes memory pressure from the walk's-end
forecast of the finish's growth, which is the growth of tables that stay.
It then drops them, and on a host with room its resident set never comes
near the line the forecast crossed: on a 118 GiB host the p90 block peaks
at 67 GiB against a line of 92, and still purged at phase A's end and at
the base's end. Those two purges, and the page faults phase B takes after
the first, cost it 4.1 s of 180 (ZEN5 096: 180.38 against 176.25 s).

In no-disk mode `auto` now purges at its two points only when VmRSS plus
the regeneration window's ceiling (16 GiB) reaches the block's pressure
line, 85 % of the host target; otherwise it prints why it skipped. A 64 GiB
host keeps both purges, for p90 and for the median (VmRSS 36.1 and 45.6,
33.1 and 33.2 GiB, against a line of 45.9): without the one at phase A's
end it admits 4 leaf programs beside the base where it admitted 24 and
proves 2.2 s slower. A resident set that cannot be read purges.

A block with a store, a LAMBDA_VM_ALLOC_PURGE list, the tree's level purges
and the watermark are as they were. LAMBDA_VM_ALLOC_PURGE_ROOM=off purges at
both points whenever memory is short, as before.

No proof byte moves: a purge returns freed pages to the OS.

Tests: the decision under room at every setting and point; the six
boundaries ZEN5 096 read, at both targets; at the line there is no room; no
reading is no room; a block with a store has no line; the line is the
pressure share and not the target.
The reason for blessing lfm/p1_commit.rs said the block path never names
the module. The block's nodes run transcript_replay's Poseidon1 arms,
which read it; what holds, and what the block-path scan enforces, is that
the block's own five files never name it. The entry also names the static
tables' generator, which has used the configuration since the Poseidon1
roots were blessed. Text of a reason string only: no test's logic moves.
The module doc gave the cost of the two boundary purges on a host with room
as "both purges and their page faults, 4.1 s". The 4.1 s is right, and it is
the purges' own time: 2.4 s at phase A's end and 1.6 s at the base's (ZEN5
099: 2.38-2.47 s and 1.62-1.65 s; 4.07 s between the rule and the rule
before). The page faults are not supported, and the commit that brought the
rule said the same of phase B in its message.

What moves phase B is the number of leaf programs the tree proves beside the
base. A block that skips the purges keeps 13 of 26 there, because the tree's
budget counts the pages the allocator still holds: phase B is 3.4 s shorter
and the tree 2.7 s longer. With the budget's share raised so that all 26
stay beside the base, and no purge, phase B is as long as with the purges
(87.27 s against 87.28 and 86.81; 83.78 and 83.58 with 13).

Comment only: no code line changes.
With no disk, phase B rebuilds the tables phase A dropped. The streamed
chunks' regenerator was started at the finish's end, 0.58 s before phase B
at the median, and its first chunk needs about a second: phase B's first
rebuilt group waited for it (0.16-0.64 s at the median, measured on a
118 GiB host with one RTX 5090).

Its thread now goes up where the streamed chunks end and the finish begins,
whether or not regeneration has armed. It already waits for every streamed
chunk's fate (`await_rank` answers nothing for a table the arm may still
drop), so it starts the regenerator as soon as the arm's drop-back and the
streamed groups' commits have decided them, while the finish still runs.
Until the finish's end its window holds only its frontier, so it does not
rebuild beside the finish; after it the host's room sizes the window, as
before. If phase A ends unarmed, every table is kept and nothing starts.

LAMBDA_VM_BLOCK_REGEN_EARLY: unset is the new start, `finish` the start as
it was, `0` the start with phase B. The head-start line gives the
regenerator's own start and its thread's.

No proof byte moves: which tables are rebuilt is phase A's decision as
before, each rebuilt table is checked against the digest taken at its drop,
and phase A's end still refuses a regenerator whose ranks are not its final
drops.

Tests: the knob's three values and the line; the early window keeps its
frontier until the finish's end and takes the room after. The proving tests
of the early start cover the three starts; they run on a card.
When phase B rebuilds both classes it interleaved them from the start: one
streamed group, then the rest's first. The rest's regenerator starts with
phase B, from phase A's final drops, and its first group's tables take it
about a second and a half. At the median its first group waited 0.31-0.45 s
(measured on a 118 GiB host with one RTX 5090), and with the streamed
chunks' regenerator ready at phase B's start it would be reached sooner and
wait longer.

Phase B now takes the groups it rebuilds nothing of, then the first
LAMBDA_VM_BLOCK_REGEN_LEAD streamed groups (default 4; 0 is the plain
interleave), then the others of both classes interleaved by their cells as
before. Each class's groups stay in group order, the order its regenerator
deposits in. The order line says how many streamed groups came ahead of the
rest's first.

No proof byte moves: the proof does not depend on the order phase B takes
its groups in (the no-disk default's interleaved order proves the bytes of
the group order).

Tests: the order for a lead, for none, for a block with no rest group and
for a lead past the class; every group once and each class in order over
random blocks; the knob.
The streamed chunks' regenerator now has its thread up before phase A has
dropped anything. With each streamed chunk's drop held back by 100 ms it
asks for fates that are not decided yet: it must wait for every one,
rebuild exactly what was dropped and leave the proof's bytes those of
regeneration off. A fate answered "kept" before it is decided leaves ranks
out, and phase A's end refuses that prove.
The planner started one plain thread a leaf the moment the statement
arrived and took none of the proving phase's groups until every program
was emitted. Nothing needs a leaf's program before the base ends except
the early leaf, and while thirteen emitters ran the phase's first groups
cost 0.61 s each against 0.45 s after them (median block, one run).

Now W3_LEAF_EMITTERS programs (default 3) are emitted at a time, in leaf
order, beside the planner's loop over the groups; each is published as it
is ready, leaf 0 first, and the planner joins the emitters before it
returns, so the tree still finds every admitted leaf published.
W3_LEAF_EMITTERS=0 is the control: one thread a leaf, all at once, the
first group taken after the last program. Scheduling only: a leaf's
program is a function of the plan, and no proof byte or program id moves.
The effect on the block's time is not measured yet.
…ob, off by default)

A block cuts KECCAK_RND, KECCAK, ECSM and ECDAS at a fixed number of rows.
The count comes from the block's first version, which built each table
whole, padded to a power of two, and copied it apart: the pieces past the
last real row hold padding rows alone. A mainnet block of 96,384 keccak
permutations builds 64 KECCAK_RND tables of 2^16 rows where 36 hold a real
row.

CutPadding names the two rules: WholeRun (the default: the tables the split
makes, as before) and LastTable (stop at the table that holds the last real
row). LastTable's tables are WholeRun's first ones, word for word. A padding
row of the four families has every bus multiplicity at zero, so the tables
left out add nothing to the bus; the verifier takes whatever count the
statement states and is not changed.

- CutPadding::tables is the one count: the finish's announced counts, its
  build jobs, the monolithic finish and the tables built again in phase B
  (the same function with the same settings) all read
  StreamSkip::cut_padding.
- LAMBDA_VM_BLOCK_CUT_PADDING=run|last, read once by the windowed builder;
  unset is run, any other value refuses the build. A table built whole and
  split by the block afterwards (the comparison arms) keeps the whole run's
  padding.
- The streamed finish prints a BLOCK CUT PADDING line: the rule and, per cut
  family, the tables built and how many of them hold a real row.

Tests, none proving: the count against a model of the split; the default
and the knob's two words; for each family, wide, packed after the build and
written packed, LastTable's tables are the split's first ones and every
table left out has all its multiplicity columns at zero, the check itself
shown to refuse a cut one table short and one table long; windowed builds
of the fixtures against the frozen table phase, every other table
unchanged; the header, the stream and phase B's jobs count the same tables
under either rule; the knob off builds the listed tables on two pinned
programs.
…hort is refused on the bus

Proving tests for a block whose cut tables stop at the last real row
(CutPadding::LastTable):

- test_keccak_multi (per table and batched) and test_ecsm_multi (batched)
  proved with the whole run padded and cut at the last real row: the
  verifier accepts both, the second with one table fewer of each cut family
  the program has.
- The same blocks with a family's last table, which holds real rows, also
  left out (KECCAK_RND per table and batched; KECCAK, ECSM and ECDAS
  batched): the refusal is BusImbalance, and with the balance check off the
  verifier accepts the same proof, so the balance is its only objection.
- The streamed prove under the process's rule
  (LAMBDA_VM_BLOCK_CUT_PADDING): KECCAK_RND's count is the rule's with the
  finish monolithic, with the finish streamed, and under live regeneration,
  which builds each dropped KECCAK_RND table again with no mismatch. Run
  once per value of the variable.
Some comments added with the last changes cited a measurement by the name
of the run that took it. Each now states the figure and what it was
measured on (the block, the host's memory or cores, the card).

Comment lines only: no code line is added, removed or moved.
@jotabulacios jotabulacios mentioned this pull request Oct 21, 2026
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