Skip to content

Reduce recursive verifier rows and update the Jolt pin - #277

Open
Acentelles wants to merge 15 commits into
LayerZero-Labs:mainfrom
Acentelles:perf/verifier-recursion-levers
Open

Acentelles wants to merge 15 commits into
LayerZero-Labs:mainfrom
Acentelles:perf/verifier-recursion-levers

Conversation

@Acentelles

@Acentelles Acentelles commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

This reduces Akita verifier work inside a zkVM guest and pins Jolt to 4e55c9a5f7033d1b88254df051e0bd8c4a4b0ec3. Against main (038db2d1), total verifier rows fall 18.794% at inner $2^{12}$ and 21.171% at $2^{20}$, with unchanged Akita proof bytes. The pin and unconditional changes account for most of the combined reduction; the optional-feature saving is reported separately below.

Changes

The pin commit updates all Jolt dependencies, patches the profile workspace to this Akita checkout to keep one field identity, and handles the profile host's verify returning Result. A separate commit records the three regenerated lockfiles.

The 11 retained changes, including their supporting tests:

  1. Signed sparse challenge evaluation uses Field::signed_sum for coefficients of magnitude one or two (unconditional).
  2. ont-static-base uses generated D64 operator-norm base tables, with the existing builder as fallback.
  3. Factored compression weights evaluate aligned event families by shared factors instead of individual events (unconditional).
  4. compression-weights-crosscheck checks grouped weights against expanded events and dense/roundtrip tests.
  5. The cross-check event-sum type gets a name so diagnostic Clippy passes.
  6. Reduced setup rows sharing one functional use weighted_dot_base_rows (unconditional).
  7. Reduced shared-row tests import Ring for their field constructors.
  8. tensor-no-carry separates tabulated tensor residuals into high/low contractions.
  9. tensor-eq-runs accumulates tensor equality weights over power-of-two runs.
  10. Tensor runs use power-of-two shifts and test Fp128 boundaries; the main-compatible dispatch replaces the earlier formatting workaround.
  11. setup-sop-structured batches the structured E/T/Z contractions through dot products and sums of products.

Two reworks make this independent of the reverted row kernels: factored weights call main's OffsetEqWindow::eval, and reduced rows keep main's eval_ring_at_pows_fast in the nonshared path. Tensor paths decline streamed residuals and preserve main's family-scalar semantics. The cross-check roundtrips also pass the current witness-chunking argument. The redundant ctutils bump is omitted.

Measurement

Jolt main plus the recursion example from #1911, with a host input-sizing patch, built against Akita main (BASE) or 6c25733560c72aca671a2a079991c8643310e807 with the four optional features off/on. All builds use Jolt field-inline; Akita hashing runs in software. AMD Ryzen 9 9950X; one run per arm and size. Counts are total real plus virtual verifier rows, not timing claims.

Inner size BASE rows OFF rows ON rows Akita bytes, every arm
$2^{12}$ 174,970,923 145,186,867 142,087,101 80,689
$2^{20}$ 357,355,426 296,255,058 281,699,170 84,460

Derived from those measured totals:

Row change $2^{12}$ $2^{20}$
OFF minus BASE: Jolt pin plus unconditional code changes -29,784,056 -61,100,368
ON minus OFF: enabling the four optional features -3,099,766 (2.135% of OFF) -14,555,888 (4.913% of OFF)
ON minus BASE: combined -32,883,822 -75,656,256

There is no pin-only control: the pure pin benefit and the total contribution of all retained changes are not isolated. The outer Jolt measurement tree is held fixed and supplies its workspace jolt-field. Complete serialized proof files are byte-identical across arms at each size; Akita proof-byte deltas are zero.

Validation and limits

The latest commit moves the Jolt pin from e1d8e02b to 4e55c9a5 (a16z/jolt main, which adds range checks on direct RAM loads); it changes only dependency pins and lockfiles. At the new head the x86 CI mirror below was re-run and every gate passed, and the rows above were re-measured. The test counts, the flipped-bit rejection and the clean rebuild reproduction were measured at the previous head.

The x86 CI mirror passed format/TOML/typos, line/proof-stream/field-identity guards, all three CI Clippy configurations, dependency hygiene, docs, proof-channel roundtrips, six schedule-drift checks, both transcript-semantics suites, doc guardrails, portable x86/AVX2, profile smoke, and cargo-deny/audit. Nextest passed 2,051 tests (17 skipped); optional-feature nextest passed 2,061 (17 skipped). Feature-specific tests, three additional Clippy configurations, static-table equality/fallback tests, and compression cross-check unit/e2e tests passed.

The nightly fuzz check failed with debug-link relocation overflow on both main and the subject. AArch64 NEON, emulated AVX-512 IFMA, fuzz campaigns and the CI timing report were not run. Schedule regeneration was omitted because planner, schedules and artifacts are untouched. This is mirror evidence; GitHub CI must validate the pushed head.

All six honest proof traces accepted. A flipped opening bit was rejected by small ON with the same ELF. Size, feature-map, dependency-lock, proof-identity and byte gates passed; BASE/OFF/ON ELFs were distinct. The clean small ON rebuild reproduced rows and ELF exactly.

Risks and switches

The pin changes dependency resolution. Signed evaluation, factored weights and reduced-row batching affect every verifier; they have no feature switch and require reverting their commits to disable. Their test-only fixes have no production switch. Reverting the pin and locks also requires reverting code that uses the new field APIs. Generated tables must match the builder; grouped-weight and tensor fallback boundaries need continued coverage. Tensor edits are not line-preserving on main; structured sums preserve feature-off statement lines but add macro complexity. Row counts can vary with crate source identity and link layout.

All optional switches default off. Remove the following features from every enabling dependency edge to disable them:

Change Feature
Static tables akita-challenges/ont-static-base
No-carry contraction akita-algebra/tensor-no-carry
Equality runs, including run shifts akita-algebra/tensor-eq-runs
Structured sums akita-verifier/setup-sop-structured and its akita-pcs forward
Compression diagnostic and its event-sum type akita-types/compression-weights-crosscheck and its akita-pcs forward (disables checking, not factored evaluation)

Moves every a16z/jolt dependency (jolt-field and jolt-poly in the
workspace and fuzz/, plus jolt-sdk and jolt-sdk-macros in
profile/akita-recursion) from 1618f4b6d to Jolt main e1d8e02b5, whose
jolt-field has the kernels the following verifier changes use
(Field::signed_sum, Field::sum_of_products, Field::dot_product,
ExtField::weighted_dot_base_rows, MulBaseUnreduced::dot_base).

In profile/akita-recursion, Jolt main's jolt-sdk pulls in jolt-akita, which
pins Akita by git revision; a patch table builds it against this checkout so
the profile graph keeps one Akita and one jolt-field. Jolt main's provable
verify function returns Result, so the host reports the verifier error
instead of negating a bool. Manifests and that host call only; the lockfiles
are regenerated in a following commit.
…base

Adds generated static base tables for the D64 selective-L2 operator-norm
sampler, with the generator script. The tables are compiled in only with the
ont-static-base feature, which is off by default; without it the sampler is
unchanged.
The compression relation table records one event per (gadget bit, source
row, coefficient block) for every recomposition and one event per column for
every compression row, and the verifier evaluated the table's multilinear
extension event by event. Each of these families is an arithmetic
progression of aligned events whose scalars are a row factor times a shared
table: the lifted gadget powers, or one evaluated compression matrix's
columns.

The builder now records each such family as one group, and
`CompressionRelationWeights::evaluate_at_point` evaluates a group's
extension by factor: the shared alpha low factor, times dyadic blocks over
the high index (a geometric table closes each block in a product of log
factors, any other table caches the block's extension), or a loop over the
per-event equality window when the stride is not a power of two. Every split
is over disjoint digit positions of one index, so the value is the
multilinear extension of the same dense table at every field point. A family
that fails any precondition (alignment, domain, overflow, a zero gadget
entry, a zero source ring dimension) is pushed event by event, so accepted
inputs and errors are unchanged. Prover-side materialization
(`into_sparse_entries`, dense accumulation) expands the groups back into
their events, so prover output is unchanged.

No proof byte, transcript or verifier decision changes. Unit tests compare
the grouped path with the per-event path and with dense multilinear
evaluation at random, zero, one and mixed points, including every
evaluation tier, unaligned runs, clipped runs, interior and trailing zero
columns and the error paths. The reduced-program test now counts and
domain-checks grouped events as well.
…event evaluation

Add the diagnostic feature `compression-weights-crosscheck` to `akita-types`
(forwarded by `akita-pcs`). No default feature enables it, and without it the
compiled code is unchanged: the only edits to existing code are a `cfg`
attribute on `evaluate_at_point` and on the recomposition dispatcher, and a
`cfg`-gated observation call at the start of `push_column_group`; every other
existing line keeps its text and position.

With the feature, `CompressionRelationWeights::evaluate_at_point` returns the
same value and also:

- evaluates the same table event by event (every group expanded into its
  events) at the requested point and at four seeded random points, and
  compares each value with the grouped evaluation;
- counts the table's groups per evaluation tier (geometric dyadic blocks,
  cached dyadic blocks, per-event equality window) and its ungrouped events.

Each recomposition call and compression row of the builders records whether
the family was grouped or pushed event by event, with the reason for a
fallback, and compares the grouped events with those of the canonical
per-event loop on the same arguments. A difference is counted and returned as
`AkitaError::Internal`, so verification fails closed. Counters are available
from `crosscheck::report()`, and each evaluation emits one `tracing` event at
INFO (target `akita_types::compression_weights_crosscheck`) whose message is a
JSON object with that evaluation's tiers and its table's builder outcomes.

Tests: with the feature, a table whose geometric closed form disagrees with
its events is rejected; builder outcomes, fallback reasons and every tier are
recorded; and `akita_fp128_e2e` runs dense, one-hot and precommitted fp128
roundtrips and checks that the cross-check ran with no difference. Every other
roundtrip in that file is cross-checked too when the feature is on.
The feature-gated event comparison returned
Result<Vec<((usize, usize, usize), E)>, AkitaError>, which
clippy::type_complexity rejects with compression-weights-crosscheck on.
Name it EventSums<E>. Feature-gated lines only; the default build is
unchanged.
Port onto akita main (LayerZero-Labs#188 ResidualTerms::{Table, Streamed}): the separated
residual is prepared per view in DenseLeftOverlap::materialize (the plan no
longer sees the equality window) and only from a tabulated residual; a
streamed residual declines to the existing walk. The family scalar, which
main now applies inside evaluate_residual, is applied to the separated value
in the same way.
Port onto akita main (LayerZero-Labs#188 ResidualTerms::{Table, Streamed}): the run
accumulator is called from DenseLeftOverlap::materialize on the same
replaced comment line; it reads only a tabulated residual (a streamed
residual declines) and, under tensor-no-carry, takes the per-view separated
residuals prepared in materialize instead of a view field. The tests build
tabulated views and no longer apply the family scalar on top of
evaluate_residual, which includes it on main.
Port onto akita main (LayerZero-Labs#188): keeps the earlier port (tabulated residuals
only, scalar on main inside evaluate_residual). The run accumulator now takes
a plain bool (some view took the no-carry separated path) instead of a
cfg-gated slice argument, because rustfmt drops a cfg-gated call argument;
the call site in DenseLeftOverlap::materialize is written in rustfmt form and
the original comment line is restored (not line-preserving on this base).
…-scan sites

Feature setup-sop-structured (default off; forwarded by akita-pcs) sums the
cached structured E, T and Z contractions of the verifier setup scan through
Field::dot_product and Field::sum_of_products. The scan-path kernels (the
identity-path D/B/A weight loop and the B role terms) keep their current code.
Field values are unchanged.

Line-preserving: each structured call site selects through lb2_alt!, opened on
the first and closed on the last line of the original statement
(rustfmt::skip on the same line); every new item is appended after the last
original line of its file, and the new module lb2.rs is declared at the end of
mod.rs. Without the feature the expansion is the original tokens on their
original lines.
Cargo.lock, fuzz/Cargo.lock and profile/akita-recursion/Cargo.lock resolved by cargo
against the new a16z/jolt revision.
@Acentelles
Acentelles force-pushed the perf/verifier-recursion-levers branch from 11dde9a to 4f781ad Compare October 30, 2026 02:02
@Acentelles Acentelles changed the title perf(verifier): cheaper Akita verification inside a zkVM guest Reduce recursive verifier rows and update the Jolt pin Oct 30, 2026
@Acentelles
Acentelles changed the base branch from markos/recursion-integration to main October 30, 2026 02:02
@Acentelles
Acentelles requested a review from a team October 30, 2026 02:02
@Acentelles Acentelles closed this Oct 30, 2026
@Acentelles Acentelles reopened this Oct 30, 2026
Update the manifest revisions and the three lockfile source IDs from e1d8e02b5 to 4e55c9a5. Dependency manifests and Jolt Cargo.lock are unchanged across this move; resolution and compilation are verified on the Linux gate host.
@Acentelles
Acentelles requested a review from quangvdao October 30, 2026 20:29

This branch has not been deployed

No deployments
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.

1 participant