Repository navigation
Prover optimisation ideas (not merge) - #729
Draft
kevaundray wants to merge 12 commits into
Draft
kevaundray wants to merge 12 commits into
kevaundray wants to merge 12 commits into
Conversation
A table's first active round now builds its unfolded state (lin, left_g, right_g over all its rows) and its message in one pass over the K columns, so the join no longer reads them a second time. Every fold then computes the next round's message from the blocks it just wrote (each task folds block k of all four quarters into block k of both next halves), so a round is one pass over the state instead of a message sweep plus a fold sweep. Below sixteen rows per half the old fold and message stay. message_k, join and their row loader are gone. Proof bytes unchanged (PROOFDIGEST f090f5e4ee18ffad). Profile (perf, xmss 900 rate 1, 3 passes per run): the sumcheck kernels were the K-column passes (message_k ~40%, join ~43%; loads and stores hottest in join, so bandwidth bound), the folded rounds only ~18%. Kernel samples per run, interleaved: base 1763/2337/2501/2291/2268/2171/2606 (median 2291, ~380 ms CPU per pass), new 1566/1700/1525/1990/1706 (median 1700, ~283 ms), -26%. Prove constraints minus Constraint evals, tracing wall on the loaded host, 10-11 runs each: base min 148 ms median 234 ms, new min 119 ms median 189 ms. Whole pass (ab.sh 4x4): base cpu min 11.72 med 11.99 s, new min 11.54 med 12.17 s, inside the noise (expected gain ~0.8% CPU). Peak memory 3.111 -> 3.131 GiB. Rebased onto e5d75f8 (after leanEthereum#724): the digests and measurements above were taken on the older base 8577d1e. On the new base the series leaves the proofs byte-identical: PROOFDIGEST 7d91b8513bdd4b03 (aggregate --xmss 900 --log-inv-rate 1), 3c41e153f8b2ee37 (aggregate --sphincs 410 --log-inv-rate 1), aa4b2ebd7a6989b2 (recursion --n 2 --xmss-per-leaf 900 --log-inv-rate 2).
Port of upstream leanEthereum#557 (d6f597e) onto main's batched triple GKR. The vertical kernels read a layer once per sumcheck round. The new `paired` kernels take the rounds in pairs: four rows sharing all but their two lowest index bits form a block, each child bilinear in those bits, and one pass sums eq(q) times the product of the four children over the blocks, a degree-four bivariate grid that gives round j's message and, once its challenge is drawn, round j+1's. The next pass folds every block into one row at both challenges and sums the next pair's grid from the folded rows while they are in registers. The round messages are the same polynomials, so the proof is unchanged. Main's properties are kept: the three trees run in one dispatch, the implicit identity tail and the identity runs are skipped (an identity quad adds only to the grid's constant term, which neither message reads, and folds to an unwritten identity quad), and the rows stay transposed between passes, eight blocks per `Quad` of F192x8 lanes, regrouped with lane deinterleaves after each fold. `F192x8::load_blocks` transposes eight runs of sixteen elements for the first pass of a layer. The pairs multiply more (the first pass of a layer costs about twice the old message pass) to read and write a layer less often, so they run only with at least PAIRED_THREADS = 8 pool threads (upstream measured even at 4 and a gain at 8); fewer threads keep the one-round vertical kernels. xmss 900, log-inv-rate 1, PROOFDIGEST f090f5e4ee18ffad unchanged (default threads and LEANVM_NUM_THREADS=1). 7950X3D, loaded host, ab.sh 3 rounds x 4 repeats, CPU and wall per proving pass, lv-base -> this: load ~75: cpu med 11.08 -> 10.99 s, min 10.76 -> 10.58; wall med 2.80 -> 2.82 s, min 2.06 -> 2.19 load ~95: cpu med 11.35 -> 11.58 s, min 10.67 -> 11.05; wall med 4.11 -> 3.91 s, min 3.20 -> 2.93 load 230-580: cpu med 12.45 -> 12.46 s, min 11.88 -> 11.84; wall med 6.54 -> 6.98 s, min 5.60 -> 5.52 Whole-pass CPU is unchanged within noise. Bus GKR stage wall (tracing, 16 threads, load ~75, 4 interleaved runs): 305/360/364/489 ms -> 267/287/288/474 ms, median about -20%; at load 160-600 it was 341-495 -> 305-492 ms. perf samples in leanvm_core::gkr over two passes at 16 threads: kernels 2525/2338 -> 2578/2884, all GKR symbols including the dispatcher's wait 3045/3188 -> 2966/3298, so the stage's CPU is about even. At one thread (pairs forced on, before the gate): GKR samples 1827/1867 -> 2114/2243 (+15-20%), whole-pass CPU med 8.19 -> 8.40 s (ab.sh 2 x 2), the regression upstream reported. With the gate a one-thread proof runs the previous kernels. Rebased onto e5d75f8 (after leanEthereum#724): the digests and measurements above were taken on the older base 8577d1e. On the new base the series leaves the proofs byte-identical: PROOFDIGEST 7d91b8513bdd4b03 (aggregate --xmss 900 --log-inv-rate 1), 3c41e153f8b2ee37 (aggregate --sphincs 410 --log-inv-rate 1), aa4b2ebd7a6989b2 (recursion --n 2 --xmss-per-leaf 900 --log-inv-rate 2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An interaction's push and pull blocks over one table's columns flush the same coordinates at the same fingerprint weights but for one (the count's slot), so their leaf formulas share all but a term or two. build_leaves now pairs each block of a shape with the block sharing the most terms with it (`Family`): per chunk of rows the shared mixed products are summed once, unreduced, and each member adds its own terms, reduces and adds its constant. A leaf is the same XOR of the same unreduced products, so the proof is unchanged. At xmss 900 the pairing removes 64M of the 155M mixed products per proof. xmss 900, log-inv-rate 1: PROOFDIGEST c7afa387f98cdb98 unchanged. 7950X3D, loaded host, spancpu.sh median span CPU, lv-head4 -> this: default threads, 4 rounds: Bus leaves 0.500 -> 0.451 s, Bus GKR 1.139 -> 1.116 (untouched), Prove 8.92 -> 8.80 LEANVM_NUM_THREADS=1, 2 rounds: Bus leaves 0.313 -> 0.253 s Rebased onto e5d75f8 (after leanEthereum#724): the digests and measurements above were taken on the older base 8577d1e. On the new base the series leaves the proofs byte-identical: PROOFDIGEST 7d91b8513bdd4b03 (aggregate --xmss 900 --log-inv-rate 1), 3c41e153f8b2ee37 (aggregate --sphincs 410 --log-inv-rate 1), aa4b2ebd7a6989b2 (recursion --n 2 --xmss-per-leaf 900 --log-inv-rate 2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The AVX-512 SkipBlock weighed every product of a row by its position's weight: per mixed point and eight slices, two carry-less products for b * c and six for the weight, five times a row (40 VPCLMULQDQ). A row's five products now go by three pivots, each a factor its products share: e2 (w0 e1 + w1 e3), e4 (w2 e3 + w3 e0), e1 (w4 e0). A weight times an embedding is linear in the pack's bits, so per-position affine byte maps (SkipWeights, built once a layer, 384 bytes a position, laid out so eight positions' maps are one contiguous register) give the weighted packs already reduced to three words; the extension is a sum of those too, and a mixed point then costs three products per pivot (18 VPCLMULQDQ). A half block runs at a time, the extension is formed a high point at a time from the 16 packs, and each output's three word sums stay in registers over the pivots, so per eight slices only 12 quantities of 16 registers are stored. The sums are a polynomial congruent to the old ones modulo the minimal polynomial, so the prover's evaluation and the proof bytes are unchanged (PROOFDIGEST f090f5e4ee18ffad). The other arms keep the old kernels behind the same SkipWeights/SkipBlock interface. Profile (head): SkipBlock was CLMUL-port bound, 208k zmm VPCLMULQDQ per block at ~2.2 cycles each (measured on this host); the new kernel is 94k per block, plus 38k GF2P8AFFINEQB that overlap the multiplier. SkipBlock::run alone, one thread, perf cycles per block (5 runs each, host loaded): head 707-747k, new 463-555k, about -33%. Per-kernel perf cycle samples (aggregate --xmss 900 rate 1 --repeat 2, two runs each, interleaved): SkipBlock head 19.22G / 19.34G, new 14.66G / 16.00G plus SkipWeights 0.18G / 0.16G, about -20%. hash --log-n 17 --repeat 3: head 18.98G / 19.54G, new 15.87G / 15.86G. Whole pass (ab.sh 3x4, load average 100-370): head cpu min 11.93 med 12.24 s, new min 11.95 med 12.34 s, inside the noise (expected about -2%). Rebased onto e5d75f8 (after leanEthereum#724): the digests and measurements above were taken on the older base 8577d1e. On the new base the series leaves the proofs byte-identical: PROOFDIGEST 7d91b8513bdd4b03 (aggregate --xmss 900 --log-inv-rate 1), 3c41e153f8b2ee37 (aggregate --sphincs 410 --log-inv-rate 1), aa4b2ebd7a6989b2 (recursion --n 2 --xmss-per-leaf 900 --log-inv-rate 2).
Round::add_pair reduced B[j] = weights[j] * (kappa + T[j + 1]) + kappa * weights[j + 2] for both rows of a pair (Karatsuba's six products plus Barrett's twelve, per row) and then took the two dot products with T[j + 2] (six each). The x86 kernel (PowerField::round_dot) now leaves B as six words and meets T[j + 2] with them as two halves of three: per entry 72 carry-less products where 96 were. B + B' is weights * (T1 + T1'), the constants cancelling, so the value at infinity needs no second B. The sums grow to nine words (RoundSums to twelve after the eq weight), congruent to the old ones modulo the minimal polynomial, so the evaluated round values and the proof bytes are unchanged (PROOFDIGEST f090f5e4ee18ffad). The other arms keep reducing B (round_dot_rows), which is also the kernel's test oracle; the shifted rows of T are built in one pass. Profile: add_pair is CLMUL bound (carry-less products are ~45% of its samples). Per-kernel perf cycle samples, hash --log-n 17 --repeat 3, three runs each interleaved: Round::add_pair 4.34G / 4.41G / 4.45G -> 3.55G / 3.63G / 3.47G, about -19%. add_pair is about 1.8% of an aggregate --xmss 900 proof's cycles, so the whole-pass effect (about -0.35%) is below what ab.sh resolves on this host. Rebased onto e5d75f8 (after leanEthereum#724): the digests and measurements above were taken on the older base 8577d1e. On the new base the series leaves the proofs byte-identical: PROOFDIGEST 7d91b8513bdd4b03 (aggregate --xmss 900 --log-inv-rate 1), 3c41e153f8b2ee37 (aggregate --sphincs 410 --log-inv-rate 1), aa4b2ebd7a6989b2 (recursion --n 2 --xmss-per-leaf 900 --log-inv-rate 2).
The AVX-512 register kernels take two eight-lane vectors of every row at a time. A row of 56 words, the L0 base encode's 51 lanes padded to a vector, is three such chunks and a last chunk of one vector, and the kernel ran both vectors of that chunk, the second all zeros: every sweep of the deep pass and of the openings' row rebuilds did 64 lanes of work for 56. The last chunk now runs the kernel on the vectors that hold lanes only, each count an inlined call of its own. Rows of whole chunks (the gathered pass's 64-word stage rows, the extension encodes' 48 words) are unchanged. Byte-identical: PROOFDIGEST c7afa387f98cdb98 (aggregate --xmss 900 --log-inv-rate 1). Tests: pcs. Measured on the loaded host against lv-head4, BENCH_SPAN_CPU process CPU per span, 7 interleaved runs: Prove > Commit (base encode and its leaf hashing) median 1.456 -> 1.322 s, min 1.369 -> 1.204 s; level commits unchanged (0.785 -> 0.769 s). Kernel alone, one thread, 11 layers over 2^11 rows of 56 words: 0.31-0.37 -> 0.26 ns per butterfly, the rate of a 64-word row. Rebased onto e5d75f8 (after leanEthereum#724): the digests and measurements above were taken on the older base 8577d1e. On the new base the series leaves the proofs byte-identical: PROOFDIGEST 7d91b8513bdd4b03 (aggregate --xmss 900 --log-inv-rate 1), 3c41e153f8b2ee37 (aggregate --sphincs 410 --log-inv-rate 1), aa4b2ebd7a6989b2 (recursion --n 2 --xmss-per-leaf 900 --log-inv-rate 2).
run_layers swept each fused group of layers over the whole buffer before the next. A deep task is an L2 budget of rows or more (2^11 rows of 56 words at L0, 2^12 rows of 48 words at the level-2 encode, 1.5 MiB), so each of its sweeps streamed it through L2 and spilled to L3. Now a buffer larger than L1 (4096 words) runs depth first: after a sweep, each block of the sweep's last layer runs its remaining layers alone, while it is in cache. Same butterflies and twiddles, in another order. With `live` (the openings' pruned rebuilds) the order stays breadth first. Byte-identical: PROOFDIGEST c7afa387f98cdb98. Tests: pcs. Kernel alone, one thread, min of 20: 12 layers over 2^12 rows of 48 words 0.341-0.347 -> 0.309-0.310 ns per butterfly, 11 layers over 2^11 rows of 48 words 0.325-0.336 -> 0.303-0.308, 2^11 rows of 56 words 0.40 -> 0.35 (before the previous commit). In the prover the deep pass waits on its DRAM reads, and the stage moves within noise (BENCH_SPAN_CPU, 7 interleaved runs, on top of the previous commit): Prove > Commit median 1.322 -> 1.294 s, min 1.204 -> 1.279 s; level commits 0.769 -> 0.772 s. Rebased onto e5d75f8 (after leanEthereum#724): the digests and measurements above were taken on the older base 8577d1e. On the new base the series leaves the proofs byte-identical: PROOFDIGEST 7d91b8513bdd4b03 (aggregate --xmss 900 --log-inv-rate 1), 3c41e153f8b2ee37 (aggregate --sphincs 410 --log-inv-rate 1), aa4b2ebd7a6989b2 (recursion --n 2 --xmss-per-leaf 900 --log-inv-rate 2).
…l deep WHIR levels 2 to 4 (rates 1/128 to 1/8192, 2^12 down to 2^4 message rows of 16 F192 lanes) plan a deep pass only: each task builds its cosets in scratch from the message, hands them to the leaf hasher, and then streamed them out as the finished codeword, 200, 100 and 50 MiB. On x86 (REBUILD_OVER_STORE) the commit keeps a codeword before its deep layers, and for these plans that is the message's replicas. So the encode now writes no buffer: it keeps one replica, the message itself (1.5 MiB at level 2), as Codeword::BeforeDeep, and a query's row runs the deep layers of its sub-block from that replica, pruned to the queried rows, as rows read from any codeword kept before its deep layers already do. Level 1 (one gathered pass, then the deep pass) and the L0 base encode are unchanged, as is the Apple path that keeps the whole codeword. Byte-identical: PROOFDIGEST c7afa387f98cdb98 (aggregate --xmss 900 --log-inv-rate 1), 729f46975ab551c1 (recursion --n 2 --xmss-per-leaf 900 --log-inv-rate 2). Tests: pcs; the deferred-deep test adds a shape whose tasks span several replicas. Measured on the loaded host against lv-head4 (this and the two commits before), BENCH_SPAN_CPU process CPU per span, 7 interleaved runs: Prove > PCS open > Commit (levels 1 to 4) median 0.785 -> 0.737 s, min 0.734 -> 0.654 s; Prove > PCS open > Open 0.078 -> 0.094 s (the rows of levels 2 to 4 are now finished on demand); Prove > PCS open 1.290 -> 1.271 s; Prove > Commit 1.456 -> 1.344 s. Whole pass (ab.sh 4x4, load average 500 to 850): CPU min 8.45 -> 8.03 s, median 8.90 -> 8.89 s; wall min 1.451 -> 1.763 s, median 2.596 -> 2.288 s. The 350 MiB of level 2 to 4 codewords are no longer allocated. PCS throughput bench (PCS_LOG_N=26, rate 1/2, 5 passes, two interleaved rounds) is noise-bound on this host: commit 3029 / 325 ms -> 287 / 401 ms, open 7097 / 394 ms -> 387 / 486 ms. Rebased onto e5d75f8 (after leanEthereum#724): the digests and measurements above were taken on the older base 8577d1e. On the new base the series leaves the proofs byte-identical: PROOFDIGEST 7d91b8513bdd4b03 (aggregate --xmss 900 --log-inv-rate 1), 3c41e153f8b2ee37 (aggregate --sphincs 410 --log-inv-rate 1), aa4b2ebd7a6989b2 (recursion --n 2 --xmss-per-leaf 900 --log-inv-rate 2).
… cell An instruction's read and store now return the cell's access count and advance it in the same touch (Mem::read, Mem::put_counted), instead of a second pass of bump_access_count over the row's cells after its reads and stores. Bumps keep flush order, so aliased operands still pair each access with its own count; DEREF's store side keeps its separate bumps. A cell is indexed past its covering check without bounds checks (SAFETY: ensure grew the pages past it). cells_of_state is specialized on the layout like state_of_cells. Byte-identical: PROOFDIGEST c7afa387f98cdb98 (aggregate xmss 900 rate 1), 729f46975ab551c1 (recursion n 2 xmss-per-leaf 900 rate 2); witness checksum of q and the virtual columns unchanged. Measured (solo execute, one thread, perf stat over 20 executes, xmss 900): user instructions per execute 778M -> 627M (-19%). Cycles per execute in execute_with itself (5 interleaved rounds, min) 420M -> 420M: the loop is bound by cache misses on this host, so the instructions saved do not show as time here. Since leanEthereum#724 a KECCAK row writes only the output cells its bytecode requests: put_counted runs on those, and a cell it does not write keeps count 0, as on main. Rebased onto e5d75f8 (after leanEthereum#724): the digests and measurements above were taken on the older base 8577d1e. On the new base the series leaves the proofs byte-identical: PROOFDIGEST 7d91b8513bdd4b03 (aggregate --xmss 900 --log-inv-rate 1), 3c41e153f8b2ee37 (aggregate --sphincs 410 --log-inv-rate 1), aa4b2ebd7a6989b2 (recursion --n 2 --xmss-per-leaf 900 --log-inv-rate 2).
GPow's reverse index (announced exponents) and its coarse index (every 64th exponent) held only the exponent and confirmed a candidate slot against the forward table, a random read of a table as large as the address space for every probe. A slot now holds (key, exponent), and a byte per slot holds eight tag bits of the keys whose home slot it is, so a lookup of an absent key, all but the last of the up to 64 probes a resolve makes, reads one byte of an array a sixteenth the slots' size. Resolve inserts the exponent it found directly. Byte-identical: PROOFDIGEST c7afa387f98cdb98, recursion 729f46975ab551c1. Measured (solo execute, one thread, xmss 900): GPow::resolve's share of the execute loop's L2 misses 23% -> 12% (in-run sample shares, so the host's load cancels). Cycles per execute in GPow, Index, hint streams and field inverses (perf, 3 to 5 interleaved rounds, min) 75M -> 56M. Rebased onto e5d75f8 (after leanEthereum#724): the digests and measurements above were taken on the older base 8577d1e. On the new base the series leaves the proofs byte-identical: PROOFDIGEST 7d91b8513bdd4b03 (aggregate --xmss 900 --log-inv-rate 1), 3c41e153f8b2ee37 (aggregate --sphincs 410 --log-inv-rate 1), aa4b2ebd7a6989b2 (recursion --n 2 --xmss-per-leaf 900 --log-inv-rate 2).
Every table filled its columns in four to six parallel passes over its trace rows (PC, FP, operands and words, counts, bytecode count), each re-reading the rows and re-decoding the instruction; KECCAK took six. FillCtx::cols now takes the list of local columns it writes, so each table fills all its columns in one dispatch and one decode per row; KECCAK lists every column but the input lanes, which are q_keccak's. JUMP's is-nonzero witness w = 1/c is a Montgomery batch inversion per line of eight rows inside the same pass, where it was a serial pass over all rows building three temporary arrays. Rows::iter is gone (no user left). Byte-identical: PROOFDIGEST c7afa387f98cdb98, recursion 729f46975ab551c1. Measured against lv-head4 (spancpu.sh 5 rounds, median process CPU, xmss 900 rate 1): Fill columns 0.266 s (parent commit) -> 0.235 s (-12%). Since leanEthereum#724 the KECCAK row also carries its OUT_TWO and OUT_ALL flags, and its output lanes are the input lanes permuted again (a row may not write its output cells); the one pass forms both per row, as main's separate passes did. Rebased onto e5d75f8 (after leanEthereum#724): the digests and measurements above were taken on the older base 8577d1e. On the new base the series leaves the proofs byte-identical: PROOFDIGEST 7d91b8513bdd4b03 (aggregate --xmss 900 --log-inv-rate 1), 3c41e153f8b2ee37 (aggregate --sphincs 410 --log-inv-rate 1), aa4b2ebd7a6989b2 (recursion --n 2 --xmss-per-leaf 900 --log-inv-rate 2).
…lds the table The interpreter grew a table of every power g^0..g^k as far as the run addressed (about 4M entries, 32 MiB, plus the copies of its doublings), serially and on the critical path, and handed it to the fill. GPow now keeps g^(64j) for the covered strides and g^0..g^63, so g^k is one product; the coarse index is built from the strides as before. Program::build makes the fill's table with primitives::field::g_powers, across the pool. Execution::gpow is gone. Byte-identical: PROOFDIGEST c7afa387f98cdb98, recursion 729f46975ab551c1; cargo test --release -p leanvm_core -p lean_compiler pass. Measured (solo execute, one thread, xmss 900, interleaved, min): cycles per execute in GPow, hints and inverses 56M -> 47M; GPow::extend is gone from the profile. Whole series against lv-head4, xmss 900 rate 1 (loaded host, load average 80 to 800 during the runs): - traced --repeat 1, 6 interleaved runs: Execute program min 142 -> 124 ms, median 204 -> 160 ms; Build witness min 40 -> 33 ms, median 54 -> 36 ms; Fill columns min 34 -> 25 ms, median 46 -> 29 ms. - spancpu.sh 6 rounds (median process CPU): Fill columns 0.274 -> 0.245 s, Build witness 0.302 -> 0.296 s, Execute program 0.280 -> 0.301 s (the span's CPU includes the Keccak block workers; within noise), Prove 8.98 -> 8.69 s. - ab.sh 3x4, CPU per pass: min 8.37 -> 8.25 s, median 8.58 -> 8.70 s (no whole-pass CPU change beyond noise); wall min 1.195 -> 0.929 s, median 1.383 -> 1.172 s. Rebased onto e5d75f8 (after leanEthereum#724): the digests and measurements above were taken on the older base 8577d1e. On the new base the series leaves the proofs byte-identical: PROOFDIGEST 7d91b8513bdd4b03 (aggregate --xmss 900 --log-inv-rate 1), 3c41e153f8b2ee37 (aggregate --sphincs 410 --log-inv-rate 1), aa4b2ebd7a6989b2 (recursion --n 2 --xmss-per-leaf 900 --log-inv-rate 2).
Benchmarks: base against this PR, on one runner0fc4679 merged into its base, against the base e5d75f8: each benchmark run in turns on one runner, base and PR 5 times each. A size is shown when it changed; a time or a peak memory when it moved the same way in every round and its median by at least 1% (the change is the median ratio, with the rounds' range). Threads shows the named configuration; Keccak GKR
Every resultKeccak GKR
|
This branch has not been deployed
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.
This PR collects twelve prover-only optimisation ideas, one commit each, for review: the constraint sumcheck, the bus GKR and its leaves, the Keccak reduction's kernels, the NTT and WHIR commits, and witness generation (execute, hints, column fill). It is not meant to be merged as is; pick or discuss individual commits. Proof bytes do not change: the PROOFDIGEST of every benchmark below equals main's, and proof size and committed words are unchanged.
Results
This branch against current main (
e5d75f858), per proving pass,ab.sh3 rounds x 4 repeats (recursion also 5 x 4):aggregate --xmss 900 --log-inv-rate 1aggregate --sphincs 410 --log-inv-rate 1recursion --n 2 --xmss-per-leaf 900 --log-inv-rate 2, 3 x 4Proof size is unchanged (322.8, 331.8 and 233.4 KiB), as are the table sizes and TOTAL_COMMITTED (2^25.418, 2^26.326, 2^23.845). Peak memory is the CLI's
peak memorywith--repeat 2, the median of three or four runs each.Per-stage process CPU,
spancpu.shmedian, main -> PR. xmss 900 in two separate runs (5 and 8 interleaved rounds), recursion in one (5 rounds):Host: Ryzen 9 7950X3D (16C/32T), shared with other jobs, load average 47 to 105 during the runs, so CPU is the steadier number and wall varies a lot. Every comparison alternates the two binaries round by round (
ab.sh,spancpu.sh). The two recursionab.shruns disagree on wall; the per-stage CPU of recursion improves in every touched stage.Ideas
Constraint sumcheck: expand each table's state once, fold with the next message (commit
210a7a93b)message_k(about 40% of the kernel samples) andjoin(about 43%, its loads and stores hottest, so bandwidth bound) each read every column; the folded rounds were only about 18%. Each later round was a message sweep plus a fold sweep.lin,left_g,right_g) and its message in one pass over the K columns. Every fold then computes the next round's message from the blocks it just wrote, so a round is one pass. Below sixteen rows per half the old fold and message stay;message_k,joinand their row loader are gone.crates/leanvm_core/src/constraints.rs.Bus GKR: two rounds per pass with at least eight threads (commit
ee76820d6)Quads ofF192x8,F192x8::load_blocks).crates/leanvm_core/src/gkr.rs,crates/primitives/src/field/x8.rs.PAIRED_THREADS = 8or more pool threads: the pairs multiply more, and at one thread they cost 15 to 20% more GKR samples, so fewer threads keep the vertical kernels. Peak resident memory moves with this commit and no other (per-commit bisection): xmss 900 3.21 -> 2.77 GiB, recursion 1.63 -> 1.56 GiB, but sphincs 410 4.20 -> 4.44 GiB. The fold keeps a spare quad buffer per tree and frees the layer's rows after the first pair; with jemalloc keeping freed pages mapped the peak depends on that buffer schedule, and the sphincs increase is not isolated further.Bus leaves: a push and a pull block share their products (commit
137dfd69f)build_leavespairs each block of a shape with the block sharing the most terms (Family, at most two members). Per chunk of rows the shared products are summed once, unreduced; each member adds its own terms, reduces and adds its constant. A leaf is the same XOR of the same unreduced products. The pairing removes 64M of the 155M products.crates/leanvm_core/src/leaf.rs.Keccak skip sums: pivots and GFNI-weighted packs (commit
48cea407b)SkipBlockwas bound by the carry-less multiplier port: it weighed every product of a row by its position's weight, 40 VPCLMULQDQ per mixed point and eight slices, 208k per block.SkipWeights, built once a layer) give the weighted packs already reduced with GF2P8AFFINEQB; a mixed point then costs 18 VPCLMULQDQ (94k per block plus 38k affine ops that overlap the multiplier).SkipBlock::runalone was about 33% fewer cycles per block.crates/keccak_gkr/src/kernels.rs,crates/keccak_gkr/src/prover.rs.Keccak round values: without reducing the factor B (commit
2e33b7205)Round::add_pairis CLMUL bound (carry-less products about 45% of its samples): it reducedB[j]for both rows of a pair (Karatsuba's six products plus Barrett's twelve per row), then took two dot products withT[j + 2].PowerField::round_dot) leaves B as six words and meetsT[j + 2]with it as two halves of three: 72 carry-less products per entry where there were 96. B + B' isweights * (T1 + T1'), so the value at infinity needs no second B. The sums grow to nine words, congruent to the old ones.add_pairruns) -9% in both xmss 900 runs, recursion 0.050 -> 0.041 s; Tail, which also calls it, is flat. On the older baseadd_pair's cycle samples fell about 19%.crates/keccak_gkr/src/power.rs,crates/keccak_gkr/src/prover.rs.round_dot_rows), which is also the new kernel's test oracle.NTT short last chunk: skip the empty vector (commit
ca23a5f5d)crates/pcs/src/ntt/kernels.rs.NTT depth first: run the layers depth first past L1 (commit
e5f837c28)run_layersswept each fused group of layers over the whole buffer before the next. A deep task is an L2 budget of rows or more (1.5 MiB at the level-2 encode), so each sweep streamed it through L2 and spilled to L3.live(the openings' pruned rebuilds) the order stays breadth first.crates/pcs/src/ntt/layers.rs.WHIR extension encode: keep only the message when every layer is deep (commit
f35ec0b61)REBUILD_OVER_STORE) the commit only needs the codeword before its deep layers, which for these plans is the message's replicas.Codeword::BeforeDeep, and a query's row runs the deep layers of its sub-block from it, pruned to the queried rows, as rows read from any codeword kept before its deep layers already do.crates/pcs/src/ntt/deep.rs,crates/pcs/src/ntt/encode.rs,crates/pcs/src/ntt/openings.rs,crates/pcs/src/ntt/tests.rs.Execute access counts: advanced in the access itself (commit
bbf8c4c33)bump_access_countwent over the row's cells again, each touch re-checking the page cover; the interpreter's loop is cache-miss bound and single-threaded on the critical path.Mem::readandMem::put_countedreturn the cell's access count and advance it in the same touch, in flush order, so aliased operands still pair each access with its own count. Cells are indexed without bounds checks pastensure.cells_of_stateis specialized on the layout likestate_of_cells. Since KECCAK-instruction with custom output size #724 a KECCAK row writes only the output cells its bytecode requests;put_countedruns on those, and a cell not written keeps count 0, as on main.crates/leanvm_core/src/cpu/execute.rs,crates/leanvm_core/src/keccak.rs.atcallingensurefirst (SAFETY comments). DEREF's store side keeps its separate bumps.Hints index: g-power slots carry their keys behind per-slot tags (commit
76c2a226e)GPow's reverse index and coarse index held only the exponent and confirmed a candidate against the forward table, a random read of a table as large as the address space per probe. On the older baseGPow::resolvedrew 23% of the execute loop's L2 misses.(key, exponent), and a byte per slot holds eight tag bits of the keys homed there, so a lookup of an absent key (all but the last of up to 64 probes a resolve makes) reads one byte of an array a sixteenth the slots' size. Resolve inserts the exponent it found directly.crates/leanvm_core/src/cpu/hints.rs.Fill columns: each table's columns in one pass over its rows (commit
46bba9b95)w = 1/cwas a serial pass over all rows building three temporary arrays.FillCtx::colstakes the list of local columns it writes, so each table fills all its columns in one dispatch and one decode per row; KECCAK lists every column but the input lanes. JUMP's inversion is a Montgomery batch per line of eight rows inside the same pass. Since KECCAK-instruction with custom output size #724 the KECCAK row also carries itsOUT_TWOandOUT_ALLflags and its output lanes are the input lanes permuted again; the one pass forms both per row, as main's separate passes did.Rows::iteris gone.crates/leanvm_core/src/tables.rs,crates/leanvm_core/src/cpu/trace.rs.colslist; a mismatch would show as a wrong witness, so the tests and digests are the check.Strided g-powers: the interpreter keeps strides, the fill builds the table (commit
0fc4679d7)g^0..g^kas far as the run addressed (about 4M entries, 32 MiB, plus the copies of its doublings), serially and on the critical path, and handed it to the fill.GPowkeepsg^(64j)for the covered strides andg^0..g^63, sog^kis one product; the coarse index is built from the strides as before.Program::buildmakes the fill's table withprimitives::field::g_powersacross the pool.Execution::gpowis gone.GPow::extendleft the profile and cycles in GPow, hints and inverses fell 56M -> 47M per execute; the traced Execute program min went 142 -> 124 ms over the three execute commits.crates/leanvm_core/src/cpu/execute.rs,crates/leanvm_core/src/cpu/hints.rs,crates/leanvm_core/src/cpu/layout.rs.GPow::pownow asserts the exponent is covered, where the table index would have panicked; the fill's table is always built rather than borrowed from the run.How to verify
Both pass on this branch;
cargo fmt --all --checkandcargo clippy --release -p leanvm_core -p keccak_gkr -p pcs -p primitives --all-targets -- -D warningsare clean. Proofs were checked by digest: throwaway builds of main and of this branch with a measurement patch print a hash of the proof bytes, and they match on every benchmark:7d91b8513bdd4b03(aggregate xmss 900),3c41e153f8b2ee37(aggregate sphincs 410),aa4b2ebd7a6989b2(recursion). Every benchmark run also verifies its proof.Commits
210a7a93bconstraints: expand each table's state once, fold with the next messageee76820d6leanvm_core: prove the bus GKR two rounds per pass with enough threads137dfd69fleanvm_core: form the products a push and a pull block share once48cea407bkeccak_gkr: skip sums by pivots and GFNI-weighted packs2e33b7205keccak_gkr: round values without reducing the factor Bca23a5f5dpcs: skip the empty vector of an AVX-512 sweep's short last chunke5f837c28pcs: run the NTT's layers depth first past L1f35ec0b61pcs: keep only the message of an extension encode whose layers are all deepbbf8c4c33execute: advance access counts in the access that reads or writes the cell76c2a226ehints: g-power index slots carry their keys, behind per-slot tags46bba9b95tables: fill each table's columns in one pass over its rows0fc4679d7hints: forward g-powers as strides times a first stride; the fill builds the table