STARK no-epoch (prove-and-retire) prover: block 25368371 — work in progress - #1013
Draft
MauroToscano wants to merge 1352 commits into
Draft
MauroToscano wants to merge 1352 commits into
MauroToscano wants to merge 1352 commits into
Conversation
…aces, with a mutation control `the_compiled_kernels_prove_the_same_bytes` proved add.elf three times through `prove_with_options`, and its control (two interpreter proofs) failed on the card: two builds of one program's traces differ. The LT, BRANCH, MUL, DVRM, EQ and BYTEWISE builders deduplicate through a std HashMap and lay rows out in its iteration order. On the laptop, two builds of add.elf's traces give different LT rows and different proof bytes. The test now builds the traces once and proves copies of them (`FixedTraces`, `prove_with_options`' prove step on a clone), at grinding 0: - the control: two interpreter proofs, byte-equal; - the compiled proof: byte-equal to them, verified, with compiled compositions counted; - the mutation control: BITWISE's kernel swapped for a mutant whose last root is off by one. The proof must change and fail to verify, or the prover must refuse it, and the mutant must have run. `one_set_of_traces_proves_the_same_bytes` runs the control alone on the path the build proves on (ignored: two proves at blowup 4). Supporting changes: - `Traces` derives Clone in this crate's tests only. - codegen: `mutant_composition_kernel` (the kernel plus one wrong statement) and `mutant_kernel_name`. - The generated source carries BITWISE's mutant last. It is not in the key table, so no program runs it. - gpu_interp: `substitute_compiled_kernel` and a call counter, compiled only under stark's `test-utils`, launch a named kernel in another's place. The 36 production kernels and the key table are unchanged.
LAMBDA_VM_GPU_COMPILED_CONSTRAINTS unset, empty or 1 now evaluates every composition that has a compiled kernel with it; 0 keeps the interpreter for every program. The banner names the setting either way. FAST job 260 (block 25368371, A B B A at 422c4b2): whole run 61.05 s with the interpreter, 60.25 s compiled (-0.80 s; base -0.70 s), identities equal in all four arms; the kernels run 2-3.8x faster than the interpreter per program at 2^20 rows. The proofs are byte-identical: the device parity test and the proof-bytes test with its mutation control passed on the card.
…easurement arm) ResidencyMode::RecomputeLdeDevice lets one monolithic proof cover a whole block on the card. Round 1 commits every table on the device and keeps only the root: the device LDE, the device tree and the trace snapshot are freed inside the admitted region, and no host LDE is downloaded. At the top of each table's fused task the trace is committed on the device again, with the same function and the same device-only gate as Retain's Round 1, and the prover refuses the proof with RecomputedCommitmentMismatch unless the new root (and the precomputed root) equals the absorbed one. From there the task is the Retain task, so the proof is byte-identical. Tables the device declines in Round 1 keep their host tree and recompute on the host, as RecomputeLde does. Retain and RecomputeLde behave as before. The PROVE SPLIT line gains a recommit[Σ] field only when a recommit ran. prover::block::prove_block executes the whole block (DECODE's root derived on the device beside the execution), builds every trace at uniform 2^21 caps and proves it with the monolithic statement under the new mode. The existing monolithic verifier checks the result. It prints the phase walls and a per-type instance census. Tests: the stark residency tests cover the new mode (byte identity, aux release; on the card: every table recommitted, and a perturbed trace refused for each table). The prover tests (ignored, GPU box) cover Retain against RecomputeLdeDevice byte identity from one build of the traces on add, test_keccak_multi and all_instructions_64 (2^5 caps), refusal of a moved trace on CPU[0] and BITWISE, and the block harness noepoch_block_prove_and_verify.
…ess's control arm
|
Benchmark Results for modified programs 🚀
|
…able commits on the device add's CPU table has 8 rows and commits on the host, so the device recommit never ran for it and the test's own guard failed (FAST2 210: "CPU[0]: the perturbation never fired"). At 160k cycles CPU[0] is 2^18 rows.
…both behind knobs LAMBDA_VM_GATE_PACKING=1: run_admitted's drivers claim the first table in walk order whose estimate fits beside what is admitted, instead of taking the next table and blocking on it (which blocks every driver behind it while a smaller table further down the walk would fit). An empty gate admits anything, as before. Off by default: the walk-order admission is unchanged. LAMBDA_VM_TABLE_TIMELINE=1: one TABLE TL line per table per admitted phase (claimed, admitted, finished; unix seconds, the PROVE SPLIT clock) and the bytes it was admitted for. Off by default. For the no-epoch block, whose fused phase ran at 2.2 tables of parallelism against the epoch path's 3.8 (FAST 351).
|
Benchmark Results for unmodified programs 🚀
|
…erifier, build stamps MaxRowsConfig gains keccak_rnd, the rows per KECCAK_RND table. Every existing constructor sets KECCAK_RND_UNCHUNKED (one table, today's shape), so existing proofs do not change. The block driver sets 2^16 (knob LAMBDA_VM_BLOCK_KECCAK_RND_LOG2: off | 5..=26). A chunk holds whole permutations (24 rows each). KECCAK_RND's constraints read only the current row, and its rounds chain through the bus. TableCounts::validate_for(AcceleratorShape) lifts the one-table bound for KECCAK_RND under AcceleratorShape::KeccakRndChunked only. validate() and every existing verifier keep Single. block::verify_block is the one verifier using the chunked shape; the shape is the verifier's constant, never read from the proof. Why chunking matters for speed as well as soundness: on FAST 352 the single 2^18-row KECCAK_RND (30.3 GiB estimate, over the 23.4 GiB gate) ran alone for 6.1 s at the head of the fused phase. Also: - the block driver builds the traces step by step (DECODE artifacts and the initial image beside the execution) and prints collect vs generate; - the recommit fault hook perturbs the last column (a committed one on every table; a preprocessed table's leading columns come from the cached tree), which is why FAST 352's gate saw BITWISE proved; - tests: the shape rule (host), and a chunked KECCAK_RND proving and verifying under the block verifier and refused by the single shape (box).
…ocessed commitments beside Round 1 stark: IsStarkProver::precommit_main makes exactly the Round-1 main commit that multi_prove would make for one table (the same domain, leaf layout, device-only gate and residency). multi_prove_precommitted takes such commits in place of its own and absorbs every root in AIR order as before; a precommitted table costs nothing at the gate. multi_prove is multi_prove_precommitted with none. The R1 per-table body is now r1_commit_table, shared by both. prover: before Round 1 the block driver derives every preprocessed table's commitment for its leaf layout on a helper thread, running beside Round 1. On FAST 353 the 46 PAGE tables sat at the tail of Round 1 at about 1.7 s each; most of their commitments are derived on the host on first use. LAMBDA_VM_BLOCK_WARM_PRECOMPUTED=0 turns it off (the A arm).
…uring the block's execution FAST 354: warming the preprocessed commitments beside Round 1 shortened Round 1 by 1.3 s and lengthened its prepass by as much, because the host derivation competed with the prove's own host work (MECHANISM-ONLY). The only costly ones are the ELF data pages' roots: zero-init and private pages have static roots. page::data_page_commitment returns a root recorded for the same INIT column, options and layout (page::record_data_page_commitment), and recomputes on the host otherwise. The data-page lazy commitments now go through it. The block driver derives both layouts' roots for every ELF data page with commit_group_device_or_host_with while the executor runs and the card is idle. The verifier recomputes every root, so a wrong record gives a rejected proof, never an accepted one. The beside-Round-1 warm is removed. Tests: the record rule (host), and device page roots equal to the host's for all_instructions_64 and the ethrex guest (box).
…d each CPU instance committed as its window arrives The executor runs on its own thread in windows of one CPU instance (max_rows.cpu cycles). A WindowedCollector walks each window as it arrives, carrying the memory and register state. The walk (collect_ops_from_cpu) now appends to a WalkOutputs accumulator; the one-call wrapper is unchanged, so its outputs are the same. For each window, one of two builder threads generates that CPU instance's trace from the window's logs (generate_cpu_window, with the run's timestamps) and makes its Round-1 commit on the device with precommit_main. All of this happens while the run is still being executed and collected. The other tables are built after the last window as before; the build leaves Traces::cpus to the caller (CollectedOps::cpu_generated_outside). prove_block_traces hands the CPU precommits to multi_prove_precommitted by AIR name. LAMBDA_VM_BLOCK_STREAM=0 is the serial producer (the A arm). Test (host): the windowed build equals Traces::from_elf_and_logs for all_instructions_64 and test_keccak_multi at 2^5 caps and fib_iterative_160k at 2^14. Tables laid out in op order are compared chunk by chunk; the six HashMap-ordered ones by row multiset.
… LDE, not the hash LAMBDA_VM_RECOMMIT_TOP_LEVELS=k (off by default). Under RecomputeLdeDevice, a plain table's Round 1 copies its device tree's levels from the root down to depth - k off the device (math_cuda::lde::download_tree_prefix, about 1/2^k of the tree) before freeing it, into TableCommit::top_tree. Its fused task then recomputes the device LDE alone (coset_lde_row_major_keep_no_tree, the keep path's LDE and trace snapshot without leaf hashing or tree; its D2H/transpose tail is now shared with the keep path as drain_and_transpose). The main-trace openings are rebuilt in top_tree_proofs: each queried 2^k-leaf subtree is hashed from the device LDE's rows with the host twins of the device kernels, its root is checked against the kept node (RecomputedCommitmentMismatch otherwise), and the path is the in-subtree siblings followed by the kept levels'. The Merkle caps come from the kept levels. Preprocessed tables, and any table whose LDE recompute the device declines, take the full recommit. Tests (box, threshold 2, knob 2): byte identity with Retain plus verification; a trace that moved is refused.
…ne line for each recommit top_tree_proofs rebuilds each queried subtree on the rayon pool. Under LAMBDA_VM_TABLE_TIMELINE=1 every table's second device commit (full recommit or LDE-only) prints a TABLE TL recommit line, so a trace can place it.
… and keeps top levels by default The shared windowed trace builder from noepoch/windowed-builder @ 1f60adc is brought in unchanged: tables::trace_builder::WindowedTraceBuilder, route_ops / RoutedSegments, StreamSkip, Traces::insert_streamed, and its tests windowed_builder_tests and trace_digest_tests. It is now the single windowed implementation (the lead's ruling), replacing this branch's CPU-only WindowedCollector and cpu_generated_outside. trace_builder.rs goes back to cc411aa plus that commit, plus KECCAK_RND chunking in build_traces, which is re-applied unchanged. block::build_streamed feeds each executor window to push() (a window is pushed once the next one arrives; the last goes to finish()). The full chunks of the eight streamed tables (CPU, MEMW_R, MEMW_A, MEMW, LOAD, LT, SHIFT, STORE) go to three committer threads, which precommit each under its AIR (named as VmAirs::new names it). insert_streamed puts the chunks back, and prove_block_traces places the precommits by AIR name. Kept top levels: stark::prover::set_default_recommit_top_levels sets the policy RecomputeLdeDevice uses when LAMBDA_VM_RECOMMIT_TOP_LEVELS is unset (still off for every other caller), and prove_block sets k = 3 (FAST 358: -4.89 s, gates green on FAST 359). Test (host): the windowed build equals Traces::from_elf_and_logs under the block's own caps, with KECCAK_RND chunked; it replaces the CPU-only test.
…he leaf, the node The block proof is one monolithic VmProof, so its recursion splits the instances across leaves (D-NOEPOCH §12): - block_replay: the Monolithic statement and Phase A over every root of the block, then z, α and the transcript's state digest. Each leaf replays this front itself instead of receiving a transcript state from another proof. - block_leaf: a leaf verifies a fixed list of instances (the fork appends each index as a program constant, so the leaf's program id commits to its list) and publishes the attestation id, the state digest, the public output and its share of the bus; exactly one leaf subtracts the COMMIT-bus target. BlockPartition refuses a list set that misses or repeats an instance; partition_by_rule is §12.2's rule. - block_node: verifies its children as an epoch node does (emit_leg), asserts the id, the state and the output equal across them and sums their shares; the top node asserts the sum is zero, the monolithic verifier's bus check. Nothing here is reached by an existing program, so every proof and program id of the epoch pipeline is unchanged.
block_tree_tests: - the block front against the host transcript (z, α and the state word), on a synthetic statement and roots; - the partition's refusals (a gap, an overlap, a repeat, an empty leaf, an index out of range) and the rule's seeding and fill; - every node binding refuses its tamper and is load-bearing: with that check removed (BlockBindings::without) the same tampered words execute; - box tier: leaves over a real small block proof execute and publish the host's values with shares that close the bus; a proved fixture tree whose top claims the block, a node refusing another block's leaf (equal output, different roots) unless the state check is removed, and a top refusing shares that do not close unless the bus assert is removed; - box tier, production scale: the whole block, base to top node, timed, with the epoch tree's WHOLE RUN line. The tree harness's sibling and sampling helpers become pub(super) for reuse.
…m the partition The partition is bound only through program identity, so the block's verifier must check the top proof against the top program it derives itself (from the trusted ELF, the block's shape and the partition constant), as the epoch tree's root check does (SOUNDNESS.md §6.9). verify_block_top is that check; compose_block_tree now returns the top proof for it. - The fixture tree adds the negative: a tree over another partition of the same block (one instance moved between leaves), every proof honest and its top claiming the same block, is refused against the derived top program and verifies against its own: the derived program is what refuses it. - The production harness uses D-NOEPOCH §12.2's lists as the partition constant on the block they were written for (NOEPOCH_PARTITION=rule forces the rule), prints whether the rule reproduces them, and runs the final check.
The harvest's production verify of the base proof is a harness assert (5.16 s on FAST 384), not work a driver does, and it sat on the recursion's critical path before the first leaf. It now runs on a helper thread beside level 0 and is joined after the top, before anything is reported: a refused block still fails the run, the joined harvest must read the leaves' transcript state, and any wait at the join is counted in the harvest. NOEPOCH_HARVEST_VERIFY=inline keeps the old order as the control arm.
FAST 385 found e679e43's block proofs refused by production's verifier in 3 of 7 runs, at random across postures, and the test said only verified=false. The two whole-block tests now initialise env_logger, so the verifier's own error! lines (the failing table's index and which check failed) reach the log.
rayon is optional in lambda-vm-prover (the `parallel` feature), but the shared WindowedTraceBuilder imported it unconditionally, so every build without default features failed — among them `make compile-recursion-elfs`, whose guests take the prover without `parallel` (FAST 388's guest step: E0433 on windowed.rs:47). The chunk jobs now run on rayon under `parallel` and in order otherwise, as the rest of trace_builder does.
…ance 0dff9a6 showed a moved instance refused at the final check. The dangerous trees are a skipped instance (its constraints never verified) and an instance verified twice; BlockPartition::new refuses both list sets, but nothing binds a prover to it. The fixture tree now builds both over an instance with a zero bus contribution, so the bus still closes and every in-circuit check passes: each tree proves and claims the same block, is refused against the top program derived from the verifier's partition, and verifies against its own program (the mutation). BlockPartition::unvalidated (test-only) emits such lists.
…ote its main snapshot
The kept-top-levels recompute (coset_lde_row_major_keep_no_tree) returns
with the trace-domain snapshot still queued on its stream: unlike the
tree-building commits it downloads no root, so nothing host-blocks behind
the snapshot. The LogUp aux build then reads the snapshot on a stream of
its own with event tracking off, and can read rows not yet written; the
aux columns come out of stale rows and the table's proof is refused
("Composition Polynomial verification failed") at random.
ResidentMainTrace now carries the producer's ready event and the aux
build waits on it device-side. LAMBDA_VM_GPU_AUX_WAIT=0 drops the wait
for the A/B and the reproduction; under LAMBDA_VM_GPU_XCHECK=1 a build
queued before its producer finished is rebuilt after it and reports
whether the first read was stale. A GPU test holds the producer stream
and checks the build waits (and, as its control, reads the zeroed
snapshot without the event).
TableChallengeShape::derive and TableVerifyShape::derive build a sub-proof's challenge and legs shapes from (AIR, trace length, options): the part count from the AIR's degree bound, the OOD blocks from its layout, the aux-root and bus-input presence from has_aux_trace and has_trace_interaction. host_table_forked and build_table_legs now call them and assert the proof's blocks agree, instead of reading the shapes out of the proof. DerivedChild::from_artifacts gives a node its child's shapes from the child's artifacts (AIR set and heights), with no child proof, so a verifier can derive a tree's node programs itself.
record_data_page_commitment claimed a wrong recorded root is always rejected because the verifier recomputes it. That holds only in another process: inside the proving process data_page_commitment returns the recorded root first, so the in-process verifier and AIR set read it back. Say so, and name the device-equals-host gate as what keeps a wrong root out.
…e shape BlockTreePlan::derive(elf, opts, &BlockShape) runs verify_proof_parts' pre-checks on the claimed shape (check_shape: accelerator shape, private-page bound, page configs, instance count, trace lengths), builds the host verifier's AIR set and takes every instance's shapes from the AIR. It fixes the partition (the rule over the closed-form costs, one more leaf while the heaviest is over the cap) and the carrier (leaf 0). The leaf emitter takes the plan: the carrier bit is leaf == carrier() and the published attestation id is computed from the preprocessed roots the leaf absorbs. The node emitter and bind_and_publish no longer take a binding set; the weakened sets are test-only entry points. derive_top emits every leaf and node and builds their artifacts with no proof; verify_block_tree verifies the top proof against that program and checks the published id and output. The harness harvests over the plan, emits leaves and nodes through it, and checks the derived top against the proved one. New tests: the plan refuses each shape the host refuses (laptop); leaf arena census, tampered L, non-canonical output halves, two carriers, part-count and contribution mutants, and the block verifier end to end (box).
test_commit_3 commits three bytes, so the statement's last output half carries one pad byte. The new fixture test executes the honest leaf and shows the same leaf refuses that half with the pad byte set: the m5 negative poc_rodata_commit could not exercise (its output fills its last half).
…hape ElfConstants holds DECODE's root, recomputed on the host from the ELF, tied to the ELF digest and the options it was computed under; BlockTreePlan::derive_with takes it and refuses another ELF's or other options'. derive is derive_with over constants computed inline, so its plans are unchanged. The production tree test computes them beside the base under NOEPOCH_ELF_BESIDE=<threads> (off by default) on a pool of their own, and the harvest joins them; the harvest prints its plan / bus target / verify split.
LAMBDA_VM_ALLOC_PURGE defaults to auto in the library and the suite env leaves it unset, so a posture word for it made the harness's own BLOCK POSTURE line read it as off-posture. The posture is now exactly the behaviour knobs the suite env sets (LFM_EXEC_PARALLEL=1 stays: the suite names it, at its library default).
HostSampler::stop expected its thread to be there and not to have panicked, and the node builder expected a slot it had just filled: both are on the production path now that the whole-block driver runs in the CLI. A sampler that could not run reports no peak; a node whose program never arrived is the builder's error.
A generator can now write its trace straight into the packed columns NarrowMain holds, with no 64-bit table at all: the writer stores each word's low bytes at a width the caller guesses per column and keeps the OR of every word written to each column. finish() narrows the columns given more bytes than they needed in place and gives NarrowMain::pack's bytes for the same words; a word wider than its column makes it return the widths the trace needs instead, for the caller to write it again. TraceTable::try_from_narrow_main gives the packed trace back where a trace cannot be held packed, so a caller never loses it. Test-only faults (one column a width too wide, one column shifted) let the prover's gates show they catch a wrong width or offset.
Under narrow storage every streamed chunk and every table the finish builds was generated at 8 bytes a cell, then packed (NarrowMain::pack). Generation plus packing is most of the producer's CPU, and the 64-bit table is most of its memory traffic and page faults. Each generator's fill is now generic over VmTable and runs through generate_main!, which takes a TraceForm. Wide is today's table and every build outside the block's narrow storage keeps it. Narrow writes the packed columns directly (stark::narrow::NarrowWriter) at the widths the table's earlier traces needed in this process (a WidthHint per table kind): the packed bytes are NarrowMain::pack's of the same words whatever the hint, so the hint changes time, never a byte. A kind's first trace, with no hint yet, is built wide and packed as before (KECCAK_RND and LT by their packed block-at-a-time builds), and its widths become the hint. Converted: the eight streamed tables (CPU, MEMW_R, MEMW_A, MEMW, LOAD, LT, SHIFT, STORE) and the finish's chunked tables (MUL, DVRM, BRANCH, EQ, BYTEWISE, CPU32, COMMIT, KECCAK, KECCAK_RND, ECSM, ECDAS, HINT). The block's generators write chunks packed through ChunkJob::generate_as, the finish through WindowedTraceBuilder::generate_packed. LAMBDA_VM_BLOCK_GPACK=0 is the old path (built wide, then packed); BLOCK NARROW reports how each trace was built. Tests: every path of the writer gives the wide table packed; every table of a windowed build in the block's configuration, on four programs, has the packed bytes and the words of the whole-run 64-bit build; one column a width too wide or shifted fails that comparison; the phase-A stream builds the same packed traces and precommits the same instances with G-pack on or off.
A box-only test builds a real block (NOEPOCH_ELF, NOEPOCH_INPUT) through the windowed builder at the block's caps, once without G-pack (every table built at 8 bytes a cell, then packed) and twice with it (the second build writing every kind at the widths the first learned), and compares every table's packed digest. The same builds and digests are checked on two small programs on every run.
Under the shared VRAM gate each artifact commit asked the gate for its device set from inside the build's parallel map, so the wait ran on a rayon worker. A worker waiting in a prove's join runs other queued jobs on its own stack, injected ones included: one that took a commit job there could park on bytes held by the prove whose frame sits beneath it, which can then never release them. Not observed in a run; reasoned from the code (I-SCHED 9.30). The walk now takes each window's device bytes (the sum of its commits' device sets that reach the card) on the thread that forks it, before the fork, and holds them until the window's last commit; the commits in the fork are told the bytes are held and ask for nothing. The derivation's own gate keeps admitting per dispatch. Every wait of the shared gate (acquire, the packing arm, claims, proof places) now refuses a rayon worker under debug assertions. Scheduling only: the same commits, the same roots.
MauroToscano
added a commit
that referenced
this pull request
Oct 5, 2026
The same writer as #1013's G-pack: a generator writes its trace straight into packed columns at guessed widths, the writer keeps the OR of every word per column, narrows the columns given more bytes than they needed in place, and names the widths the trace needs when a word did not fit. Its bytes are NarrowMain::pack's for the same words. Here a trace holds its packed columns as multilinear's NarrowColumns (the same layout), so NarrowMain::into_parts is public and TraceTable::try_from_narrow_main takes the columns, giving them back where a trace cannot be held packed.
MauroToscano
added a commit
that referenced
this pull request
Oct 5, 2026
…ctly Each generator's fill runs through generate_main! (tables::gpack), as on #1013: TraceForm::Wide is today's 64-bit table; TraceForm::Narrow writes the packed columns directly at the widths the table's earlier traces needed, narrows or rewrites on a miss, and builds a kind's first trace wide then packs it to learn them. The bytes are NarrowColumns::pack_row_major's of the same words whatever the hint. On the block (BlockOptions::gpack, LAMBDA_VM_BLOCK_GPACK=0 the old path): - each streamed chunk is written packed by its job and laid out narrow from its packed columns (table_of_narrow), with no 64-bit table and no transposition, when the groups are held narrow; - with pack_finished, the finish writes each table it packs packed (WindowedTraceBuilder::generate_packed), KECCAK_RND's chunks included; - BLOCK GPACK reports how each trace was built. Tests: every path of the writer gives the wide table packed; every table of a windowed build in the block's configuration, on four programs, has the packed bytes and words of the same build without G-pack; one column a width too wide or shifted fails that comparison; the block proves with the same partition and tables (the same bytes under the deterministic grind) with G-pack on or off; a box-only test compares every table of a real block.
MauroToscano
added a commit
that referenced
this pull request
Oct 5, 2026
… checks are debug assertions The shared VRAM gate's waits (an admission, the whole gate, a claim) refused a rayon caller with a release assert!, as did the card permit (a hold on a rayon worker, a second hold on one thread, two holders at once) and the proof place under the armed gate. The callers make each of these unreachable: every wait runs on a plain thread (the admission driver's, an artifact build's before its fork, a tree node's) and one hold per thread. Under the no-production-panic rule they are now debug assertions, as on #1013 (60bbf53): the gate's three share refuse_rayon_wait. The tests that rely on the panics (the gate's rayon refusal, the permit's re-entry and rayon-worker refusals) compile only with debug assertions; without them the re-entry and rayon tests would park instead of failing.
MauroToscano
added a commit
that referenced
this pull request
Oct 5, 2026
…'s hold never refused, the whole gate with one worker Three edges i-sched2's review of the shared VRAM gate named (I-SCHED 9.33): - An artifact window that reaches no device (host commits, empty shapes) still asked the gate for 0 bytes, a blocking call that refuses a rayon caller. admit_window now returns None at 0 bytes, as on #1013 (60bbf53). - Under the armed gate hold_gated refused a rayon caller for every phase, though only a multi_prove waits there (its latch, then a proof place); an artifact build's hold never waits. The refusal is now the prove's only. - With one worker an ungated hold (a W-LFM build or prove) returned before taking the card, and so before taking the whole gate, leaving it beside admitted bytes. It now takes the whole gate before those early returns; with more workers, still after the card, so a holder of the whole gate never waits for the card. A test for each: a one-worker ungated hold waits for admitted bytes; a window of empty shapes on a rayon worker asks nothing; under the armed gate a build's hold on a rayon worker passes while a prove's is refused.
LAMBDA_VM_BLOCK_REGEN_PROBE=1 prints two lines and changes nothing else: the first-touch census of the windows the walker walks (the memory bytes whose carried state each window reads, the size of a per-window entry state), and the main traces phase B starts from split by how they could be rebuilt: the streamed chunks, the ordered class (KECCAK_RND, ECDAS, CPU32, EQ, BYTEWISE, BRANCH) and whole-run tables. build_streamed now returns how many chunks of each streamed table it handed out.
LAMBDA_VM_BLOCK_REGEN=shadow: phase A records a recipe per streamed instance (its chunk, the windows it spans, the digest of its packed trace taken where it was packed) and phase B runs the sequential regenerator beside the prove on threads of its own: a fresh execution, the regeneration walk (the walk's one body without the lists only the finish reads), a slicer that cuts each table's list as the hand-out does, and generators that generate, pack and digest-check every recorded instance and drop it. Nothing it builds reaches the proof. It reports mismatches, failures, its CPU, and the would-wait of a plan that proves the regenerable instances last in completion order. Any other value of the knob is an error, a regenerated mismatch is a report entry and never a panic, and LAMBDA_VM_BLOCK_REGEN_GENERATORS sets the generator count (default 3).
The box readouts parse these lines; printing them from a real regeneration lets a readout be dry-run on the code's own output.
…nless a caller drops one A packed main trace can be dropped after its Round-1 commit (TraceTable::drop_main_for_regen): its shape and digest go into a slot of a RegenWindow and its bytes are freed; a caller's regenerator deposits it again in phase B. A deposit is checked against the dropped trace's shape and digest; the window bounds the bytes deposited ahead of their takers. Phase B: the fused walk takes the dropped tables last, in rank order; each driver waits for its slot before its VRAM permit (FusedReady composes the spill read-back and the slots); the fused task takes the bytes before any device work. A wrong trace is RegeneratedTraceMismatch, a regenerator that failed, stopped or never started is RegeneratedTraceFailed, as is a trace dropped before its Round-1 commit. Nothing hangs: the window's last producer dropping fails every slot not deposited, a panicked task closes the windows, and the prove's end closes them. With no dropped trace the walk, the readiness and every read are what they were.
…that never blocks From R-REGEN (r-regen's review of R2): - R1: the window reserves by rank from its frontier (the lowest rank not yet taken or failed) and always admits the frontier. The prefetch's byte rule deadlocked the walk-order arm when generators outnumbered drivers (out-of-order deposits filled the window while the rank a driver waited on could not deposit); a regenerator depositing in rank order still stays inside the window. - R5: a fused task's take never blocks (it holds its VRAM permit); a slot not deposited is an error and settles failed, so a late deposit is refused instead of parked for nobody. Readers outside the prover wait first. - A4: readiness is read without the window's lock (the gate's packing scan polls it under the gate lock). - A7: the shape is always checked at deposit; a test-only switch turns the digest off so the checks behind it can be tested. - A1: PrecommittedMain::recommits_on_device, for a caller to drop only traces with a device recommit behind the digest. The review's pacing model runs on the real slots and driver loop, and the box tests gain the digest-off negatives (never accepted; on the card, refused by the kept-top check).
… lock; the second-layer card test From r-regen's review of aa01315: N1, a slot's digest is computed before the window's lock, so concurrent drops do not queue behind it; N4, a card test where every regenerated trace is faithful and one packed bit is flipped before the device widen, refused by the kept-top check. The pacing test now runs the review's live configurations too (k = 3, G = 3; k = 8, G = 3), and the dropped-trace readers say they belong beside the fused phase.
… order; the card twin requires the kept-top refusal From r-regen's review of e59fb09: the window reserves by (rank, id), so the walk sorts the dropped tables by the same key and equal ranks cannot cross (the liveness argument's premise that walk order is the window's order). The digest-off card test now requires RecomputedCommitmentMismatch from the kept-top check instead of also accepting a verifier rejection, so it fails if that layer is removed.
LAMBDA_VM_BLOCK_REGEN=auto|always (default off; the default path is unchanged). Phase A records each streamed chunk's recipe; once the spill policy would move an instance off the host, a chunk whose fused task recommits on the device is dropped instead of spilled (its packed bytes freed after their digest, a RegenSlot left in their place), and the resident ones are dropped back in rank order until their accounted bytes reach host + spill reserve + regen reserve - (target - 2 GiB), computed once at arming. Drops run on a thread of their own, off the committers' lock, joined before phase A collects its traces. What cannot be regenerated still spills; with no spill store the policy still decides and the rest stays resident. The regen reserve counts only once armed. Phase B starts a regenerator only when something was dropped: it walks the run on phase A's windows (the ranks are phase A's hand-out order), hands the dropped chunks out in rank order, fails a chunk not cut before a higher rank (R10), fails every undelivered slot on each exit, keeps draining after the window closes, and a guard closes the window on every exit of the prove. An AIR set that disagrees with the traces is now an error on the block path (try_air_trace_pairs), not an assert. Laptop tests: drops and byte-identical regeneration under always and auto, unarmed auto drops nothing, the drop-back stopping rule, the slicer skip, every regenerator exit, the prove's end, windows other than phase A's refused, and the AIR count refusal.
The regenerator generates each dropped chunk in phase A's form (generate_as(TraceForm::Narrow) under G-pack, else 64-bit then packed) and moves the packed trace out instead of copying it. The regen reserve default goes from 6 to 10 GiB: BIG 636's phase-B peak VmRSS with the shadow regenerator less without it (+5.90 GiB at p90) plus the 4 GiB deposit window. The always test regenerates in both forms.
S1: a drop the drop thread finds no longer droppable stays counted resident (the spill refuses a shared packed copy too, so there is no spill to fall back on). S2: install_regen_main also checks the column count (RegenSlot::cols). S3: phase A's window length travels in the plan, and a regenerator on other windows refuses the whole plan before anything runs. S4: drop-back counts bytes only once queued. S5: auto never arms with the spill policy off (documented). S7: the laptop matrices trimmed and the regeneration tests run one at a time (block_regen:: 57 s, 3.6 GiB peak).
With LAMBDA_VM_BLOCK_SPILL=off and LAMBDA_VM_BLOCK_REGEN=auto, the auto policy still decides, with no store, and nothing is written to disk. Before arming it decides as today; on arming, drop-back takes every resident regenerable instance (not only the need) and every later regenerable instance is dropped; what cannot be regenerated stays on the host and counts resident. The BLOCK REGEN dropped line says "(no disk)" and the spill line "no store". Every other combination is unchanged. Laptop tests: unarmed no-disk drops nothing and builds the resident traces; armed, every streamed chunk is dropped (drop-back took them all), nothing spills, and the regenerator brings each back.
MauroToscano
added a commit
that referenced
this pull request
Oct 5, 2026
The same writer as #1013's G-pack: a generator writes its trace straight into packed columns at guessed widths, the writer keeps the OR of every word per column, narrows the columns given more bytes than they needed in place, and names the widths the trace needs when a word did not fit. Its bytes are NarrowMain::pack's for the same words. Here a trace holds its packed columns as multilinear's NarrowColumns (the same layout), so NarrowMain::into_parts is public and TraceTable::try_from_narrow_main takes the columns, giving them back where a trace cannot be held packed.
MauroToscano
added a commit
that referenced
this pull request
Oct 5, 2026
…ctly Each generator's fill runs through generate_main! (tables::gpack), as on #1013: TraceForm::Wide is today's 64-bit table; TraceForm::Narrow writes the packed columns directly at the widths the table's earlier traces needed, narrows or rewrites on a miss, and builds a kind's first trace wide then packs it to learn them. The bytes are NarrowColumns::pack_row_major's of the same words whatever the hint. On the block (BlockOptions::gpack, LAMBDA_VM_BLOCK_GPACK=0 the old path): - each streamed chunk is written packed by its job and laid out narrow from its packed columns (table_of_narrow), with no 64-bit table and no transposition, when the groups are held narrow; - with pack_finished, the finish writes each table it packs packed (WindowedTraceBuilder::generate_packed), KECCAK_RND's chunks included; - BLOCK GPACK reports how each trace was built. Tests: every path of the writer gives the wide table packed; every table of a windowed build in the block's configuration, on four programs, has the packed bytes and words of the same build without G-pack; one column a width too wide or shifted fails that comparison; the block proves with the same partition and tables (the same bytes under the deterministic grind) with G-pack on or off; a box-only test compares every table of a real block.
The unarmed half of no_disk_auto_drops_every_regenerable_and_spills_nothing read the real host under auto, so on a Linux host with min(cgroup, MemTotal) of about 16 GiB or less (CI's ubuntu-latest) it armed at the first decision and failed. It now reads the host as 0 against a 64 GiB target, as the armed half's fake does. Before: LAMBDA_VM_BLOCK_SPILL_TARGET_GIB=5.6 failed at live_tests.rs:564 (Auto (no disk), 6 instances, armed at 0.0 s). After: it passes.
…s back (R-REGEN P1 A1) Spill::consider's no-disk short-circuit (once armed, every droppable instance is Fate::Drop) had no test that fails without it: the stream tests' fake host stays at the target after arming, so baseline auto drops the same instances. The new unit test arms on a host read as the target, then reads 0 (under the target) and asserts a regenerable instance still drops under no disk, is kept without it, and a non-regenerable one stays resident and counted.
… P1 A2) SpillPolicy::Off said "keep every trace", live.rs's module doc and Spill::consider's doc said drop-back stops at the target: neither holds in no-disk mode. The docs now say that with LAMBDA_VM_BLOCK_REGEN=auto the off policy decides as auto with no store and drops every regenerable instance once armed, and that an unparsable LAMBDA_VM_BLOCK_SPILL value, which reads as off, enters it too.
With LAMBDA_VM_BLOCK_SPILL and LAMBDA_VM_BLOCK_REGEN both unset, the
block now runs no disk: spill off, regeneration auto. Auto still decides
with no store; once it arms, every regenerable instance is dropped and
rebuilt in phase B, and nothing is written to disk. The P1 gates
measured it at p90 (BIG 655: +2.13 s whole, peak 105.86 -> 98.36 GiB,
0 bytes written) and at full gas (ULTRA 072: +0.84 s, 105.73 -> 101.16
GiB, 0 bytes).
A knob that is set keeps the meaning it had: when either is set, an
unset spill is auto and an unset regen is off, as before. So
SPILL=auto alone is the old spill tier and REGEN=off alone falls back
to it, rather than to no relief at all.
The BLOCK POSTURE line now ends with the spill policy and regen mode
the knobs come to ("= no disk" at the default) and the knobs as set.
The parser tests pin both defaults and that set values parse alike
either way. No proof byte depends on the policy.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
One STARK proof per block, with no epochs (the prove-and-retire / VADCOP shape). This is a second prover next to #1009's epoch-based one. #1009 stays the reference and this branch does not touch it. The branch starts from #1009's head
cc411aa2c, so the diff against main includes #1009. Compare againstcc411aa2cto see only this work.Whole block: 31.78 s (FAST 456 at the head
08ecc4310, mean of 3 default arms), against the epoch tree's 60.15 s (#1009; FAST 389's same-binary reference, not re-run since). Base 21.28 s, recursion 10.25 s (level 0 5.18 · interior 4.88).Head
d8a422962(10-05): the generators write the block's packed traces directly (G-pack). Every streamed chunk and every table the finish packs is written straight into its packed columns, with no table at 8 bytes a cell and no separate pack pass;LAMBDA_VM_BLOCK_GPACK=0restores the old path. Prover-only, the same bytes: under the fixed trace hash and the deterministic grind the base digest is0224203e…with G-pack on and off, and every table of the 1× block has the old path's packed bytes (BIG 634). Median block 25475471 on BIG, one binary, 3 + 3 (BIG 635): generator thread-seconds 399.8 → 105.7 (−73.6 %), phase A −7.47 s (t −10.2), base −7.35 s (t −10.6), phase B +0.10 s, VmRSS −3.28 GiB. See G-pack.Head
58a58be95(10-05): the production CLI proves and verifies a whole block.cli prove-block <ELF> --input <block> -o <proof>runs the whole-block harness's own driver (lfm::block_tree::prove_block_tree; the harness is now a wrapper around it, its output unchanged but for a firstBLOCK POSTUREline), andcli verify-block <proof> <ELF>runs the block verifier over the proof file. The binary installs its jemalloc as the prover's allocator hooks, so theautopurge and the late leaf emission act in the shipped CLI, and it sets the measurement posture for every knob the environment leaves unset. Prover and CLI only: no proof byte moves. ULTRA 016, 8 + 8 against the harness after a warm-up: CLI − harness whole −0.161 s (t −2.4), base digest0224203e…and top2ef0e0d2…equal the harness's. BIG 632: the p90 25481021 proves through the CLI at 112.33 GiB with both purges (whole 553.77 s) andverify-blockpasses in 72.86 s. See Production CLI.Head
4fc4b7b12(10-05): LogUp k4 is the default on the base tables (Mauro, 10-05;LAMBDA_VM_ZF_LOGUP=pairrestores the previous bytes). 1× tree on ULTRA, 8 + 8: base −0.89 s (t −15.6), whole −0.92 s (t −11.7); the p90 proves and verifies under k4 at the defaults (BIG 626: whole 555.29 s, 113.50 GiB, against 590.95 s and 115.79 GiB under pair in BIG 591).Head
035aef5d6(10-03): the p90 and full-gas blocks prove end to end at the defaults on BIG (128 GiB host, 120.69 GiB cgroup), and the block verifier passes on both. p90 25481021 (52.0 M gas, 612.3 M cycles, 20.1× the bench block): whole 590.95 s at 115.79 GiB (BIG 591). Full gas 25431071 (60.0 M gas, 602.9 M cycles): whole 541.51 s at 110.62 GiB (BIG 125). Before this head,autoalone ran out of memory in level 0 on the p90 (BIG 120). Prover and harness only: digestac406fc8…is unchanged (FAST 515). At 1× nothing arms or purges (default against spill off, base −0.065 s, t −0.6, 8 + 8; FAST 515 + 516). Four changes:autocounts the cgroup's working set (the charge less its inactive file pages), not its page cache.autofell from +3.6 to +1.12 s (BIG 118 → BIG 122).auto, once the budgets arm). Blocks that fit pay nothing.LAMBDA_VM_ALLOC_PURGE=off|all|<points>. It acts in the lib's test builds (the harness's jemalloc); the production CLI has no purge yet. (Since58a58be95the CLI purges too, through its allocator hooks.)Head
00871b913(10-03): under the shared VRAM gate, a carrying proof now claims its resident bytes plus one headroom shared with the claims in force. These tight claims are on by default;LAMBDA_VM_SHARED_GATE_CLAIMS=wholerestores the first form. This is scheduling only: digestac406fc8…and top93aa7097…are unchanged. Median block (BIG 474, 5 + 5): recursion −1.25 s (t −5.4), with level-0 claim waits falling from 8–10 s a run to 0. Whole −0.05 s: the base moved +1.18 s, which is scatter. At 1× there is no regression (FAST landing gate: recursion −0.01 s). See Recursion: sibling proofs share the card.Head
edddc6873(10-03): the trace spill store is on by default asauto. A block that fits spills nothing. A block above the host's limit spills the committed traces that would not fit, instead of running out of memory.LAMBDA_VM_BLOCK_SPILL=offkeeps every trace. This is prover-only: digestac406fc8…is unchanged. BIG 117 at the median block: the default spilled 0 B, with phase-A end −0.97 s against off; forced to a 70 GiB target it spilled 30.1 GiB, cost +1.43 s and verified. The same head adds the spill writer's step timings and splits the AIRs out of BLOCK MEM's unnamed heap; both are diagnostics only. See Disk spill: auto by default.Head
a93018974(10-03): the block tree's sibling proofs now share the card through one VRAM gate, on by default (LAMBDA_VM_SHARED_VRAM_GATE=0restores the exclusive card permit). This is scheduling only: digestac406fc8…and top93aa7097…are unchanged. Against564dd02bb(FAST 871): 1× recursion −0.68 s, whole −0.48 s. Median recursion −4.72 s (BIG 471, measured before the arming drained the device); at this head −4.24 s (BIG 472). See Recursion: sibling proofs share the card.Head
564dd02bb(10-03): adds LogUp k4 as an opt-in (LAMBDA_VM_ZF_LOGUP=k4; defaultpair= the same bytes; under k4, median phase B −9.56 s and 1× whole −1.04 s).Head
8f2ce57d5(10-03): three block-tree changes, all with the same proof bytes and program ids (FAST 673 merge gate green: 1× top93aa7097…, pinned ids unchanged; median top6d85454d…, BIG 586).NOEPOCH_TREE_EMIT_LATE=auto): when leaf 0 puts the other leaves' programs at ≥ 8 GiB, they are emitted in phase B onto pages the retired traces freed; smaller trees emit at the shape as before. Median, spill off (BIG 585): base-phase VmRSS −10.37 GiB, whole-run peak 98.99 → 86.49 GiB, recursion −0.31 s, whole −2.15 s; inert with spill on.NOEPOCH_TREE_EMIT_LATE=offrestores the old behaviour, and A/Bs against it must set that.verify_block_tree): each program and its artifacts are dropped once its child is derived. Verifier alone in a fresh process at the median (BIG 587): peak 32.33 → 22.89 GiB, derive −1.48 s.LAMBDA_VM_BLOCK_DERIVE_HOLD=levelrestores the old hold.Note on drivers: the whole-block numbers here come from the test harness's tree driver (
block_tree_pipeline,cfg(test)). The library has the base prover (block::prove_block), the per-node program emitters (BlockTreePlan::leaf_program/node_program) and the production verifier (verify_block_tree), but no production driver that chains them into a whole-block proof yet; that is a landing item.Head
dacbd5b4e(10-03): KECCAK_RND, ECDAS and KECCAK compose on a bounded-slot interpreter by default (LAMBDA_VM_GPU_INTERP_SI=0restores the slot-file interpreter). Prover-only: digestac406fc8…unchanged, FAST 784's landing gate green (1× default − off −0.05 s).Constraint composition on a bounded-slot interpreter (prover-only; proof bytes unchanged). The three programs too large for the compiled kernels (KECCAK_RND, ECDAS, KECCAK) now compose on a bounded-slot GPU interpreter. The slot-file interpreter it replaces (from OpenVM v1) keeps every value of a row in a per-thread file in global memory: 4,350 words for KECCAK_RND, 2.1 GiB per composition, not counted by the VRAM gate. The new lowering evaluates each constraint's cone on demand in constraint-index order, reads trace cells as operands, and fits a row in 16–128 words of shared memory or a local array, recomputing past the budget; each program gets its measured best shape. On one binary, the three programs compose in 0.58–0.63× the time. At the median block (25475471, 3 + 3) the deciding figure is card work −0.43 s (t −1.9), with the slot-file scratch 80 GiB a run → 0 and the phase-B device peak −1.9 GiB (t −3.4); base −1.03 s is not significant (t −1.0); at 1× base −0.06 s. The digest is unchanged (
ac406fc8…);LAMBDA_VM_GPU_INTERP_SI=0restores the slot-file interpreter. Running all 39 programs on it is slower (1.25–2.1× the compiled kernels, +0.16 s at 1×), so the compiled kernels stay for the other 36.Head
d740eb5d5(10-03): the block tree emits each node's program as soon as its children land, instead of level by level (NOEPOCH_TREE_NODE_EMIT=levelrestores the old order). This is scheduling only: the same top program id in all 14 arms.NOEPOCH_TREE_EMIT_WINDOW; −12.9 GiB median base peak for +6.2 s recursion) and a cap on concurrent derivation builds (LAMBDA_VM_BLOCK_DERIVE_BUILDS).Head
86e71de77(10-03). Three landings sincef805464f6:aa726ce2b: block fan-in 4 (Mauro approved 10-02; see (f)).5e4176961: KECCAK and ECSM chunked, and tree partition rule v2, a format change. See the format section below.7ef261724+86e71de77: producer and memory work, all prover-only with identical proof bytes:LAMBDA_VM_BLOCK_GENERATORS=6restores six);LAMBDA_VM_BLOCK_SPILL=alwaysturns it on).The median block 25475471 on BIG (job 112, Zen 3 host):
ac406fc8…unchanged.Disk spill (BIG 111, base only):
The walker levers with eight generators end the median's phase A 3.45 s sooner (BIG 468) and cost +0.14 s on the 1× base (FAST 470).
Head
f805464f6: two more producer and memory changes, both prover-only with identical proof bytes:6cb6cd557, smaller per-step records: the CPU op shrinks from 128 to 96 B (arg2, the branch decision and the ECALL kind are derived) and MEMW_A ops become 48-byte aligned rows. FAST 612 at 1×: base −0.38 s, digest equal. BIG 424 at the median: phase-A end −6.0 s.f805464f6: LT ops kept as segments rather than concatenated, and the packed KECCAK_RND / LT finish builds back on by default. BIG 107 picked them over capping wide KECCAK_RND chunks: at the median they save about 14 GiB and end phase A 2.6 s sooner.BIG 108 at this head: 1× verified at 15.1–16.2 GiB with digest
ac406fc8…. The median block verifies at 81.8 / 82.4 GiB with base 212.4 / 216.0 s. Phase B scatters run to run by about 2.7 s (sd) at the median.Head
330057f2a: the packed KECCAK_RND / LT finish builds are now off by default (LAMBDA_VM_BLOCK_PACKED_BUILD=1turns them on), and the memory knobs that had no effect are removed. Two one-binary A/Bs at the median (BIG 104, 105): packed builds save 13–15 GiB of host peak but cost phase B ≈ 3–6 s. BIG 105 at this head: 1× verified at 18.32 GiB, digestac406fc8…; median block 96.2 GiB, base 218–224 s on BIG.Head
161b82415: six generator threads by default (LAMBDA_VM_BLOCK_GENERATORS=0keeps the committers generating). With the faster walk the three committers, which also generated every chunk, fell behind: at the median block 32.8 GiB of op lists waited and phase A ended 19 s after the finish. Generators build and pack each chunk; the committers only commit. KECCAK_RND and LT in the finish are built packed a block at a time (LAMBDA_VM_BLOCK_PACKED_BUILD=0builds them wide). BIG 103/104 on the median block 25475471: base 231.8 → 222–226 s, host peak 96.1 → 82.7–83.0 GiB; 1× base flat; digestac406fc8…unchanged. Within one binary, packed builds cost phase B ≈ 6 s at the median against wide builds and save 14.75 GiB; see the head above for the current default.Head
3be24abab: narrow storage on by default (merged froma2cd24207). Main-trace cells are stored at about 2 B each instead of 8 and widened on the card;LAMBDA_VM_BLOCK_NARROW=0keeps them wide. BIG 100 (A/B on one binary, the 128 GiB box): proof digests packed = wide =ac406fc8…; 1× host peak 31.2 → 17.7–19.0 GiB; 1× base −1.29 s on that box. A median mainnet block (25475471, 9.78× this one, 941 sub-proofs) proves and verifies within 128 GiB: host peak 106.87 GiB, base 259 s on BIG's Zen 3 host (BIG 100; that binary predates the E1+E4 producer changes below). BIG 102 re-gated the merged head: 1× verified, digestac406fc8…, peak 19.29 GiB.Head
2c440b8dc: base 19.61 s (FAST 602, mean of 2). It adds two producer changes to19fe5e9e5, whose memory changes are in Memory: a lean walk (the walker builds only what the tables need) and the executor's guest memory in 64 KiB pages instead of a hash map of words. A B B A on one binary: base 20.38 → 19.61 s (Δ −0.77, no overlap); proof bytes equal underLAMBDA_VM_FIXED_TRACE_HASH=1+LAMBDA_VM_DETERMINISTIC_GRIND(digestac406fc8…in both arms); host peak flat. At a median-size block (25475471, harness, windows only) the producer's windows phase falls from 42.4 to 25.2 s and the executor from 108 to 11 ns per cycle.LAMBDA_VM_WALK_LEAN=0andLAMBDA_VM_EXEC_MEMORY=wordsrestore the old paths. The whole tree has not been re-run at this head.The whole block, epoch tree vs no-epoch tree (FAST 389, one binary at
e4ff3f8fb, means of 2 arms)Block 25368371. Both trees run on one test binary, and its md5 is checked after every arm. The epoch arm is #1009's record harness, unchanged.
a0954fd1bthe lists are the plan's: D-NOEPOCH §12.2's rule over the closed-form costs, a pure function of the block's shape. On this block the rule gives other lists than §12.2's, which came from legmodel.py's costs, but the leaf tiers are the same. FAST 392 atcda620461(means of 2): level 0 6.21 s, interior 7.19 s, harvest 2.03 s, whole 40.38 s, which is 389's numbers. The verifier's own derivation of the top program takes 8.35 s; that is outside the whole, and the derived top equals the proved top. A tree over another partition, a skipped instance or a duplicated instance is refused at the final check (fixture gate).The plan's gate and the harvest lever (block 25368371, means of 2 arms, 389's posture)
cda620461(the plan)a684f4f3f(+ ELF constants beside the base)a1a3a2c22(+ F1 follow-ups)e7406d6b5(+ rebuilt windowed builder)6b86432a2(+ verifier derive)verify_block_tree, no proof read, outside the whole)e7406d6b5and FAST 397's6b86432a2are both on this branch;6b86432a2is the head.-x) ofnoepoch/windowed-builder6e99c74..1d24a2c, not a merge of that branch (90 WHIR commits, 6 conflicting files), plus ourparallelgating, which compiles the recursion ELFs (5fe8316, now byte-identical to the builder branch's bcccdef).verify_block_tree_with(elf, &ElfConstants, …)lets a consumer compute the per-ELF constants once (3.05 s cold), and each node level is now emitted and built in parallel (level 1: 1.31 s wall for 5.0 s of work). Split at 397: constants 3.05 · plan 0.16 · leaves 2.17 · L1 1.31 · L2 0.93 · L3 0.72 · check 0.24.noepoch/stark-s3at3992a56f9, gated by FAST 399; A/B on one binary each):f6445c8b6(FAST 452, gated: −2.48 s againstNOEPOCH_TREE_AHEAD=0, P8 10/10). The leaf programs are emitted beside the base. Leaf and node artifacts are built on the card during level 0, and each node program is emitted as soon as its children's artifacts exist. Base +0.01 s; every child proof verified.c9c29cb5b,NOEPOCH_BUILDER_FIRST, off): recursion −0.60 s in its A/B, but not by the mechanism pre-registered. It cannot pre-empt a leaf'smulti_prove, so the level-1 artifacts are still late; what it did was remove level 0's slow mode (≈ −0.4 s pooled over 451–453). Not the default.7da2a45d0; the default since08ecc4310, gated by FAST 456 and 457): recursion −0.29 s, level 0 −0.40 s. Level 0 was bimodal (5.2 or 5.8 s). In the slow runs, one leaf'smulti_proveheld the card for 1.2 s at about a third busy, starting at the instant the builder began emitting the level-1 programs on the global rayon pool (FAST 454's card trace). With a 4-thread host-only pool of its own, the stalled hold is gone in every run.NOEPOCH_EMIT_POOL=0restores the global pool.e029731f7; FAST 456): base −0.57 s (P8, 5 against 5 on one binary, no overlap), replicating WHIR's FAST 417 (−0.52 s).LAMBDA_VM_BUILDER_CONCAT=serialrestores the old copy.NOEPOCH_TREE_EAGER): recursion −0.06 s, because level 1 is limited by the card, not by waiting. The card trace (FAST 454) puts the idle inside the recursion's card holds at 1.2–1.8 s, 13–19 %.f6445c8b6(FAST 452); 35.03 s withNOEPOCH_TREE_AHEAD=0. Fan-in 4 was measured on the inline path (33.82–33.95 s there).a1a3a2c22through production's block verifier, all verified (bases 24.52–25.17 s), so the race fix (a) is shown on the plan's code.ElfConstants) and the per-(AIR, length) shapes can be reused across blocks.Memory: toward typical blocks (head
19fe5e9e5)A median mainnet block (25475471) is 9.78× this one, and the host peak grows about linearly with the block. The head carries three changes for that; none changes a proof byte.
drop_streamed_ops, default on;LAMBDA_VM_BLOCK_DROP_OPS=0keeps them)BLOCK_RECOMMIT_TOP_LEVELS)LAMBDA_VM_FIXED_TRACE_HASH=1)LAMBDA_VM_FIXED_TRACE_HASH=1andLAMBDA_VM_DETERMINISTIC_GRIND, two processes produce the same block proof (digestac406fc8…); a random-key control differs. Byte-identity gates on the block use this.What it is
VmProof. Every table is cut into instances of 2^21 rows, and KECCAK_RND into 2^16-row instances. There is no L2G table and no global proof; memory uses the monolithic PAGE argument.ResidencyMode::RecomputeLdeDevice: after Round 1 only each instance's root is kept. Each instance's fused task commits its trace on the device again, and the prover refuses the proof unless the new root equals the absorbed one, so the proof bytes are unchanged.RetainandRecomputeLdebehave as before.prover::block::prove_block/verify_block. The block verifier is the only one that accepts a chunked KECCAK_RND (AcceleratorShape::KeccakRndChunked). Every other verifier keeps KECCAK_RND to one table.LAMBDA_VM_GATE_PACKING(VRAM-gate packing admission),LAMBDA_VM_TABLE_TIMELINE(per-table timeline),LAMBDA_VM_RECOMMIT_TOP_LEVELS(kept top levels).WindowedTraceBuilder, and every full chunk of CPU, MEMW_R, MEMW_A, MEMW, LOAD, LT, SHIFT and STORE is committed as soon as it exists. Host tests show it equals the serial build.0fe8e4f2f; k = 3 before) areprove_block's default. The ELF data pages' preprocessed roots are computed on the device during execution.Base, block 25368371 on FAST (A/B on one binary; the epoch base is #1009's
prove_continuationon the same binary)e7406d6b5, FAST 396)At
e7406d6b5the block base is about 4.5 s below the epoch base (21.55 against 26.05). The first shared builder cut Round 1's span from 6.3 to 2.4 s, but its window builds sat on the executor's path (execute 1.35 → 6.9 s). With the rebuilt builder and the walk on its own thread, execute is 3.35 s and phase A 7.13 s, and the prove is 14.4 s against the serial build's 18.4 s. Nsight on FAST shows the fused phase is 98.6 % card-busy, so the block is card-bound. Most of that card time was the second hash, which kept top levels remove: phase B recomputes the LDE alone, and the openings rebuild each queried 8-leaf subtree and check it against the kept node. The architecture's main gain is in the recursion (above).S0 census: main 3.301 G elements, aux 1.036 G.
Format change: ECDAS chunked; KECCAK_RND and ECDAS heights capped
Landed at
23b3c8173(FAST 531 gates GREEN; FAST 533 A/B on 25512221: base −0.17 s, no effect on time, as expected; ECDAS cells −25 %, host peak −0.40 GiB).ECDAS is one row per double/add step (≈ 382 per ECSM call, ≈ 4.2 calls per transaction), so a median mainnet block
makes ≈ 420 k rows: one table at 2^19 proves 127.91 bits at DEEP batching on a ≈ 22.5 GiB device set, and a p90 block's
2^20 (126.91 bits, 44.7 GiB) no longer fits a 32 GiB card. The block now cuts ECDAS into instances of at most 2^17 rows;
a scalar multiplication may straddle two instances, its steps chaining only through the Ecdas bus, keyed by the call's
timestamp and the step's
(round, op).AcceleratorShape::KeccakRndChunkedis renamedBlockChunkedand lifts theone-table bound for ECDAS as for KECCAK_RND (count bounded by the sub-proof cross-check). New verifier constants, checked
in
verify_blockand in the tree'scheck_shape: every KECCAK_RND instance ≤ 2^16 rows (129.43 bits) and every ECDASinstance ≤ 2^17 (129.91 bits). This closes G1 (REV-JUDGE item 13, R-NOEPOCH-S3 F1-M1) for KECCAK_RND and ECDAS
only; every other table's height is still bounded by two-adicity alone, and the rest of G1's per-type list (ECSM,
KECCAK, the CPU family, the fixed tables) stays parked.
LAMBDA_VM_BLOCK_KECCAK_RND_LOG2takes 5..=16 (nooff).A block whose ECDAS fits one 2^17 table (25368371: 2^16) builds the same ECDAS table as before (one chunk of every step is the table
generate_optionalbuilt; by construction, not byte-compared on a block); 25512221 (2^18 today, 128.91 bits, 0.04under the minimum of record) now proves 2^17 + 2^16. The epoch and recursion verifiers (
Single) are unchanged.Format change: KECCAK and ECSM chunked; tree partition rule v2 (
5e4176961)Landed with the any-block target. Approved by the lead; Mauro to confirm. It is listed for the cryptography review as S-6 and S-9.
The caps. KECCAK is cut into instances of at most 2^18 rows (129.213 bits) and ECSM into instances of at most 2^17 (129.488 bits), as ECDAS is.
LAMBDA_VM_BLOCK_KECCAK_LOG2(2..=18) andLAMBDA_VM_BLOCK_ECSM_LOG2(2..=17) lower the caps, for tests.Partition rule v2 (
PARTITION_COST_MODEL = 2). Rule v1 pinned every chunk of a table to one leaf. Now the first instance keeps its seeded leaf and later chunks of KECCAK, ECSM and ECDAS fill by load. Without this, a p99 block's ≈ 15 ECDAS chunks overflowed the leaf cap and the plan was refused. The verifier derives the partition by the rule from the shape alone.Evidence:
Known cost (deferred): a table just over a power of two is cut after padding, so for example 2^20 + 1 KECCAK calls make 8 instances, about half of them padding. This matters only on keccak-heavy blocks.
G-pack: generators write the packed traces directly
Default since
d8a422962.LAMBDA_VM_BLOCK_GPACK=0builds each table at 8 bytes a cell and packs it afterwards, as before. Under narrow storage the producer used to generate every main trace as a 64-bit table and then pack it (NarrowMain::pack). At 1× the generators spent more time packing than generating (ULTRA u004a: generate 6.25 thread-s, host pack 9.21).stark::narrow::NarrowWriterstores each word's low bytes at a per-column width chosen up front, and keeps the OR of every word written to each column.finish()narrows in place the columns that were given more bytes than their largest word needs. When a word did not fit, it names the widths the trace needs instead. Either way the bytes areNarrowMain::pack's of the same words, whatever the guess.VmTableand runs throughgenerate_main!(tables::gpack) in aTraceForm:Wideis the old table, used by every build outside the block's narrow storage.Narrowwrites the packed columns at the widths the table kind needed so far in the process (oneWidthHintper kind) and writes the trace again on a miss. A kind's first trace is built wide and packed, to learn the widths (KECCAK_RND and LT through their packed block builds).ChunkJob::generate_as(TraceForm::Narrow), and the finish runs underWindowedTraceBuilder::generate_packed().BLOCK NARROWreports how each trace was built: written packed, narrowed after, written again, or built wide then packed. At the median about 600 traces are written packed, 105 narrowed after, 4 written again and 23 built wide then packed.LAMBDA_VM_FIXED_TRACE_HASH=1+LAMBDA_VM_DETERMINISTIC_GRIND=1gives digest0224203e…with G-pack on and off. Every table of the 1× block built through the windowed builder has the old path's packed bytes (138 tables, twice: before the widths are learned and after).58a58be95: −73.5 % and phase A −7.75 s.Production CLI: prove-block and verify-block
Since
58a58be95. Until this head every whole-block number came from the test harness; the shipped CLI could not prove a block as a tree, had no CUDA build and never purged.lfm::block_tree::prove_block_tree: the base, the harvest, the leaves, the interior and the top, with every in-run check (the base and every child verified beside the run, each leaf's published words, the top's claim, the final check) returning an error instead of panicking. Every knob is read once, byBlockTreeConfig::from_env, under the harness's names and defaults. The harness test is a wrapper that keeps its post-run block verifier; the proof readers it used (HostTable,TableLegs, the child harvest) live inlfm::harvest, re-exported under their old names.alloc_purge::AllocatorHooks(statistics andarena.<all>.purge), so the purge points underauto, the BLOCK MEM columns and the late leaf emission run as in the harness. It still compiles in the never-purge posture; a CLI unit test now reads that setting back, and another shows the installed purge returns freed pages (each fails under its mutation).prove-blockandverify-block, every knob inlfm::block_tree::POSTUREthat the environment leaves unset is set before any thread starts:TABLE_PARALLELISM=8,LAMBDA_VM_VRAM_BUDGET_MB=24000(only whennvidia-smireports a card of at least 31 GiB),LAMBDA_VM_GATE_PACKING=1,LAMBDA_VM_MAX_ROWS_LOG2=21,LFM_PRECOMPUTED_TREE_CACHE_CAP=64,LFM_EXEC_PARALLEL=1,LFM_TREE_SIBLINGS_L0=8,LFM_TREE_SIBLINGS=4. ABLOCK POSTUREline on stderr says what was set; the harness prints how its own environment compares.BlockTreeProof: the claimed shape, the public output and the top node's proof, behind a magic, a version and a pipeline tag.verify-blockisblock_plan::verify_block_treeover those claims, unchanged: the plan and the top program are derived from the trusted ELF under the block presets, and nothing about the format is read from the file.--digestunder the fixed trace hash and the deterministic grind, the CLI's base digest equals the harness's digest test at the same sha (0224203e…, the k4 reference), top2ef0e0d2…; a small asm guest proves and verifies through the CLI.cli prove-blockwith only the instrumentation knobs set: whole 553.77 s, VmRSS 112.33 GiB, purges phase-a 121.39 → 78.73 GiB and base 109.30 → 52.96 GiB, 54.56 GiB spilled;cli verify-blockin its own process passes in 72.86 s at 37.47 GiB (BIG 626, the harness at the same base: 555.29 s, 113.50 GiB).LogUp: four interactions per aux column (the default since
4fc4b7b12)Default
k4(Mauro, 10-05);LAMBDA_VM_ZF_LOGUP=pairrestores the previous bytes. Each base table commits four bus interactions per LogUp aux column instead of two, where that commits fewer extension columns: groups of four have degree 5, which blowup 4 admits, at the price of four composition parts instead of two. The rule is per table and verifier-side (⌈N/k⌉ aux columns + parts, ties keep pairs): KECCAK_RND 516 + 2 → 258 + 4, ECSM 290 → 145, ECDAS 194 → 97, CPU 10 + 2 → 5 + 4, MEMW_A 10 → 5; LT, STORE, MEMW_R, LOAD, PAGE and the small tables keep pairs. The LFM chips keep pairs; #1014 is untouched.ProofFormat.logup(stark), the group/accumulator emitters for k ≥ 3 (one body for the prover folder, the verifier folder and the IR capture), the host and device aux builds grouped by arity, the four-part composition split on the card (radix-2 twice) and its host mirror, compiled kernels for the k4 table programs, the knob atblock_base_options, and one new verifier refusal (parts > blowup).=pairthe 1× digest is ac406fc8 and every program id, kernel key and golden is as before (the small-tree id pin derives under pair and passes; the 44-program golden holds); with the knob unset the base tables prove k4 (1× tree top 2ef0e0d2).4fc4b7b12(Mauro 10-05; ULTRA 018: both arms verify, base −0.89 s, whole −0.92 s; BIG 626: the p90 verifies under k4 at 113.50 GiB).Recursion: sibling proofs share the card through one VRAM gate (on by default)
Before: the recursion's card permit was a mutex, so one proof at a time ran inside
multi_prove. The card then sat idle while the holder ran its host stages: uploads, absorbs, queries. At the median block that was 24.6 s of card idle inside the holds (BIG 469).Now: every
multi_provein the tree admits its tables through one process-wide byte gate, and the artifact commit takes its bytes from the same gate. Sibling proofs overlap wherever their bytes fit. Three parts keep the gate's account matching the card:Retainprove keeps each table's main LDE, trace snapshot and tree on the card from its Round-1 commit until its fused task ends. Those bytes stay in the gate the whole time: the Round-1 task carries them past its own permit, and the fused task takes them over and is admitted only for the rest of its set.multi_proveat once.LAMBDA_VM_SHARED_VRAM_GATE=0restores the exclusive permit.LAMBDA_VM_SHARED_GATE_TRACE=1prints the gate's account (SGATElines). The gate acts only while the tree arms it for concurrent proofs, so the base and every single-proof path are unchanged.Bytes: unchanged, since the gate only reorders admission. The digest is
ac406fc8…, and every A/B arm has the same top program id.Measured:
a93018974against564dd02bb, 4 + 4):Tight claims (head
00871b913, on by default). A claim is a held part R plus a headroom H.LAMBDA_VM_SHARED_GATE_CLAIMS=wholerestores the first form (residents plus the largest table's whole set).278e6a8c6); no regression.Readout note: FAST 871's "claims in force" row read OUT because it counted from the optional trace, which that gate ran without. From each claim's own log line, every gate-on run peaked at 3 claims and the off runs had none.
Tests:
Disk spill: auto by default
What it does. Once Round 1 has committed an instance, its packed main trace can go to a spill file that phase B reads back ahead of its walks. The words that come back are the words that went out, so no proof byte depends on the policy.
LAMBDA_VM_BLOCK_SPILL=auto(default) |off|always|<GiB>(a resident budget for committed packed traces).How
autodecides (prover/src/block.rs,spill_target_bytes/spill_decision). A committed instance is spilled when the host's bytes, plus the reserve, plus the instance's own bytes would pass the target.memory.current, v1memory.usage_in_bytes) less its inactive file pages frommemory.stat, which the kernel reclaims first. Sincecf253d235; the charge alone counted the page cache, and at 1× on FAST it read 25.57 GiB against a working set of 17.09.LAMBDA_VM_BLOCK_SPILL_TARGET_GIB, if set;MemTotal, less 10 GiB. The limit is v2memory.max, or v1memory.limit_in_bytes, read at the process's cgroup path and then at the hierarchy root, which is what a container without a cgroup namespace sees. Since278e6a8c6: FAST 509 read 47.5 GiB on FAST's v1 limit, where the v2-only rule read 49.9;fb0dc0057, underautothey arm only once a spill is plausible (the host's bytes plus the reserve reach 85 % of the target) and then stay armed;alwaysand a fixed budget arm them from the start. At the median they never arm (waits 0.01 / 0.18 s, BIG 122). At the p75 they armed at 59.7 s, for a phase-A cost of +1.12 s against off; armed from the start, the same block cost +3.6 s (BIG 118).Measured, median block 25475471 on BIG (3 runs per arm, one binary per job):
auto(default)auto, 70 GiB targetalwaysalwaysalways(first measurement)ac406fc8…holds under the default (BIG 117) and underalways(BIG 114–116).Blocks up to full gas on BIG (128 GiB host, 120.69 GiB cgroup; one run each):
edddc6873035aef5d6035aef5d6autopurged at phase-a (121.19 → 82.32 GiB) and at the base (116.53 → 48.45). Its verifier passed cold and warm (66.59 / 61.61 s, pool high-water 17.39 GiB; BIG 125). With both purges forced on the pre-gate train it read 549.32 s at 111.30 GiB (BIG 1215).The allocator purge. The harness's jemalloc never purges (
dirty_decay_ms:-1), and a phase rarely reuses the pages the phase before it freed, so the host ratchets from phase to phase. Onearena.<all>.purgehands every arena's dirty pages back.autopurges only once the spill's budgets arm, so a block that fits printsALLOC PURGE <point>: skipped (auto, no memory pressure)and pays nothing (FAST 515 at 1×, BIG 123 at the median).What the cost follows. While the writer runs, the generators slow by ≈ 16 %. The writer's two passes over every spilled byte (digest, then the aligned copy) and the frees of the spilled buffers both contribute; BIG 115 and 116 could not split them further. At the median, phase A is bound by the generators during the walk, so the walk waits on them.
alwayshands ≈ 39 GiB to the writer during the walk, for +6–7 s.always: digest 22, aligned copy 28, pwrite 14. The pwrite runs at the disk's own rate (3.7 GiB/s raw). The BLOCK SPILL line prints the split.Measured and not landed (each has a FAILED-LEVERS row):
Tests:
always, a zero budget andautobuilds the resident traces;autoreads the host's working set, and the target reads v2 and v1 cgroup limits;autopurges only under memory pressure.