Skip to content

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

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

MauroToscano wants to merge 1860 commits into
mainfrom
noepoch/whir

Conversation

@MauroToscano

@MauroToscano MauroToscano commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

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

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

What it is

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

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

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

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

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

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

1. The group partition is in the statement

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

Verifier. The verifier checks:

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

It then rebuilds every group's stack layout itself.

Security. Proven bits are unchanged.

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

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

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

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

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

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

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

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

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

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

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

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

3. ECDAS split; table heights capped

What.

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

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

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

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

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

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

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

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

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

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

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

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

Measured.

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

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

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

What.

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

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

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

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

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

What.

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

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

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

    The statement absorbs Q and the grind bits.

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

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

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

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

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

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

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

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

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

Gates.

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

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

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

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

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

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

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

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

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

Next:

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

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

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

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

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

Narrow trace storage (M4, lane i-m4)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Prover-only; the bytes are the same:

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

What changes:

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

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

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

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

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

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

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

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

@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Benchmark Results for modified programs 🚀

Command Mean [ms] Min [ms] Max [ms] Relative
head ecsm 3.0 ± 0.1 2.9 3.1 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head hashmap 107.7 ± 2.8 104.8 113.1 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head keccak 123.7 ± 2.3 121.1 127.7 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
head syscall_commit 88.4 ± 0.6 87.8 89.4 1.00

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Benchmark Results for unmodified programs 🚀

Command Mean [ms] Min [ms] Max [ms] Relative
base binary_search 39.0 ± 0.6 38.3 40.2 1.00
head binary_search 40.6 ± 0.8 39.6 42.0 1.04 ± 0.03
Command Mean [ms] Min [ms] Max [ms] Relative
base bitwise_ops 41.0 ± 1.0 39.6 42.2 1.02 ± 0.03
head bitwise_ops 40.3 ± 0.8 39.1 41.5 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
base fibonacci_26 42.8 ± 0.4 42.4 43.3 1.00
head fibonacci_26 44.9 ± 0.6 44.1 45.9 1.05 ± 0.02
Command Mean [ms] Min [ms] Max [ms] Relative
base matrix_multiply 39.5 ± 0.5 38.8 40.0 1.00
head matrix_multiply 41.5 ± 0.9 40.6 43.4 1.05 ± 0.03
Command Mean [ms] Min [ms] Max [ms] Relative
base modular_exp 41.2 ± 0.6 39.9 42.1 1.01 ± 0.02
head modular_exp 40.7 ± 0.6 39.9 41.7 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
base quicksort 44.6 ± 0.2 44.4 44.9 1.00 ± 0.02
head quicksort 44.6 ± 0.8 43.4 45.7 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
base sieve 44.8 ± 0.9 43.1 45.8 1.00
head sieve 47.2 ± 0.8 46.3 48.6 1.05 ± 0.03
Command Mean [ms] Min [ms] Max [ms] Relative
base sum_array 54.8 ± 0.4 54.0 55.3 1.16 ± 0.02
head sum_array 47.1 ± 0.7 46.1 48.0 1.00

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

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

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

Cherry-picked from mem/m1 74e1df1 onto #1014, with the streamed finish's build marks kept.
MauroToscano added a commit that referenced this pull request Oct 3, 2026
MauroToscano added a commit that referenced this pull request Oct 3, 2026
…es are laid out

#1014 now lays the streamed chunks out on three workers (0498480), and phase
A waits on the groups the rest closes, which close only once the whole rest
is laid out. With b1's waves the AIR order's prefix is laid out first, so
pack_rest_as_laid_out (the rest's sink, groups identical by test) is now on in
production: each wave's tables go to the packer as the wave ends, and a group
closes once its own tables are laid out. Laid out all at once, the first table
in AIR order was ready only with the last, which is why packing as laid out
read +0.21 s before (FAST 422).

The real-block tests' BLOCK_WHIR_PACK_REST=0|1 now overrides the production
choice instead of replacing it with off.

Readouts for the finish's place in phase A: each group's commit start
(from@, beside done@), and every rest table's layout end and group in AIR order
(BLOCK REST TABLES).
MauroToscano added a commit that referenced this pull request Oct 3, 2026
MauroToscano added a commit that referenced this pull request Oct 3, 2026
MauroToscano added a commit that referenced this pull request Oct 3, 2026
MauroToscano added a commit that referenced this pull request Oct 3, 2026
MauroToscano added a commit that referenced this pull request Oct 3, 2026
…s its tables, the block memory log) into nw2/r2ii

No conflicts. multilinear_block.rs: the memory log's phase-B marks and the
group observer sit side by side at the end of each group (the observer first,
then the group's columns are released and the mark taken). block_whir.rs and
the W3 harness: the new BlockOptions fields and the memory log's plumbing
next to the group observer's.
MauroToscano added a commit that referenced this pull request Oct 3, 2026
… proof to an observer; the block tree's first leaf executes during phase B) into m4b/b2

No conflict. R2-ii's two full BlockOptions literals in lfm/whir_block_tests.rs
already carry b2's pack_finished: true, so its hand-over tests run with the
finish's tables packed, as production does.
MauroToscano added a commit that referenced this pull request Oct 3, 2026
…14fea1

crypto/stark/src/narrow.rs and crypto/stark/src/spill.rs are #1013's files,
byte-identical to mem/stark-f1 @ e5214fea1 (i-mem3's F1: the page-backed
Bytes, the streaming Digester, the fused writer, over the S1/S2 store). #1013
is their one source of truth: #1014 never edits them, re-imports any change
from there, and they merge as one when both PRs reach main.

Wiring only: the two modules in lib.rs (dead code allowed there, since some
parts serve #1013's prover alone), math's page-bytes feature, and libc
unconditional as the store needs it (it was behind disk-spill). #1013's prover
glue (TraceTable::spill_main) and its spill tests are not imported; #1014's
adapter and tests follow.
MauroToscano added a commit that referenced this pull request Oct 3, 2026
#1013 landed 4cb0a07 (the writer's digest / copy / pwrite step timings on
SpillStats, SpilledMain::is_resident). crypto/stark/src/spill.rs is again
byte-identical to noepoch/stark @ edddc68; narrow.rs was already. Readouts
only: nothing #1014 calls changed.
MauroToscano added a commit that referenced this pull request Oct 3, 2026
… reserve

LAMBDA_VM_BLOCK_SPILL now reads as #1013's (prover/src/block.rs SpillPolicy …
spill_decision @ edddc68): auto (and unset) | off | always | <GiB> resident
budget. auto spills a committed table once the host's bytes (the larger of
VmHWM and the cgroup's memory.current), the reserve and the table would pass
the target (LAMBDA_VM_BLOCK_SPILL_TARGET_GIB, else the cgroup's memory.max
less 10 GiB, else MemTotal less 10 GiB). Phase A decides per table and never
past the writers' queue; groups 0 and 1 count but never spill.

The reserve is #1014's own, inferred from the median block (BIG 565): the
finish's p5 transient (25.5 GiB) and the tree's leaf programs alive through
phase B (16.3 GiB), about 1.25 GiB per total G cells or 1.65 per G cells
committed so far, plus 6 GiB for phase B's bump and the read-back window.

BlockSpill carries the policy's choice (SpillWanted) and the kept bytes and
committed cells it reads. A block that fits spills nothing; the store opens
for every policy but off. Tests: the knob and the decision (unit), a budget
the block fits in spills nothing and one of zero spills (block).
MauroToscano added a commit that referenced this pull request Oct 3, 2026
…@ 278e6a8

The rule #1014 copies from #1013 read only cgroup v2's memory.max and
memory.current. On a v1 host (FAST) the target fell through to MemTotal
less 10 GiB, and the host's charge was not read at all. #1013 fixed both
in 278e6a8; this re-copies spill_target_bytes, spill_target_from,
cgroup_memory and host_bytes_now from that sha, with its two tests (the
cgroup files from fake trees, and the target rule), cited as before.

The only changes from #1013's text are the citations and the tests'
temporary directory, which gets a name of its own so the two copies of
the test never share one if they ever run in one process.
MauroToscano added a commit that referenced this pull request Oct 3, 2026
…cf253d2)

The host measure #1014 copies from #1013 took max(VmHWM, the cgroup's
charge), and the charge counts page cache. FAST charged 40.58 GB at 17:28Z
with 15.18 GB of it inactive file pages, which would have made auto spill
the last groups of a 1x block that fits.

This re-copies #1013's measure from its landed head 035aef5 (commit
cf253d2): HostReading reads VmHWM, the charge and memory.stat's inactive
file pages (v2 inactive_file, v1 total_inactive_file, by their exact key),
and the host's bytes are max(VmHWM, charge - inactive_file). CgroupValue
lets cgroup_memory read a whole-file number or one memory.stat key. The
BLOCK SPILL line ends with the largest reading auto decided on, as #1013's
does. With #1013's two tests; the text differs only in citations and the
test's temporary directory. The rest of #1013's train (lazy queue budgets,
the allocator purge) is not copied: #1014 has no queue budgets.
MauroToscano added a commit that referenced this pull request Oct 3, 2026
#1013 added one test-hook field (ProveOverrides::tight_claims, behind
cfg(test)/test-utils) with its tight resident claims (1cc3a70, 00871b9).
#1014 keeps the store byte-identical to #1013's: git diff 035aef5 --
crypto/stark/src/{narrow,spill}.rs is empty. Nothing in #1014 reads the
field.
MauroToscano added a commit that referenced this pull request Oct 4, 2026
MauroToscano added a commit that referenced this pull request Oct 4, 2026
The spill code (stark, multilinear, block_whir) and the streamed top (executor, proof, the W3 tree) touch disjoint code; the one shared file, whir_block_tests.rs, merges without conflict (the spill side adds BlockSpillPolicy::Off to two test fixtures).
MauroToscano added a commit that referenced this pull request Oct 5, 2026
…2/stream

The p4 port touches only prover/src/tables/trace_builder.rs; the streamed top and its readiness gate touch the executor, proof.rs and the W3 harness. No file in common, no conflict.
MauroToscano added a commit that referenced this pull request Oct 5, 2026
MauroToscano added a commit that referenced this pull request Oct 5, 2026
…114; min proven bits unchanged at 130.393)

The default flip of LAMBDA_VM_ZF_WHIR_GRIND_BITS: the port of #1010's
5364aa2 and its pin follow-up 2687e48. PRODUCTION_WHIR_GRIND_BITS is 18;
LEGACY_WHIR_GRIND_BITS = 20 keeps the legacy format and is the opt-out
(LAMBDA_VM_ZF_WHIR_GRIND_BITS=20). It reaches #1014's WHIR proofs: the
block's group chains and prepared openings, verify_block_whir, and the W3
leaves' in-guest verifier. The W3 tree's own STARK LFM proofs do not read it.
The binding WHIR phase is unchanged (the unground fold at 27 variables,
130.393 bits); the query phases move 130.926 -> 130.907.

Measured behind the knob on c55dec4: ULTRA 019 1x phase B -0.344 s
(t -12.9), whole -0.270 s, recursion -0.010 s; BIG 628 median whole
-3.42 s, both arms verified. The opt-out reproduces the previous 1x proof
byte for byte (top digest 0da6fea9 under fixed hash keys and deterministic
grind); the new default's is b9b4826d.

The W-leg sizing pins follow the process's grind bits, as 2687e48 did on
#1010.
MauroToscano added a commit that referenced this pull request Oct 6, 2026
…all, commit pairs off the pool)

ULTRA 096 (median 25475471, #1014 + cap 256, one binary, 3 + 3 runs plus
2 + 2 single-lever arms) read LEVER EFFECTIVE on every pre-registered row:
phase-A end -6.66 s with every run of the pair below every run without it,
whole block -6.86 s (154.67 -> 147.81 s), finish 15.8 -> 9.3 s at most,
KECCAK_RND's span 12.0 -> 5.4 s at most, the largest phase-A commit
2.93 -> 0.68 s, the card's wait for the rest 6.65 -> 1.77 s, max RSS
-0.97 GiB. The wave alone gave -5.92 s whole, the commit pairs alone
-0.47 s. At 1x (ULTRA 095) the pair is neutral (whole -0.016 s, t -0.4) and
the top digest is unchanged.

Both become the defaults; the knobs stay as opt-outs:
LAMBDA_VM_KR_PACKED_WAVE=4 builds the chunks four at a time again, and
LAMBDA_VM_BLOCK_COMMIT_OFF_POOL=0 puts the commit pairs back on the global
pool. The proof bytes are the same either way.
MauroToscano added a commit that referenced this pull request Oct 6, 2026
…W_WINDOW=1)

A #1014 block leaf's LFM_BALU is 70,692 rows a full group, so a 4-group
leaf holds 282,768 rows and pads to 2^19. 34,200 of a group's rows are
the lean fold's point x0^-1 = prod g^(-2^i b_i): one Select and one Mul a
bit through edsl::pow_bits.

pow_bits_windowed drops the identity multiply (the first factor read is
the accumulator) and reads each of the first two bit pairs through a
4-way Select mux over the constants {1, f0, f1, f0 f1}. A fold then pays
three fewer Muls and two more Selects at four or more index bits, two
and one below. A full group goes -6,498 LFM_BALU and +4,104 LFM_SELECT,
so a 4-group leaf's LFM_BALU drops to 256,776 (under 2^18) while its
LFM_SELECT stays under 2^19 at 480,168.

The value is the same and so is the Select semantics. The knob is read
at program emission like LAMBDA_VM_WHIR_FOLD_CLASSIC and defaults off,
so today's programs and ids are unchanged. pow_bits itself is untouched,
so the STARK paths keep their programs. The lean fold's row and constant
closed forms follow the setting.
MauroToscano added a commit that referenced this pull request Oct 10, 2026
…E_DOWNLOAD (default off)

The port to #1014 of #1013's 6d25865: the same changed lines in
device.rs, lde.rs, stark's gpu_lde.rs and the new device test, applied to
functions that were identical on both PRs.

On a precomputed-tree cache miss the preprocessed commit downloads the full
node buffer into a zeroed pageable Vec<u8> and stark copies it once more
into Vec<[u8; 32]>. #1014's median recursion pulls about 14.7 GB this way
at 2.75 GB/s (I-PROF4 B.5.1). LAMBDA_VM_TREE_DOWNLOAD=staged sends it
through a staging pair straight into the node vector and queues the
multiplicity tree behind the last chunk; the device peak stays one tree
and the nodes are the same bytes.
MauroToscano added a commit that referenced this pull request Oct 10, 2026
The port to #1014 of #1013's 814a4b6. LAMBDA_VM_TREE_DOWNLOAD unset
(or any unknown value) now selects the staged download; `legacy` or
`pageable` keeps the old pageable copy as an opt-out. The nodes are the
same bytes on both paths. A unit test pins the default.

On #1013, legacy vs staged on the median 25475471, one binary per job:
recursion -2.94 s (t -15.9, ULTRA 094) and -6.26 s (t -27.6, RYZEN 002),
with the base digest and the top program id unchanged (ULTRA 093, 100).
#1014's own gate is I-COPIES §10.
a_block_with_keccak256_calls_across_keccak_tables_proves_and_verifies proves
a block whose KECCAK tables are cut to a few rows, as
keccak_and_ecsm_split_into_chunks_prove_and_verify does in 324 s on a GitHub
runner, past the list's 42 s line (21 s on a 9950X box). It still runs in
the full reproduction on the GPU machines and locally.
Each leaf emits the constraint leg of the AIRs it verifies, so KECCAK's sponge
mode and its round-chip tokens on the mode bits move the first and last leaf
programs under both query checks (the leaf count, 5, does not move). The test
now measures and prints every pinned value before comparing any, so one run
shows all of them when the leaves move.
…y test

The box test proves and verifies each load-bearing forgery with its check
left out, but the check was left out only on the threads of a rayon pool the
test built, and the prover proves each table on plain scoped threads: the
prover kept the check while the verifier dropped it, so every forgery read
as refused either way. The test now leaves the checks out
process-wide through a test-only switch (a guard puts every check back when
it drops, also on a panic), and refuses to run unless its runner says it is
alone in the process (LAMBDA_VM_KECCAK256_BOX_ALONE=1, --exact
--test-threads=1), since it loosens any other KECCAK proof made meanwhile.
Outside the tests nothing is ever left out.
The longest bus message of any block AIR is now KECCAK's link between two
rows of one Keccak-256 call: the call's timestamp, the block index, the bytes
left, the next block's address (two words), the digest address (two words)
and the 200-byte state, 207 values and the bus id, 208 against the round
chip's 204. The census reads (1475, 208); the block-wide LogUp bound at the
group maximum moves 136.79 -> 136.77 bits at 1024 groups and 140.79 -> 140.77
at 64 (log2(209/205) = 0.03 bits).
test_utils keeps one prototype of each AIR a process builds, keyed by name
and proof options, and serves clones of it afterwards. In the box test the
first KECCAK AIR was built before any check was left out, so the three checks
that live in a bus interaction (the digest address on the link, lane 0's
memory read, the round-chip tokens on the mode bits) stayed in the AIR the
prover and verifier used, while the walk left them out: the KeccakSponge,
Memw and Keccak buses then refused those forgeries, not the
Memory bus. With every check left out in the traces, every bus, Memory and
Commit included, balances on the laptop for all three.

Each prove now takes a query count no earlier prove in the process used, so
its AIRs are built under the checks it leaves out, and the test asserts that
the KECCAK AIRs it proves with carry exactly the interactions those checks
make.
ECALL -12 (u64::MAX - 11) recovers the secp256k1 key of a signature in
one call: x10 = output (Q.x, Q.y big-endian, then a status doubleword),
x11 = r || s (big-endian), x12 = the message hash (big-endian), x13 =
the parity of R's y. Status 0 carries the key u1*G + u2*R with
u1 = -z/r and u2 = s/r mod N. Status 1 means the call bailed: the key
bytes are zero and the caller recovers the key itself. The executor
bails exactly when the chips cannot represent the answer: r or s
outside [1, N), r not the x of a curve point, u1 = 0, or
x(u1*G) = x(u2*R). It errors on a parity above 1, on overlapping
signature and hash regions, and on an address range past its low limb.

crypto/ecsm gains the reference (k256), the witness the chip will
prove (the three products mod N, R's curve relations and parity, both
chains and the final addition row), and builders for a chain on a full
point and for one addition row. The ECSM witness now builds from a
point either root of y, which the second chain needs.

Tests: k256-signed signatures recover their key on both parities, every
bail class bails in the reference and the witness, a hash at or above N
is reduced as the recovery reduces it, the chains have one row per step;
the executor's errors.
One row per call (1,543 columns, 981 constraints, degree 3). Under OK
(the row computed the key; MU - OK is the bail status it writes) it
proves 0 < r, s < N; w = 1/r, u1 = -z*w and u2 = s*w by byte
convolutions mod N, u1 and u2 as bits below N and nonzero; R = (r, yR)
on the curve with yR < p and yR[0] = 2*H + PAR, PAR the x13 value; the
chains A = u1*G and B = u2*R through ECDAS, keyed at TS and TS + 1; the
final addition Q = A + B as one ECDAS addition row keyed at TS + 2,
with xA, xB < p and xA != xB (an inverse witness over the eight word
differences); Q < p. A bail writes a zero key. ECDAS is unchanged. Every
ECALL timestamp is a multiple of 4, so the three keys never meet
another call's; the second chain's key is what keeps the two chains'
Bit messages apart.

The table is a counted chip like HINT: TableCounts gains `ecrec` (last
field), so the statement tags move to V7, the recursion input to
version 7 and the LFM statement replay to 23 counts. The walk, both
finishes, the lean pass, VmAirs and the AIR registry carry it.

Tests: constraints hold on computed, bailed and padding rows; the other
root of R, a wrong inverse or scalar bit, r = N, coordinates at p,
xA = xB and a nonzero key on a bail are each refused at the constraint
level; the counted BITWISE lookups are the table's sends; the collector
keys the ECDAS rows at T, T+1, T+2; prove tests on the new test_ecrec
program (two keys and a bail), a forged key and a forged bail refused;
block tests where a chain row borrows a neighbouring call's or chain's
key, and where the two chains run with swapped scalars (every
constraint holds, only the TS + 1 key refuses it). The spec gains an
ECREC chapter.
The recovery calls ECREC with the signature and hash in place
(big-endian, as the call takes them) and the recovery id's low bit, and
returns the key when the status is 0. On a bail it runs the recovery
as before (hinted decompression, the ECSM path, k256), which decides
valid and invalid exactly as it did: a bail can only cost cycles.

Median block: 330 calls, none bailed; ECSM and hint calls 1,320 and
990 -> 0; 130,579,100 -> 126,834,376 instructions; public outputs
equal on all 25 blocks.
The Keccak-256 and ECREC changes each added their assembly program to the
finish, lean-pass and parallel-walk suites and bumped the list lengths by
one from the same base; together each list holds one more.
…CPU stride

The bounds s < N, u1 < N, u2 < N, xA < p and xB < p had no test of their
own: removing the first three left every ECREC row test green. Each new test
builds a row that is consistent except for one out-of-range value (the
relation's quotient and carries recomputed for it) and requires that bound's
closing carry, and nothing else, to refuse it. Without u1 < N or u2 < N a
chain could end in G + G through ECDAS's chord branch, where lambda is free;
without s < N a signature Ethereum refuses would recover a key.

The second chain and the final addition are keyed at TS + 1 and TS + 2, which
is sound only while both offsets stay below the CPU timestamp stride; a
compile-time check now ties them to it. A prove test shows the two calls'
final additions cannot swap results (refused by the bus alone).
The executor stops the run (no proof) when the signature and hash overlap or
an operand crosses a 2^32 address-word boundary, and the guest wrapper passed
the caller's pointers unchecked. ecrecover_fits now checks the executor's own
conditions first, and the wrapper recovers the key in software when they fail,
so the call can never make a block unprovable. The status doubleword starts
nonzero, so a call that wrote nothing would read as a bail rather than as the
key (0, 0). A stale comment said recid 2 or 3 made decompression fail; the code
reads only the low bit, so they behave as 0 and 1.
The argument that ECREC's keys T, T + 1, T + 2 never meet another call's rests
on every ECALL timestamp being a multiple of 4: the preprocessed column gives
4(i + 1), and each CPU table's base 4 S_k gives the rest. The spec named only
the first half.
ecrec_chains_are_keyed_apart_on_the_bus and
ecrec_chains_swapped_between_keys_are_refused prove test_ecrec as blocks
whose ECDAS tables are cut to a few rows, as the 324 s
keccak_and_ecsm_split_into_chunks_prove_and_verify does (about 100 s for the
two and two others on a 9950X box). They still run in the full reproduction
on the GPU machines and locally.
The ECREC table's count joined the continuation-epoch statement the
StatementReplayV0 program replays (statement V7, 8 bytes more), so its
roots[0], roots[7] and program_id moved. The entry is compute_lfm_registry's
output at the ECREC cut, pasted unchanged; the program's heights, chunk count,
hasher and chip set, and the other five registered programs, came back
identical.
The ECREC table joins the AIRs a leaf verifies and the epoch statement gains
its count, so the first and last leaf programs move again under both query
checks (the leaf count, 5, does not move).
The ECREC table's count joined the continuation-epoch statement (23 counts),
eight bytes more, so the statement before the public output is 280 bytes,
not 272; 280 is a multiple of four too, so no shift moves.
The census of the longest message and the most fractions a row is meant to
cover every AIR a block may carry; it left ECREC out. With it the census is
unchanged, (1475, 208): ECREC's messages are shorter than KECCAK's sponge
link and it carries fewer interactions than BLAKE3.
The notes under thoughts/shared/, thoughts/blake3/ and others/, and the
RESUME.md, RESUME-MMCS.md and HASH-PINNING.md files at the root, are design
history, hand-off notes, and the one-off scripts and oracles that produced
constants now committed as code. No build, CI job, test or script uses
them; they stay reachable in the history of this branch.

The one part of thoughts/blake3/ that CI runs is moved, not dropped: the
round-parameterised upstream BLAKE3 C (reference-impl/) and the three oracle
files it checks against (blake3_ref.py and the two vector files) now live in
tooling/blake3-second-source/. The `test-blake3-second-source` target, the
formal-verification reference test and the comments that cite these files
point at the new location; the CI step calls the make target, so the
workflow is unchanged.
bench_vs/lambda/blake3-absorb-bench/ is a hand-run in-guest cycle harness
and scripts/gen_blake3_bench.sh generates a BLAKE3-saturated guest for a
one-off bench. No make target, CI job, test or script uses either.
Removes tests that measure, time or print rather than guard the prover.
No CI list, named test list or benchmark or gate script names any of them;
most are #[ignore]d and only ever ran by hand.

- Eleven whole probe and bench files: the tenant-width census
  (per_table_census_tests), the Poseidon1 width-16 chain census
  (p1w16_census_tests), the Poseidon1 tree-node census
  (p1_node_census_tests), the round-1 LDE-rebuild probe (c5_probe), the
  trace-digest printer, the WHIR identity-line printer (whir_byte_gate),
  the RPX grind bench, counted grind and occupancy sweep, and the
  math-cuda LDE-engine and upload benches. The few non-ignored tests
  among them check the censuses' own premises and arithmetic; the
  emitters they read are pinned by their own tests.
- Ignored timers, censuses and production-scale measurement runs in the
  per-table aggregator, WHIR chain, WHIR global, WHIR one-chain, wrap,
  multilinear bench, transcript count, argue stage-1, lean program census,
  BLAKE3 probe, RPO/RPX throughput, RPX chip, program census, Poseidon1
  width-16 host/chip/socket/device, decode, decode residency, statement
  alignment, shape profile, one-LDE-buffer, sumcheck, table transpose, WHIR
  encode and RPX device-path tests.
- Three non-ignored tests that assert nothing and only print a cost
  (deep_leg_cost, the batched-leg comparison, the bus-leg shape).
- The helpers, constants, imports and emptied test modules only those tests
  used; one import that only hash-metrics code still needs is now gated on
  that feature.

76 tests in all. Kept on purpose: two probes in files the VM-call change
edits (machine_tests, whir_p1_census_tests); the WHIR production tree, the
node-emission timing split and the real block's page census, whose removal
would force edits to a live test harness or to test-only code in library
files; and every probe a current benchmark or gate script still runs.
…over

No CLI mode reaches either: prove-block runs the no-epoch block prover and
its STARK-proved LFM tree. Removed:

- the multilinear continuation (the epoch WHIR base and the cross-epoch
  global proof) and its recursion programs: whir_global, whir_real_epoch,
  whir_real_global, block_root, global_parent, global_split,
  whir_statement, and the epoch-program items of whir_epoch;
- the LFM prover over stacked WHIR: whir_proof, whir_leg, whir_wide and
  lfm_prover_knob (LAMBDA_VM_LFM_PROVER, _LFM_ROOT_PROVER, _LFM_WIDE), the
  batched-argue measurement knob only the epoch base read, and a
  device-entry counter only its card test read;
- their tests, the continuation transcript-pin bench, and the epoch-tree
  drivers in per_table_aggregator_tests. The child harvest and the node
  tests the block tree's tests share stay.

whir_epoch keeps the table and group walks the block leaves emit.
decode_table_index moves into block_whir with its test. The statement
alignment tests run on the monolithic statement now, and the argue
stage-1 and batched-argue tests keep the VM's tables. Twenty lines leave
.config/ci-slow-tests.txt with their tests.
Nothing selects either: the block recursion proves under Poseidon1 at
width 16, the legacy LFM proofs under RPX, and the BLAKE3 arm stays for
the BLAKE3 experiments. Removed:

- HasherKind::Poseidon and HasherKind::Rpo, their chip layouts, constraint
  bodies and trace fillers, the Poseidon-original permutation, and the RPO
  chip's round witness;
- the RPO and Poseidon commitment configurations (RpoStarkHash,
  PoseidonStarkHash and their transcripts) and the matching
  CommitmentHash and DeviceHash variants with their device arms, all of
  which aborted or refused;
- rpo_chip_tests and poseidon_chip_tests.

The surviving HasherKind discriminants and program-identity tags keep
their values; the retired ones (1 and 3, tags 2 and 4) are never reused,
so no program id moves. The RPO permutation, its constants and its
known-answer vectors stay: RPX runs on its rounds and constants, and
those vectors are their external anchor. Tests that used RPO or Poseidon
as "another hasher" use RPX or BLAKE3 now; two lines leave
.config/ci-slow-tests.txt.
LFM_WHIR_PREFETCH=1 built the next table's fraction tree during the
current table's per-table argue. It never became the default, and off
was the serial path: every table's tree is built at its own turn, which
is what the per-table prove now does unconditionally. The deferred tree
it used stays, for the batched argue.
…, opt-in

The round-0 encode runs the Möbius transform as five passes over the
stack, after a memset and a device copy that assemble the stack from its
parts; every pass is bound by memory bandwidth. LAMBDA_VM_WHIR_MOBIUS_WIDE=1
takes the first 11 levels over contiguous tiles of 2^11 and the rest in
strided tiles of up to 8 levels: three passes. For a stack the card already
holds (a resident store or a buffer's slices) the first pass gathers it from
its parts, so the copy and the memset are not run. Every element gets the
same operations in the same level order, so the coefficients, the codeword
and its tree are the same words; by default nothing changes.

The card test compares the codeword under both transforms for host, resident
and sliced sources at 2^11 to 2^22 (mixed parts at unaligned offsets, gaps,
edge words) and for a committed whole stack and its root; an ignored bench
times the encode at the production 2^27 shape after checking its codeword.
…stack

At a 2^11 stack the ragged part (1,000 words at 2^10 + 2^8 + 17) ran past the
end, so today's host encode panicked inside its copy before any codeword was
compared. The ragged part now shrinks with the stack, the layout asserts it
fits with a named message, the encodes fail by name, and every comparison
prints its parity line before it asserts so the box read-out counts the
compares that ran. The timed bench now asserts the wide arm took the gather
and today's did not.
The round-0 encode's forward transform runs in the column engine's
ceil(log_n / 8) passes, four at the production 2^28 codeword, and every
pass is bound by memory bandwidth. LAMBDA_VM_WHIR_NTT_WIDE=1 plans the
encoding in ceil(log_n / 10) passes instead ([10, 9, 9] at 2^28).

The pass kernel now takes up to 12 levels: three register rounds with two
shared-memory exchanges, a radix table of 2^(k-1) roots built from the
windowed table, and a tile skewed every 16 rows so that a wide pass's
first-round half-warps do not share banks. Kernels of 9 and 10 levels are
instantiated, forward only. Passes of up to 8 levels keep today's table,
tile and operations, the trace LDEs keep today's plan, and by default
nothing changes.

The codeword's values are today's; only a value's non-canonical
representative may differ, which the hashes and the serialisation
canonicalise. The host KAT replays every pass of 4..12 levels against the
textbook levels, and the wide plan against the naive DFT and against
today's plan with edge words planted. The card test compares canonicalised
codewords at every size where the plans differ from 2^9 to 2^29, against
today's plan and the legacy encoding, and a committed stack's root; an
ignored bench times the encode at the production shape under four arms:
today, the wide Möbius transform, the wide NTT, both.
…NTT, opt-in

A first-pass block of the encoding's spread NTT stages 2^12 positions,
which at spacing 2^lb are 2^(12 - lb) consecutive coefficients: exactly
the low Möbius levels the wide transform's contiguous pass runs. The
Möbius levels act on distinct index bits and commute, so under
LAMBDA_VM_WHIR_MOBIUS_FUSED=1 the strided levels run first (the first of
them gathering a resident stack from its parts) and the low levels run on
the staged coefficients inside the NTT's first pass (F_MOBIUS, one level
a phase). That removes one read and one write of the whole stack.

Where it does not apply (the legacy encoding, a stack the first pass
holds whole) the encoding is the wide transform's or today's, and by
default nothing changes. The field values are today's; the subtractions
run in another order, so a word's representative may differ, which the
hashes and the serialisation canonicalise.

The host KAT checks the fused first pass against the whole transform on
the host followed by today's spread transform, in both NTT plans and at
spacings 1 to 16. The card test compares canonicalised codewords from
host, resident and sliced sources under both NTT plans, and a committed
stack's root; an ignored bench times the encode at the production shape
under five arms.
…cube of 4

Today every fused table hands its last five rounds (a cube of 32) to the
host, which walks the rule through the program interpreter once for every
index pair and node. For KECCAK_RND that host tail is about 7 ms a table,
the card idle meanwhile: one 30 ms stretch a KECCAK_RND group at the
median block. A card round costs about one walk whatever the cube.

`LAMBDA_VM_ZC_DEVICE_TAIL=1` (default off) keeps the rounds of a program of
at least 1000 lowered steps (KECCAK, KECCAK_RND, ECSM, ECDAS, ECREC) on the
card down to a cube of 4, so the host runs two rounds instead of five. The
messages are the same field values, so the proof is the same bytes. The
cross-check keeps today's crossover, since it replays today's rounds.

Under the split each batched argue prints its fused tables' card and host
rounds by program length (BATCHED ARGUE ZC TAIL), and a test proves the host
reference's bytes with the tail forced to a cube of 4 and of 2.
…M_FIRST_PROVE, off)

The tree's first prove pays three first-use costs on the tree's critical
path: its per-domain prove constants (LDE-size boundary-zerofier
inversions and their card copies) in its host preparation after the
leaves' artifacts are published, the DEEP's whole LDE coset uploaded
from pageable memory inside its card window, and the staging pairs'
pinned allocations inside the first build's and first prove's windows.

With LFM_WARM_FIRST_PROVE=1 the early leaf's thread makes the staging
pairs when it starts and, after its fill, warms the constants and the
DEEP cosets for its own tables from an AIR set built from shape-only
artifacts (every height, chunk and merge of the real ones, roots zero).
Every prove also warms its DEEP cosets before it takes the card. Each
warmed value is the one the prove caches for itself, keyed by domain,
so no proof byte moves. Off by default; every prove's host preparation
is printed in both arms.
…EEP_HOISTED, off)

deep_composition_ext3_fused_m* forms gamma*(value - ood) and multiplies by
the pole's inverse for every column and pole: two ext3 products a term, and
at the recursion's shapes it runs 2.4-4.5x a copy of the bytes it reads.
Summing per pole first, sum_j g_jk (t_j - o_jk) = sum_j g_jk t_j - C_k with
C_k = sum_j g_jk o_jk (and C_h for the composition parts) computed once on
the host, leaves one ext3-by-base product per main column and pole and one
multiply by each inverse a row. A micro-bench at the doubled leaf's XALU
(2^22 rows at blowup 4) read it 3.06x faster, at the copy's speed.

With the knob on, deep_composition_ext3_fused_keep launches
deep_composition_ext3_hoisted_m* (row stride 1, the R4 callers'; any other
stride keeps the fused kernel). The codeword is the same field element,
written canonical; the fused kernel's words are canonical but for a ~2^-32
chance a word, and nothing downstream reads a word's representation (the
commitment hashes canonicalise, as the fused kernel's own parity tests rely
on). Both arms print the setting and its launches (W3 DEEP HOISTED).

Off by default; off, nothing new runs.
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.

2 participants