Skip to content

perf(planner): cap setup size while planning recursive schedules, and guard planner cost in CI - #262

Open
quangvdao wants to merge 6 commits into
mainfrom
quang/planner-speedup-316a25
Open

quangvdao wants to merge 6 commits into
mainfrom
quang/planner-speedup-316a25

Conversation

@quangvdao

@quangvdao quangvdao commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The schedule planner is the offline tool that chooses protocol parameters for every supported configuration. Its output is checked in under artifacts/schedules/ and artifacts/schedule-catalog.tsv. This PR makes it much faster for recursive configurations, without changing any output, and adds a CI check that catches growth in the planner's search size.

1. A setup-size ceiling for recursive configurations (perf(planner) commit)

Recursive configurations have a fixed selection rule: of all candidate schedules, the one with the smallest public setup wins, and the setup size is first rounded up to a power of two. Until now the planner built and compared every schedule of every size, and only then kept the smallest.

The planner now searches with a ceiling on setup size:

  • The first pass allows only the smallest possible setup (one field element) and drops any partial schedule that exceeds it.
  • If no schedule fits, the next pass raises the ceiling to the smallest setup size the previous pass dropped. This repeats until a schedule fits.
  • Each pass starts with an empty cache, so a pass never reuses results computed under a different ceiling.
  • If raising the ceiling would overflow, the last pass runs with no ceiling at all. A badly chosen ceiling can therefore cost time, never change the result.
  • The diagnostics line lists each pass with its ceiling and the work it did.

A configuration that sets an explicit setup budget still runs once, as before. Direct (non-recursive) configurations are unchanged.

2. A CI check on planner search size (feat(planner) commit)

  • For each catalog row the generator now records four work counts: subproblems visited, subproblems solved, candidate schedules generated, and full schedule descriptions built. These counts depend only on the code, not on the machine.
  • The baseline is checked in as artifacts/schedule-planner-cost.tsv.
  • The drift job compares against it. It fails if any count on any row grows by more than 25%, or if a row appears or disappears. A PR that intentionally makes the search larger updates the file and says why.
  • The counts measure how large the search is, not how expensive each step is. A change that makes edge, terminal or grinding pricing slower, without visiting more subproblems or building more candidates, passes this check. That is most of what slowed feat!: one sparse challenge message per fold group #255 down, so this check alone would not have caught it.
  • The regeneration step now has a 35-minute timeout instead of the 6-hour default. It is the backstop for slower individual steps. CI measured the step at about 18 minutes on this branch, and runner variance alone has moved it by about 1.3×.

Motivation

The planner has been getting slower with each new protocol option, and nothing in CI noticed. On main, the drift job regenerates every artifact on every PR, which takes about 23 minutes. A recent regression on #255 made one row ten times slower, and it was found only after the job hit its 6-hour timeout. The new timeout would stop such a run after 35 minutes; the search-size counters catch only the part of a regression that makes the search larger.

Why the ceiling gives exactly the same schedule. The full argument is in the first commit message.

  • Setup can only grow as a schedule is built, so dropping a partial schedule over the ceiling never drops a complete schedule that fits.
  • Every pruning rule the planner uses for recursive configurations removes a candidate only in favour of one with no larger setup. A candidate over the ceiling therefore never removes one under it.
  • No step keeps a fixed-size "top k" list whose members could change when large candidates are left out.
  • So the winner of the first pass that finds anything is the winner of the full search: the full search's winner has the smallest setup, so it fits under that ceiling.

Measured results. All runs are on an M4 Max, comparing main e423a3903 and this branch side by side.

Run main this PR Speedup
fp64_dense_recursive nv21 100.6 s, 2.9 GB 1.85 s, 78 MB 54×
fp64_dense_recursive nv25 129.3 s, 3.2 GB 15.4 s, 0.87 GB 8.4×
Recursive rows, total planning time (27 rows) 1405 s 222 s 6.3×
Direct rows, total planning time (74 rows) 814 s 826 s unchanged
Full catalog, 2 workers as in CI (wall time) 1146 s 558 s 2.05×
Full catalog, peak memory 9.3 GB 3.3 GB 2.8×

Work counts for the two benchmark rows:

Row Version Subproblems visited Subproblems solved Candidates Descriptions built
nv21 main 2,820,481 229,567 11,763,154 6,912,265
nv21 this PR 15,059 4,446 447,863 128,866
nv25 main 3,038,100 281,536 15,835,001 7,615,630
nv25 this PR 429,804 33,478 3,221,376 2,444,074

On CI the regeneration step took about 17.6 minutes on this branch (runs 40244375273 and 40250457121), against 23 to 30 minutes on main with the same runner type. That is less than the 2× local speedup suggested.

What this PR does not fix. Direct configurations now account for 79% of planning time, and this PR leaves them alone. Two ideas for them were measured and dropped:

  • Caching their partial results with a key that leaves out the current error bound would never hit. On one row, 340,087 lookups had 340,087 distinct keys.

  • A setup ceiling would repeat pruning that the planner already does for their first level.

  • Skipping subproblems that cannot beat the best schedule found so far (branch and bound). We tested the strongest version possible. The starting best was the final winner, and each subproblem's bound was its true best cost, taken from a finished run. Real bounds can only be weaker.

    • On fp128_dense nv24 and fp64_onehot nv28 it skipped about 1% of subproblems.
    • On fp64_dense nv20 and nv26 it visited more subproblems than the normal search, not fewer (102,786 → 129,965 and 146,801 → 232,256). The cost cap hides schedules that the normal search uses to prune its first level.
    • Even a search that visited only the subproblems both runs need would save at most 15% on fp64_dense nv20.

    Every run picked the same schedule, but the work saved is too small, so branch and bound does not help direct configurations.

  • Reusing repeated candidate searches. For each subproblem the planner searches candidates twice, once using the source's measured coefficient size and once without it. Subproblems that differ only in that size repeat the second search exactly, 2.6 to 3 times each. Caching it gave identical output and cut direct-row planning time by 10% (864 s to 778 s summed over 74 rows). But the repeats are spread across the whole search, so the cache must be large: peak memory for the full catalog rose from 2.6 GB to 5.4 GB. Capped at 65,536 entries it saved 6.5% for 0.5 GB on the heaviest row. Neither is worth it, so it is not in this PR.

Their remaining cost is spread over shared parameter-sizing code, a few percent per function. Similar caches of witness sizing and edge pricing would each repeat the memory trade-off above for at most about 10%.

Testing

  • Ran tests for modified crates. All nextest runs used --cargo-profile ci-test --profile ci.
    • akita-planner --features catalog-gen (lib, bins, tests): 157 passed. This includes the unpruned-search comparison tests and the exhaustive tests. The ignored catalog-row test also passes when run.
    • akita-params, akita-schedules, akita-challenges: 309 passed.
    • akita-config --test trusted_schedule_artifact: 26 passed.
    • Review-fix commit af430d53a (timeout, comments, docs, cache-count accumulation): cargo test -p akita-planner --lib -- diagnostics::, 3 passed.
  • cargo fmt --all --check passes
  • Relevant cargo clippy mode passes: all three CI configurations.

Other checks:

  • Full regeneration. All 23 families regenerate byte-identical to artifacts/schedules/, and 115 of 115 catalog rows match artifacts/schedule-catalog.tsv.
  • Cost check. It passes, and the regenerated counts equal artifacts/schedule-planner-cost.tsv byte for byte with 2, 4 and 6 workers.
  • Repository gates. taplo fmt --check, typos, cargo machete --with-metadata, both Rust line-cap scripts, check-crate-deps.sh for the six crates, check-shared-field-identity.sh, check-external-schedule-artifacts.sh and scripts/tests (179 passed).

New tests

  • padded_setup_budget_passes_match_one_unbounded_pass, in test/adaptive_search/setup_budget.rs (0.9 s):
    • Runs one-hot recursive nv 14, 16 and 18, on the same restricted search spaces as the unpruned-search tests.
    • The ceiling search must pick the same schedule bytes and estimate as one pass with no effective ceiling.
    • The ceilings must rise strictly, and the last one must equal the chosen schedule's setup size.
  • padded_setup_budget_matches_one_unbounded_pass_on_catalog_rows (ignored, about 100 s):
    • The same check on fp32_dense_recursive nv 20 and 22 and on fp128_dense_recursive nv 20.
    • It is ignored because the comparison without a ceiling takes about 30 s per row. The drift job already compares every recursive row against artifacts that the old full search produced.
  • diagnostics.rs::setup_budget_passes_record_work_since_their_start: checks the per-pass work counts and the diagnostics text.
  • In bin/planner_cost_snapshot.rs:
    • a write-and-read round trip;
    • growth past 25% fails: exactly 125% passes, 126% fails;
    • an added or removed row fails.

Changed tests

No assertion is weakened.

  • In generated_families/tests.rs, two tests now read the schedule from the new PreplannedSchedule struct. One of them also checks that a work count was recorded.
  • emit/materialize.rs::emitter_rejects_preplanned_rows_after_runtime_geometry_changes builds a PreplannedSchedule with zero cost. Its assertions are unchanged.
  • candidate/recursive/tests.rs::combined_terminal_and_fold_views_match_independent_searches (test(planner) commit) used only a recursive configuration. For those, the combined search returns the two independent searches unchanged, so the comparison could not fail. It now also runs the direct selection rule, which is the only path that uses the combined search. Its assertions are unchanged.

Deleted tests: none.

Security Considerations

The planner runs offline. It changes how fast schedules are found, not which schedules are chosen, and the regenerated artifacts are byte-identical.

  • Verifier acceptance behavior is unchanged, or the intended change is specified.
  • Verifier-reachable malformed inputs return typed errors instead of panicking. No verifier-reachable code changes.
  • Transcript labels, challenge order, and domain separation are unchanged, or the intended change is specified.
  • Serialization changes preserve canonical decoding and bounded untrusted input handling. No serialization changes.
  • New dependencies, Git dependencies, or CI actions are justified and pass supply-chain policy. No new dependencies or actions; the drift job only gains a flag and a 35-minute timeout.
  • Unsafe code is unchanged, or each new unsafe block has a local safety argument.

Breaking Changes

None. No protocol, transcript, proof format, descriptor or version changes.

#255 touches the same planner files, so whichever of the two lands second needs a rebase.

🤖 Generated with Claude Code

@github-actions

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 25, 2026
@github-actions

github-actions Bot commented Oct 25, 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: 18

planner-schedule

Schedule tables, DP fallback, proof-size planning

Code paths touched:

  • crates/akita-planner/README.md
  • crates/akita-planner/src/bin/gen_schedule_artifacts.rs
  • crates/akita-planner/src/bin/planner_cost_snapshot.rs
  • crates/akita-planner/src/diagnostics.rs
  • crates/akita-planner/src/emit/materialize.rs
  • crates/akita-planner/src/emit/mod.rs
  • crates/akita-planner/src/generated_families.rs
  • crates/akita-planner/src/generated_families/tests.rs
  • crates/akita-planner/src/planner.rs
  • crates/akita-planner/src/schedule_params.rs
  • crates/akita-planner/src/schedule_params/candidate/recursive/tests.rs
  • crates/akita-planner/src/schedule_params/suffix_dp.rs
  • crates/akita-planner/src/schedule_params/suffix_dp/search.rs
  • crates/akita-planner/src/schedule_params/suffix_dp/state.rs
  • crates/akita-planner/src/test/adaptive_search.rs
  • crates/akita-planner/src/test/adaptive_search/setup_budget.rs

Consider updating:

  • book/src/how/configuration.md
  • book/src/usage/profiling.md
  • specs/planner-*.md
  • specs/PRUNING.md

ci-tooling

CI workflows and repo scripts

Code paths touched:

  • .github/workflows/ci.yml

Consider updating:

  • docs/ci-test-timing.md
  • docs/documentation.md
  • specs/archive/2026-Q3/ci-test-timing.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 25, 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.018 s
+9.5%
0.809 s
+1.4%
0.901 s
-1.5%
12.4 ms
+2.2%
29.5 ms
+1.4%
Fp32 one-hot nv34, direct setup check 0.036 s
+0.2%
0.521 s
-0.1%
0.965 s
-0.1%
14.2 ms
+0.8%
54.2 ms
-1.1%
Fp64 dense nv29, direct setup check 0.052 s
+0.2%
0.781 s
-2.0%
0.887 s
+1.7%
13.9 ms
+0.9%
36.2 ms
+0.1%
Fp64 one-hot nv35, direct setup check 0.089 s
+0.3%
1.671 s
+0.4%
1.289 s
-0.9%
17.0 ms
-3.0%
86.2 ms
+0.0%
Fp128 dense nv28, direct setup check 0.083 s
-1.4%
1.052 s
+1.7%
0.989 s
-1.0%
13.3 ms
+2.4%
43.8 ms
-0.2%
Fp128 one-hot nv36, direct setup check 0.224 s
-0.5%
4.708 s
-0.6%
1.911 s
-8.5%
23.9 ms
-0.4%
145.2 ms
+0.5%
Fp128 one-hot nv36, recursive setup check 0.762 s
-3.3%
4.814 s
+0.1%
2.107 s
-0.8%
17.5 ms
-4.0%
44.6 ms
+0.3%
Fp128 multi-group (final nv34, 4 polys total), direct setup check 0.238 s
-0.2%
2.251 s
+0.3%
1.044 s
-0.9%
17.2 ms
+2.8%
88.7 ms
+0.2%
Fp128 multi-group (final nv34, 4 polys total), recursive setup check 0.390 s
+1.5%
1.585 s
+1.8%
1.092 s
+0.4%
11.8 ms
+2.6%
22.1 ms
-0.0%
Fp128 multi-group W8R2 (final nv32, 4 polys total), recursive setup check 0.261 s
+1.4%
0.626 s
+0.6%
1.714 s
-1.5%
27.3 ms
+2.0%
74.0 ms
+0.2%
Fp128 one-hot nv32 W2R2, direct setup check 0.040 s
+0.6%
0.345 s
-0.0%
0.569 s
-0.4%
15.5 ms
-0.3%
43.9 ms
+0.0%
Fp128 one-hot nv32 W4R2, direct setup check 0.044 s
-0.5%
0.346 s
+0.3%
0.708 s
+1.7%
18.1 ms
+4.6%
52.2 ms
-0.1%
Fp128 one-hot nv32 W8R2, direct setup check 0.044 s
+0.6%
0.345 s
+0.5%
0.924 s
+0.2%
20.0 ms
+3.1%
62.3 ms
+0.2%

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%
4643.0 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%
1038.2 MiB
-0.1%
Fp64 dense nv29, direct setup check 32.0 MiB
+0.0%
291.2 MiB
+0.0%
1.8 MiB
+0.0%
4858.7 MiB
-0.2%
Fp64 one-hot nv35, direct setup check 128.0 MiB
+0.0%
917.2 MiB
+0.0%
1.8 MiB
+0.0%
1859.1 MiB
+0.1%
Fp128 dense nv28, direct setup check 64.0 MiB
+0.0%
585.0 MiB
+0.0%
1.5 MiB
+0.0%
5181.9 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.4 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%
5707.2 MiB
+0.0%
Fp128 multi-group (final nv34, 4 polys total), direct setup check 256.0 MiB
+0.0%
1831.9 MiB
+0.0%
3.2 MiB
+0.0%
2902.3 MiB
-0.1%
Fp128 multi-group (final nv34, 4 polys total), recursive setup check 256.0 MiB
+0.0%
1899.0 MiB
+0.0%
1.6 MiB
+0.0%
3498.9 MiB
-0.2%
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%
1738.8 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%
684.5 MiB
+0.1%
Fp128 one-hot nv32 W4R2, direct setup check 64.0 MiB
+0.0%
345.0 MiB
+0.0%
1.5 MiB
+0.0%
754.9 MiB
-0.0%
Fp128 one-hot nv32 W8R2, direct setup check 64.0 MiB
+0.0%
345.0 MiB
+0.0%
1.5 MiB
+0.0%
841.5 MiB
-0.2%

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.

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

CI test timing

  • Report generated: 2026-10-26T17:33:05Z.
  • Source: 30313f7 on quang/planner-speedup-316a25.
  • Workflow run: 40362638669.
  • Main baseline: a4c7e09.
  • Previous run: 14855d0.

Run summary

Wall s Main wall s Main Δ Ratio Tests Skipped Failed Status
1684.0 1423.0 +18.3% 1.18x 2016 0 0 ok

Wall time spans 2 parallel nextest slice shards.

Slowest tests

Rank Duration s Test
1 446.6 akita-planner::schedule_params::suffix_dp::chunk_ownership_tests::contracting_chunk_search_matches_unpruned_complete_objective
2 171.1 akita-planner::schedule_params::adaptive_search_tests::adaptive_frontier_matches_unpruned_traversal_and_hand_priced_role_optima
3 61.1 akita-planner::planner::setup_prefix_challenge_hook_tests::catalog_binding_revalidates_recursive_setup_prefix_challenge_hook
4 34.0 akita-planner::planner::totality_tests::noncontractive_multi_chunk_root_can_beat_contractive_candidates
5 33.3 akita-planner::planner::totality_tests::contractive_winner_remains_selected
6 31.0 akita-planner::generated_families::tests::generation_uses_requested_geometry_for_preplans_and_grouped_producers
7 26.3 akita-planner::schedule_params::adaptive_search_tests::adaptive_nv36_minimizes_setup_envelope_before_first_direct_setup
8 24.8 akita-planner::planner::totality_tests::valid_small_grouped_root_has_a_schedule
9 23.6 akita-planner::schedule_params::adaptive_search_tests::production_suffix_selects_l2_with_the_typed_response_model
10 22.9 akita-pcs::dynamic_backends::portable::offloaded_route_uses_only_local_prefix_handles
11 19.1 akita-pcs::akita_small_field_e2e::combined_recursive::fp32_combined_setup_proves_recursive_and_onehot_families
12 17.9 akita-planner::schedule_params::adaptive_search_tests::relation_cutover::bounded_suffix_dp_matches_unpruned_fixed_cutover_search
13 16.3 akita-pcs::dynamic_backends::dynamic_backend_failures_cleanup_and_retry
14 16.2 akita-pcs::akita_fp128_e2e::heterogeneous::bounded_dense_roundtrip_over_u64_coefficients_at_every_catalog_size
15 15.7 akita-pcs::transcript_hardening_proptest::seed_corpus_covers_nv_basis_and_batch_shapes
16 14.1 akita-setup::tests::disk_persistence::combined_recursive_prefix_registry_persists_and_serves_each_family
17 13.4 akita-pcs::runtime_catalogs::independent_runtime_catalogs_prove_verify_and_reject_each_other
18 11.6 akita-pcs::akita_fp128_e2e::fp128_onehot
19 10.4 akita-pcs::akita_small_field_e2e::selective_l2::fp32_ext4_l2_pcs_roundtrip_and_stage2_rejections
20 10.0 akita-sis-estimator::search_mode_parity::full_exhaustive_parallel_matches_certified_search_on_medium_subset

Regressions vs main

Δ s Baseline s Current s Test
57.9 388.7 446.6 akita-planner::schedule_params::suffix_dp::chunk_ownership_tests::contracting_chunk_search_matches_unpruned_complete_objective
27.7 143.4 171.1 akita-planner::schedule_params::adaptive_search_tests::adaptive_frontier_matches_unpruned_traversal_and_hand_priced_role_optima
12.5 21.5 34.0 akita-planner::planner::totality_tests::noncontractive_multi_chunk_root_can_beat_contractive_candidates
11.0 15.2 26.3 akita-planner::schedule_params::adaptive_search_tests::adaptive_nv36_minimizes_setup_envelope_before_first_direct_setup
10.7 12.9 23.6 akita-planner::schedule_params::adaptive_search_tests::production_suffix_selects_l2_with_the_typed_response_model
10.4 14.4 24.8 akita-planner::planner::totality_tests::valid_small_grouped_root_has_a_schedule
9.6 21.4 31.0 akita-planner::generated_families::tests::generation_uses_requested_geometry_for_preplans_and_grouped_producers
8.3 8.1 16.3 akita-pcs::dynamic_backends::dynamic_backend_failures_cleanup_and_retry
7.8 6.3 14.1 akita-setup::tests::disk_persistence::combined_recursive_prefix_registry_persists_and_serves_each_family
7.0 12.0 19.1 akita-pcs::akita_small_field_e2e::combined_recursive::fp32_combined_setup_proves_recursive_and_onehot_families
6.4 3.6 10.0 akita-sis-estimator::search_mode_parity::full_exhaustive_parallel_matches_certified_search_on_medium_subset
6.4 16.5 22.9 akita-pcs::dynamic_backends::portable::offloaded_route_uses_only_local_prefix_handles
6.1 3.2 9.3 akita-pcs::backend_ownership::signed_block_poly::signed_block_poly_i8_full
6.0 3.1 9.1 akita-pcs::backend_ownership::signed_block_poly::signed_block_poly_i16_full
5.4 7.9 13.4 akita-pcs::runtime_catalogs::independent_runtime_catalogs_prove_verify_and_reject_each_other

New slow tests

No new tests ≥30s vs main baseline.

V6 ranks schedules by padded setup first. With no explicit policy budget,
V6 planning now searches under a power-of-two setup budget that starts at
one field element. A pass that finds no schedule records the smallest
setup it rejected, and the next pass uses that setup padded to a power of
two. Fold candidates whose own level setup exceeds the budget are dropped
when they are generated, and pending schedules over the budget are dropped
in `child_choice`. An explicit policy budget keeps its single pass, and V5
is unchanged.

Exactness:

- Admission under a power-of-two budget B is the same as padded setup
  <= B. Every V6 comparison that removes a candidate keeps a side whose
  padded setup is no larger: both projection dominance rules, both
  primary strict-dominance checks, the root and recursive guided bounds
  (`is_strictly_worse_than`, `setup_envelope_is_strictly_worse_than`,
  `is_strictly_worse_for_recursive_parent`, `..._payload`), and the
  level prune, whose coordinates include the level setup. So an
  over-budget candidate never removes an in-budget one.
- No selection is a bounded top-k. The setup-first and payload-first
  lists in each `ParentObservableKey` bucket are dominance frontiers
  with no size limit, built by retain-then-push. The bounded split domain
  (`BoundedBalancedExtremesV1`), the precommit opening-product cap and the
  grinding query limit depend only on geometry, never on candidate
  outcomes. Memo eviction only forces recomputation, and every pass
  starts with a fresh memo, including the setup-prefix cache, so nothing
  computed under one budget is read under another.
- Together, these mean the in-budget part of every frontier equals the
  in-budget part of the unbudgeted frontier. Insertion order is
  unchanged, an in-budget insert is rejected or evicts members only
  because of in-budget members, and an over-budget insert never touches
  in-budget members.
- Setup is a maximum over levels, the terminal and offloaded children,
  so it never decreases as a partial schedule grows. Offloaded edges
  charge the same level setup as direct edges. Rejecting a partial
  schedule or level fold over B therefore never rejects a completion
  within B. Terminal candidates are not filtered, but they reach the
  root only through `child_choice`.
- A pass that selects W returns the unbudgeted winner, because that
  winner's padded setup is at most W's, so it fits and stays in the
  frontier. A pass that rejects nothing is exactly the unbudgeted
  search. A skipped budget below the next padded rejected setup admits
  nothing new. If the next budget overflows, the last pass runs without
  a budget. The starting budget affects only the number of passes.

Diagnostics report each pass's budget, generated candidates and
descriptor builds (`setup_budget_passes=`). The search-work counters
sum over all passes.

Tests: one-hot recursive nv 14/16/18 on the three restricted domains
of the unpruned-oracle fixtures must select the same descriptor bytes
and estimate as one pass with a non-binding explicit budget. The final
budget must equal the selected padded setup, and earlier budgets must
increase strictly and stay below it. An ignored test checks the same
for fp32_dense_recursive nv20/22 and fp128_dense_recursive nv20; it
takes about 100 s.

fp64_dense_recursive nv21: 100.6 s -> 1.85 s. nv25: 129 s -> 15.4 s.
The output is byte-identical to main.
The generator now records four deterministic search-work counters per
row (suffix calls, completed states, generated fold candidates and
descriptor builds). `--planner-cost-snapshot` writes them and
`--planner-cost-baseline` fails when a row is added or removed or any
counter grows by more than a quarter. Preplanned scalar rows carry the
counters of the search that produced them.

The tracked baseline is artifacts/schedule-planner-cost.tsv. The drift
job checks it and bounds the regeneration step at 20 minutes.
@quangvdao
quangvdao force-pushed the quang/planner-speedup-316a25 branch from 855dca2 to dfe4da3 Compare October 25, 2026 23:21
@quangvdao quangvdao changed the title perf(planner): budgeted V6 setup search and a per-row planner cost guard perf(planner): cap setup size while planning recursive schedules, and guard planner cost in CI Oct 25, 2026
The test that compares the combined terminal-and-fold search with the two
independent searches used a recursive configuration. Under the recursive
selection rule the combined search returns the independent searches as
they are, so the comparison held trivially, and the two-walk search that
direct configurations use had no equivalence test.

The test now runs both selection rules, each with and without keeping the
split frontier. Every case must match the independent searches exactly
and keep an L2-routed fold, as before.
- Raise the schedule regeneration timeout from 20 to 35 minutes. CI measured
  the step at about 18 minutes, and runner variance alone has moved it by
  about 1.3x, so 20 minutes left little headroom.
- Say what the planner cost baseline actually counts: search size (suffix
  calls, completed states, candidates, descriptors). Slower per-candidate
  pricing does not move those counters; only the timeout catches it.
- Correct the suffix memo comment: entries are optima under the context's
  setup budget, which is not in the memo key, so each budget pass needs a
  fresh memo.
- Accumulate setup-prefix cache hits and misses across budget passes, like
  the other diagnostics counters, instead of keeping only the last pass.
  Diagnostics only; planner output is unchanged.
@omibo

omibo commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Minor: an unreachable guard in the widening step.

  • Location: planner.rs:912-915, .filter(|&next| next > limit).
  • Evidence: admits records only values greater than limit. Here limit is the pass budget, so next_pow2(rejected) ≥ rejected > limit always holds and the filter never removes anything. The only Some(None) path is overflow.
  • Impact: Readers may infer a case (a non-increasing budget) that cannot happen.
  • Proposed edit: Drop the filter, or turn it into a debug_assert!(next > limit) if you want the invariant documented.

The setup budget records only sizes above the pass limit, so the
`next > limit` filter on the padded widening step could never remove a
value. Replace it with a debug assertion that documents the invariant;
overflow remains the only path to the final unbudgeted pass.
@quangvdao

Copy link
Copy Markdown
Collaborator Author

@omibo Agreed, the filter was unreachable: the pass budget is limit itself, so every recorded rejection already exceeds it. Replaced it with debug_assert!(rejected > limit) to document the invariant; overflow is now visibly the only path to the final unbudgeted pass. Fixed in 5c1b07d.

@quangvdao
quangvdao marked this pull request as ready for review October 26, 2026 18:05
@quangvdao
quangvdao requested a review from a team October 26, 2026 18:05

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

no-spec PR has no spec file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants