Skip to content

perf: Tune ix shard counts and recalibrate for proving via --ram-budget - #601

Draft
samuelburnham wants to merge 7 commits into
mainfrom
ap/aiur-cost-dump
Draft

perf: Tune ix shard counts and recalibrate for proving via --ram-budget#601
samuelburnham wants to merge 7 commits into
mainfrom
ap/aiur-cost-dump

Conversation

@samuelburnham

Copy link
Copy Markdown
Member

Aiur cost profiling and budget-aware sharding

Summary

This branch adds execution-derived per-constant Aiur cost profiles and uses
measured prover-RAM estimates to make sharding self-correcting. Over-budget
shards are split before proving, audits can emit the partition they actually
validated (including measured peaks), and the aiur-sharded-env benchmark now
audits the same 100-GiB budget used to seed its manifests.

The CLI distinguishes the local prove limit (ix prove --max-ram, defaulting
to 85% of available RAM) from the target machine used by an execution-only
audit (ix check --ram-budget). Static ix shard --max-ram G sizing now uses
the environment's block-size distribution instead of raw .ixe file size.
Explicit --shards N behavior is unchanged.

Updated shard model

For each atomic block b, let s_b be its serialized owned bytes (not .ixe
file bytes):

S(env) = sum_b [28,201 * s_b + 681.08 * s_b^(3/2)]

N_seed = clamp(
  round(233 * (S(env) / 1.254e14)^1.10 * (400 / G)^1.20),
  1,
  max(1, number_of_blocks)
)

G is the requested per-shard RAM budget in GiB. The 400 is only the
calibration reference, so its factor is exactly one at 400 GiB. The per-block
s_b^(3/2) term captures block shape, the 1.10 exponent scales across
environment sizes, and the 1.20 budget exponent is fitted from the 100-GiB
runs below. This remains a seed: measured execution splits any over-budget tail
before proving.

400 GiB seed counts

At the reference budget, the budget exponent has no effect; this table shows
the size-axis correction relative to the previous raw-file-size model.

Environment Previous seed Score-model seed
Init 14 8
InitStd 24 16
Batteries 24 14
Lean 36 22
ISLB 38 23
Mathlib 233 233
FLT 241 246

The eight-shard Init partition measured 266–323 GiB with no split waves, and
Batteries needed one heavy-tail correction (14 → 15). Mathlib's unchanged
233-shard seed produced 242 measured leaves: eight initial shards were over
budget, all final shards landed at 161–397 GiB, and the run completed in 17:20
with zero failing constants. An older 241-shard FLT audit found six shards at
437–648 GiB; each fit after one model-selected halving, with the final maximum
at 99.7% of budget. Mathlib is the normalization point; FLT's new 246 count is
an extrapolation.

100 GiB calibration

Environment Model seed Tested start Initially over budget Split waves Final shards Check time End-to-end
Init 43 42* 0 0 42 44.9 s 49 s
ISLB 122 122 4 3 128 100.9 s ~155 s
Batteries 74 74 1 3 79 75.2 s ~130 s

* Init used the adjacent 42-shard calibration probe; it was clean, and the
final implementation was verified to emit 43. ISLB and Batteries' remaining
splits were localized heavy tails rather than broad under-seeding. For
comparison, the linear-scaling probes (Init at 32 and ISLB at 93) caused 16 and
28 initial misses respectively; a tested 1.36 exponent instead over-seeded
ISLB at 152 shards. The model selects 1,230 shards for Mathlib at 100 GiB; that
extrapolation was not run.

Validation

Focused Rust model tests, the full ix build, and isolated 100-GiB Init, ISLB,
and Batteries calibration runs pass.

arthurpaulino and others added 7 commits August 28, 2026 15:38
…filing

Adds an execution-time virtual-gas meter to the Aiur runtime: every
constrained query touch is priced at its circuit width, and a memo hit
replays the callee's recorded span instead of just the touch, so each
function-query entry records its standalone cost — the cost as if
nothing were memoized, independent of execution order.

- querymap: per-map weight/profile fields, a vspans u64 arena, and
  replay_at/finish methods that maintain the meter for both engines
  (the interpreter and the codegen'd kernel share them).
- execute + codegen: Call/Store/Load/Return sites routed through the
  new methods; generated fns snapshot record.virt at frame entry
  (aiur_ixvm.rs / aiur_multi_stark.rs regenerated).
- ffi: every ExecuteResult now carries an opaque QueryRecordHandle
  owning the execution's QueryRecord, inspected from Lean via the
  rs_aiur_qr_* accessors (fnLen, fnKey, fnMult, fnVspan, memKey).
  A profile : Bool is plumbed end-to-end (Lean Full wrappers ->
  externs -> executors -> QueryRecord::new); prove paths never
  profile.
- ix check --profile <file>: appends one "<addr_hex> <vspan> <mult>
  [name]" line per check_const query, the address recovered by
  dereferencing the Addr argument (last input slot) through
  memory[32]; names joined via ixonEnv.addrToName on the shard paths.
  Intended as order-independent per-constant weights for smarter
  re-sharding.

Meters are side-band (multiplicities and outputs untouched): the
kernel FFT cost is unchanged (lake exe ix check Nat.add_comm pin
exact), native and bytecode-interpreter profile dumps are
byte-identical, and the aiur-cross suite passes.
Per-constant weights come out of the rayon shard batch without
round-tripping records to Lean: `const_weights` reduces each record
in the task that owns it (48-byte `(addr, vspan, mult)` rows, one per
checked constant), next to the peak reduction and before
`gate.release` — admission keeps bounding peak RSS by the shards in
flight rather than the whole partition.

`prove_from_execution` is hoisted out of the two prove paths, taking
the record BY VALUE: it is the witness phase's dominant residency and
dies the instant the traces exist, before the LDE/commit/FRI phases
that set the prover's peak.

`prove_ixvm_within_budget` then measures `peak_prove_bytes` on the
REAL record in the gap between execution and the witness phase: over
budget, the record is dropped and the peak comes back instead of an
OOM part-way into an FFT. Over budget is a RESULT, not an error —
`proveBlocksWithinBudget` halves the block list and recurses (a shard
is a plan, never part of a statement: halving yields claims exactly as
valid as the parent's, each half's grown frontier discharged by its
sibling), bottoming out at a single block, the atom the kernel checks
together. The budget defaults to 85% of `MemAvailable`, the check
batch's gate policy; `ix prove --max-ram G` takes the same units
`ix shard` sized the partition with, and the run returns the partition
it actually produced.

Rejection costs one execution (0.65s) against a full prove (4.7s) on
the 360-constant fixture; a 3.45 GiB shard against a 2 GiB budget
recursed to four parts of 1.51-1.81 GiB in 11s, the parts partition
the parent exactly (82+86+78+78 consts), and all proofs verify against
unchanged claim digests.
An over-budget shard is now cut into the part count the peak model
projects will fit, in one step, instead of bisecting blindly:
suggested_split_parts re-evaluates peak_prove_bytes_by with each
circuit's raw counts scaled 1/n (n = 2, 4, ...) until it fits — the
model's power-of-two padding, fixed gadget floor and max-of-phases
handled by construction. The estimate is optimistic (parts re-execute
dependencies shared across the cut), so every part is still gated on
its own executed record; optimism is the right bias, since an
under-split costs one cheap re-execution while an over-split pays the
per-proof floor forever. Measured on the 241-shard FLT/ram400
partition: 6 shards over budget (437-648 GiB vs 400), every one
resolved by a single suggested halving — max recursion depth 1, final
partition maxing at 99.7% of budget. Equal-cumulative-vspan cut
placement was implemented, benchmarked against equal block counts on a
360-const fixture, init and lean, lost on parts/depth/executions on
every env (vspan tracks time, not rows), and is not kept.

ix prove --exec-only runs the same split loop on executions alone
(execute_peak_ixvm: execute, measure, suggest — no STARK), and
ix check --ixes --max-ram G is its batch twin: one wave loop where
wave 0 is the plain batch check, over-budget shards are cut and their
parts re-batched under the rayon scheduler + RAM gate, and the wave
count is the split depth. --profile records vspans from wave 0 only —
they are partition-invariant, so the first full pass already carries
every constant's row.

Both drivers now finish by enumerating exactly which shards failed,
and can emit the partition the run ACTUALLY produced as a .ixes via
rs_shard_manifest_from_partition (--out-ixes): coverage validated,
own/foreign/cross-ingress and assumption roots rebuilt from the static
profile, and each shard's measured prover peak recorded in a trailing
manifest section (older readers ignore it; 0 = unmeasured planner
output) — the scheduling signal, since STARK wall is ~linear in peak
(92-112s at 229-278 GiB vs 217s at 472 GiB). Emitted manifests are
fixed points: re-running on one produces zero splits and re-emits it
byte-identically.

Also: ix prove --texray streams the per-phase timing/RSS lines
(execute / witness / stark stages) — Mathlib shards measure ~30s
execute vs ~100s STARK on this engine, retiring the arena-port
urgency; the prove-path --max-ram docs now match the FFI (0 = detect
85% of MemAvailable, not unchecked); the checkAddrsWithEnv docstring
now matches the batch-index return.
Quality/dedup across the splitting and manifest work: GatedProve
(Proved/Split/Measured) replaces the Result whose Err arm meant
success under exec_only; one raw_of record->rows mapping serves the
peak model and the split suggester; detected_ram_budget() carries the
85% policy once, on the kernel's RAM_USABLE_FRAC; decode_addr_lists
uses Address::unpack and the blob encoders use toLEBytes; pure helpers
are structural folds. Ownership is assigned in ONE env pass per run
(ownedConstsPer) with split parts inheriting their parent's constants
(partitionOwned) — removing a full env rescan per shard and per split
node (~1 h of setup at Mathlib scale). The manifest emit takes the
caller's live EnvHandle; the measured-peaks section is presence-gated,
strictly validated, and read back by summary(). The uncorrelated Lean
RAM projection (projectedProverBytes) is deleted: predicted
135/258/462 GiB where measured RSS was 278/229/265, while the Rust
three-phase model tracked +1.2-4.5%.

The check audit's budget flag is --ram-budget: it names the
destination prove box the partition is destined for, which the audit
machine cannot detect — unlike ix prove's --max-ram, which defends the
local box and detects 85% of MemAvailable when omitted.
gibBytes/toGib live in Ix/Common.

Seed sizing settles on 20,000x env bytes against 2/3 of the budget,
Mathlib-class-normative: amplification is env-dependent (init
12.5-14k, FLT 19.6k, Mathlib 20.2-21.7k) and Mathlib's shape is the
common case for real Lean libraries — a 17k midpoint seed put 129/132
Mathlib shards over a 400 GiB budget. Measured at 233 seeds: 8 over,
depth-2 cascade, 242 measured shards at 161-397 GiB, zero failing
constants, 17:20 wall. Small envs over-shard instead (init: ~20% extra
STARK, first run only, reclaimed by the printed consolidation count).
EXEC_RSS_PER_OWNED_BYTE returns to 2500 — the exec footprint is also
env-dependent (~1000x ISLB, ~2300x Mathlib) and the gate's contract is
never-OOM, so the slope carries the worst measured env (the 1100x
refit OOM'd full-width Mathlib at 486/495 GB).

Explorations implemented, measured, and removed en route (results are
the keepable part): re-seed/calibration execution phases (re-execute
the env to learn what the prove's own gated execution measures free);
streaming cut-and-prove fusion (hint-row promotion is a full
constrained replay under accumulation-based multiplicities); static
byte->peak regressions (12-60% error, non-transferable); raw-sum group
planning (bound held at 0.92-1.00x truth but fill equaled naive
byte-sharding — summed fine segments count the shared core K times,
and the claim spine is not the union of its parts'). Standing
conclusion: planning signals must see row unions, only execution sees
unions, so the prove run's gated execution is the planner and the seed
only has to land in the right decade.
The backend passes the same 100 GiB runner budget to both stages:
ix shard seeds for it and ix check --ram-budget cuts any seed the
spread pushes over it in place, so the row's shards metric counts the
leaf partition the run actually validated. The measured check-time
window stays the wave-0 batch call, keeping the timing metric
comparable; a shards increase on first run reflects real splits at
this budget and takes a one-time re-pin.
@samuelburnham
samuelburnham marked this pull request as draft August 28, 2026 19:12
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