Skip to content

perf(transcript): sample FRI query indices via a masked bit reservoir#817

Open
diegokingston wants to merge 4 commits into
mainfrom
feat/transcript-sample-bits
Open

perf(transcript): sample FRI query indices via a masked bit reservoir#817
diegokingston wants to merge 4 commits into
mainfrom
feat/transcript-sample-bits

Conversation

@diegokingston

@diegokingston diegokingston commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

FRI query indices live in the folded LDE domain of size `lde_length / 2`,
which is always a power of two. Because the range is a power of two, taking
the low `k = log2(domain/2)` bits of uniform hash output is exactly uniform
— no rejection sampling is needed. The current `sample_u64(domain>>1)` path
already produces the right (unbiased) values, but does so wastefully: one
full Keccak squeeze per query, of which only ~k of 256 bits are used.

Add `sample_bits(bits)` backed by a byte reservoir on `DefaultTranscript`:
one 32-byte squeeze now serves ~256/k query indices instead of one Keccak
permutation each — a ~10x reduction in query-phase permutations (8 vs 80 for
80 queries at k=24; see `sample_bits_amortizes_keccak_squeezes`). This is
purely a performance change (same range, unbiased either way); it matters
most for the recursion-guest verifier, where each permutation is a precompile
ecall. This mirrors Plonky3, which samples FRI indices with `sample_bits`
(mask, no rejection) over a hash-refilled reservoir, reserving rejection for
prime-field samples.

- `IsTranscript::sample_bits(bits)` with a safe default (`sample_u64(1<<bits)`)
  so other transcript impls keep compiling; `DefaultTranscript` overrides it
  with the reservoir version.
- The reservoir is cleared on every absorb (`append_bytes`), so sampled bits
  stay bound to the full prior transcript (matches Plonky3's HashChallenger,
  which clears its output buffer on `observe`). `state()` is unchanged
  (hasher-only); the reservoir is always empty at absorb/serialize boundaries.
- Both `sample_query_indexes` sites (prover + verifier) switch to `sample_bits`;
  they must — and do — stay bitwise identical.
- Adds a `keccak_squeezes()` profiling counter used by the amortization test.

Note: this changes the Fiat-Shamir sampling schedule, so existing serialized
proofs won't verify; prove+verify are updated together (roundtrip tests pass).
@diegokingston

Copy link
Copy Markdown
Collaborator Author

/bench-verify

@github-actions

Copy link
Copy Markdown

Benchmark started on the bench server. The verifier bench takes ~5 min; the recursion-guest cycle comparison then adds guest builds — a few minutes when cached, up to ~1h on a cold run. The bench server is occupied until it finishes.

@github-actions

Copy link
Copy Markdown

Verifier benchmark — d5c4ebf000 vs main (20 pairs)

=== Verify ABBA result ===

Metric main PR Δ
Verify time (per-side) 3.891s 3.888s -0.08% ⚪
Proof size 230.99 MiB 230.99 MiB +0.00% ⚪

Per-side (⚠️ PR REJECTS the baseline's valid proof — likely a VERIFY REGRESSION, not a format change): A/B/B/A cancels machine drift but not proof-specific variance — read the Verify-time Δ as approximate.

  pairs: 20   mean A (PR): 3.888s   mean B (main): 3.891s
  [parametric] paired-t   mean -0.08%   sd 0.66%   se 0.15%
               95% CI: [-0.39%, +0.23%]   (t df=19 = 2.093)
  [robust]     median +0.09%   Wilcoxon W+=110 W-=100  p(exact)=0.8695  (z=+0.17)

  run-to-run jitter:    A CV 0.43%   B CV 0.51%        (lower = steadier)
  within-session drift: -0.17% over the run, 1st->2nd half +0.05%

INCONCLUSIVE — effect not separable from 0 at n=20 (point estimate ~+0.09%). Add pairs to resolve.

Drift-free interleaved A/B/B/A measurement. - = PR faster. Trust the verdict when paired-t and Wilcoxon agree.


Recursion guest cycles (main vs PR)

=== Recursion-guest cycle comparison — single query (blowup=2, 1 query) — deterministic to ~±100k cycles ===
REF_B (baseline) origin/main c511b31 guest=recursion-min.elf
REF_A (PR) d5c4ebf d5c4ebf guest=recursion-min.elf

Metric REF_B (baseline) REF_A (PR) Δ (A-B)
Guest cycles 99.5M 99.1M -0.4M (-0.39%)
Keccak calls 3885 3884 -1

note: cycles reproduce to ~±100k (build codegen + proof nondeterminism); treat sub-100k deltas as noise, not signal.

raw (exact integer counts)
ref_b_sha=c511b31b7ff264ee69f536e3c6b5f40602953837 ref_b_elf=recursion-min.elf ref_b_cycles=99491236 ref_b_keccak=3885 ref_b_execute_wall_s=2
ref_a_sha=d5c4ebf00024887bef7d45db5dc371171ac40ad4 ref_a_elf=recursion-min.elf ref_a_cycles=99099958 ref_a_keccak=3884 ref_a_execute_wall_s=2
delta_cycles=-391278 delta_keccak=-1

@MauroToscano MauroToscano left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting this on pause for now: measured at the production regime (see the "Measured" section in the description), the improvement is too small right now — −0.32% guest cycles / −0.47% keccak — to prioritize a Fiat–Shamir sampling-schedule change. The approach and numbers check out; we'll revisit when the bigger levers have landed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants