WHIR recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 60.20 s - #1004
Closed
MauroToscano wants to merge 1067 commits into
Closed
MauroToscano wants to merge 1067 commits into
MauroToscano wants to merge 1067 commits into
Conversation
The cap on the genesis leg is a budget, and the quantity that decides whether a real block fits under it is a function of the ELF and the touched page list alone — no proving. This counts it, per page and in one line, so the number can be read off a box run rather than argued. It prints the split as well as the total: many pages with a handful of entries each is a different situation from one dense page, and a total alone cannot tell them apart.
…esidue Two gates whose absence was the same defect in two places — a check reading a quantity that cannot move under the failure it is for. The cross-epoch wrap's published set is now compared word for word against the roots the DRIVER took from the host verification's own return, not against anything this program derived. The emitter reaches the same roots by accumulating `num_polys()` over the group layouts it built, so the two index arithmetics have to agree — which is what a bookend root published for the wrong epoch would break, and what nothing else in the suite could see, because such a program executes perfectly. It carries its own anti-vacuity arm: two epochs whose bookends were committed under equal roots would make a transposition invisible, so the roots are asserted pairwise distinct first. The global statement's pad identity was pinned at ONE shape, where the pad is zero and both sides of the comparison are zero with it. A form wrong by a multiple of eight would have agreed. The sweep asserts the identity at every residue and asserts that it REACHES every residue, because a sweep that only visited pad-0 shapes is the same vacuous check with more iterations.
Beside the OFFSET ramp's gates and on the same terms, because the two legs make the same kind of claim and fail in the same two ways. The value arm runs the EMITTED leg against a real page's INIT column — `page::preprocessed_columns`' own column 1, not a hand-built vector — and compares it against the host's 2^18 fold and against the host closed form, three derivations with no two sharing an author. Its anti-vacuity assert is on the ANSWER: a leg whose coefficient loop emitted nothing returns the interned zero, and over a column with support the true value at a random point is not zero. The bit order gets its own arm because a convention agreed with itself is not pinned: the reversed point must give a different value AND must disagree with the fold, so the claim is checkable rather than shared. The row form is a DELTA between the leg and a control that publishes the same one word, which cancels the hints, the publish and the compiler's overhead. Three column shapes, including the empty one, where the leg costs its complements and nothing else.
… is really for The genesis cap is a budget and the quantity that decides whether a real block fits under it has never been measured. This is the arm that measures it: a block bundle, the census printed BEFORE the program is built — so that if the census is over the cap, the refusal lands in a log that already carries the number instead of as a panic with no context — then the program, its arena and its F1 decomposition. Nothing it asserts is a count. The block's shapes move with the posture and pinning them would pin the posture; what it asserts is structural and holds for any program. It SKIPS on a missing ELF and says so on its own line, so a laptop run cannot be read as a block run, and it prints the ELF's full 64-hex sha256 rather than a prefix.
…-epoch plan `whir_real_global.rs` is byte-identical to 2f07293's again. The page configs the route table reads are built where they are used — in the cross-epoch plan, from one call of `global_memory_configs` with the arguments `global_airs_for` itself passes. The emitter now NAMES its ELF and refuses a different one. That is better than the field it replaces, not just less intrusive: the genesis binding is the ELF, so taking it explicitly and asserting its digest against the one the harvest recorded makes 'emitted against the ELF the proof was verified against' a build failure instead of a trust. An emitter handed another ELF would intern another page's genesis bytes and refuse hundreds of thousands of rows later, with nothing naming why. The census is prove-free and card-free, as it should always have been: the touched page list is an EXECUTION fact, so it comes from `block_page_census` — the guest run with every prove and trace build omitted — and the census needs no proof, no AIR and no bundle. Its box arm refuses to run unless the caller states the ELF's and the input's full sha256, because the page list is a function of both and a census quoted against the wrong input is this campaign's own recurring defect. The refusal arms are rewritten around inputs rather than a derived flag: one restates `num_private_input_pages`, the other rebuilds the AIR set through the verifier's own helper with a different count. Both are disagreements a real caller could produce. The cap is renamed MAX_SPARSE_INIT_ENTRIES and documented as a PLACEHOLDER owed a census, with the fallback named where it is refused — a host commitment absorbed in the roots block, opened through a Prepared that carries a table list — so the choice is made against the number. And the statement the interned genesis bytes owe is written where they are interned: not 'a root equals compute_precomputed_commitment's inputs' but 'the interned entries are the ELF's genesis bytes at those page bases'.
…he caller `verify_global_bookends` read the process knob for itself, which meant nobody could ask it the one question that matters: was the bundle in front of it proven under this hash? `whir_hash_knob::selected()` says what the process proves under, never what a proof was made with. The agreement is the verification — the transcript's sponge is part of the configuration, so a bundle proven under the other hash diverges from the first squeeze — and a function that chooses for itself cannot be pointed at it. So the bookend form takes `H` and the dispatch moves out to its two callers, which is the arrangement the epoch half already had at `verify_epochs_bookends`. Nothing about what is checked changes: both entry points pick the same `H` from the same knob the macro read one level down. The byte gate and the transcript pin pair are the proof of that, and both are unmoved — keccak 0bc7b999…a60ee25e, rpx fcfbf8fe…6d044682, IDENTITY-LEN 6904 under both; prove (585155, 186256, 3251) and verify (585307, 186286, 3251). It is also `pub(crate)` now, because a driver that could only reach the `bool` form had to re-derive the bookend roots from `stacks` — a second spelling of the group split, which is the drift this lineage keeps finding. The new test is the cross-epoch sibling of the epoch's hash-refusal arm, and it could not have been written before: it asserts that EXACTLY ONE of the two hashes accepts, which is at once the control and the refusal. The shape is knob-independent on purpose, since the bundle is proven under whatever the suite was started with, and it prints which direction the run exercised rather than leaving a reader to infer it. Two more things this file's suite was missing. Every refusal test now asserts its untampered bundle VERIFIES through the same call first. Without that a verifier broken in any way keeps them green, because one that rejects everything rejects the tampered bundle too; on the restated-page-set arm it carries more weight still, since that arm accepts `Err` as well as `Ok(false)`. Breaking the cross-epoch AIR set reddened one of nineteen tests before these controls. And `a_swapped_bookend_root_is_caught_by_the_binding` reaches the comparison the file is named for. Nothing else did: the arm that moves a root inside an epoch's own proof is refused by the epoch half, which returns before the cross-epoch half runs at all, so its claim that "both halves still verify on their own" was false for the tamper it performs and is withdrawn here. Swapping two epochs' roots inside the CROSS-epoch proof leaves every root a valid commitment and only the order wrong, which is the one tamper `proved == chained` exists to catch.
…urface `real_global_from_whir_continuation_under::<H>` now exists and means something: the verifier's bookend form takes `H`, so the driver can ask whether the bundle was proven under the hash it was told, exactly as the level-0 driver does. The dispatching wrapper above it stays the production entry point. The bookend roots come from that verification instead of being re-derived here. The earlier draft rebuilt them from `stacks` because only the `bool` form was reachable, and a second spelling of the group split is the thing the AIR set was extracted to prevent — the same argument, applied to the values the published set carries. The module-scoped `dead_code` allow is gone. Its condition was a production reader that does not exist yet, so the honest remedy is to publish the surface rather than suppress the reports: the module, the struct's fields and its accessors are `pub`, and `WhirGlobalAirs` with `refs()` and `groups()` are `pub` beside them, since the accessor returning one cannot be public while the type is not. Measured, not asserted: `cargo clippy -p lambda-vm-prover --lib` reads 0 errors and 0 dead_code mentions with the allow removed, where it read three errors with it needed. The level-0 driver's allow is untouched; its condition is documented and it has its own reader coming. Each refusal arm here gained the honest-path control too — the untampered bundle must harvest through the same call before the refusal below it proves anything.
`zeroing_the_nonces_does_not_make_a_ground_proof_reproducible` was
intermittently red — three green of four full-suite runs on one tree,
eight of eight in isolation, on a path no recent commit touches.
Its early return guarded on ALL the nonces while the assertion below
sees only what `after_the_grind` captures: `final_value` per chain and
`(next_root, ood_value)` per round. A chain's LAST round has both of
those `None`, so a run whose only differing nonce is there passes the
guard and then compares two equal lists. ? INFERRED from the failing
output, whose per-round tuples were identical and whose third entry was
`(None, None)`, rather than from instrumenting the search:
left: [(.., [(Some([118, 57, ..]), Some(..)), (Some([99, 165, ..]), Some(..)), (None, None)])]
right: [(.., [(Some([118, 57, ..]), Some(..)), (Some([99, 165, ..]), Some(..)), (None, None)])]
So the guard now reads the nonces of the rounds that have a successor,
and a probe makes the narrowing observable rather than asserted: it
clones the proof, flips a nonce in every successorless round, and
asserts the round exists at all, that the probe really moved a nonce,
that the narrowed guard does NOT see it, and that nothing the assertion
compares moved either. Widening the guard back turns the third of those
red without waiting for the flake to recur.
The doc sentence claiming a failure here "would fail LOUDLY and name the
reason — which is the correct outcome, not a flake" is withdrawn in
place. The observed failure named nothing; a guard that admits a state
its assertion cannot satisfy is a check that cannot pass, not a report
about the prover.
…r/lfm-l0 Thirteen of seventeen overlapping files conflicted. Every resolution below is semantic; none is "take one side" except where stated, and the reasons are here rather than in a note because the next reader of this history is the one who needs them. ## The encoding: two branches that both reached the same version number DOMAIN_TAG was V4 on this lineage (it appended TableCounts::blake3) and V4 on main (it appended the six accelerator counts). Two different encodings, one suffix — exactly what the tag exists to prevent. Every tag, with what it was on each side and what it is now: LAMBDAVM_STARK_STATEMENT lineage V4 | main V4 -> V5 LAMBDAVM_CONTINUATION_EPOCH lineage V3 | main V4 -> V5 LAMBDAVM_MULTILINEAR_STATEMENT V1 -> V2 LAMBDAVM_MULTILINEAR_CONTINUATION_EPOCH_V1 unmoved LAMBDAVM_CONTINUATION_GLOBAL_V2 unmoved LAMBDAVM_MULTILINEAR_CONTINUATION_GLOBAL_V1 unmoved The three that moved are the three whose statement absorbs the shared per-table count list, which grew by six. The two GLOBAL tags bind no per-table counts, so nothing in their encoding moved. Every suffix is the same byte length as the one it replaces, so no length-derived constant moves with them. ⚠ MULTILINEAR_CONTINUATION_EPOCH is the one to read twice: its statement DOES absorb the count list, through the same shared helper. It is left unmoved deliberately — the WHIR epoch encoding is not this port's to version, and the one thing that would force it (binding is_final there, as the STARK statement now does) is explicitly out of scope and recorded as an owed parity question. Whoever takes that question up bumps this tag with it. RECURSION_INPUT_VERSION had the same collision and it is the dangerous one. The prefix version IS checked — recursion_archive_bytes refuses a number it does not recognise before the guest's in-place read — so an OLD blob fails legibly. What that check cannot catch is two DIFFERENT layouts sharing one number, which is what we had: the lineage's v3 is the one-epoch-proof format, main's v3 is the six accelerator fields, neither archive can read the other, and both claimed 3. Keeping either suffix would have been a silent misread at the wrong offsets. The merged format carries both changes and is v4, so every recursion guest ELF must be rebuilt; one built before the bump refuses a v4 blob rather than misreading it. ## The count list The merged count list is TWENTY-ONE, and that number was read rather than assumed: the two sides' additions are disjoint. Main's TableCounts has no blake3 field, the lineage's has no accelerator fields, and the fourteen split families are shared — 14 + 6 + 1 = 21. Each of the twenty-one reads a distinct source in Traces::table_counts (twenty Vec lengths and num_blake3_ops), so no table is counted twice. Getting this wrong in the other direction would not have failed a test: both sides of the transcript pin derive the term from NUM_TABLE_KINDS, so a double-absorbed blake3 would have moved the pin and still passed it. statement.rs keeps this lineage's shared helper (absorb_table_counts / table_count_values) rather than main's inlined loop, and the six accelerator counts are added to it. That is the whole reason the WHIR statements pick the change up for free: multilinear_continuation::absorb_epoch and multilinear_prove both call the same helper. NUM_TABLE_KINDS 15 -> 21, and the exhaustive destructure that makes a new TableCounts field a compile error moves with it. lfm/statement_replay.rs carries a second, independent copy of that number (NUM_TABLE_COUNTS) for the guest-side replay: 15 -> 21, plus the trailing is_final byte the host now appends. Nothing in the type system ties the two constants together; what catches a drift is the host-versus-machine challenge equality in lfm/algebraic_transcript.rs and its hand-written array literal, which fails to compile if one moves without the other. ## AIR sets and table counts FIXED_TABLE_COUNT 11 -> 5. The lineage's eleven minus main's six accelerators is exactly main's five (BITWISE, DECODE, HALT, KECCAK_RC, REGISTER), so the two lists agree with no residue. BLAKE3 stays a counted chip and joins main's accelerators in TableCounts::validate's at-most-one list, where its own upper bound used to sit alone. VmAirs takes main's counted vectors for the six accelerators and keeps this lineage's conditional BLAKE3. Git line-merged both models into a body that pushed the singular AIRs and then looped over the vectors; that could not have compiled, and it is why this file was read rather than merged. BLAKE3 now closes the accelerator group instead of keeping its old slot between KECCAK_RC and ECSM, because that slot no longer exists — air_trace_pairs and air_refs make the identical choice, which is the only obligation, since those two orders are the proof's sub-proof layout. ## Two of main's hunks were NOT taken, and one of its fixes was Not taken: main builds keccak_rc and register with with_preprocessed (the commitment alone) where this lineage builds both with with_preprocessed_columns (commitment plus the column generator). Taking main's would have reintroduced the defect 5e3df0c fixed — the multilinear verifier checks the precomputed columns' claimed openings, and an empty column list is checked in zero iterations, which left REGISTER's INIT and FINI prover-chosen on the WHIR path. Git reported this as an ordinary content conflict with nothing to say that one side was a soundness fix. ANY FUTURE MERGE OF MAIN MUST MAKE THE SAME CHOICE. On the production VM path every preprocessed AIR takes the columns form — bitwise, keccak_rc, register, the page AIRs, and continuation.rs's. A main-side hunk that reverts one of them to the commitment alone is a soundness regression wearing the clothes of a whitespace conflict. Not taken: prove_elfs_tests' bus harness keeps hash_pin's pinned transcript and BlockVerifier, where main's hunk used a plain DefaultTranscript and Verifier. Main's improvement to that harness IS taken — a prover failure now panics instead of returning false, so a negative test cannot pass because proving fell over. Main's target-moved recheck is rewired to the same pinned transcript as the accepting arm; on a plain one the two arms would have differed by hash as well as by target, and a rejection would no longer have been evidence about the target. Taken: main's missing count_merkle call in the field_element Merkle backend. merkle_nodes was NOT a subset of merkle on this lineage's byte path — the algebraic path's count_merkle_node_direct bumps both counters, the byte path's hash_new_parent bumped only the node one. The printed merkle figure therefore moves by exactly merkle_nodes on keccak runs. No pin reads it. ## hash_metrics: the same module, written twice An add/add conflict. Both branches independently wrote crypto's hash_metrics with the same feature name, the same Counts type and the same closing doc paragraph. Resolved as a union: this lineage's transcript counters (which is where the WHIR transcript pin is read from) plus main's perms counter and count_finalize(nbytes). Both count_finalize and count_total survive on purpose — only the byte sponges can report a byte count, so the algebraic backend keeps count_total and leaves perms alone, which is right, because perms counts keccak-f permutations. crypto/stark/src/grinding.rs takes this lineage's re-export shim. Main's two count_grinding calls are not lost: the implementation moved to crypto's own grinding.rs, where both calls already sit in the same two functions main instruments. Both Cargo.toml conflicts were the same hash-metrics feature declared identically on both sides. ## Consequences this merge is expected to have The STARK wraps' programs move, and in both directions. Empty legs are elided, and separately the LFM statement's fixed part goes from 215 to 264 bytes, so its trailing residue moves from 3 to 0 mod 4 and the Phase-A shift-3 splice statement_replay's own doc complained about should disappear. The WHIR transcript absorb totals move by six per epoch statement; the pin derives that term from NUM_TABLE_KINDS rather than carrying it as a literal, so the expected value tracks. recursion_smoke_test's keccak_permute census numbers predate this merge and are a print rather than a gate; they are flagged in place.
…s, not during E0506: `airs()` borrows the whole driver and the lie this arm tells is a field of it, so the precondition read and the restatement could not both hold the same value. The precondition now reads in its own scope and the AIR set is re-taken after the field moves. Nothing about what the arm proves changes. It still restates an INPUT — a bundle whose page is a private-input page saying the run had none — against an AIR set that is untouched, so the route expects OFFSET and INIT from a table presenting OFFSET alone.
THE REFUSAL WAS A CONFIGURATION MISMATCH, not a defect in the emitter, and the gate log said so on a line above the failures: the process ran under LAMBDA_VM_WHIR_HASH=keccak256. The machine's transcript is the ALGEBRAIC sponge — `whir_transcript.rs`' own header says the mirror is `DefaultTranscript<E, RpxTranscriptHash>` — while the cross-epoch prover and verifier both dispatch on that process-wide knob. So the bundle carried a keccak challenge stream, the machine replayed an RPX one, every challenge diverged from the first squeeze and the honest proof refused at its first table. The level-0 suite documents this exact failure at `driver_bundle` and solves it by re-proving its epochs under a literal RpxWhir; that door is shut here, because `prove_global` and `verify_global` take no hash parameter and re-implementing either in a test would be a second derivation of the cross-epoch prover. So the three executing arms are `#[ignore]`d with the knob named in the reason AND refuse when run in the wrong posture. Both halves: the ignore keeps a default run from going red over a configuration it never set, and the refusal stops a deliberate run in the wrong posture from producing a DivByZero nobody can place. A silent skip would have been neither — green in every default run, and unable to fail. A refusal now names its site. `locate_addr` maps the reported address to the instruction that wrote the cell and its neighbours, because a DivByZero is always a failing equality and the address always names the difference. THE POOL is asserted as what is provable and pinned as what is not. Two emitters intern words no cost form reports — `emit_newton_step`'s interpolation weights, and the chains' own constants, which `own_constants` says in its doc it does not name. So the words the form NAMES must all be interned, which is exact and is what a deleted leg breaks, and the unnamed remainder is pinned at the measured 24 with its source named. The total assert is gone: `ops` is defined as the total minus the other three, so given their asserts it had two sides that could not differ. Also: clone_on_copy on two Copy field elements, and rustfmt.
W1g's reading — that the INIT leg cannot be gated on `test_private_input_xpage` at all — is right, and the answer is the second fixture rather than a comment claiming coverage. So the route table now names, per arm, the run that reaches it and what that run cannot show. The part worth writing down is the third row: NEITHER fixture shows the MIX. A block carries both kinds of page at once under a sixteen-group split, while at fixture scale the page group is a singleton and shape-identical to a bookend's. So the route table's behaviour on a mixed set rests on the block instrument, and saying that is the difference between a stated gap and a false guard.
THE ARM COULD NOT HAVE FAILED, AND ITS MUTATION COULD NOT HAVE FIRED. It ran on the `data_page_touch` bundle, which reaches ONE epoch — the box read its published set as `publics 6`, which is `2 + 1 x 4`. With one epoch the pairwise-distinctness guard iterates zero times, and reversing a one-element published order is the identity, so the transposition mutation the arm exists for was a no-op against a check that was vacuous. Two of this campaign's catalogued failures from one fixture choice. `test_private_input_xpage` reaches three epochs, so both become real, and the arm now refuses a bundle of fewer than two rather than leaving the next reader to notice.
Its closing assert compared `census.len()` against the touched page count, and the comment above it called that "a count that can disagree". It cannot: the census maps one entry per config and `global_memory_configs` maps one config per base, so the identity holds by construction and no input reaching this arm could make it false. A check that cannot fail, with a comment asserting the opposite. It now checks relations between fields the census reads SEPARATELY, which is where a wrong column would actually show: a page that loads no genesis bytes must have no nonzero entries, a page cannot hold more nonzero genesis than it loads, and a private-input page — whose genesis the verifier never recomputes — must be charged none. The structural identity is stated as an argument instead of dressed as a check.
W1g's `whir/lfm-global-airs` @ 1f7a0cd into the cross-epoch program's branch: the verifier generic over the hash with the dispatch moved to its callers, `real_global_from_whir_continuation_under::<H>` beside the knob-dispatching form, the driver's fields and accessors `pub`, and the identity test guarded on the nonces its assertion can see. Clean over six files, none of them this branch's, because the route decision was moved onto `GlobalPlan` precisely so `whir_real_global.rs` would stay byte-identical while W1g worked on it. ⛔ IT DOES NOT RETIRE THE EXECUTION ARMS' `#[ignore]`. The new `_under::<H>` names a hash for the VERIFY; `prove_global` still takes no hash parameter and dispatches on the process knob inside itself, so in a default process the bundle is still PROVED under keccak and the machine's algebraic transcript still cannot replay it. The posture guard stays until the prover moves too.
THE FORM EXISTED AND COULD NOT BE USED. `sumcheck_round_consts` has always counted the pairs `(1/(j+1), -j/(j+1))` a degree-d round interns, and a count is exactly what a program-level pool cannot consume: the builder interns on the canonical word, so a degree-13 round and a degree-3 round SHARE their first two steps, and adding their counts charges four words twice. There is no scalar that composes. That is why the cross-epoch pool carried an unnamed remainder, and why `StackedCost::own_constants` could only disclaim the chains' constants rather than name them — they are these, reached through the chains' own sumcheck rounds. THE LAW that makes one call enough: the pairs NEST in the degree, so the union over every round of every leg is the set for the LARGEST degree among them. `sumcheck_round_constants(D)` is therefore a whole program's Newton pool, with D read off the shapes — the GKR ladder's 3, the reduce's 2, the chain's 2, and the tables' own `sumcheck_degree()` — and never off a proof. It returns a deduplicated SET, not a count, because these are field elements and nothing forbids two pairs colliding in Goldilocks; a collision would make the true pool smaller, and the identity against the count form is asserted over a range so that such a collision is discovered as a finding rather than as an unexplained gap. The emitter and the form now share one derivation, so the pool a program pays and the pool a form predicts are the same expression. The gate against the EMITTER therefore computes its oracle a second way, in the extension field throughout, because a test whose oracle shares the function under test agrees with itself. The cross-epoch F1 asserts its pool BY VALUE in both directions and the pinned remainder is deleted.
The harness stage below builds against `real_global_from_whir_continuation_under::<H>` and `WhirRealGlobal`'s published fields, neither of which exists at this branch's base. This is the merge that brings them.
The cross-epoch F1 found the interpolation weights interned by every sumcheck leg and named by no form. The epoch program has the identical gap and has never had it measured — no F1 there compares a pool at all, only staged row deltas — so this reads the number off the assembled program and prints it for the ladder note to aim at. It asserts what is exact and prints the rest: the Newton set through the degree the program reaches must be interned, which can fail, and the remaining constants are reported rather than pinned, because naming them is other forms' work. `interned_newton_degree` reads that degree out of a pool, which gives the cross-epoch F1 a SECOND SOURCE for a number it otherwise derives from the shapes alone. The two must agree, and a disagreement is a real finding: a leg running at a degree no shape predicts, or a form naming one no leg reaches.
…ree harness Level 0 is followed by the WHIR GLOBAL stage and the interior by the ROOT over `fan_in + 1` children, so the WHIR driver composes a block artifact rather than stopping at a closed interior and printing that it is not one. `prove_whir_global_child` is a SIBLING of `prove_global_child`, not a generalisation, and the reason is a type: that one takes the STARK `continuation::ContinuationProof` and returns `Option<(RealGlobal, RealChild)>`, while the WHIR bundle is `multilinear_continuation::ContinuationProof` and its harvest is `WhirRealGlobal`. What it produces is a plain `RealChild` — that type holds the harvest of an LFM PROOF and knows nothing about what the proof verified, which is why one child type serves both families. Four differences from the STARK stage, each with its reason in the code: - it returns `Result<_, String>` and never an `Option` the caller turns into a `return`. There is no sizing arm here and nothing to stop for, so a stage that cannot build REFUSES WITH THE REASON and the caller panics with it. Inheriting the STARK shape would end the run green having composed nothing; - no slicing, no cache and no parent: at k = 1 the published layout IS `GlobalLayout`, which is the field the driver already carries, so `GlobalPublishes` and `SlicePartition` do not appear on this path; - ONE host verify. The STARK stage verifies explicitly and then `real_child` verifies again; its three reasons are all inapplicable here, and the third is answered by taking the verify through `real_child_timed` so the stage PRINTS its seconds instead of hiding it inside a harvest; - the `WhirRealGlobal` is dropped before the interior runs. It owns a clone of the cross-epoch proof and the whole cross-epoch AIR set; what survives into the root is the child and two `usize`s. The root is NAMED, not inferred: `LFM_TREE_PROVE_ROOT=1` with `LFM_TREE_ROOT_OPTION=A|B`, which carries no default. Unlike the STARK arm it needs no cache directory — this driver caches nothing, so it proves base, level 0, the global child, the interior and the root in one run. The interior stops at `RootOption::child_level(top)`, the same named rule the STARK driver reads, and the child count is checked in the driver where the levels are still in view rather than inside `emit_l2g_compare`'s refold guard a root emission later. The knob refusals now carry one reason each. A blanket message would have survived this change and gone on saying "the cross-epoch WHIR program is not written" about knobs whose real objection is that the wrap is unsliced or that nothing here caches. The fixture arm is renamed for what it now proves: it runs the global stage, the interior at option A's child level and the root, and its "interior closes to one proof" assert becomes the option's own count guard plus the artifact's width. Two limits are stated in its doc: at `test_private_input_xpage` the cross-epoch shape is three bookends and one PRIVATE page, so the OFFSET+INIT route 30 of the block's 35 pages take has no table and no gate here; and only option A is emitted at fixture scale.
… closes THE SAME DEFECT, TWICE. `fold_coset_consts` has always counted what `emit_fold_coset` interns — `two_inv`, the `pow_bits` factors `g^(2^i)`, and each level's stride — and a count is exactly what a program-level pool cannot take, because counts ADD where values MERGE. That is why 24 words survived the cross-epoch F1's `unnamed` assert while this very fold suite was green on their number. ⚠ AND THEY DO NOT NEST, which is the difference from the sumcheck round's Newton pairs. Those nest in the degree, so one call at a program's maximum is its whole pool. These are `g^e`: round r folds over the base domain squared once per scheduled variable, so the same exponent set under a different generator gives different field elements. A program's fold pool is a genuine UNION over its distinct domains, and a form taking a maximum here would be wrong in a way the other is not. The values form also retires an assumption the count had to make. The count's own doc calls its last term 'assumed distinct from every g^e … an assumption about a discrete log, not a proof'. Deduplicating by value does not need it. The exponent set is extracted so the count and the values share ONE derivation, the chain-level union advances the domain by k squarings a round exactly as the emitter advances it, and the fold suite's existing count gate gains a VALUES arm in both directions — a word named but not interned would make a pool over-count, and a word interned but unnamed is the same gap one level down. Why the values looked like powers of two: in Goldilocks 2^48 is -1, so the small-order roots of unity are clean powers of two and 2^48, p-2^24, 2^39, p-2^60 are generator powers, not weights.
…column that is not the first The cross-epoch INIT hybrid needs a prepared commitment over the dense genesis pages' INIT columns. Those columns live one per GLOBAL_MEMORY page table, each claimed at that table's own reduced point, and INIT is preprocessed column 1 of `[OFFSET, INIT]` — so neither "which table" nor "which column" survives the `(table index, leading count)` shape `Prepared` and `PreparedCheck` had. Both now carry `at: &[PreparedColumn]`, one entry per stacked column, naming a table and one of its preprocessed columns. DECODE is the single-table prefix case and reaches it through `leading_columns`, so nothing about what DECODE means moves. No change below `crypto/stark`: `Claimed::PerColumn` has always resolved the point per column — it is what every group opening in `multi_prove` uses — so the generality was in `stacked_eval` all along. A single-table commitment produces the same weight shares under `PerColumn` as under the `Shared` it replaces, because `Claimed::point` hands back that one point for every column and `Claimed` reaches nothing but the weight; the epoch byte gate is the assertion of that rather than this paragraph. `check_preprocessed` takes the set of settled indices instead of a leading count, and `multi_verify` asserts the set is distinct — the distinctness a count got for free, and without which one stacked column could stand in for two skipped checks. The claim a prepared column is settled against is resolved through `preprocessed_source`, the same `slot_of` -> `kinds` -> `source.column` chain `check_preprocessed` walks, rather than by assuming preprocessed column `c` is value `c`. The identity does hold today — `LeafLayout::build_live_over` registers main columns first and in index order — but the opening and the skipped check must agree about which claim they mean, and one derivation is how they cannot disagree. Tests: a two-table stack over EQ's column 1 and LT's column 1, settled at the two points those arguments reduced to; the honest round trip first, then the two columns swapped at the verifier, then one column named twice. The two stacked columns are asserted to differ before anything is proven, so the swap arm cannot be a no-op.
…lter that must precede it Which cross-epoch genesis pages a prepared opening carries, as one closed form that the prover, the verifier and the in-guest emitter each evaluate from their own inputs. They must reach the same set: the stack's root is absorbed in the roots block, so two sides that disagree about it diverge at `z` and the proof dies with nothing in it that names why. A page joins when its sparse leg alone would cost more than the ENTIRE prepared leg could. That is conservative on purpose — every page that joins pays for the whole stack by itself — and it is a pure function of that page's own nonzero count, so the answer never depends on which other pages joined or in what order they were considered. The marginal-plus-fixed alternative is cheaper by a few tens of thousands of rows and buys an order-dependent set; on the block the choice is moot, because the three dense pages are 12x to 24x over the budget and the 27 zero pages are four orders of magnitude under it. `PREPARED_LEG_ROWS` is a constant and not a call because this module sits below `crate::lfm`, where the chain cost forms live. A routing rule the protocol depends on must not move with the machine's row accounting. Its provenance is V1i's measured 175,066 chain rows at 24 variables — larger than the block's 20-variable stack, so the budget errs toward leaving pages sparse — and is to be asserted where it can be computed rather than restated here. The private-input pages are filtered out FIRST, and not as an optimisation. The prover builds their configs with the private bytes and the verifier with an empty vec, so a threshold that read those bytes would select different sets on the two sides. The filter is on `is_private_input`, which both derive from `page_base` and `num_private_input_pages` — values the cross-epoch statement already binds — and the reason is that a private page presents no INIT preprocessed column to settle at all. Tests reproduce the box census of block 25368371 to the row: the three dense pages' legs plus 27 interned zeros is 10,249,056, the figure `cens2` read. The selection is pre-registered as exactly `0x0`, `0x40000` and `0x280000`, with the existing fixture's 112-entry page asserted sparse — the fixture gap, stated rather than hidden. A private page holding 20,000 nonzero bytes is asserted not stacked, and the prover's and verifier's views of that page are asserted to plan identically.
… prover can be asked which one `verify_global_bookends` stopped reading `whir_hash_knob::selected()` for itself at 1f7a0cd; `prove_global` still did, and that is what kept the cross-epoch execution arms `#[ignore]`d behind a posture guard. A function that reads the process knob can only be asked what THIS PROCESS proves under, never what hash a bundle in front of it was argued with, so a test that wanted a bundle under a named hash had to re-implement the prover. It is now `prove_global<H>` with the dispatch at its two callers, which is the arrangement `prove_epoch` and `verify_global_bookends` already have. The second reason is coming: the genesis stack is a `StackedCommitment<F, H>`, whose type names the hash, so it cannot be built outside a dispatch and handed in — the same argument that made `prove_epoch` generic so DECODE's commitment could outlive one call. In `prove_continuation` the epochs and the cross-epoch proof now share one dispatch. They read the same knob before, in two places that happened to agree; a bundle whose two halves were argued under different sponges is no longer spellable. The test this makes possible asserts the agreement in BOTH directions and without the knob: each proof is built under a named hash and checked under both, so the diagonal accepts and the off-diagonal refuses. W1g's sibling could only say that exactly one of the two accepts, and its own doc records why — under a default keccak suite a verifier dispatch pinned to keccak is indistinguishable from a correct one. The diagonal is asserted first, as the control a refusal needs, and the two proofs are asserted to differ so the four readings are not about one object. The boundaries come from `for_each_epoch` with a closure that proves nothing, which is the same walk `prove_continuation` uses and costs the execution alone. Re-deriving them would have been a second spelling of the epoch split. This retires the reason for V1j's three `#[ignore]`s on the executing cross-epoch arms. Those tests are V1j's and are untouched here.
… dense genesis pages The protocol half of the INIT hybrid. `prove_global` commits the dense pages' INIT columns as one stacked polynomial and hands it to `multi_prove`, whose roots block absorbs its root before `z`; `verify_global_bookends` recomputes the same commitment from the page configs its AIR set was built from and settles the opening through a `PreparedCheck`. Each column is settled at its own page table's reduced point, which is what the multi-table `Prepared` exists for. `WhirGlobalAirs` now carries the configs its page AIRs were built from. The verifier needs them to decide the stack's membership, and calling `global_memory_configs` a second time to get them is precisely what that struct's own doc forbids — two call sites that agree today is the shape that let REGISTER's preprocessed columns and its root describe different tables. The plan comes from `WhirGlobalAirs::genesis_stack`, so the table index a stacked column names is derived from that set's own bookend count rather than restated by a caller. ★ NOTHING IN FLIGHT MOVES. `genesis_prepared_for` returns `None` when no page is dense enough, and every existing fixture is entirely sparse — the worst is `data_page_touch`'s 112 nonzero entries against a threshold of 9,725. Those proofs absorb no extra root and are byte for byte the ones they were. Only a bundle with a dense genesis page changes, and none exists yet outside the block. So a fixture was written rather than an assertion added: `dense_data_page_touch.s` is `data_page_touch` with its touched cell surrounded by 32 KiB of non-zero bytes on each side. The fill is on BOTH sides because where `.data` starts inside its page is the linker's business — with `n` bytes either side the counter's own page holds at least `n` of them at any offset, while a single trailing fill would leave a counter near the end of a page with almost none. The floor is 32,768 nonzero entries, 3.4x the threshold, so the fixture is not sized to just barely qualify. ⚠ There is deliberately no `agrees_with` on the genesis stack. DECODE asserts its commitment's blowup and folding against each epoch's config because it is built once per ELF at a config of its own shape; this one is built per proof from the very config the same call hands `multi_prove`, so a comparison between them could not fail. The note says what a per-ELF cache would have to bring back with it, and states the cost this pays instead: one commitment over `n_dense * 2^18` elements per prove and per verify, against 10,249,056 rows of in-guest sparse evaluation. Tests: the dense fixture's plan is printed page by page and asserted to put exactly one page over the threshold BEFORE anything is proven — without that precondition the test would take the `None` path and pass while checking nothing — then the proof is asserted to carry an opening and to verify. The sparse control asserts the opposite for `test_private_input_xpage`: no opening, because a route that fired everywhere would make the "nothing moves" claim false.
…ld budget asserted where it can be computed TWO THINGS THE ROUTE OWED. ★ The provenance of the fourth per-ELF pin. The in-guest verifier cannot recompute the genesis stack's commitment, so it interns the root as program text exactly as it interns DECODE's, and the statement owed out of band is "this root is the commitment to the ELF's genesis bytes at the dense page bases, under this blowup and folding". Nothing inside the program checks it; this is the evidence for it. The test rebuilds the stacked columns from the ELF's PT_LOAD segments byte by byte and compares the roots. ⚠ The second derivation is the whole value: built through `build_initial_image_paged` or `page::preprocessed_columns` — the functions the production path uses — it would compare a value with itself and pass for any pair of agreeing bugs. It also asserts each rebuilt column carries more nonzero entries than the threshold that put its page in the stack, because two all-zero stacks match and say nothing. The comment says in as many words that these are NOT the 35 univariate roots `recursion::precomputed_commitments` builds. Those are Merkle roots over each page's LDE codeword and are what `program_id` folds; no multilinear verifier compares one, and a stacked WHIR commitment has no per-page subtree to match against them. ★ `PREPARED_LEG_ROWS` is asserted in `lfm`, where the cost form lives. The constant sits in `genesis_stack` below `lfm`, because a routing rule the prover, the verifier and the emitter must agree on cannot depend on the emitter's row accounting — but a constant nothing checks is a number that drifts. The assertion is a BAND: the budget must be at least the 20-variable stack the block uses and at most the 24-variable one it was read from, which is the conservatism the threshold's own doc claims. An equality would redden on any schedule change, which is a different finding from a drifted routing rule. ⚠ PRE-REGISTERED AS A READING THAT MAY GO EITHER WAY. V1i measured 175,066 at 24 variables "at the harness's own posture", and this asserts at `config(112, 20)` — blowup 2, fold 4, the production figures' shape. If the two postures disagree, this goes red and the finding is that the constant's provenance is at a posture the production path does not use, which is worth knowing and is the reason to assert rather than assume. A second test makes the insensitivity argument against the cost form rather than the constant: across every stack width from 18 to 25 variables the chain figure stays between the block's dearest sparse page (18 rows) and its cheapest dense one (2,100,474), so the same three pages are selected at any of them.
…pied from a log `genesis_stack`'s unit tests encode the block's census as numbers transcribed from a box log. That checks the closed form's arithmetic and nothing else: a routing rule whose pre-registration rests on a transcription is a rule nobody has run against the program it routes. This arm executes the block guest once — no proving, no card, the same shape as the census arm it sits beside — rebuilds the page configs from the ELF, evaluates the plan on them, and asserts the dense set is EXACTLY `0x0`, `0x40000`, `0x280000`. ⚠ The assertion is the SET AND ITS ORDER, not a count: three pages of the wrong three would satisfy a count, and the stack's column order IS page-base order, so the order is part of what the opening means. A second assertion requires the pages left sparse to cost under a hundredth of the pages stacked, because the hybrid's whole claim is that the genesis bytes are concentrated. It REFUSES rather than skips when the two shas are unstated, and SKIPS with its own line when the ELF is simply absent. Two outcomes with two meanings: a routing quoted as a fact about one program and one input must not be producible from an unnamed pair, while a missing build product is not a failure of the check.
…fix contract stays put
The lead ruled against generalising `check_preprocessed` from a leading count to
a set of column indices, on V1j's recommendation and for the smaller review
surface. This puts that contract back and shapes the prepared commitment to fit
it instead.
What generalised is the TABLE LIST — the cross-epoch genesis stack covers one
GLOBAL_MEMORY table per dense page, each settled at its own reduced point, which
is the part DECODE's single-table `Prepared` could not describe. What did not
generalise is which columns of a table an opening may settle: still its leading
`n`, still a count, and `verify`/`check_preprocessed` are byte-identical to what
they were.
So a cross-epoch page's stack carries BOTH its preprocessed columns rather than
INIT alone. The page loses its OFFSET ramp, which on the block is 51 rows lost
against 282 gained by three more stack columns — about 230 rows on a hybrid
costing order 10^5, in exchange for a contract in `crypto/stark` not moving.
`prefix_at` is where the two meet, and it refuses rather than reinterprets. A
`PreparedColumn` list CAN describe `{1}` or `{1, 0}` or two tables interleaved,
none of which a count can express — so a commitment shaped that way is an error,
not a silent reinterpretation that would have the host skip columns the opening
never settled while every value gate stayed green.
`prepared_runs` is the one walk that enforces it and also pins the stack's COLUMN
ORDER: `at` visits each table once, in the order the stack's columns were
committed, so run `k` settles table `k`'s claims. A reordering would settle one
table's columns against another's commitment — the failure the AIR set's
single-derivation rule exists to prevent, one level down.
The shared `slot_of` resolver goes with the set change, per the same ruling. The
gather survives because a stack spanning several tables still needs one, but each
run is contiguous and is that table's own leading columns, which is exactly the
old single-table behaviour extended.
Tests rewritten to the new shape: the honest two-table round trip first, then the
two tables swapped at the verifier, then a non-prefix, then two tables
interleaved. The two tables' stacked columns are asserted to differ before
anything is proven, so the swap arm cannot be a no-op.
…e emitter gets what the verification consumed
Three rulings, applied together because they touch the same objects.
THE SPLIT LIVES IN `continuation.rs`, not in a module of its own above it. Three
parties evaluate it — the cross-epoch prover, its verifier, and the in-guest
emitter — and they must reach the same answer or the stack's root differs and
the transcript dies at `z` with nothing naming the cause. `crate::lfm` depends
on `continuation` and not the reverse, so a rule the prover and verifier
evaluate cannot live there or call anything that does. Its tariff constants sit
beside it, and `lfm::whir_chain_tests` pins them against the cost form.
A DENSE PAGE STACKS BOTH ITS PREPROCESSED COLUMNS. `check_preprocessed` skips a
prefix, and INIT alone is not one, so `{0, 1}` it is: the page loses its OFFSET
ramp and the stack gains a column. About 230 rows on the block against a hybrid
costing order 10^5, in exchange for the prefix contract not moving. A side
effect worth having: two columns means one prefix bit even at a single dense
page, so a P=1 fixture already exercises the stacked layout's prefix indicator
rather than degenerating to a flat stack.
THE EMITTER GETS THE OBJECTS THE VERIFICATION CONSUMED. `verify_global_bookends`
now answers a `GlobalVerdict` carrying both the bookend roots and the genesis
stack, and `WhirRealGlobal.prepared_roots` becomes `prepared: Option<GlobalPrepared>`
holding the roots, the destinations, and the `StackedLayout` and `Domain` CLONED
off the commitment. Handing over only roots would have the emitter rebuild those
two from the column heights and the config — a second derivation of the object
the host actually committed, which is the defect the single-derivation rule
exists to prevent, one level down.
`GlobalPrepared`'s doc keeps two obligations in two sentences on purpose: its
roots are the fourth owed per-ELF pin for the DENSE pages, while the pages left
sparse owe something else entirely — their nonzero genesis entries interned as
program constants, bound by the program id. Written as one sentence, whichever
is actually unchecked would look covered by the other.
The stack's COLUMN ORDER is asserted where the columns and their destinations
meet, not left to the fact that two walks of the same list agree today. Column
`k` is settled at `at[k]`, so a length mismatch or a reorder would settle one
page's values against another page's commitment with every individual value gate
still green.
`agrees_with` returns, on both the committed and the published form, with its
condition stated: it cannot fire while the commitment is built from the very
config the same call hands `multi_prove`, and it fires the day this becomes the
per-ELF cache it obviously should be. That is the difference between a check
that cannot fail and a check whose caller has not arrived yet.
…order is the caller's Two findings handed over by the gate lane, both in this module. ⛔ `stacks[..num_epochs]` PANICKED where the contract is to reject. Under a mutation it produced "range end index 3 out of range for slice of length 2" — a prover-side panic on a path whose whole job is to answer `Ok(None)` or an error, which is the no-prod-panic policy's exact shape. It is now an `InvalidTableCounts` naming both numbers. It is not attacker-reachable as written: `global_groups` builds `sizes` from the same `num_epochs` two lines above, so the length is right by construction. That is the reason it survived, and it is not a reason to leave it — a panic that is unreachable today and one that is unreachable by construction are different things, and only the second survives someone rewriting the construction. ⚠ AND THE "canonical page-base order `global_memory_configs` hands back" was an unsupported attribution. ✓ That function is a one-to-one `map` over the page bases it is given, with no sort, dedup or filter, so it PRESERVES an order rather than imposing one. Canonicality comes from `touched_page_bases`, which collects through a `BTreeSet` — and on the VERIFIER side the list arrives in the bundle, where it is a claim rather than a fact, bound by `absorb_global` and by the GlobalMemory bus. The doc now says that, because the old wording would have left a reader believing a duplicate base was impossible on the path where it is merely caught.
Two independent rules decide a genesis page's fate. `continuation::is_dense` decides whether the prepared opening carries it; `MAX_SPARSE_INIT_ENTRIES` decides whether the sparse leg is willing to emit it, and refuses above the cap because a column that dense has no cheap closed form. A page the threshold leaves sparse and the cap then refuses would have NO ROUTE AT ALL: too dense to emit, not dense enough to stack, and the refusal would fire on a program nobody could fix by moving either constant alone. So the two must overlap with the threshold strictly tighter, and this asserts it — 9,724 entries at the densest page left sparse, against a 60,000-entry cap, 6.2x of margin. ★ That is the state the block was actually in. V1j's block bundle arm refused at that cap — 116,692 nonzero entries on page `0x0` against a cap of 60,000 — because the prepared route did not exist and every genesis page went to the sparse leg. ⛔ Once the threshold routes the dense pages to the opening, the cap should never fire again, and a refusal from it after this lands is not a page needing a bigger cap: it is these two constants having drifted apart. The test says so where a reader meets the cap.
The lean fold the lane measured behind LAMBDA_VM_GAP_R4 becomes the emission: three LFM_XALU rows a folded value and no base row, where the classic fold spent five XALU rows, a Div and a step Mul. The folds were 57 % of a WHIR wrap's chain rows. Measured on block 25368371 (one RTX 5090, ABBA at d379cef): WHIR -2.85 s (control spread 0.10 s), level-0 instructions -8.36 M (-20 %), census -154.7 M cells, LFM_HASH rows unchanged in every wrap. The fold is verifier arithmetic inside the emitted program, so WHIR wrap and global-wrap program ids move and no proof format does; both emissions compute the same field value. LAMBDA_VM_WHIR_FOLD_CLASSIC=1 keeps the classic emission, instruction for instruction, as the A/B arm and rollback switch. It parses like the other switches (0 or 1, anything else stops the run), the emission is named on stderr either way, and with_fold_emission still lets a test build either. Tests. Figures derived by hand for the classic fold keep their values and now say so, run under an explicit Classic override: the census quote (185,509 rows), the auto-cap and first-fold derivations, and the genesis budget bands. PREPARED_LEG_ROWS stays 175,066, the classic 24-variable chain it was read from: it is a routing constant the prover, verifier and emitter share, not a figure to re-tune with the emitter. Under the lean fold the stacks cost less (20 variables 137,569 -> 108,469 rows, 24 variables 175,066 -> 140,146), so the budget over-charges them, the conservative direction, and every band test now also asserts it still covers the 20-variable stack under the lean fold. The production default chain (first6, cap auto, Q = 112) is pinned under both emissions: rows 203,426 -> 150,811 (shape rows 202,690 -> 150,075) for the default, the classic figures beside them, permutations 16,443 under both; the emitted twin checks both. The lean-only chain tests now cover both emissions: execution on accepted proofs, the five tamper sites, and the F1 closed forms. New: the fold setting's default and its malformed value.
The split the lane measured behind LAMBDA_VM_GAP_R2 (LFM_HASH as two power-of-two instances where one table would be mostly padding) pays on one pipeline and loses on the other, so it becomes a policy with a per-pipeline default rather than a knob or an unconditional rule. Measured on block 25368371 (one RTX 5090, ABBA at d379cef, on top of the BITWISE drop): - STARK: the wraps carry 276-299k hash rows in a 2^19 table and split to 2^18 + 2^15, 74.5 M census cells off each; the block ran 1.90 s faster (level 0 -1.80 s, card time at level 0 -1.94 s). - WHIR: the wraps carry 172-195k rows in 2^18 and a split removes only 2^16 rows, 21.3 M cells, less than the extra sub-proof costs (card time at level 0 +0.44 s). Each split child also costs its parent one more sub-proof to re-verify, which took four of the five fan-in-3 level-1 nodes across a power of two in LFM_XALU (+45.1 M cells each): the block ran 2.80 s slower, interior +2.30 s. chunking::HASH_SPLIT_DEFAULT is false here, the WHIR pipeline's best; the STARK pipeline's branch flips that one constant. LAMBDA_VM_LFM_HASH_SPLIT (0 or 1) overrides it either way and anything else stops the run; the process names its setting and its source on stderr. Off, every program is one table and no digest moves, so nothing is re-blessed here. The GAP labels leave the split's code and docs. HASH_SPLIT_MIN_SAVING_LOG2's doc now states the counter-pressure the block measured (15-20k more hash rows and 185-271k more instructions in the parent per split child) instead of an estimate. wrap_tests::report_census counts a split LFM_HASH as one more sub-proof, as verify does, so the census check holds under either setting. New tests: the setting's default and override (without stating the default's value, which differs per branch) and its malformed value.
The BITWISE, WHIR-fold and hash-split settings name themselves on stderr once per process so a log states which programs it proved. eprintln! writes each piece of its format separately to the unbuffered stderr, and box logs merge both streams: a whole stdout line (a census panel line, say) can land between two pieces and gain the banner's first half as a prefix, which a reader that finds a line by its start then misses. lfm::airs::announce writes the finished line in one call; the three banners go through it.
LAMBDA_VM_LFM_KEEP_BITWISE=1 claims the machine as it was, digest for digest. Pinned directly: keeping BITWISE in TrivialV0@2 and FriToyV0@2, the two registry programs that send it nothing, reproduces the program_ids their rows carried before the re-bless (ffaff6ee..., 55b64ced...). The test reads the process setting to check which mask the build took, so it holds with the opt-out set as well as without.
… drop BITWISE, the WHIR coset fold is emitted lean, the LFM_HASH split is a named policy R1: a recursion program carries BITWISE only when one of its chips sends it a lookup (opt-out LAMBDA_VM_LFM_KEEP_BITWISE=1, which reproduces today's programs; TrivialV0 and FriToyV0 re-blessed). R4: the WHIR coset fold is emitted in about three rows a value (opt-out LAMBDA_VM_WHIR_FOLD_CLASSIC=1). R2: the LFM_HASH split is LAMBDA_VM_LFM_HASH_SPLIT, off by default on this pipeline (it costs the WHIR block +2.80 s); the STARK pipeline turns it on with a separate one-line commit. Each setting names itself on stderr in one write. Measured on WHIR (jobs 52, 53): R1 -4.70 s, R4 -2.85 s, in separate ABBAs. rec-int carries the harness commit f81f0a8, merged above.
… default 25 The WHIR stack cap, how wide one stacked polynomial may get, was the constant MAX_STACK_VARS = 25 in the stark crate. It is now a field of the chain format, `ChainFormat::stack` (`StackVars`, 1..=27), carried in the `ChainConfig` both sides already build from: `commit_grouped` and the prepared commitments on the prover's side, `stacks()` in the host verifier and in the LFM's WHIR emitters. Nothing reads it from a proof. The ZF format parses it from LAMBDA_VM_ZF_WHIR_STACK (25 | 26 | 27, the stacks proved, verified and measured to fit on the 32 GiB card) and prints it in the banner (`whir_stack=…`) and on the WHIR schedule line. The default stays 25: every production proof is unchanged. The default moves to 27 in its own commit once the stack's ABBA is read. A prepared commitment (DECODE, the genesis stack) records the cap its layout was built under, and `agrees_with` refuses a reuse under another. The query count is now charged the tallest stacked polynomial as well as the widest table: a group of narrow tables can stack taller than any one of them, and `stack_height` over all of a proof's shapes bounds every group's. At every production shape the charged height does not change (the widest table already stands at or above the cap) and Q stays 112. It can move only for a proof whose stack crosses a round edge that no single table reaches, which only small test programs do. A proof stacked under one cap is refused, as an error, by a verifier at another, in both directions (caps 5 and 7 on three small tables, with the honest controls).
… cap The stack cap now decides the layout a prepared commitment (DECODE, the genesis stack) is handed, and `agrees_with` compares it. This is the negative test for that comparison: a DECODE commitment built under 27 is refused by an epoch at 25 and the reverse, each as an error naming the stack. DECODE fits one polynomial under either cap here, so the roots are equal and only the config check can refuse. With the stack left out of the comparison, the test fails.
…ACK=25 rolls back Measured on block 25368371 in an ABBA (job 152, one RTX 5090, one binary for every arm, arms A B B A): stack 27 against 25 is −13.80 s WHOLE RUN (99.20 → 85.40; A spread 0.60 s, B spread 0.40 s). - By stage: base −6.75 s, level 0 −7.10 s, interior +0.05 s. - By mechanism: the openings the wider stack removes are −9.38 s (50 base chains and 331 rounds instead of 145 and 866), against +1.98 s of commits. - Both B arms proved and verified with the identities, the census and the per-phase ledger peaks registered from E2c. No fallback, no host argument, no refused room turn. - The argument's reserved peak is 24,218 of a 25,688 MiB budget; the device peak 30,576 and 30,736 MiB. This needs the NTT grid split and the WHIR room park and resize, both in the base. What moves, and why. Every WHIR base proof whose groups hold more than 2^25 cells (every real epoch) now stacks into 2–3 polynomials of 2^27 instead of 8–11 of 2^25. With it, every WHIR recursion program id moves (the wraps, the nodes, the global wrap and the root): the wrap programs are emitted from the inner layout. No STARK proof moves, because only the WHIR layouts read the stack. Q stays 112. Re-derived for that cause: - the production-default chain config pins stack 27; - the prepared leg's one-chain pricing limit moves from 64 to 256 genesis pages, because 2 · pages · 2^18 cells must fit 2^27 (the test is renamed for the new limit); - the marginal walk now covers every single-chain bracket up to the production stack, 24 to 27, and the form holds exactly at the two new ones (115 at 26, 117 at 27); - the banner reads `whir_stack=27`, and the schedule line runs to n=27; - `StackVars::WIDEST` quotes the largest device peak of the three runs at 27.
The transcript pins were measured at the legacy WHIR format and skip at any other, printing how to reach it. With the stack at 27 by default, reaching the legacy format also needs LAMBDA_VM_ZF_WHIR_STACK=25, so the line says so.
…ap is 27 by default The WHIR stack cap becomes a ZF format lever, LAMBDA_VM_ZF_WHIR_STACK (25 | 26 | 27), 27 by default; LAMBDA_VM_ZF_WHIR_STACK=25 rolls back to today's bytes. Only the WHIR layouts read it; STARK proofs do not move. The cap travels in the chain config both sides build from, and a prepared commitment built under one cap is refused under another. Measured on WHIR (ABBA27, job 152, with the kernels and the room): -13.80 s whole run, 145 -> 50 base chains.
…d of the prover thread Both bases did each epoch's host preparation on the prover thread, just before its prove: the bitwise multiplicities, the AIR set, the epoch-local L2G trace, and on WHIR also materializing every table's main columns and checking the preprocessed ones. The global proof's preparation ran after the last epoch, in front of its prove. None of it touches the card, and the prover thread is the critical path. By default that work now runs ahead of the prover thread. STARK: a trace builder prepares each epoch after building it (`prep_epoch`), and the producer prepares the global proof once it has handed over the last epoch (`prep_global`); the prover only proves (`prove_prepped_epoch`, `prove_prepped_global`). The pipeline scope still spawns exactly the producer, the builders and the prover, and the global PROVE still runs after the scope has joined, so it never overlaps an epoch prove. WHIR: the producer prepares each epoch before the hand-off (`for_each_epoch_overlapped_prepped`, `prep_epoch_ahead`) and the global proof after the last one (`prep_global_ahead`). The proofs are the same either way; only which thread does the host work, and when, changes. `LAMBDA_VM_BASE_PREP_ON_PROVER=1` keeps the preparation on the prover thread; the setting is read once and named on stderr (`BASE PREP: ...`). `prove_continuation_scheduled` takes the schedule as a parameter, and a test per pipeline proves one run under both and compares every root that does not follow HashMap order, the statement values and the verified output. Also: `prove_continuation_keeping_decode` (both pipelines) returns the DECODE derivations the base made alongside the bundle, for a caller that reconstructs every epoch afterwards; `prove_continuation` is a wrapper that drops them. Measured on block 25368371 (FAST, RTX 5090, ABBA palindromes at 109b705, the preparation behind a temporary knob): WHIR 106.95 -> 101.55 s (-5.40), the prover thread's preparation 6.73 -> 0.00 s; STARK 121.45 -> 119.70 s (-1.75), the global prove starting 0.07 s after the last epoch prove instead of 0.98 s. Program identities and the census unchanged.
…iving them again Both production tree drivers proved the base, dropped the DECODE derivations it had made from the ELF, and derived the same values again in level 0's lead-in before the first wrap could start, with nothing on the card: the univariate DECODE commitment (STARK `EpochConstants::load`, WHIR's root) and, on WHIR, the prepared opening. The drivers now prove the base with `prove_continuation_keeping_decode` and hand its derivations to level 0 (STARK `EpochConstants::load(.., Some(c))`, WHIR `whir_level_zero(.., Some(&BaseDecode))`); the WHIR fixture tree does the same. They are the same function of the same ELF and options, so no program identity moves. A base loaded from a cache has none to hand over, and the lead-in derives them as before. `LFM_TREE_REDERIVE_DECODE=1` restores the second derivation; the setting is read once and named on stderr (`L0 DECODE: ...`), and the lead-in's own lines say which ran. Measured on block 25368371 (FAST, RTX 5090, ABBA palindromes at 109b705, behind a temporary knob): WHIR 106.95 -> 105.45 s (-1.50), the lead-in 3.13 -> 1.81 s, DECODE derivations 1.31 -> 0.00 s; STARK 121.45 -> 121.00 s (-0.45, inside the A pair's 0.50 s spread), `EpochConstants::load` 1.24 -> 0.00 s, the lead-in 5.57 -> 3.93 s. Program identities unchanged.
… of the prover thread, level 0 reuses the base's DECODE I4: each base epoch and the global proof are prepared ahead of the prover thread (the STARK trace builders or the WHIR producer); opt-out LAMBDA_VM_BASE_PREP_ON_PROVER=1, named once on stderr (BASE PREP: ...). I2: level 0 takes the base's DECODE derivations instead of deriving them again, in both production tree drivers; opt-out LFM_TREE_REDERIVE_DECODE=1 (L0 DECODE: ...). No proof byte moves. Measured on the pre-K1 base (job 151): WHIR I4 -5.40 s, I2 -1.50 s; STARK I4 -1.75 s, I2 -0.45 s. I1, I3 and I5 are not carried.
…d pair of its own The per-table scheduler's driver threads are not rayon workers, so every one of them staged through the shared slot-0 pinned slab. Uploads went one single-buffered chunk at a time, and a driver copying a retained LDE out held the slab's mutex through the whole host copy while the others queued with the card idle. The slab also grew to the largest retained LDE's next power of two and stayed pinned for the rest of the process. By default the row-major commit's trace upload and its retained-LDE download go through a pair of 32 MiB pinned buffers lent to that one transfer. That covers both upload sites, the row-major expansion and the column-major engine's. - htod_staged: the host fills one buffer while the previous chunk's DMA drains the other. It returns once the last chunk is queued; each buffer's event guards it for the next borrower. - dtoh_staged_into: two chunks in flight, each landed chunk copied straight into the Vec's spare capacity with no zero fill, and the in-place transpose queued right behind the last chunk's read. - At most 8 pairs (512 MiB pinned, 16 allocations), made on demand and never grown or freed; a transfer beyond that waits for a pair. LAMBDA_VM_STAGING_SHARED_SLAB=1 keeps the shared slab. The setting is read once and named on stderr (`[gpu] transfer staging: ...`). Both tree drivers print the staging counters after the base and at the end: bytes and host seconds per path, pairs, waits, and the shared slabs' pinned footprint. Measured on block 25368371 (FAST, RTX 5090), ABBA palindromes at 5cbdf06, on the pre-engine base 169b668 behind a temporary knob. STARK: 121.30 -> 115.50 s (-5.80, A spread 1.20); the base 48.3 -> 44.0 s; under nsys the base's card idle 9.89 -> 5.32 s; host peak -4.41 GiB, which is the slab: 4.01 GiB after the base against 0.13. WHIR: 107.60 -> 105.50 s (-2.10); level 0 -1.3 s. Program identities unchanged. On this base the column-major engine's upload takes the same pairs; that site is new here and not in the measurement above. Tests. Device: exact round trips across chunk boundaries, the closure contract, the buffer-reuse hazard, 12 threads over 8 pairs, and root / host LDE / handle parity of the base, ext3, split-tree and column-major engine commits through either staging, with the staged path's bytes counted. Card-free: the slab footprint and the staging line's shape.
Level 0 opened with host work alone: every first-round wrap's prologue at once (reconstruct, emit, arenas, and on WHIR the epoch harvest), with nothing on the card. None of it needs the card or the global proof, only the ELF and the epoch proofs the base finished long before. By default the tree drivers now start a lead-in before the base. Its helpers (two by default) wait until the base reports its epoch count, then build the prologues of wraps 0..want (want = level 0's first pool round) from copies of the leading epoch proofs, with the same functions the pool calls, so the programs are the pool's own. Level 0 takes each prologue instead of building it. A prologue no helper started is built by the pool as before, and a panicking one is handed back. Nothing in the lead-in takes the card permit or holds device memory of its own. It uses the base's DECODE derivations as the base shares them: the STARK commitment, and on WHIR the root and the prepared opening, whose derivation is a device commit. The base reports through an EpochObserver installed for the calling thread (with_epoch_observer): the epoch count from the producer as soon as the final epoch is executed, each proved epoch, and the DECODE work. That holds on both preparation schedules and both pipelines; with no observer installed, the pipeline is unchanged. Level 0 still takes its own DECODE derivations from the base, and LFM_TREE_REDERIVE_DECODE=1 still re-derives them there; the lead-in's copies are only for its prologues. BaseDecode now holds the Arc the base shares. An I4 schedule test reads epoch positions through the slice-based epoch_chain_position. LFM_TREE_PROLOGUES_AT_LEVEL0=1 builds the prologues at level 0's start instead; LFM_TREE_TAIL_PROLOGUES and LFM_TREE_TAIL_HELPERS size the lead-in. The driver prints `L0 PROLOGUES: ...` either way, and at level 0's start how many prologues were ready. Measured on block 25368371 (FAST, RTX 5090), ABBA palindromes at 5cbdf06, on 169b668 behind a temporary knob, before the base-prep and DECODE-handoff changes. WHIR: 107.60 -> 104.40 s (-3.20, A spread 1.20); the lead-in 3.19 -> 0.07 s; level 0 -3.1 s; base unchanged; device peak +592 MiB, from the harvest's MLE evaluations running unreserved beside the base's tail. STARK: 121.30 -> 118.65 s (-2.65); the lead-in 5.54 -> 0.22 s and level 0 -6.1 s, but the base +3.4 s, because the two helpers slow the base's epoch proofs. Program identities unchanged. On this base the DECODE handoff already removes part of the lead-in, so the gain here is smaller than above. Tests. Card-free: the hand-off (order, the count gate, handing back an unstarted, failed or context-failed prologue, waiting on one in progress, close). Fixture scale, on both bases: the observer sees the count once and every epoch byte for byte, and a prologue built from the leading epochs emits the pool's own program and arenas.
…te does Every staged_transfers test forces its path with the thread override, and only the block-scale tree drivers read the lead-in's setting, so no card-free test read either setting from the environment. One test each now does, and prints the line that names it: - staged_transfers: staging_pairs_enabled() is !LAMBDA_VM_STAGING_SHARED_SLAB, with its `[gpu] transfer staging: ...` line; - the tree tests: lead_in_enabled() is !LFM_TREE_PROLOGUES_AT_LEVEL0, with an `L0 PROLOGUES setting: ...` line. The gate runs each with --nocapture under the default and under the opt-out and counts the named lines.
…nned staging, level 0's first wrap prologues built in the base's tail I6: each row-major commit transfer is staged through a pinned pair of its own instead of the worker's shared slab; opt-out LAMBDA_VM_STAGING_SHARED_SLAB=1, named once on stderr ([gpu] transfer staging: ...). I7: level 0's first wrap prologues are built by helpers in the base's tail; opt-out LFM_TREE_PROLOGUES_AT_LEVEL0=1, sized by LFM_TREE_TAIL_PROLOGUES / LFM_TREE_TAIL_HELPERS (L0 PROLOGUES: ...). No proof byte moves. Measured on the lane's base (IDLE-B box2): I6 STARK -5.80 s, WHIR -2.10 s; I7 WHIR -3.20 s, STARK -2.65 s net.
…xes K3/K4/K5 Brings the HASH lane's six commits onto candidate C2 (7d41668): the half-warp Merkle tops (K3), the work-queue grind (K4), the limb-multiply permutation variants (K5), each still behind its LAMBDA_VM_GAP_* knob, their parity tests and host KATs, the serialised grind-counter tests and the queue grid's context fix. No conflict; the next commits make the three fixes the defaults.
…imb permutation by default The three RPX device fixes measured EFFECTIVE on the WHIR block (job 160, wt300-307: K3 -3.90 s, K4 -6.45 s, K5 variant 5 -10.05 s against 107.15 s, identities identical in every arm) are now the defaults. Each keeps an opt-out, and its default lives in one constant in `rpx_paths`, so a pipeline that needs one off flips one line: - LAMBDA_VM_RPX_WARP_MERKLE (WARP_MERKLE_DEFAULT): narrow levels and the tail on rpx_merkle_level_warp / rpx_merkle_tail_warp; =0 walks a thread per parent (rpx_merkle_level, rpx_merkle_tail). - LAMBDA_VM_RPX_GRIND_QUEUE (GRIND_QUEUE_DEFAULT): rpx_grind_search_queue on a card-filling grid; =0 is rpx_grind_search on LAMBDA_VM_GRIND_GRID. - LAMBDA_VM_RPX_LIMB_PERMUTE (LIMB_PERMUTE_DEFAULT): every RPX kernel from rpx_v5.cubin (32-bit limb multiply, square_n unrolled by four); =0 loads rpx_v0.cubin, the 64-bit multiply. Each variable takes 0 or 1 (anything else aborts) and each switch prints one line on first use, "[gpu] RPX Merkle: ...", "[gpu] RPX grind: ...", "[gpu] RPX permutation: ...", naming the path and whether it came from the pipeline default or the variable. The queue's grid line becomes "[gpu] RPX grind queue: grid ...". build.rs now builds rpx.cu twice (variants 0 and 5) instead of five times. The LAMBDA_VM_GAP_* knobs and the gap_hash module are gone. The parity tests move to prover/tests/rpx_device_paths.rs, named for what they compare (the per-parent walk, the stride grind), and the temporary wording leaves the kernels, the host KATs and the docs.
The PROVE SPLIT line's "r4_grind (n/airs on device)" took its delta of gpu_lde::gpu_grind_calls(), the keccak arm's counter. An RPX grind that runs on the device counts in gpu_grind_calls_rpx(), so under RPX every line read 0/airs while every table ground on the card: on the WHIR block (job 160) the root proof printed 0/11 beside the harness's own count of 11 RPX device grinds for it. device_grinds_now() sums both arms and report() takes its delta of that. rpx_grind_device gains a test that reads it around one RPX device grind (the keccak-only count reads 0 there and fails).
The result lines still carried the campaign's fix ids (K3, K4, K5) and called the old paths "shipped", which stopped meaning anything once the new paths became the defaults. They now say what is compared: the warp walk against the per-parent walk, the queue grind against the stride grind, the limb primitives and the permutation variants. Output only; every check is unchanged and both binaries still pass.
…er half-warp, a queue grind, the limb permutation K3: a Merkle level and the tail compress one permutation per half-warp (opt-out LAMBDA_VM_RPX_WARP_MERKLE=0). K4: the device grind claims nonces from a work queue (LAMBDA_VM_RPX_GRIND_QUEUE=0). K5: the whole RPX module runs the limb-multiply permutation variant (LAMBDA_VM_RPX_LIMB_PERMUTE=0). Each opt-out accepts 0 or 1 and names itself once on first use. Every digest, root and grind nonce search is byte-identical to the previous kernels (host known-answer tests and device parity). The prove split now counts RPX device grinds. Measured on the pre-K1 base: WHIR K3 -3.90 s, K4 -6.45 s, K5 -10.05 s; STARK K3 -1.00 s, K4 -0.50 s, K5 -7.95 s.
The DEEP and out-of-domain denominators were inverted by a global
Montgomery scan: compute_denoms plus five scan kernels, each a full pass
over the domain with prefix and suffix scratch, although every row needs
only its own few inverses. By default now:
- compute_and_invert_denoms_ext3_dev runs one kernel,
invert_denoms_rowwise_ext3_k{1..8}: each thread builds its row's
denominators and inverts them in registers with one base-field
inversion (adjugate over norm, the norms batched by Montgomery's
trick, kernels/ext3_inv.cuh). More than 8 per row keep the scan.
- the fully resident R4 DEEP inverts its own row's 1 + K denominators
(deep_composition_ext3_fused_m{1..4}) and needs no inverse buffer; it
falls back to the buffered kernel above 3 points or on any
precondition miss.
- the single-point OOD sums (the R3 composition parts: 1-2 columns, so
1-2 blocks on the card) run on the row-chunked multi kernel, and the
multi kernels' chunk count loses its 64 cap.
The values are the same field elements; raw limbs may differ by p, which
nothing downstream observes. Measured on block 25368371 on one RTX 5090,
ABBA behind a switch on 169b668: STARK (one_row=auto) -0.70 s whole
run against a 0.50 s A spread and -2.35 GiB device peak; WHIR kernels
-49.8 % with the wall inside the noise.
LAMBDA_VM_DEEP_INV_LEGACY=1 restores all three (the scan, the buffered
DEEP, one block per OOD column), read once per process with a banner,
as LAMBDA_VM_LDE_LEGACY does for the LDE. The legacy paths stay public
for tests/deep_inv_parity.rs, whose tests name both paths per call;
tests/deep_inv_setting.rs checks that the process setting is followed.
gpu_fused_deep_calls() counts the fused dispatch, and
cuda_path_integration asserts it follows the setting: a table that
silently fell back to the buffered kernel would still verify.
recompute_lde_produces_byte_identical_proofs compares the bytes of two proves of one instance, one per residency mode, at the test options' grinding factor of 1. Under `parallel` the CPU nonce search is rayon's find_any (crypto::grinding::generate_nonce), so the two proves can return different valid nonces; the nonce is absorbed before the queries are drawn, and every opening after it moves. The test fails whenever the two searches disagree, whichever residency mode runs: on a laptop it failed 11/20 at d1dc455 and 15/20 at 7d41668, and 20/20 passed with LAMBDA_VM_DETERMINISTIC_GRIND=1 (the smallest nonce) or without `parallel` (a sequential find). The residency tests now prove at grinding factor 0, as zf_golden_tests already does for the same reason. The residency mode acts on the main LDE, which the grind never reads, so the comparison loses nothing it could catch.
Contributor
Author
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.
The number
Block 25368371 on the FAST box (Ryzen 9 9950X, RTX 5090 32 GB), RPX commitments, 15 epochs at 2^21. At this head,
d1dc45514, the two default arms of the last ABBA read 59.8 s and 60.6 s (mean 60.20 s). Host peak 19.6 GiB,device peak 26.9 GiB (27,570 MiB).
Each step below is its own ABBA on one binary: two arms per setting, alternated.
In the last row's A arms, every fix in the next table is switched off by its opt-out. Their program ids and census equal
those before the batch (
668a89a4c). The one change with no opt-out of its own, the WHIR encoding through the engine,is in both arms. A fifth arm, the defaults with only the level-0 lead-in off, read 61.4 s, so the lead-in is worth
−1.20 s here. The STARK PR (#985) measured the same batch at −28.75 s (107.55 → 78.80 s).
The gap fixes
Each fix was measured first in its own ABBA, mostly on the base before the engine. Those rows do not add up to the
cumulative −39.65 s; the last row above is the measurement.
LAMBDA_VM_ZF_WHIR_STACK=25LAMBDA_VM_RPX_LIMB_PERMUTE=0LAMBDA_VM_RPX_GRIND_QUEUE=0LAMBDA_VM_BASE_PREP_ON_PROVER=1LAMBDA_VM_LFM_KEEP_BITWISE=1LAMBDA_VM_RPX_WARP_MERKLE=0LFM_TREE_PROLOGUES_AT_LEVEL0=1LAMBDA_VM_WHIR_FOLD_CLASSIC=1LAMBDA_VM_STAGING_SHARED_SLAB=1LAMBDA_VM_NO_WHIR_FUSED_FOLD=1,LAMBDA_VM_WHIR_LEAN_ROUNDS=0LFM_TREE_REDERIVE_DECODE=1LAMBDA_VM_LDE_LEGACY=1, which also reverts every other LDELAMBDA_VM_DEEP_INV_LEGACY=1LAMBDA_VM_NO_WHIR_ROOM_PARK=1,LAMBDA_VM_NO_WHIR_ROOM_RESIZE=1LAMBDA_VM_LFM_HASH_SPLIT=1turns it onAlso in the batch, with no knob:
What is in the branch
for the block.
(−8.3 s).
inside level 0's pool, saved −11.9 s. Together they took the block to 128.3 s.
ZfFormat(prover/src/zf_format.rs) parses sixLAMBDA_VM_ZF_*knobs once and prints oneZF FORMAT:banner. The default iscap=auto whir_cap=auto fri=dp one_row=0 whir_folds=first6 whir_stack=27.end of each tree's first path, so the proof structs are unchanged.
chain.
8–11 of 2^25.
LAMBDA_VM_ZF_ONE_ROW=auto) are built on host, on the GPU andin-guest, but are off here: they cost +3.2 s on this pipeline. They are on in the STARK pipeline's PR (Per-table GPU recursion (RPX) with ZisK-style proof formats: block 25368371 in 78.80 s #985).
ZfFormat::LEGACYstays pinned by a golden test. The RV64 recursion guestverifies only the legacy format.
crypto/math-cuda/src/lde_cm.rs,kernels/ntt_cm.cu).zero fill, and a transpose before a row-major commit.
launch in registers and shared memory, so a 2^22 transform is three passes. The coset spread is fused into the
first pass, and the output is column-major, so the commits lose their transpose.
commit's encoding. LDEs that keep a host copy (below 2^19 rows) stay on the old path.
LAMBDA_VM_LDE_LEGACY=1sends every LDE back to the per-level pipeline.gap-fix/ntt(da2da9d93),gap-fix/wbatch-int(7e3eac501),gap-fix/harness(f81f0a80f),gap-fix/rec-int(c679a771b),gap-fix/stack-int(8c2450ff6) andgap-fix/idle-a-int(d3c76d2ed), eacha signed merge;
gap-fix/idle-b-int(bdb2d37b6) andgap-fix/hash-int(e041e9fb0), each a signed merge;gap-fix/kern-int, one commit: this head.the stack.
main: perf(alloc): compile jemalloc's never-purge policy into the binary #996 (jemalloc never-purge compiled into the CLI).Soundness
Query counts and blowup are unchanged.
The format levers
cap node the query index selects. Path lengths are checked exactly, including at c = 0.
changes, in a term that stays more than 50 bits below the dominant one.
layout Plonky3 uses. The query index is uniform over the whole domain.
fewer rounds, and queries stay 112 per round.
The gap fixes
all take the layout from
global_layout(shapes, cap), never from a proof.table. It stays 112 at every production shape.
security/zisk_calc.py): the WHIRchain minimum is 130.393 bits at 27, against 130.926 at 25. The pipeline minimum is unchanged at 128.946 bits, set
by the query phase of every LFM proof.
message, so a cheating prover can re-draw α₁ by varying h₁ without grinding again. The bits above are the unground
ones. Where the grinds sit and how many bits they take are unchanged in this PR (see Open decisions); K4 changes only
how the card searches for the nonce.
sender, its honest multiplicities are all zero and the table constrains nothing.
instantiated chip's interactions, stored in the artifacts and folded into
program_id, and the verifier re-checksit against the mask it was handed. No proof supplies it.
LAMBDA_VM_LFM_KEEP_BITWISE=1reproduces the legacy registry digests.emissions compute the same field value on accepted and on tampered chains (tests). The WHIR wrap program ids move.
program_idand never read from aproof. Tests refuse a forged tail root, the single-table door and a wrong chunk root.
parity through either staging, on the card;
reference, and the fault suite under both settings.
the 64-bit multiply. Its bytes were shown equal three ways:
with a failing control). It also replays the cooperative kernels lane by lane (the half-warp Merkle kernels and
the queue grind) against the shipped ones. CI runs it on every PR;
path and, where cheap, against the host oracle (
rpx_device_paths);Fixed along the way
tables, which rejected honest proofs that publish values.
failed, the error was dropped, and every such commit fell back to the host; the base took 647 s. The grid now splits
into z, and device commit and tree errors are logged and counted.
source codeword dropped.
GPU.
Gate and CI
The batch was gated at this head,
d1dc45514, on the FAST box: 81 steps, every one at its exact pre-registered count.The standard steps:
The 75 targeted lines cover:
the room on the card;
tests;
For each switch of the last two rounds, a gate line runs each setting and reads the banner its process printed. The
previous candidate without K6 (
41549ebad) passed its own 70-step gate. The cumulative ABBA in the first table ranafter the gate.
In CI at this head, these pass: lint, the host known-answer tests (including the RPX lane-by-lane replay), the prover
test build, the stark cuda-feature tests, and the CLI and executor tests. The spec structure check fails on a key the
spec tooling does not know (
spec/src/blake3.toml:constants). The prover shards were still running when this waswritten.
Open decisions
one_row(off here, auto in Per-table GPU recursion (RPX) with ZisK-style proof formats: block 25368371 in 78.80 s #985) andthe LFM_HASH split (off here, on in Per-table GPU recursion (RPX) with ZisK-style proof formats: block 25368371 in 78.80 s #985). A per-pipeline default would let one PR carry both.
previous head: K4 and K5 halved them. P2-W would save about −1.5 to −2 s here [estimate, scaled by the grind
time].
LAMBDA_VM_MEMPOOL_RELEASE_MB=0, which releases thedevice memory pool; the code's default retains it. Retaining measured −4.50 s on this pipeline (before the engine).
No code changes; the decision is which configuration to report.
budget at 27. A block that adds one polynomial to such an epoch moves that argument's reservation to the host:
counted, proof unchanged, slower. The device peak at this head is 27,634 MiB of the card's 32,607 MiB. Worth a run on
a heavier block before relying on 27 there.
keeps 128 bits.
iteration.