Skip to content

perf: cheaper decoding and challenge setup for RISC-V guest verifiers - #188

Merged
markosg04 merged 11 commits into
mainfrom
markos/guest-decode-perf
Oct 27, 2026
Merged

markosg04 merged 11 commits into
mainfrom
markos/guest-decode-perf

Conversation

@markosg04

@markosg04 markosg04 commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Verifier-side changes that cut instruction counts on a RISC-V zkVM guest, where sub-word memory operations and software floating point are expensive. Proofs, transcripts, and verifier outputs are unchanged.

  • Golomb–Rice: codes that fit one aligned 64-bit word decode from that word, and unary runs are counted a byte at a time. A test covers every width at every bit offset and byte alignment.
  • Proof atoms: atoms encode into an inline, word-aligned 64-byte buffer instead of a heap Vec. Longer encodings spill to the heap; a test covers both cases.
  • Challenge setup: SparseChallengeConfig::log2_support_bits memoizes per (D, counts), and the op-norm trig tables compute each certified first-octant enclosure once.
  • Paired tensors: evaluation looks up per-bit factor pairs, and dense-left materialization collects residual offsets once instead of recursing per output coordinate. The residual tables reserve fallibly, after the work bound is checked.

The support-bits memo holds at most 64 families and takes its lock once per call. A test pins the word-window path's quotient cap at every bit offset and byte alignment (it fails if the cap check is removed).

Review guide

Read in this order (one commit; every change must leave outputs and rejections unchanged):

  1. crates/akita-params/src/golomb_rice.rs: BitReader::word_window and the fast path in golomb_rice_decode_one_from.
    • Check the quotient cap and the end-of-buffer fallback.
    • Tests cover every width and offset, and the cap at every bit offset and byte alignment. The cap test fails if the check is removed.
  2. crates/akita-transcript/src/proof_stream.rs: AtomBytes, which writes the same bytes as the previous Vec encoding.
  3. crates/akita-algebra/src/offset_eq/tensor_pair/{evaluate,materialize}.rs: basis_pair_factors and collect_residual/evaluate_residual, replacing contract_residual_tensor_axes. Allocation is fallible and happens after the work bound.
  4. crates/akita-challenges/src/config.rs (the bounded support-bits memo) and sampler/op_norm.rs (the first-octant table; octant_reduce bounds the index for every degree).

These are standalone; none depends on the other PRs.

Validation

On this branch, rebased on main 401e79728: fmt, the three CI clippy lanes, the guardrail and dependency scripts, cargo machete, and taplo, and the workspace tests (1,861 passed). On the combined series at markos/recursion-integration 78d31e124, a16z/jolt#1911's recursion example accepts a fresh proof in the guest, rejects one with a flipped opening-proof bit, and accepts in input mode.

Series

These PRs upstream the Akita changes that a16z/jolt#1911 needs to verify Akita proofs inside a Jolt RISC-V guest in under 2^26 trace rows. That PR now pins markos/recursion-integration in this repository (main plus these five) instead of a personal fork, and repins to main once they merge.

PR Change Jolt pin
#186 In-place trusted setup and terminal NTT cache views current
#187 Selected-row catalog verifier views current
#188 Cheaper decoding and challenge setup on guests current
#189 Blake2b, Keccak, and NTT inline backends a16z/jolt#1911 head
#190 Verifier dot products through jolt-field row kernels a16z/jolt#1911 head

The first three are independent and build against the Jolt revision main pins today. The last two use Jolt APIs added in a16z/jolt#1911 (jolt-inlines-ntt and jolt-field's row kernels), so they share one commit pinning Jolt at that PR's head, a commit on a16z/jolt that is not yet on its main.

Merge order: once these land, a16z/jolt#1911 pins Akita main instead of the fork, and a follow-up here repins Jolt to its merge commit. Alternatively, the Jolt-side APIs can land first in a smaller a16z/jolt PR, so the last two pin a Jolt main commit from the start.

A per-change row ladder for the guest is in specs/recursion-guest.md on a16z/jolt#1911.

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

Warning

This PR has more than 500 changed lines and does not include a spec.

Large features and architectural changes benefit from a short spec before implementation review.
See CONTRIBUTING.md for the spec workflow.

If this PR is a bug fix, small cleanup, or otherwise does not warrant a spec, feel free to ignore this message.

@github-actions github-actions Bot added the no-spec PR has no spec file label Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Documentation blast radius (advisory)

These regions may need doc/spec/book updates based on changed paths.
This is not a merge gate. See docs/documentation.md.

Changed files in this PR: 9

sis-security

SIS floors, norm bounds, challenge sampling

Code paths touched:

  • crates/akita-challenges/src/config.rs
  • crates/akita-challenges/src/sampler/op_norm.rs

Consider updating:

  • book/src/how/security.md
  • book/src/foundations/lattices-sis.md
  • specs/archive/2026-Q2/akita-sis-*.md
  • specs/archive/2026-Q2/sis-euclidean-estimator.md
  • specs/fold-linf-rejection.md
  • docs/security-posture.md

transcript

Fiat-Shamir transcript and instance binding

Code paths touched:

  • crates/akita-transcript/src/proof_stream.rs
  • crates/akita-transcript/src/proof_stream/atom_encoding_tests.rs

Consider updating:

  • book/src/how/transcript.md
  • book/src/usage/commitment-api.md
  • specs/transcript-*.md

field-algebra

Field traits, packed SIMD, extension towers

Code paths touched:

  • crates/akita-algebra/src/offset_eq/tensor_pair/evaluate.rs
  • crates/akita-algebra/src/offset_eq/tensor_pair/materialize.rs
  • crates/akita-algebra/src/offset_eq/tensor_pair/mod.rs
  • crates/akita-algebra/src/offset_eq/tests/tensor_pair_batches.rs

Consider updating:

  • book/src/foundations/rings-and-fields.md
  • book/src/foundations/ntt-crt.md
  • book/src/how/optimizations.md
  • specs/archive/2026-Q3/akita-field-refactor.md
  • specs/archive/2026-Q3/crt-ntt-*.md
  • specs/archive/2026-Q3/avx-simd-port.md

Per-PR checklist: spec Status / acceptance criteria; book owning page; AGENTS.md if contracts changed; archive spec after fold.

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

PCS Profile Benchmark

13 of 13 profiles passed.

Times are medians of 3 measured runs after 1 discarded warmup run. Peak RSS is the largest measured value.

Each sample verifies the same proof first with the configured multi-threaded pool and then with one thread. Both timings reuse the same verifier setup.

Merge-base comparisons are available for 13 of 13 profiles. For matching profiles, the head and merge-base binaries ran interleaved on the same runner.

Benchmark shards

CI shard Profiles
1-fp32-base Fp32 dense nv30, direct setup check
Fp32 one-hot nv34, direct setup check
2-fp64-base Fp64 dense nv29, direct setup check
Fp64 one-hot nv35, direct setup check
3-fp128-base Fp128 dense nv28, direct setup check
Fp128 one-hot nv36, direct setup check
Fp128 one-hot nv36, recursive setup check
4-multi-group-direct Fp128 multi-group (final nv34, 4 polys total), direct setup check
5-multi-group-recursive Fp128 multi-group (final nv34, 4 polys total), recursive setup check
6-multi-group-recursive-w8r2 Fp128 multi-group W8R2 (final nv32, 4 polys total), recursive setup check
7-distributed Fp128 one-hot nv32 W2R2, direct setup check
Fp128 one-hot nv32 W4R2, direct setup check
Fp128 one-hot nv32 W8R2, direct setup check

Public opening statements

Public opening statement Profiles
Over Fp32, one committed 30 variable multilinear polynomial with 2^30 coefficients is opened at one 30 coordinate point. Fp32 dense nv30, direct setup check
Over Fp32, one committed 34 variable multilinear polynomial with 2^34 coefficients is opened at one 34 coordinate point. Fp32 one-hot nv34, direct setup check
Over Fp64, one committed 29 variable multilinear polynomial with 2^29 coefficients is opened at one 29 coordinate point. Fp64 dense nv29, direct setup check
Over Fp64, one committed 35 variable multilinear polynomial with 2^35 coefficients is opened at one 35 coordinate point. Fp64 one-hot nv35, direct setup check
Over Fp128, one committed 28 variable multilinear polynomial with 2^28 coefficients is opened at one 28 coordinate point. Fp128 dense nv28, direct setup check
Over Fp128, one committed 36 variable multilinear polynomial with 2^36 coefficients is opened at one 36 coordinate point. Fp128 one-hot nv36, direct setup check
Fp128 one-hot nv36, recursive setup check
Over Fp128, 4 polynomials in 3 groups: one 16 variable polynomial at its own point, one 16 variable polynomial at its own point, and 2 34 variable polynomials at one shared point. Fp128 multi-group (final nv34, 4 polys total), direct setup check
Fp128 multi-group (final nv34, 4 polys total), recursive setup check
Over Fp128, 4 polynomials in 3 groups: one 16 variable polynomial at its own point, one 16 variable polynomial at its own point, and 2 32 variable polynomials at one shared point. Fp128 multi-group W8R2 (final nv32, 4 polys total), recursive setup check
Over Fp128, one committed 32 variable multilinear polynomial with 2^32 coefficients is opened at one 32 coordinate point. Fp128 one-hot nv32 W2R2, direct setup check
Fp128 one-hot nv32 W4R2, direct setup check
Fp128 one-hot nv32 W8R2, direct setup check

One-hot profiles generate deterministic witnesses with one 1 in every consecutive chunk of 256 coefficients. This witness shape is not a separate public claim.

Direct evaluates the public setup contribution during Stage 2. Recursive carries the same check through a Stage 3 setup-product sumcheck. Both modes execute the complete fold schedule and terminal verification.

The chunked profiles W2R2, W4R2, W8R2 divide the witness relation into the stated number of exact chunks for the first two fold levels.

Generated profiles may select different A, B, and D ring dimensions at different fold levels. The short profile names omit those dimensions.

Each sample generates deterministic witnesses and opening points, prepares setup, commits, proves, serializes the proof, checks its size, prepares verifier setup, and verifies the claimed openings. It does not test malformed proofs.

Phase time

Profile Setup Commit Prove Verify, multi-threaded Verify, single-threaded
Fp32 dense nv30, direct setup check 0.024 s
+0.1%
1.140 s
+0.4%
1.350 s
+0.1%
19.1 ms
-1.1%
46.7 ms
-2.7%
Fp32 one-hot nv34, direct setup check 0.047 s
-0.1%
0.731 s
-1.6%
1.451 s
-0.2%
19.5 ms
-6.5%
90.0 ms
-0.8%
Fp64 dense nv29, direct setup check 0.052 s
+1.3%
0.794 s
-0.2%
0.883 s
+0.3%
13.0 ms
-5.1%
34.7 ms
-4.1%
Fp64 one-hot nv35, direct setup check 0.092 s
+0.1%
1.677 s
+1.6%
1.311 s
+2.2%
16.5 ms
-3.5%
83.6 ms
-1.4%
Fp128 dense nv28, direct setup check 0.080 s
-1.1%
1.029 s
-4.9%
0.990 s
-0.1%
12.4 ms
-3.1%
42.8 ms
-2.7%
Fp128 one-hot nv36, direct setup check 0.229 s
+0.1%
4.718 s
+0.3%
2.106 s
-1.1%
23.7 ms
-4.5%
143.5 ms
-1.1%
Fp128 one-hot nv36, recursive setup check 0.681 s
-0.3%
5.233 s
+0.1%
1.999 s
-3.8%
17.7 ms
-4.1%
43.5 ms
-2.7%
Fp128 multi-group (final nv34, 4 polys total), direct setup check 0.146 s
+0.6%
2.419 s
-0.4%
1.190 s
+3.4%
19.9 ms
-0.8%
112.0 ms
-1.3%
Fp128 multi-group (final nv34, 4 polys total), recursive setup check 0.542 s
-0.6%
2.226 s
+1.4%
1.701 s
-1.1%
17.2 ms
-7.0%
34.2 ms
-3.5%
Fp128 multi-group W8R2 (final nv32, 4 polys total), recursive setup check 0.365 s
+2.0%
0.588 s
+0.0%
1.708 s
-1.5%
25.4 ms
-13.4%
67.0 ms
-9.8%
Fp128 one-hot nv32 W2R2, direct setup check 0.037 s
-0.6%
0.316 s
+0.4%
0.660 s
+1.3%
16.1 ms
-6.6%
48.4 ms
-2.4%
Fp128 one-hot nv32 W4R2, direct setup check 0.041 s
-0.7%
0.316 s
+0.5%
0.795 s
-1.2%
18.7 ms
-1.8%
57.4 ms
-3.0%
Fp128 one-hot nv32 W8R2, direct setup check 0.040 s
-1.6%
0.316 s
+0.9%
1.055 s
+0.0%
20.9 ms
+3.9%
67.1 ms
-4.0%

Proof size and protocol shape

Profile Fold A/B/D schedule Total proof Terminal response Terminal response planned maximum Fold levels Grinding retries
Fp32 dense nv30, direct setup check 2048/256/256 → 128/128/128 75,294 bytes
+0.0%
43,729 bytes
+0.0%
45,688 bytes
+0.0%
7
+0.0%
None
Fp32 one-hot nv34, direct setup check 2048/256/256 → 256/128/256 → 128/128/128 75,398 bytes
+0.0%
43,718 bytes
+0.0%
45,688 bytes
+0.0%
7
+0.0%
None
Fp64 dense nv29, direct setup check 2048/128/128 → 1024/128/128 → 128/128/128 → 64/64/64 77,301 bytes
+0.0%
45,155 bytes
+0.0%
47,620 bytes
+0.0%
8
+0.0%
None
Fp64 one-hot nv35, direct setup check 1024/256/128 → 256/128/128 → 128/128/128 → 64/64/64 77,438 bytes
+0.0%
45,181 bytes
+0.0%
47,620 bytes
+0.0%
8
+0.0%
L6: 1 / 1 / 1
Fp128 dense nv28, direct setup check 1024/64/64 → 128/64/64 → 64/64/64 80,581 bytes
+0.0%
51,978 bytes
+0.0%
54,262 bytes
+0.0%
7
+0.0%
None
Fp128 one-hot nv36, direct setup check 512/128/64 → 512/64/64 → 64/64/64 81,777 bytes
+0.0%
51,987 bytes
+0.0%
54,262 bytes
+0.0%
8
+0.0%
None
Fp128 one-hot nv36, recursive setup check 512/128/64 → 64/64/64 84,962 bytes
+0.0%
51,984 bytes
+0.0%
54,262 bytes
+0.0%
8
+0.0%
None
Fp128 multi-group (final nv34, 4 polys total), direct setup check 512/128/128 → 512/64/64 → 64/64/64 81,624 bytes
+0.0%
51,948 bytes
+0.0%
54,262 bytes
+0.0%
8
+0.0%
None
Fp128 multi-group (final nv34, 4 polys total), recursive setup check 512/128/128 → 64/64/64 89,769 bytes
+0.0%
51,963 bytes
+0.0%
54,262 bytes
+0.0%
8
+0.0%
None
Fp128 multi-group W8R2 (final nv32, 4 polys total), recursive setup check 512/64/64 → 128/64/64 → 64/64/64 83,746 bytes
+0.0%
52,016 bytes
+0.0%
54,262 bytes
+0.0%
8
+0.0%
None
Fp128 one-hot nv32 W2R2, direct setup check 512/64/64 → 64/64/64 81,550 bytes
+0.0%
51,986 bytes
+0.0%
54,262 bytes
+0.0%
8
+0.0%
None
Fp128 one-hot nv32 W4R2, direct setup check 512/64/64 → 256/64/64 → 64/64/64 81,782 bytes
+0.0%
51,992 bytes
+0.0%
54,262 bytes
+0.0%
8
+0.0%
None
Fp128 one-hot nv32 W8R2, direct setup check 512/64/64 → 256/64/64 → 64/64/64 82,329 bytes
+0.0%
51,970 bytes
+0.0%
54,262 bytes
+0.0%
8
+0.0%
None

Grinding retries are rejected attempts at each fold, listed in measured-run order. Zero means the first sampled nonce was accepted.

Each multi-group profile has two precommitted nv16 singleton polynomials and two polynomials in the displayed final group.

Native proof accounting

Profile Non-nonce proof Nonce messages Nonce maximum
Fp32 dense nv30, direct setup check 74,785 bytes
+0.0%
509 bytes
+0.0%
1,016 bytes
+0.0%
Fp32 one-hot nv34, direct setup check 74,886 bytes
+0.0%
512 bytes
+0.0%
1,020 bytes
+0.0%
Fp64 dense nv29, direct setup check 76,803 bytes
+0.0%
498 bytes
+0.0%
992 bytes
+0.0%
Fp64 one-hot nv35, direct setup check 76,941 bytes
+0.0%
497 bytes
+0.0%
996 bytes
+0.0%
Fp128 dense nv28, direct setup check 80,202 bytes
+0.0%
379 bytes
+0.0%
758 bytes
+0.0%
Fp128 one-hot nv36, direct setup check 81,347 bytes
+0.0%
430 bytes
+0.0%
856 bytes
+0.0%
Fp128 one-hot nv36, recursive setup check 84,432 bytes
+0.0%
530 bytes
+0.0%
1,048 bytes
+0.0%
Fp128 multi-group (final nv34, 4 polys total), direct setup check 81,196 bytes
+0.0%
428 bytes
+0.0%
854 bytes
+0.0%
Fp128 multi-group (final nv34, 4 polys total), recursive setup check 89,163 bytes
+0.0%
606 bytes
+0.0%
1,204 bytes
+0.0%
Fp128 multi-group W8R2 (final nv32, 4 polys total), recursive setup check 83,280 bytes
+0.0%
466 bytes
+0.0%
930 bytes
+0.0%
Fp128 one-hot nv32 W2R2, direct setup check 81,122 bytes
+0.0%
428 bytes
+0.0%
848 bytes
+0.0%
Fp128 one-hot nv32 W4R2, direct setup check 81,352 bytes
+0.0%
430 bytes
+0.0%
856 bytes
+0.0%
Fp128 one-hot nv32 W8R2, direct setup check 81,890 bytes
+0.0%
439 bytes
+0.0%
868 bytes
+0.0%

Memory and setup size

Profile Setup vector Prepared NTT cache Verifier NTT cache Peak RSS
Fp32 dense nv30, direct setup check 20.0 MiB
+0.0%
154.0 MiB
+0.0%
1.2 MiB
+0.0%
4641.3 MiB
+0.2%
Fp32 one-hot nv34, direct setup check 64.0 MiB
+0.0%
354.1 MiB
+0.0%
1.2 MiB
+0.0%
1040.1 MiB
+0.0%
Fp64 dense nv29, direct setup check 32.0 MiB
+0.0%
291.2 MiB
+0.0%
1.8 MiB
+0.0%
4867.6 MiB
+0.0%
Fp64 one-hot nv35, direct setup check 128.0 MiB
+0.0%
917.2 MiB
+0.0%
1.8 MiB
+0.0%
1858.7 MiB
-0.1%
Fp128 dense nv28, direct setup check 64.0 MiB
+0.0%
585.0 MiB
+0.0%
1.5 MiB
+0.0%
5183.0 MiB
+0.0%
Fp128 one-hot nv36, direct setup check 344.0 MiB
+0.0%
2832.0 MiB
+0.0%
3.0 MiB
+0.0%
4413.7 MiB
+0.0%
Fp128 one-hot nv36, recursive setup check 512.0 MiB
+0.0%
3600.0 MiB
+0.0%
1.5 MiB
+0.0%
5326.5 MiB
+0.0%
Fp128 multi-group (final nv34, 4 polys total), direct setup check 256.0 MiB
+0.0%
1319.9 MiB
+0.0%
3.0 MiB
+0.0%
2390.4 MiB
-0.2%
Fp128 multi-group (final nv34, 4 polys total), recursive setup check 256.0 MiB
+0.0%
1400.0 MiB
+0.0%
1.5 MiB
+0.0%
2702.2 MiB
+0.1%
Fp128 multi-group W8R2 (final nv32, 4 polys total), recursive setup check 128.0 MiB
+0.0%
620.5 MiB
+0.0%
1.5 MiB
+0.0%
1568.5 MiB
-0.1%
Fp128 one-hot nv32 W2R2, direct setup check 64.0 MiB
+0.0%
321.0 MiB
+0.0%
1.5 MiB
+0.0%
703.8 MiB
+2.8%
Fp128 one-hot nv32 W4R2, direct setup check 64.0 MiB
+0.0%
345.0 MiB
+0.0%
1.5 MiB
+0.0%
756.8 MiB
+0.1%
Fp128 one-hot nv32 W8R2, direct setup check 64.0 MiB
+0.0%
345.0 MiB
+0.0%
1.5 MiB
+0.0%
846.1 MiB
-0.0%

Deltas are shown only for profiles with a matching merge-base case. Negative is smaller or faster.

The uploaded report.md benchmark artifact contains the detailed fold schedule, proof-size breakdown, terminal response segments, and Golomb diagnostics.

@markosg04
markosg04 force-pushed the markos/guest-decode-perf branch from f3ee260 to d202349 Compare October 6, 2026 06:14
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

CI test timing

  • Report generated: 2026-10-27T18:32:38Z.
  • Source: 1de9f2b on markos/guest-decode-perf.
  • Workflow run: 40551241229.
  • Main baseline: 33cfd11.
  • Previous run: 21f2eaa.

Run summary

Wall s Main wall s Main Δ Ratio Tests Skipped Failed Status
1674.0 1708.0 -2.0% 0.98x 2032 0 0 ok

Wall time spans 2 parallel nextest slice shards.

Slowest tests

Rank Duration s Test
1 484.2 akita-planner::schedule_params::suffix_dp::chunk_ownership_tests::contracting_chunk_search_matches_unpruned_complete_objective
2 178.9 akita-planner::schedule_params::adaptive_search_tests::adaptive_frontier_matches_unpruned_traversal_and_hand_priced_role_optima
3 97.6 akita-planner::planner::setup_prefix_challenge_hook_tests::catalog_binding_revalidates_recursive_setup_prefix_challenge_hook
4 59.6 akita-planner::planner::totality_tests::contractive_winner_remains_selected
5 35.1 akita-planner::schedule_params::adaptive_search_tests::relation_cutover::bounded_suffix_dp_matches_unpruned_fixed_cutover_search
6 27.4 akita-planner::planner::totality_tests::noncontractive_multi_chunk_root_can_beat_contractive_candidates
7 26.2 akita-planner::generated_families::tests::generation_uses_requested_geometry_for_preplans_and_grouped_producers
8 21.1 akita-pcs::dynamic_backends::portable::offloaded_route_uses_only_local_prefix_handles
9 19.5 akita-planner::schedule_params::adaptive_search_tests::adaptive_nv36_minimizes_setup_envelope_before_first_direct_setup
10 18.0 akita-planner::planner::totality_tests::valid_small_grouped_root_has_a_schedule
11 16.8 akita-pcs::akita_small_field_e2e::combined_recursive::fp32_combined_setup_proves_recursive_and_onehot_families
12 16.7 akita-planner::schedule_params::adaptive_search_tests::production_suffix_selects_l2_with_the_typed_response_model
13 16.6 akita-pcs::akita_fp128_e2e::heterogeneous::bounded_dense_roundtrip_over_u64_coefficients_at_every_catalog_size
14 16.1 akita-pcs::scheme::tests::cross_mode::proofs_cannot_replay_across_valid_quotient_and_reduced_schedules
15 13.5 akita-pcs::dynamic_backends::dynamic_backend_failures_cleanup_and_retry
16 13.1 akita-pcs::transcript_hardening_proptest::seed_corpus_covers_nv_basis_and_batch_shapes
17 12.6 akita-pcs::akita_fp128_e2e::fp128_dense
18 12.3 akita-planner::schedule_params::adaptive_search_tests::adaptive_search_supports_direct_multi_chunk_policy
19 10.1 akita-pcs::akita_fp128_e2e::fp128_onehot
20 10.0 akita-pcs::akita_small_field_e2e::selective_l2::fp32_ext4_l2_pcs_roundtrip_and_stage2_rejections

Regressions vs main

Δ s Baseline s Current s Test
71.1 107.8 178.9 akita-planner::schedule_params::adaptive_search_tests::adaptive_frontier_matches_unpruned_traversal_and_hand_priced_role_optima
35.8 61.7 97.6 akita-planner::planner::setup_prefix_challenge_hook_tests::catalog_binding_revalidates_recursive_setup_prefix_challenge_hook
10.2 17.1 27.4 akita-planner::planner::totality_tests::noncontractive_multi_chunk_root_can_beat_contractive_candidates
9.6 16.6 26.2 akita-planner::generated_families::tests::generation_uses_requested_geometry_for_preplans_and_grouped_producers
7.1 12.3 19.5 akita-planner::schedule_params::adaptive_search_tests::adaptive_nv36_minimizes_setup_envelope_before_first_direct_setup
6.6 11.4 18.0 akita-planner::planner::totality_tests::valid_small_grouped_root_has_a_schedule
6.4 10.2 16.7 akita-planner::schedule_params::adaptive_search_tests::production_suffix_selects_l2_with_the_typed_response_model
6.0 10.2 16.1 akita-pcs::scheme::tests::cross_mode::proofs_cannot_replay_across_valid_quotient_and_reduced_schedules
5.1 7.5 12.6 akita-pcs::akita_fp128_e2e::fp128_dense

New slow tests

No new tests ≥30s vs main baseline.

@markosg04
markosg04 force-pushed the markos/guest-decode-perf branch from d202349 to 8f5f6ad Compare October 6, 2026 18:34
@markosg04
markosg04 marked this pull request as ready for review October 6, 2026 23:17
@markosg04
markosg04 requested a review from a team October 6, 2026 23:17
Verifier-side changes that cut instruction counts on a RISC-V zkVM guest,
where sub-word memory operations and software floating point are
expensive. Proofs, transcripts, and outputs are unchanged.

- Golomb-Rice decoding reads codes that fit one aligned 64-bit word from
  that word, and counts unary runs a byte at a time.
- Proof atoms encode into an inline, word-aligned 64-byte buffer instead
  of a heap Vec.
- SparseChallengeConfig::log2_support_bits memoizes per (D, counts), and
  the op-norm trig tables compute each certified first-octant enclosure
  once.
- Paired tensor evaluation looks up per-bit factor pairs, and dense-left
  materialization collects the residual offsets once instead of
  recursing per output coordinate.
@markosg04
markosg04 force-pushed the markos/guest-decode-perf branch from 8f5f6ad to 22e2a32 Compare October 9, 2026 17:18
@markosg04

Copy link
Copy Markdown
Collaborator Author

/ai-review

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

AI review attempt 1/3 reserved. Failed or incomplete attempts also count toward this PR's limit.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Usefulness: benefit supported. The guest-verifier motivation is supported by the code: these changes remove repeated logarithm and Taylor evaluations, per-field encoding allocations, and duplicate tensor recurrence work on existing consumers. They add no dependencies or public format changes. The main costs are cache synchronization and residual-table memory; the latter needs the correction below. Exact guest instruction savings were not independently verified.

Approval not recommended: new P0-P3 findings in this review.

for view in &mut views {
if !view.family.scalar.is_zero() {
view.residual
.try_reserve_exact(view.residual_len)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P2] This reserves the full Cartesian product even when zero weights eliminate every residual. For example, a scalar-one family with axes unit(2, 1, 1), dense(0, 0, vec![F::zero(); 2]), and unit(1 << 26, 0, 1) passes the 1 << 28 work bound but reserves 1 << 27 entries before collecting none—2 GiB for an eight-byte field on a 64-bit target. The base implementation skips the zero-weight branches and returns a two-element zero output with negligible scratch space. This turns a cheap valid input into an allocation failure on bounded-memory guests. Can we count live residuals or grow the table fallibly as entries are emitted, and add a zero-heavy regression test?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in b7f51c0. Each axis reports its nonzero-weight count in closed form: len for unit axes, the count of nonzero entries for dense axes, and the product of per-bit nonzero factor counts for bit-product axes (fields have no zero divisors). The residual table reserves exactly the product of those counts, so your example reserves nothing. The regression test eq_pair_tensor_materialization_of_zero_weight_residuals_is_zero uses that family. On hosts that overcommit memory the old reservation was never touched, so the test checks the result and the code path; the memory saving only shows on hosts that commit memory up front.

@github-actions github-actions Bot added the ai-reviewed An automated AI review has been published on this PR label Oct 9, 2026
The residual table reserved the full Cartesian product of the
non-destination axes even when zero weights removed every entry, so a
cheap valid family could reserve 2^27 entries and collect none. Each
axis now reports its nonzero-weight count in closed form, and the
table reserves the product of those counts.
markosg04 added a commit that referenced this pull request Oct 12, 2026

@RadNi RadNi 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.

Inline feedback: one malformed-metadata correctness issue, plus suggestions on residual memory budgeting, exact-output regression coverage, and isolated guest performance measurements.

Comment on lines +113 to +115
EqPairTensorWeights::BitProduct(bit_factors) => {
bit_factors.iter().fold(1, |count, factors| {
count * factors.iter().filter(|factor| !factor.is_zero()).count()

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.

[P2] Validate axis geometry before counting nonzero entries

The preceding materialization work bound uses axis.len, while this count trusts the backing factors. Since axis fields are publicly mutable, a residual axis with len = 2 and 64 bit-factor pairs of [1, 1], alongside a destination axis unit(2, 1, 3), passes a nominal work check of four coordinates but computes 2^64 here on a 64-bit target. This panics with overflow checks enabled and wraps otherwise. I reproduced the panic using the branch's tensor/materialization source in a small harness with substitute field/equality operations; the base path completed. This demonstrates the arithmetic regression, not reachability from malicious proof bytes.

Please validate dense-weight lengths and bit-product factor counts against axis.len before allocation sizing, reject inconsistent geometry with AkitaError, and add a malformed residual-axis regression test. The outer product of live counts also needs to rely on that validated invariant.

Comment on lines +298 to +301
.fold(1, |count, (_, axis)| count * axis.nonzero_len());
view.residual.try_reserve_exact(live).map_err(|_| {
AkitaError::InvalidInput("paired tensor residual table allocation failed".into())
})?;

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.

Suggestion: bound residual-table bytes separately from arithmetic work

Counting only live entries addresses the earlier zero-weight allocation issue, but a valid all-nonzero family with axes unit(2, 1, 3) and unit(1 << 27, 0, 1) still passes the 1 << 28 work limit and reserves 1 << 27 residual entries. On a 64-bit target with an eight-byte field, the (usize, F) table alone is about 2 GiB, despite the output having only two elements. The previous recursive contraction used much less auxiliary memory. Fallible reservation does not by itself establish a practical memory budget for a bounded-memory guest.

Could we cap precomputation bytes separately and retain streaming contraction when that budget is exceeded? A regression test can exercise the budget/fallback decision using a small test budget rather than allocating gigabytes.

}

#[test]
fn atom_bytes_encode_inline_and_spilled_lengths() {

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.

Suggestion: pin actual atom encodings and a transcript challenge

This test verifies the inline/heap container with synthetic bytes, but it does not exercise the changed field-atom and extension-atom encoding paths. Existing prover/verifier round trips can still succeed if both sides change identically.

Please add byte-for-byte comparisons of actual FieldAtom encodings against canonical field bytes and ExtensionAtom encodings against concatenated canonical coefficient encodings. A pinned transcript challenge for a fixed message sequence would also independently check the PR's claim that transcript bytes and challenges remain unchanged. Keep the current length-boundary test as coverage for the storage choice.

let cos_phi = taylor_cos((phi_lo, phi_hi));
let sin_phi = taylor_sin((phi_lo, phi_hi));
// octant_reduce bounds r by d/4 for every degree.
let (cos_phi, sin_phi) = first_octant[r];

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.

Suggestion: compare certified tables exactly with the previous construction

The existing approximate-reference checks verify enclosure accuracy, but do not establish that this optimization preserves every fixed-point entry and the global eps_root exactly. Those values affect the acceptance boundary used by challenge sampling.

Could we retain the previous per-angle construction as a test-only reference and assert exact equality of base_cos, base_sin, and eps_root across the supported degrees and representative scales? That would directly substantiate the unchanged-output/rejection claim for this refactor.

// families; the bound keeps arbitrary callers from growing the table.
const MAX_CACHED_FAMILIES: usize = 64;
type SupportCacheEntry = ((usize, usize, usize), f64);
static CACHE: std::sync::Mutex<Vec<SupportCacheEntry>> = std::sync::Mutex::new(Vec::new());

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.

Suggestion: include isolated guest performance measurements

The PR description reports validation and combined-series guest integration, but does not give isolated before/after guest instruction or trace-row counts for these optimizations. The new memo also makes cold-cache and warm-cache costs different.

Please include reproducible before/after measurements for this PR, ideally broken down by optimization, with cold-cache versus warm-cache results and peak memory for residual materialization. Including the fixture, guest revision, and build configuration would make the claimed benefit and the memory tradeoff directly reviewable.

@markosg04

Copy link
Copy Markdown
Collaborator Author

/ai-review

@github-actions

Copy link
Copy Markdown

AI review attempt 2/3 reserved. Failed or incomplete attempts also count toward this PR's limit.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Usefulness: benefit supported. The guest-verifier motivation is supported by existing consumers: these changes remove per-atom encoding allocations, repeated Taylor evaluations, repeated support calculations, and redundant tensor work without adding dependencies or intentionally changing public formats. The main costs are cache synchronization and residual-table memory. The optimization premise remains useful, but the confirmed residual-sizing issues below need resolution. Exact guest instruction savings were not independently verified.

Approval not recommended: unresolved feedback from existing discussion.

Unresolved feedback from existing discussion:

  • RadNi’s comment 4331364217 remains unresolved. EqPairTensorAxis fields are public, but materialize_eq_tensor_left does not revalidate their geometry. The work check in tensor_pair/materialize.rs:251–280 uses axis.len, while nonzero_len in tensor_pair/mod.rs:107–119 trusts the backing factors. A residual axis with len=2 and 64 pairs of [1,1] passes a four-coordinate work check alongside unit(2,1,3), then computes 2^64, panicking with overflow checks or wrapping otherwise. Validate weight geometry before counting and sizing allocations, including the outer live-count product.
  • The memory regression raised in RadNi’s comment 4331364224 remains unresolved. tensor_pair/materialize.rs:291–302 reserves every live residual with no separate scratch-memory budget or streaming alternative. Valid axes unit(2,1,3) and unit(1<<27,0,1) pass the 1<<28 work limit but require approximately 2 GiB of residual storage for an eight-byte field on a 64-bit target, despite producing only two output elements. The base implementation streamed these residuals. Fallible reservation reports allocation failure rather than preserving support on bounded-memory guests; bound precomputation memory and retain a bounded-memory contraction path.

markosg04 and others added 3 commits October 26, 2026 17:58
Revalidate every axis's weight table against its public len at both
tensor entry points, so live-residual counting cannot overflow on
mutated axes. Cap dense-left residual tables at 1 MiB and fall back to
the streaming contraction from main above that budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
markosg04 and others added 2 commits October 26, 2026 18:21
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@markosg04

Copy link
Copy Markdown
Collaborator Author

Addressed RadNi's review (4331364217, 4331364224) and the transcript suggestion:

  • Axis geometry (a52faaa): EqPairTensorAxis::validate_weights checks that dense weights have len entries and bit-product factors describe exactly len coordinates. It runs at the top of materialize_eq_tensor_left and eval_boolean_pair_tensor_families, in EqPairTensorFamily::new, and inside nonzero_len (now fallible), before any work count or allocation. The outer live-count product uses checked_mul. Regression: materialization_rejects_weights_beyond_axis_len (the len = 2, 64 [1,1] pairs case) returns InvalidInput.
  • Residual memory (a52faaa): residual tables have their own budget, MAX_RESIDUAL_TABLE_BYTES = 1 MiB, separate from the work bound. Views over budget use the streaming contraction from main, so bounded-memory guests keep a path. The largest table in the PCS e2e suites is about 1.3 KB. Regression: dense_left_residuals_above_table_budget_are_streamed (the unit(2,1,3) + unit(1<<27,0,1) case streams; a mixed case matches a brute-force eq_eval_at_index oracle on both paths).
  • Atom encodings (7f9954d, d1b744b): atom_encodings_and_challenges_match_frozen_bytes pins field and extension atom bytes, NARG bytes, and a Blake2b challenge, all computed at main.
  • Not done: exact op-norm table comparison and isolated guest measurements.

@markosg04

Copy link
Copy Markdown
Collaborator Author

/ai-review

@github-actions

Copy link
Copy Markdown

AI review attempt 3/3 reserved. Failed or incomplete attempts also count toward this PR's limit.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Usefulness: benefit supported. Existing decoder, transcript, challenge-sampling and tensor consumers benefit from fewer allocations and repeated computations without dependency or wire-format changes. The new residual-table budget preserves a bounded-memory streaming path. Costs include global cache synchronization and maintaining two residual evaluation modes; their duplicated traversal is avoidable. Exact guest instruction savings were not independently verified.

Approval not recommended: new P0-P3 findings in this review.

}
}

fn collect_residual<F: Field>(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P2] The tabulated and streaming modes now have separate implementations of the same residual-axis walk here and in contract_residual_tensor_axes: skipping the destination axis, reading coordinate weights, pruning zeros, multiplying weights and checking offsets are all duplicated. Changes to this geometry/error-handling logic consequently need matching edits in both paths. Can we share one residual-term visitor with an initial offset/weight and a leaf callback—appending to the table or accumulating the weighted equality value—and delete the second recursive traversal? That keeps the memory-budget fallback without duplicating its policy.

…aming modes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@markosg04
markosg04 merged commit 4f60e73 into main Oct 27, 2026
57 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-reviewed An automated AI review has been published on this PR no-spec PR has no spec file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants