Repository navigation
Conversation
The pack fold weighs each bit of a run of eight blocks by a fixed eq weight, so a run is one 32-byte BitFold row per position and instance. A task folds four side by side positions' 64 rows with one fold_columns call in place of 64 F192 table lookups per position and block: GFNI on x86 with GFNI, the AVX2 nibble shuffles on AVX2 without it, and elsewhere (aarch64) the byte tables as before, a row at a time. A task taking several side by side positions of a lane is also the shape of leanEthereum#730 (cde0b65); here it is four, the positions whose sixteen instances fill one 64-row fold. Same field elements, so the proof is unchanged: the per-position table fold stays as the test oracle (fold_packs_equal_the_byte_tables, six shapes, short runs and padding blocks included). 1 thread, pinned, Zen 5 (indicative): fold_packs at 2^16 instances, depth 12, new/old over alternated pairs in one process: - AVX-512 + GFNI build: 0.33 (26.6 -> 8.8 ms) - haswell build (AVX2 nibble shuffles): 0.76 to 0.77 (20.5 -> 15.7 ms) - built without AVX2 (byte tables, PCLMUL on): 0.84 (21.7 -> 18.3 ms) A run of fewer than eight blocks is still folded as 32 columns, so on the builds without GFNI a small batch folds its packs slower than the tables did: at 2^9 instances 0.50 -> 0.93 ms on the haswell build (the fold_packs span of the hash_batch bench). From 4,096 instances the runs are full.
fold_packs took the eight words of a position after chi from state_words, an array::from_fn over a per-word closure that LLVM left out of line: a call per position and block (409,600 at 2^17 instances and depth 12) of about 210 scalar instructions, with 24 loads each bounds-checked on its own and `along`'s divide by 5. state_words now takes the words of the position and of the two along its row as three slices of eight words, and applies chi and iota to them whole. It is inlined into the fold_packs task: on AVX-512 three 64-byte loads, two of them folded into a vpandnq and a vpternlogq with the iota mask broadcast; on AVX2 the same in two ymm halves; on aarch64 with SHA3, four bic and four eor3. The words are the same, so the proof is unchanged. 1 thread, pinned, Zen 5 (indicative): fold_packs at 2^16 instances, depth 12, new/old over 60 alternated pairs in one process: - AVX-512 + GFNI build: 0.89 to 0.90 (9.0 -> 8.1 ms) - built without AVX2 (byte tables, PCLMUL on): 0.94 (18.8 -> 17.6 ms) - haswell build: 0.97 (15.2 -> 14.7 ms), and 1.02 in a second build of the same harness: no reliable gain there
spare_slices built a subset-sum table of the eq table for every eight steps and looked each spare slot's transposed bytes up in it: at 2^17 instances and depth 12, 4.2M F192 additions and 14.7M lookups, over a 2^17-entry eq table. The eq table is the product of one on the low eight instance variables and one on the rest. So a chunk of 256 steps is one 64-row fold_columns by a BitFold of the low table, row z holding bit z of the chunk's steps as their transposed bytes already give it, and only the 64 sums meet the chunk's entry of the high table. One BitFold serves the whole call and the full eq table is not built. Same field elements, so the proof is unchanged: the table version stays as the test oracle (spare_slices_equal_the_byte_tables, six shapes, 7 to 53 spare slots). 1 thread, pinned, Zen 5 (indicative): spare_slices at 2^16 instances, depth 12, its 14 spare slots filled with random words, new/old over alternated pairs in one process: - AVX-512 + GFNI build: 0.19 to 0.20 (6.7 -> 1.3 ms) - haswell build (AVX2 nibble shuffles): 0.49 (7.5 -> 3.7 ms) - built without AVX2 (byte tables, PCLMUL on): 0.71 (6.0 -> 4.3 ms) With the spare slots all zero, as the hash_batch bench has them, the haswell build goes 6.9 -> 3.2 ms and the byte-table build stays level (5.9 -> 5.8 ms). Without carry-less multiply (plain x86-64, aarch64 without +aes) the 64 products a chunk and slot are software and cost more than the additions they replace (plain x86-64 at 2^11 instances: 1.2 -> 3.8 ms); no prover is built that way. At 2^9 instances the fold's setup shows on the haswell build: 0.06 -> 0.13 ms.
nconsigny
force-pushed
the
pr/keccak-open-bitfold
branch
from
November 11, 2026 18:13
4fe1c56 to
31f338b
Compare
Benchmarks: base against this PR, on one runner31f338b merged into its base, against the base dd759d2: 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.
What and why
open::fold_packs(the input opening's pack fold) weighs every bit of a run of eight blocks by a fixedeqweight, and summed them with 64 F192 subset-sum table lookups per position and block. A run is therefore one 32-byte row per position and instance, which is whatprimitives::bit_fold::BitFoldfolds: a task now folds four positions' 64 rows with onefold_columnscall (GFNI on x86, the AVX2 nibble shuffles without GFNI, the same byte tables elsewhere). One commit,crates/keccak_gkronly, +103/-26 (65 of the added lines are the test).Measured
Ryzen 9 9955HX3D (Zen 5, 16C/32T), 16 threads,
--repeat 3 --cooldown 1, main (e5d75f8) vs this branch run in turns (ABBA). Δ is the median of the paired differences, [min, max]. Openings is theOpeningsspan of the traced pass.aggregate --sphincs 500 --log-inv-rate 2aggregate --sphincs 480 --log-inv-rate 2aggregate --sphincs 500 --log-inv-rate 1aggregate --sphincs 480 --log-inv-rate 1hash --log-n 18 --log-inv-rate 2aggregate --xmss 1400 --log-inv-rate 2Openings falls from 11-15 ms to about 7 ms on every workload. The end-to-end Prove change is within the noise (per-pass Prove spreads about ±30 ms within a run): the rate-1 and leanXMSS rows have 3 pairs, and the leanXMSS as-run excess sits in spans this change does not touch (Keccak reduction, Skip, of one traced pass). One machine, one
target-cpu=native(AVX-512 + GFNI) build; the AVX2 and portable arms were not timed. Single thread, pinned, 2^18 instances, depth 12 (indicative, shared box):fold_packs112-131 ms to 37-48 ms.Proof bytes and soundness
Prover-only: the fold computes the same field elements, so proofs are byte for byte identical. A
recursion --n 1 --sphincs-per-leaf 16 --leaf-log-inv-rate 2leaf proof (199,521 B) has the same sha256 as main's, and proof sizes were equal on every benchmarked workload. The verifier is untouched. No newunsafe; the weights are read withget(..).unwrap_or(ZERO), every other index is bounded by constants (block < 8, pack < 4, 16 * dz + 16 <= 64), andBitFold::new/fold_columns::<32>assert on constants that always hold.Tests
fold_packs_equal_the_byte_tables: the previous per-position table fold stays in the test module as the oracle, and the new fold must equal it on six shapes (0 to 70,000 instances, depth 6 to 24), short runs and the padding block included.cargo test --release -p keccak_gkrpasses withtarget-cpu=native(GFNI + AVX-512),haswell(AVX2, no GFNI) andx86-64(byte tables); aarch64 was not built locally.clippy -D warnings,fmt --checkanddocare clean.Memory
One
BitFoldof 256 weights per call, which holds the same 32 byte tables as before (about 196 KB) plus its GFNI matrices, and a 2 KiB row buffer on each task's stack. Peak RSS is unchanged (every paired delta within 0.01 GiB).