Skip to content

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

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

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

…no byte

Against a codeword opened with the switch off: the same root, paths, and paths
with their cap, with no build; then a covered eviction takes the tree, the next
opening builds the same tree and keeps it whole again.
…tention line

Appended after the futile misses: whole trees on|off (N openings served).
Flips LFM_WHIR_WHOLE_TREES to on: the tree a commit builds stays on the
card whole, inside the codeword's promise and behind the same evictor as
a leaf layer, and an opening with a matching kept tree reads its paths
and builds nothing. LFM_WHIR_WHOLE_TREES=0 keeps leaf layers as before.

Measured on the block (A B B A): the whole run -1.00 s, the base -0.90 s,
the openings' tree rebuilds -0.82 s; 952 openings served a kept tree.
The argue's peaks in epochs 3-5 reach 25,633 / 25,306 / 25,681 of the
25,688 MiB budget. That is safe because everything kept is evictable: a
request the kept bytes can cover evicts them and succeeds, one they
cannot cover fails either way, so fallbacks cannot rise and the worst
case is the old rebuild.

A unit test pins the default and the kept-tree size. The comments that
said a tree is never held past its build (whir.rs, stacked_eval.rs,
whir_commit.rs, gpu.rs) now state the invariant that replaced it:
nothing of a tree is held outside a promise.
…s guard

Every card test whose counts or byte figures depend on what a commit
keeps now forces LFM_WHIR_WHOLE_TREES both ways instead of running at
whatever the default is: the per-codeword counts, the reservation test,
the blocking-key test, the process-wide counter identities, and in
whir_cap.rs the served, rehashed and evicted cap regimes.

H4's group guard used to forbid a tree held past its build. It now
asserts the invariant that replaced that rule: four unopened commits
keep exactly the object the mode names, the ledger grew by exactly four
codewords and those objects, the driver holds less than one tree and one
codeword beyond the promise (four trees kept outside it would read four
trees there), and what is kept is really held (more than the next
smaller retention). Under leaf layers the old two-sided bound is kept
as it was.
…its slot

An opening served a kept whole tree reads it through its own Arc after
the slot's lock is let go. The promise used to be given back by the
slot's drop, so an eviction during that read (or the codeword dropping)
returned the tree's bytes to the ledger while the buffer was still held:
for as long as the read lasted the ledger counted less than the card
held. With whole trees that read happens on every served opening.

The buffer now carries its own promise (KeptNodes): grown into the room
when it is admitted, given back by its own Drop, after the free is
enqueued, on the last handle. It holds its room strongly, so a room can
no longer drop, giving back every byte it counts, while a buffer it
counts is still held; that only happens during the evictor's walk,
which can hold a slot past its codeword, and it keeps the rest of the
room counted for that long.

The evictor passes over a tree an opening is reading, and does not count
it as reclaimable: dropping the slot's handle would give nothing back
and lose the tree. A request only that tree could cover now fails,
counted, instead of being handed bytes the card still holds. Passes are
counted (retention_read_passes) and noted on the eviction lines.

The card test holds a reader (hold_kept_tree, test-only) across an
eviction and across its codeword's drop, and asserts the ledger counts
the tree until the reader lets go, then gives back every byte.
…and a capture hook

multilinear::fused computes a table's zerocheck batch the stage-1 way
and sends today's messages: the bus's two rules as one column L (S1-2),
the eq weights pulled out of the rounds with A_j(1) from the claim
(S1-4, Gruen), and rounds 0 and 1 from one base-field pass over the
4-row groups on the {0..d}^2 grid (S1-5), with a corner skip for traces
that satisfy their AIR and a corner check that refuses the first row
that does not. Today's rounds and S1-2 alone run through a counted copy
of today's round loop. Every variant counts its work by phase, and
fused::model states the same counts in closed form for any height.

Host only: nothing in the prover calls it. The only production change is
multilinear_table::argue_capture, a test hook that records a table's
argue inputs while a test has armed its width; disarmed it is one
atomic load and records nothing.

Tests: every variant against batch::prove over today's batch (rounds,
point, transcript, next challenge, factor values) on a synthetic table
at every height to 2^7 and on all 26 VM tables and 10 W-LFM chips over
random columns; model == counters throughout; the base-field
precondition on all 36; negative controls (a wrong L coefficient, the
corner skip on a broken trace, a flipped cell). The real-trace half
proves a program to capture one and is ignored, for the box.
D-ARGUE stage 1's device twin of multilinear::fused, behind
LAMBDA_VM_ARGUE_FUSED=1 (every table, or the committed widths
LAMBDA_VM_ARGUE_FUSED_WIDTHS lists): rounds 0 and 1 from one base-field
pass over the 4-row groups on the {0..d}^2 grid (zc_grid01, zc_bus_u),
both folds at once into a quarter-size ext3 copy (zc_fold2), the bus as
one column L there (zc_bus_column), and the later rounds walking the
constraint part alone at the integer nodes {0, 2..d} under split eq
weights (zc_round_gruen, zc_halve), down to today's host crossover. The
messages come out of the sums through the reference's own assembly
(fused::message), so every message is today's.

The constraint program is the base DAG with an ACC step after each root
(gpu_fused::lower_fused), so roots are summed as they are made. The
lifted factors are only read, so LAMBDA_VM_ARGUE_FUSED_XCHECK=1 can run
today's device rounds after the fused ones with the same challenges,
compare every message and the factors at the crossover, time both
(one ARGUE FUSED line per table, with R = fused / today) and check the
grid's corners per row.

S1-1 on today's kernel: LAMBDA_VM_ARGUE_INT_NODES=1 takes a round kernel
that interpolates with the componentwise base multiply when every node
is (k, 0, 0).

Both knobs default off, and off is today's path unchanged: the table
prove builds nothing for the fused rounds, batch::prove_resident is
prove_resident_with(.., None, ..).

Tests: the lowered blob against today's zerocheck rule on all 36 tables
(host); on the card (box): the fused rounds against today's host rounds
on every table at 2^8 and the three at 2^10 and 2^12 with the
cross-check confirming each, the integer-node rounds, and two negative
controls - a wrong bus coefficient refused by the cross-check, a broken
trace refused by the corner check - each with a mutation switch that
makes its check inert.
They flip process-global switches (the fused rounds, the cross-check,
the faults), which would reach any proving test running beside them in a
suite run. Ignored, so a gate runs them with --ignored --test-threads=1.
A committed table carries no name, so each ARGUE FUSED line gives its
signature - roots, bus terms, walk slots - beside its factor count, and a
declined table prints a line of its own under the cross-check or
LAMBDA_VM_ARGUE_FUSED_LOG. LAMBDA_VM_ARGUE_INT_NODES=1 prints one banner
when it is first read, so an arm's log shows it took effect.
The upload declines a table under 4096 cells (gpu::worth_the_device),
and the prover then argues it on the host, so a run at 2^8 of EQ's 12
factors never reached the card: FAST job 271's one red. Each table now
runs at 2^8 or the first height above it with 4096 cells.
A table without roots has a zero constraint part and d_C = 0, so its
grid {0,1}^2 is all corners; with the corners skipped - the production
setting, no cross-check - no grid point is left, and the launch sizing
divided by that empty row count. FAST job 273's B arm panicked there
("attempt to divide by zero", argue_fused.rs:101) at the first such
table; its X arm, whose cross-check computes the corners, proved and
verified with all 390 fused tables confirmed.

An empty point list now launches nothing (T is zero) and sizes no slot
file; U is still computed. A host test pins the sizing (it reproduced
the panic at the same line before this change). Two card tests close
the gap that let it through, both with the production switches: every
table without roots against today's host rounds, the corners skipped;
and test_keccak proved under the B arm's switches, with and without the
cross-check, and verified.
The FAST A/B at 9e27289 read EFFECTIVE twice (jobs 274 and 270, pooled
over 4 A and 4 B arms): base -2.80 s, the argue -2.94 s, the whole run
-2.75 s, every B arm's base below every A arm's, identities equal on all
eight arms, no fallback; the cross-check arm confirmed every one of 390
fused tables' messages against today's rounds.

LAMBDA_VM_ARGUE_FUSED and LAMBDA_VM_ARGUE_INT_NODES are now on unless set
to 0, and 0 is today's rounds exactly. Each prints a banner when first
read, and a unit test pins each default. A fused session counts as a
device sumcheck (gpu::sumcheck_calls, sumcheck_rounds), and the split's
ARGUE ZEROCHECK line ends with the fused sessions, the declined ones and
their device time, so an arm's log shows which rounds it ran.
…is refused

With the fused zerocheck on by default, the tall-table knob tests pin it
off: every other argue knob acts on today's rounds. Two tests on top: the
tall tables proved with the fused rounds (alone, and with the device
columns, tables and gathered reads) give today's canonical bytes table by
table and whole, the same transcript and next challenge, and verify - on
a device the fused arm shows its sessions; and with the fused rounds' bus
coefficient off by one the proof parts from today's at CPU and does not
verify.

The fused faults are per thread now: the fused rounds run by default, so a
fault armed for one test must not reach a proof another test runs beside
it, and a table's argument runs on the calling thread.
…AMBDA_VM_ARGUE_GKR_GRUEN

D-ARGUE S1-3 (D-BATCH M1-1). A device GKR layer's round sends
s(t) = E·eq1(u_j, t)·H(t), with H(t) the layer's h summed under
eq(u_{>j}, ·). The card now sums H(1) and H(2) only - every node by
additions, no eq factor and no program walk - and folds the previous
challenge on the way in; the host puts E·eq1 back, takes H(0) from the
claim (the card sums it where 1 - u_j has no inverse) and extrapolates
H(3). The weight is two small tables a layer (the tail's variables and
one level per card round), not a layer-sized eq table folded each round.
The rounds stop at a cube of LAMBDA_VM_ARGUE_GKR_GRUEN_TAIL (default 64)
and hand the host today's five factors there.

Every message is today's field element, so the transcript and the
canonical bytes do not move. Off by default: off is today's rounds.

- math-cuda: kernels gkr_eq_levels_ext3, gkr_round_gruen,
  gkr_gruen_finish; GruenLayer (tree layers and the rebuilt input
  layer); SumcheckSession::fold_first for the cross-check's shadow.
- multilinear: gkr_gruen (knobs, the host's half of a round, counters,
  a per-thread fault, a host reference of the device algorithm);
  DeviceTree::prove_layer_gruen; LAMBDA_VM_ARGUE_GKR_GRUEN_XCHECK walks
  today's program over the same folded halves each round and compares
  rounds and factors; the ARGUE GKR line ends with the Gruen counts.
- tests: the host reference equals today's rounds, challenges and
  factors at m 1..9 over every tail split, with H(0) direct or derived,
  and with coordinates of one; a wrong claim changes it. Device tests
  (tests/argue_gkr_gruen.rs) and stark's tall-table byte identity and
  fault test run on a card.
…H B-2)

A new proof format behind ChainFormat.argue (default PerTable, today's):
ArgueFormat::Batched { bin_log_cells } runs one lockstep LogUp-GKR ladder
per bin of tables (multilinear::gkr_lockstep), one front-loaded constraint
sumcheck over every table with virtual padding by the product of the
missing variables (multilinear::front_loaded), and no claim reduction for a
table whose committed factors are all unshifted: its factor values are its
column claims. A shifted table keeps today's reduction at its prefix point.

The batched proof is its own type (stark::multilinear_table::batched::
BatchedMultiProof), so MultiProof and today's bytes do not move; the
openings are shared code, factored out of multi_prove/multi_verify
unchanged. Each side refuses the other format's config
(Error::ArgueFormatMismatch). argue_plan is the one function that bins the
tables (first-fit-decreasing by input cells under the cap).

Tests: round trips across heights and bins, one-row and rootless tables, a
shifted table; every D-BATCH negative (bus output, ladder halves at a mid
step, GKR and constraint rounds, factor values, swapped tables, padding by
scaling, suffix of xi, cross-table weight, table claim, bin partition,
reduction shape, violated constraint, bus multiplicity, preprocessed column,
cross-format); a mutation test per new verifier check. Box tests for every
VM table and the W-LFM chips are #[ignore]d.
…he epoch bookend

The first box run argued 22 of the census's VM labels. The round trip now
also proves test_ecsm (ECSM, ECDAS), the load/store programs (LOAD, MEMW),
hint_min (HINT) and every epoch of two continuations (the L2G bookend,
DECODE's prepared opening, two commitment groups), and asserts that every
label of lean_program_census::vm_airs was argued batched and verified.
…GRUEN=0 the opt-out

FAST job 330 (4 + 4 arms on #1010 + Q1b): base -1.77 s, the argue
-2.08 s, the whole run -1.57 s, the GKR 4.24 -> 2.13 s, the same program
ids; its cross-check arm compared 3,381 real layers with the old rounds
and found every one equal. A unit test pins the default.
Continuation AIRs carry no name (they print as `unknown`), so FAST2 206
argued and verified the L2G bookend of every epoch yet reported the census
label L2G missing. The bookend is pushed last, and is now recorded by its
label.
A committed stack can now be retired: each codeword is dropped and only
the root and the top of its tree (every level but the bottom few, never
into the Merkle cap) stay on the host. Reviving it re-encodes the same
codewords from the columns, on the card with no tree built
(math_cuda::whir::encode_codeword_*), and the first round's paths are
read from the kept top plus the dropped levels re-hashed from the queried
cosets. The re-hashed subtree root is checked against the kept node
before any path leaves, so a codeword that is not the committed one is
refused (RecomputedCodewordMismatch) instead of opened.

This is what the block prover holds between committing a group and
opening it. Nothing on an existing path moves: commit_from is split into
encode + tree with the same calls, and a commitment that kept its tree
reads its paths exactly as before.

Tests: a retired and revived stack opens to the same bytes as one that
kept everything (drop 0, 1, 3, more than the depth; and under a cap);
revived over other columns it refuses to open (the kept-node check is
load-bearing: disabling it fails that test).
…by group

multilinear_block: the tables split into groups a card holds (contiguous
runs, at most N stacked polynomials each, a function of the shapes and
the format that both sides compute). Phase A commits each group and
retires its codewords; every root goes into the transcript and (z, alpha,
beta) are drawn once; phase B re-uploads each group's columns, argues its
tables and opens its stack on the fork S_post || g. The verifier mirrors
the forks, refuses roots no layout claims, and checks the bus balance
once over every table of the block.

Per-group stamps (upload, commit, retire; upload, argue, encode, open)
are returned for the W1 readout. No existing proof path is touched.
A separate entry point beside #1010's epoch pipeline, which it does not
touch: execute the block, build its traces at 2^21 rows a table,
split KECCAK_RND into 2^16-row tables (today's tallest), and prove every
table in one multilinear_block proof under a statement tag of its own
(MULTILINEAR_BLOCK_TAG; the monolithic statement otherwise). Memory is
the monolithic PAGE argument. The groups (3 polynomials of 2^27 at most,
today's heaviest epoch) and the stack are verifier-side constants
(BlockFormat); the KECCAK_RND height and the tree levels kept are the
prover's. A block statement may declare several KECCAK_RND tables, up to
a bound checked before any AIR is built.

Tests: round trips at one group and over many; KECCAK_RND in chunks;
proofs that verify at every kept tree depth, byte-identical from the
same traces under a deterministic grind; refusals for a group proved on
another fork, a block missing a real KECCAK_RND chunk (on the bus; the
balance check is load-bearing), a tampered column claim, bus output,
public output, table height, an extra root and an inflated count. An
ignored test proves and verifies a real block with the phase stamps.
…ock's fallbacks

block_whir_epoch_reference_on_a_real_block proves the block through
#1010's epoch pipeline (prove_continuation at 2^21) with the base split on
and prints the summed argue, openings and commit of every epoch and the
cross-epoch proof: the same-binary control the block's phase B is read
against. The block test now prints the device's host fallbacks, so a
readout that ran on the host says so.
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Benchmark Results for modified programs 🚀

Command Mean [ms] Min [ms] Max [ms] Relative
head ecsm 2.6 ± 0.1 2.5 2.9 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head hashmap 109.4 ± 1.1 108.0 111.4 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head keccak 136.9 ± 3.0 131.1 142.0 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head syscall_commit 87.6 ± 2.1 85.5 91.3 1.00

…3, first cut)

multi_prove_batched now runs where today's argue runs: each bin's trees are
built from the factors on the card (else here), the ladder steps every
active tree's layer on the card through a steppable LayerSession
(DeviceTree::layer_session, the session prove_layer opens) with the
batched step's per-tree program eq*(w_p*num + w_q*den), hands the cube to
the host at today's crossover, and sums the trees' rounds on the host
before the one shared challenge. The constraint sumcheck drives each
table's fused zerocheck (FusedStepper, prove_fused's rounds made
steppable; prove_fused is now a loop over it) in lockstep through
front_loaded's RoundStepper, a table's message read at D_max nodes by
interpolation past its own degree. A table the card declines runs on the
host. The host reference (Where::Host) is what the negative tests prove;
the card must produce its bytes.

Not yet: one sync per round across trees (each session reads its own
round back), the crossover on the bin's summed half-cube, and the VRAM
plan (every table's lifted factors stay resident from its tree to the end
of the constraint phase), which waits on M1-2.
…up during the argue

Phase A no longer waits for every table: a producer lays each group's
tables out (transposition, preprocessed check, layout; the group's tables
in parallel) and hands the group over while the card commits the group
before it (BlockCommitted::commit_streamed, one group of slack). In
phase B the next group's columns go up on a helper thread during the
current group's argument, so only the part the argument did not cover is
paid.

For the readout: the trace build's phase ends and each phase-5
generator's finish (trace_builder::build_stamps, off unless the block
prover turns it on), phase A's per-group wait for its tables, and the
first-round path counters (open_many calls served from kept tree tops,
leaves re-hashed): a revived commitment builds no tree, so nothing
re-commits.
…H S0)

Under LAMBDA_VM_BASE_SPLIT only, and byte-identical: the argue's "rest" -
what is neither a device GKR layer nor a zerocheck round, 3.19 s a block
known only by subtraction - is timed per region of a table's prove:
interactions, tree, output, gkr (its host prefix apart), setup, batch,
factor values, claim reduce, column evaluation, release. One
`ARGUE REST #k` line an epoch sums them against the argue's wall, and
one `ARGUE AIR #k t NAME` line a table gives the shape D-BATCH's plan is
sized from (n, columns, factors and how many read a shifted row,
interactions, k, degree, roots, widest bus message) with its tree, gkr
and core times.
…lves

prove_argue and verify_argue run phases G, C and Rd from the shared
challenges to every table's column claims; the roots block, the bus
balance, the preprocessed checks and the openings stay in
prove_batched_on and verify_batched_with. No transcript moves. The halves
exist for the in-guest leg's gate, which argues a few tables with no
commitment, as the per-table leg's gate calls prove.

The real-table card-vs-host byte test compares the roots and the argue:
the openings grind under the production config, and the parallel nonce
search makes any two honest proofs part there (FAST 340: test_keccak's
card and host proofs both verified and differed).
… B-4a)

emit_batched_argue is verify_argue emitted: per bin its outputs and one
lockstep ladder (a shared mu, each step's relation asserted), one
front-loaded constraint sumcheck (xi, lambda, the tables' rules weighted
lambda^{3t}, each padded by the suffix product of the challenges it
lacks, asserted), the factor values absorbed, a shifted table's reduction
and an unshifted one's direct column claims. It derives every challenge
from the transcript it drives; the plan is argue_plan's.

batched_argue_cost predicts its rows, constants (by value) and sponge.
Gated against real batched argues of three tables (two shifted), in one
bin and in three: the verdict equals the host's verify_argue, the form
equals the emitted census exactly (1517 and 2026 rows), and six forgeries
the host rejects cannot execute. On that fixture the leg is 1517 rows and
109 permutations against the per-table legs' 2322 and 182.
Table::columns_blocked: row blocks in parallel, a band of 64 columns at a
time, so each row's cache lines are read once instead of once per
column. Same columns in the same order (pinned against Table::columns
over one-column, sub-band, band-plus-remainder and KECCAK_RND-wide
shapes, partial last blocks). Used by block_whir's layout only: FAST 361
showed the per-column strided read's memory traffic tripling phase A's
upload when the two overlap. Laptop timing (38 x 2^21): 1.02 s -> 0.54 s.
Under LAMBDA_VM_BASE_SPLIT, an ARGUE TREE line an epoch splits the tree
region S0 measured at 1.80 s a block: the factors lifted, the input layer's
programs lowered and written, the levels folded, the output read. The card
runs the first three asynchronously, so LAMBDA_VM_ARGUE_TREE_SYNC=1 (a
diagnostic, never a wall measurement) makes each part wait for its own
kernels and charges it its card time. Byte-identical.
…byte-identical)

Every long list the windowed builder grows over the run (the routing's
segments: BRANCH, EQ, BYTEWISE, STORE, the MUL / DVRM / SHIFT / BITWISE /
LT parts; the kept rest: ECDAS, CPU32, KECCAK, COMMIT, BLAKE3, ECSM, HINT,
the in-walk lookups; the compact LT stream) was a Vec grown by extend once a
window. A full Vec doubles: an allocation as large as the list and a copy of
it on the accumulator thread. At p90 after S1 the largest is BRANCH, 2.44 GiB
in one Vec (its next doubling 4.9 GiB); the builder term stepped up to +1.36
GiB between two memlog ticks of the walk (+3.21 before S1, BIG 670); the 195
Mgas scout died on such a step (+24.2 GiB, RYZEN 015).

BlockVec stores a list in blocks of a fixed length: the first grows as a Vec
does up to that length, every later one is allocated whole, and a full block
never moves. A block is a table's chunk (2^21 ops at the production rows for
ops of 32 bytes or less) or at most 64 MiB; ECDAS's are the block proof's
2^17-row cut (about 245 MiB). The compact LT stream goes in 64 MiB blocks,
an op's two varints never straddling two, its sparse index addressing
(block, offset).

The finish reads every such list as the concatenation of its blocks (S1's
Segmented): a chunk the block length divides is a block, borrowed; a chunk
that straddles two blocks or segments is copied as it is generated. The
lists the routing makes from several segments (MUL, DVRM, SHIFT, BITWISE,
LT) are their segments' blocks in the routing's order, so assembling them no
longer copies them either; a whole-run build's lists are one block each.
Phase 4 counts block by block, MUL and DVRM instance by instance of the
concatenation. STORE's streamed chunks are its front blocks, moved out.
BLOCK MEM prints each builder list's largest single allocation at windows
walked (builder largest).

Tests: a block list is the Vec it was appended, pushed or a window at a
time, at block lengths 1 to 64 and every list length around them (empty,
one, exactly full, one past), every range and every chunk size, a chunk of
the block length borrowed; chained and taken from the front in order; the
block length fits 64 MiB and the chunk. The compact LT list expands to what
was pushed with its stream cut into blocks of a few ops. ECDAS's block is
the block's ECDAS cut. The windowed drop test builds the whole-run tables
at every window and chunk size. Mutations each fail one: a block overfilled
by one, the front taken one element late, a chain's length dropped, the
compact decoder crossing a block one byte late, MUL's segments out of order.
Every table's digest over 13 asm programs, whole-run and windowed in six
configurations, is S1's (351 digests, fixed trace hash; a probe, not kept).
…s, packed (byte-identical)

The finish built each of the three whole and wide (they stayed wide under
pack_finished), and the block then copied each apart with split_rows: at p90
ten tables, 5.09 GiB at eight bytes a cell, the rest's only wide tables. Their
layout then transposes them into one 1 MiB column at a time on the layout
thread, which lands on fresh pages while the finish's freed extents sit
unused: VmRSS +4.7 GiB with live flat from `finish done` to `rest laid out`
in every p90 run (BIG 660, 668, 669).

A row of each reads its own op alone (one permutation call, one scalar
multiplication, one double/add step) and their padding rows are constants,
so rows [k*R, (k+1)*R) of the whole padded table are the ops in that range
and the padding to R rows. generate_{keccak,ecsm,ecdas}_rows_as build a
table of R rows from such a slice (the whole-table generators are that at
the padded height), and cut_tables builds the cuts the split makes, the
first alone until G-pack knows the family's widths and then the rest in
parallel, each packed as it is built and read from the list's blocks (a
cut that is a block is borrowed: ECDAS's blocks are its cut).
StreamSkip::{keccak,ecsm,ecdas}_rows and WindowedTraceBuilder::cuts_at_finish
carry the block's cut sizes;
BlockOptions::finish_cuts turns it on (production, unless
LAMBDA_VM_BLOCK_FINISH_PLAN=0, the A arm: whole, wide, then split), and the
block no longer splits tables the finish cut (a packed table has no rows for
split_rows to copy).

Tests: for each family, every op count around the cut sizes and every cut
from one row to past the whole table, built wide, packed after the build and
written packed, the cuts are split_rows of the whole wide table word for
word, with the same heights, packed exactly when asked, the ops in one
block or in blocks of three that the cuts straddle; no ops give no
table; a cut that is not a power of two is refused. The windowed build with
the cuts at the finish is the whole-run build with the three split, table
for table, on the keccak and ECSM fixtures at cuts of 1 to 64 rows.
…dentical)

build_traces is now FinishPlan::new, which runs what needs the whole run
before any table (LT's segments, the BITWISE histogram, HALT and the
REGISTER final state, the public output, the PAGE layout) and counts every
table it will build without generating one (RestHeader: the table counts,
the page configs, the public output and the BLAKE3 count, what a statement
is read from), and FinishPlan::emit_all, phase 5 as it was. emit_all refuses
tables the header did not count (Error::Prover). One path still serves the
block's windowed finish, the epochs and the whole-run build.

This is the enabler for streaming the finish: a statement's shape is known
before the tables are, and the tables can then be made and handed on in
AIR order. Nothing is streamed yet and nothing moves in time.

generate_page_tables is split into page_configs_of (the layout) and
page_trace (one page's table), the same loop; Traces::runtime_page_ranges
is runtime_page_ranges_of over the configs.

Tests: the table phase as it was (build_traces at 6cbf1c4) is frozen
test-only as the oracle (finish_oracle, switched on per thread, counting its
runs so a test knows its oracle side ran). The plan builds the oracle's
tables word for word on whole-run builds (13 asm programs, small, 4-row and
production chunks), on non-final and final epochs with and without the L2G
bookend, and on windowed builds in six configurations (streamed ops kept or
dropped, the derived LT ops raw and concatenated, KECCAK_RND chunked at the
finish or streamed, the KECCAK / ECSM / ECDAS cuts, packed and written
packed, the MEMW-derived LT streamed) at two chunk sizes and two windows.
The header is the oracle's counts, runtime pages, page count, public output
and BLAKE3 count before any table is built, and every family miscounted by
one either way is refused.
…3c-ECDAS; byte-identical)

The windowed builder keeps every ECDAS row to the finish, and a row
(EcdasStep and its timestamp) is about 1.87 KB, 1.5 KB of which are its
three carry arrays, [i64; 64] each: at p90 the kept ECDAS list is 1.78 GiB,
the builder's largest after BRANCH, and transfer-heavy blocks grow it fastest
(7-16 GiB projected at 200 Mgas, D-FINISH 2.6).

The trace range-checks every carry as its offset 16-bit value. A
CompactEcdasOp keeps the row's other fields as they are and its carries as
those offset u16 values, about 0.72 KB a row. A row with any carry outside
0..2^16 once offset keeps all three arrays as they are (boxed), so every row
widens back to exactly the op it was made from whatever the input; the
release build has no range assert to rely on.

The builder holds ECDAS as compact rows (in its 2^17-row blocks, now about
91 MiB each), and so does every collected run. Each reader widens a row as
it reads it, never a list or a cut: the generator fills a table from rows
in any form (generate_ecdas_rows_of), and phase 4 counts a row's lookups
from its widened op. The tables are the same.

Tests: every row widens to the op it was made from, carries at both ends
of their offset range held compact, and a row with one carry outside it (by
one either side, at i64's ends, far out) held as it is; a compact row is
under 40% of the op. A table built from compact rows is the table of the
ops, wide and packed (with the fallback rows where the debug checks are
off). Phase 4 counts compact rows' lookups as the ops'. Mutation: rows held
compact whatever their carries (the fallback dropped) fails both. Every
table's digest over 13 asm programs, whole-run and windowed in eight
configurations (the cuts at the finish included), is the build's without
this (455 digests, fixed trace hash; a probe, not kept).
`kept rest ecsm` and a walked window's lists counted ECSM ops at their
inline size, so the double/add steps every witness holds (one Vec of
ECDAS steps a call, ~768 KiB at 200 Mgas, ~10 GiB in all there) showed
up only as unnamed memory. They now count each op's steps buffer too,
and the largest single allocation of the list takes it into account.
Logging only; the tables are unchanged.
… byte-identical)

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

Tables are unchanged: a test builds a call's ops for six scalars and
checks the ECDAS rows against a fresh witness's steps, the ECSM row
against the fresh witness, and that the ECSM op keeps no steps.
…yte-identical)

The finish built every table of the rest in one parallel scope, so all of
p5's allocations landed together (T200-R: +40.3 GiB in one step at
gen_decode), and the layout took the rest only once the whole of it
existed. The finish now plans the rest as units in AIR order (a job a
table, or a streamed chunk's slot; each family's ops held by its jobs
alone and freed with the last) and streams them: waves of at most half
the gate's budget are built in parallel and handed on in order from the
builder thread. A byte gate (BLOCK_REST_LAYOUT_BYTES) holds each table's
bytes from before it is built until the packer places it; the lowest
table not yet placed always passes, so a budget under one table cannot
deadlock, and a job's error or a stopped packer stops every waiter.

The layout thread builds the run's AIRs from the header the finish sends
first, takes each table as it comes (checked to be the next AIR in order,
of that AIR's width, every AIR reached) and lays it out narrow; the
packer places the rest as it comes, after the streamed chunks, as
before. The tables, their order and the groups are the same.
LAMBDA_VM_BLOCK_FINISH_STREAM=0 keeps the monolithic finish (the A arm);
the epochs, the continuations and the inline path keep it always.

Tests: the stream is the monolithic finish table for table, in AIR order
with every streamed slot, in every windowed configuration the block uses,
at budgets of one byte, 2 GiB and none; the streamed tables are the
header's AIRs' widths; a one-byte budget completes; a job's error and a
stopped packer stop the stream and every waiter within a timeout; a
miscounted header is refused before a table is handed on; the layout
refuses a table out of order, of the wrong width, past the AIRs or
missing. Mutations caught: the lowest-pending rule removed, two units
swapped, a chunk range off by one, the width check removed, the gate not
stopped on an error.
…entical)

The BRANCH segment is the builder's largest list (2.44 GiB at p90, 5.70
at 200 Mgas, 32 bytes an op) and lives until its tables are built. It is
now kept as a stream: a flags byte and three zigzag LEB128 varints an op
(the pc from the previous op's, the offset, the register the cheapest of
as-is / from the previous op's register / from the op's own pc), in
64 MiB blocks never reallocated, with a sparse index every 4096 ops so
any range expands on its own (as CompactLt). Every op takes at most 31
bytes whatever its values, so it is lossless with no raw form. The
finish reads it as a compact segment: p4 counts its lookups range by
range, the tables expand each chunk as it is built.

Tests: any range, across the index and across small blocks, expands to
the ops pushed, fields at both ends of u64 / i64 included; the worst op
round trips at 31 bytes and a common one takes a few; the BRANCH table
of the expanded ops is the table of the ops. The finish's oracle and
stream tests pass on it. The digest probe is identical against
b0f8c68 (455 and 351 table-set digests). Mutations caught: the
register codings swapped on decode (the round trip fails, 35 of 455
probe digests differ); an index mark's pc wrong (the round trip fails).
…c (byte-identical)

S3c-BRANCH's stream (varint blocks, a sparse index, any range expanded
on its own) moves to `delta::DeltaStream<C>`, generic over a `Codec`
that writes each op from the ops before it and keeps that part of them
as a state the index records. BRANCH becomes its first codec, with the
same coding, so EQ and BYTEWISE can follow with codecs of their own.

The index's spacing is a field (4096); a test can make every stream on
its thread mark every few ops, so chunks and p4 slices start past index
marks. A finish test builds windowed runs with marks every three ops
and checks they make the same tables as the default spacing, and a
codec test reads chunks of every size around the spacing.
The EQ segment (0.75 GiB at p90, 24 bytes an op) is now a delta stream:
a flags byte (invert, and how each operand is coded) and two zigzag
varints, `a` as it is or from the previous op's, `b` as it is, from the
op's own `a` (equal operands cost a byte) or from the previous op's,
each the cheapest. Every op takes at most 21 bytes, so any op is kept
losslessly. The finish reads it as a compact segment; p4 counts its
lookups range by range.

Tests: any range, across the index and small blocks, and chunks of
every size around the index's spacing (and at marks every three ops)
expand to the ops pushed, operands at both ends of u64 / i64 included;
the worst op round trips at 21 bytes and a common one takes three; the
EQ table of the expanded ops is the table of the ops.
…e-identical)

The BYTEWISE segment (1.03 GiB at p90, 24 bytes an op) is now a delta
stream: a flags byte (how each operand is coded, and the opcode when it
is under 8; a larger one follows in a byte of its own) and two zigzag
varints, `a` as it is or from the previous op's, `b` as it is (a mask),
from the op's own `a` or from the previous op's, each the cheapest.
Every op takes at most 22 bytes, so any op is kept losslessly. The
finish reads it as a compact segment; p4 counts its lookups range by
range.

Tests: any range, across the index and small blocks, and chunks of
every size around the index's spacing (and at marks every three ops)
expand to the ops pushed, operands at both ends of u64 / i64 and an
opcode past the flags byte included; the worst op round trips at 22
bytes and a byte mask on a small value takes three; the BYTEWISE table
of the expanded ops is the table of the ops.
prover/src/lfm/program_budget.rs and prover/src/alloc_purge.rs are
#1013's at 82e777c, byte for byte: LAMBDA_VM_TREE_PROGRAM_BUDGET (auto,
off or a size; AHEAD = 4) with its ordered streaming emitter, and
LAMBDA_VM_TREE_LEVEL_PURGE. block_whir's spill target becomes
crate-visible, as the budget's host target. Nothing calls them yet.
Every leaf and node program of the W3 tree is admitted under
LAMBDA_VM_TREE_PROGRAM_BUDGET (auto by default, against the spill
target) before it is emitted, in prove order. The leaves the budget
admits at the statement are emitted beside phase B as before; the rest
by four streaming threads in the tree, as it admits them. Each node is
admitted on one of the builder's plain emitter threads (an admission
never waits on a rayon worker) and emitted on the builder's pool. A
prover claims a program as it takes it and lets it go once done with
it (a leaf after its proof, a node after its fill), which frees the
program and gives its bytes back.

The leaves' artifacts are published all at once after the last while
every leaf's program is there (today's order: published one by one,
ULTRA 101 cost the 1x tree 0.24 s), and each as it is built from the
first leaf whose program is not, so the publisher never waits for a
program while it holds what the provers need. W3_LEAF_ARTIFACTS_EACH=1
forces per-leaf publication. After a tree level's last proof,
LAMBDA_VM_TREE_LEVEL_PURGE may purge the allocator where the host is
short.

On a host with room every program is admitted by room and the schedule
is today's. Programs are functions of the plan and their children's
artifacts alone, so no proof byte moves.
…estamp

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

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

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

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

LAMBDA_VM_BLOCK_UNVERIFIABLE_CLOCK=1 proves past the bound only in a release
build: HALT and HINT debug-assert 32-bit timestamps, so a build with debug
assertions refuses instead of proving into them.
…n, ported)

A block table dropped after its group's commit leaves a RegenSlot: the
shape and the spill store's digest of its packed columns. A regenerator
deposits the columns again in phase B; the deposit is checked against
the slot, paced by rank from the window's frontier, and every exit of
the regenerator settles what it did not deposit. #1013's stark::regen
over NarrowColumns, the form a block holds, instead of a trace's
NarrowMain. Nothing uses it yet; the proof is unchanged.
…med chunks

The walk's one body is generic over whether it lists the ops only the
finish reads; without them it is the regeneration walk (#1013's R1),
which advances the same state and emits only the streamed tables' lists.
RegenBuilder walks a run again from its logs with it and cuts each
streamed table's list as the hand-out does, STORE's from the CPU ops as
the routing filters it, in the hand-out's order within a window. Fed
phase A's windows it rebuilds phase A's chunks in phase A's order.
Accumulator::absorb_counted reports the ops a window added to each
streamed list, for a recorder. Nothing in the prove calls them yet.
…off by default

LAMBDA_VM_BLOCK_REGEN=shadow: phase A records a recipe per streamed chunk
(its chunk, the windows it spans, its place in the hand-out, the spill
store's digest of its packed columns taken on the layout thread that
generated them), and phase B runs #1013's sequential regenerator beside
the prove on threads of its own: a fresh execution, the regeneration
walk, the slicer and generators writing packed, each chunk checked
against its recipe and dropped. Nothing it builds reaches the proof. It
reports mismatches, failures and its CPU, and plans the chunks against
phase B's groups as they ran: in group order, and with the groups that
hold no streamed chunk first (each group's phase-B span is now stamped).
Any other value of the knob is an error; a mismatch is a report entry,
never a panic. The shadow records nothing where phase A streams chunks
the regenerator does not cut (KECCAK_RND, the MEMW-derived LT ops) or
generates them 64-bit.
…entical)

block_prove_in_order proves the groups in a given permutation: each group
proves on its own fork S_post ‖ g and nothing one group proves enters
another's transcript, so each group's share is the same in any order;
the shares are assembled in group order, the next group in the order is
restored and uploaded beside this one's argue, and spilled tables are
read back in phase B's order. block_prove_on_forks_observed is the group
order; rebuilt_last orders the groups whose columns phase B rebuilds
last. A test proves reversed and interleaved orders (spilled too) to the
group order's bytes; an order that is not a permutation is refused.
…, off by default

LAMBDA_VM_BLOCK_REGEN=auto|always: phase A drops a streamed chunk after its
group's commit instead of keeping or spilling it — its packed columns leave
after the spill store's digest, on a drop thread, and a RegenSlot stands in
the parked table — and phase B's regenerator (#1013's run_live: a fresh
execution, the regeneration walk, generators writing packed) deposits each
into its slot, paced by rank from the window's frontier. A table's group
waits on its slot before its upload; a deposit that is not the dropped
columns refuses the table there, and the kept tree top's check stands
behind the digest. Phase B takes the rebuilt groups last, in rank order,
so the regenerator has the rest's phase B as a head start; any other
order of them is refused.

auto arms on the policy's first wish to move a table off the host, or at
the walk's hand-off: every parked droppable chunk is dropped back and
every later one dropped; always drops every one (the identity test mode).
With the spill off, auto still decides with no store: no disk, what
cannot be rebuilt stays on the host. A block that never arms drops nothing
and starts no regenerator. Every exit of the regenerator settles its
slots, and the prove's end closes its window. The proof is unchanged.
Phase B proves the rest's groups first, and the regenerator, held to a
4 GiB window ahead of the frontier, idled through that head and then ran
at parity with phase B: at T200-R its takers waited 2.97 s (RYZEN 049).
With 16 or 32 GiB they waited 0.0 s (RYZEN 052) while the run's peak,
set in phase A, did not move.

The window is now adaptive when LAMBDA_VM_BLOCK_REGEN_AHEAD_GIB is unset:
a pacer beside the regenerator reads VmHWM and VmRSS every 250 ms and sets
it to VmHWM - (VmRSS - parked) - 4 GiB, clamped to [4, 16] GiB. Above the
floor it holds only memory the run has already reached, so it cannot raise
the peak. RegenWindow::set_ahead resizes a window under its lock: a larger
one admits the depositors it held, a smaller one holds the next deposits.
The knob still fixes the window. Regeneration stays off by default.
An oscillating adaptive window is a finding even when phase B never waits:
the BLOCK REGEN window pacer line now says how many times the pacer grew
and shrank the window.
…own churn

At p90 with no disk the adaptive window shrank 109 times and grew 28 in
455 readings (RYZEN 057). Its reading, VmHWM - (VmRSS - parked) - 4 GiB,
churns with every chunk deposited or taken, whose pages the allocator
keeps, and drifts as phase B's resident set grows.

The pacer now takes the reading rounded down to whole GiB, and only once
the reading is a GiB or more away from the window either way. The window
stays under a step above the reading, so the run's peak less 3 GiB is
still never passed, and churn inside a step moves nothing.
…lock

alloc_purge.rs is #1013's at 07931f4, byte for byte: LAMBDA_VM_ALLOC_WATERMARK
(auto by default) runs one monitor thread for the block. Every 0.5 s it reads
VmRSS, and once the block's memory is short, VmRSS has reached 90 % of the
host target and the allocator holds at least 4 GiB of freed pages, it purges
every arena, at most once per 5 s. Under off it only samples, for the ALLOC
PEAK lines. prove_whir_block_tree starts it against the block's spill target
and prints its end line after W3 RECURSION. A host with room never fires it
(#1013: RYZEN 053, 0 fires at 1x, the median and p90 on 128 GiB).
…he's live bytes

The W3 driver prints one ALLOC PEAK line for the base and for each tree
level at its last proof: the watermark monitor's highest-VmRSS reading of
the window, with jemalloc's allocated and resident bytes at it, and the
precomputed-tree cache's live node bytes (a new reading in the stark
crate: inserts add, the cap's evictions remove), as #1013's block tree
does (507e098). Measurement only.
…host target

program_budget.rs is #1013's at 07931f4, byte for byte: auto admits a
tree program by room only while VmRSS plus the programs not yet emitted
stays under a share of the target (ROOM_SHARE 0.75; knob
LAMBDA_VM_TREE_PROGRAM_BUDGET_SHARE, 1.0 is the line at the target, the
budget before). Above the line, programs come in by the AHEAD rule alone,
so what the allocator's watermark (at 0.9 of the target) returns is not
spent again on programs held ahead (#1013: RYZEN 050 against 053). The W3
driver passes the share with its spill target; a nonsense share is an error.
… block)

LFM_PRECOMPUTED_TREE_CACHE_CAP, when the environment leaves it unset, is
set by the CLI's posture from the host target (block_whir's spill target):
64 entries (about 6.2 GiB once full) from a 64 GiB target up, 16 below it,
as #1013's CLI does (49c4bb5). A miss only rebuilds a tree whose root is
the key, so nothing moves in a proof. The BLOCK POSTURE line compares the
knob against the same rule, and a note says when the posture set 16.
…second after a fire that returned little

The trigger read jemalloc's resident − allocated, which also counts the
free space inside active pages that a purge cannot return. At the median
on a 32 GiB emulated host a fire read that gap at 4 GiB or more and
returned nothing (VmRSS 25.52 → 25.50 GiB), and its 5 s spacing then held
the next fire past level 1's peak (29.16 GiB, RYZEN 058b).

The trigger now reads resident − active (dirty pages and metadata). A fire
that returned at least 1 GiB starts the 5 s spacing; one that returned
less lets the next come no sooner than 1 s later, so purges that find
little cannot spin (each takes every arena's lock). The fire line and the
ALLOC PEAK lines print jemalloc's active bytes and what was purgeable.
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