Skip to content

feat(aiur): Accurate whole-env sharding based on execution measurements - #537

Draft
samuelburnham wants to merge 7 commits into
mainfrom
sb/aiur-planner
Draft

feat(aiur): Accurate whole-env sharding based on execution measurements#537
samuelburnham wants to merge 7 commits into
mainfrom
sb/aiur-planner

Conversation

@samuelburnham

Copy link
Copy Markdown
Member

Aiur sharding: measured scan-and-cut

What this does

Shard boundaries for Aiur proving are now measured, not predicted. ix shard env.ixe executes the env's check schedule through the codegen'd Aiur kernel — the same thin-frontier CheckEnv claims a prover pays for — with a running cost readout, and cuts a shard wherever the measured cost reaches the RAM budget. The previous counter-based cost model (fit on ingress bytes / substitutions / def-eq counts) mispredicted real content by up to 16× in both directions and OOM'd shards it called safe; execution is the mandatory prefix of proving, so measuring it prices the prove exactly.

Design

  • Two-term cut: a shard ends when RAM-per-fft · measured fft + measured record bytes reaches the budget's usable headroom — both resources the prove actually consumes.
  • One RAM mechanism, no reactive control: each scan worker gets a record-byte cap fixed at startup (0.70·box RAM / workers), enforced inside kernel execution. Fleet RAM is workers × cap by construction. A block that outgrows the cap alone is skipped and named in failed.csv, same as a kernel-rejected block — pathological content is excluded by name, not engineered around.
  • True per-shard costs: every shard is re-priced with one cold CheckEnv — exactly the claim its prove executes — so manifest costs carry no scan-side inflation. The .ixes manifest (format v2) stores them per shard; ix prove orders heaviest-first from it.
  • Lazy witness fault-in: the witness IOBuffer faults env bytes on demand, so witness RAM tracks the touched set, not the shipped closure — for scan and prove alike.
  • Parallel scan via chunked schedule + work stealing; --fail-fast (default) / --no-fail-fast uniform across ix shard / check / prove; whole-env rows in the aiur-shard bench backend (scan time, peak RSS, shards, fft totals).

Validation (250 GiB budget, 32-core / 250 GB box)

env blocks scan wall peak RSS shards total fft max pred. prove RSS
Init 51,003 152 s 91 GiB 40 1,719 BFFT 165.4 GiB
InitStd 87,847 255 s 127 GiB 76 3,330 BFFT 159.7 GiB
Lean 152,596 383 s 123 GiB 108 5,086 BFFT 164.6 GiB

All blocks covered (zero skips), partitions deterministic, every predicted prove RSS under budget. Scan cost is ~2% of the pack's prove cost. Measured prove check: shard predicted 76.1 GiB / 60.5 s proved at 74.8 GiB / 59.4 s.

Usage

ix shard Init.ixe --max-ram 250 --out Init.ixes   # scan → manifest + costs sidecar
ix check --ixe Init.ixe --ixes Init.ixes          # check shards
ix prove --ixe Init.ixe --ixes Init.ixes          # prove, heaviest-first, resumable

The shard-pipeline E2E runs in lake test -- --ignored ixvm (also in PR CI).

Known limits

  • FLT/Mathlib contain a handful of pathological blocks (kernel-rejected or with prove costs beyond any single-box budget); scans name and skip them rather than rescue them, and the coverage gate reports the exclusions.
  • The fft→RSS line is fit on 13 measured Init proves with 3 spot checks on other envs (−1.7% / +2.9% / +6%); a broader stratified prove sample is follow-up work.

…acking

Port of sb/measured-ingress's planning layer onto the addr-first kernel
(#529), reworked for its lazy fault-in semantics.

Profiling (.ixprof v3): the recording kernel's try_get_const consults are
drained per constant into a touch graph — the measured set a lazy checker
faults in — persisted as a presence-flagged CSR section alongside a new
block-level reference graph and a subst_unique counter (splitmix64-keyed
dedup of substitution work items: the post-memoization volume a memoizing
executor pays). Both graph sections are optional, so older readers'
formats stay decodable by version gate alone.

Planner: partition_for_aiur_ram bin-packs to a host-RAM cap with
faulted-set byte accounting — under #529 the witness always ships the
full closure, but only faulted bytes cost ingress trace rows, so a
shard's byte feature is owned ∪ touched(owned) (full reference closure
as the over-approximating fallback when no touch graph was recorded).
The stub/promotion machinery of the original branch is dropped entirely:
lazy fault-in computes at run time exactly the set the stubs predicted,
with no replay divergence and no repair ladder, and the manifest format
stays byte-identical with main (owned-only).

Cost models: nlogn(x) = x·log2(x+2) feature form matching Aiur's
width·height·log2(height) FFT structure; prove/execute time and RAM
coefficients carried over from the pre-#529 aiur bench-suite fits.
They are placeholders pending a refit against this kernel — the circuit
shrank (deleted positional machinery), the byte feature narrowed to the
faulted set, and the recording counters moved under #442 — with
provenance noted at the definitions. The costs sidecar
(<out>.costs.csv) persists per-shard features incl. subst_unique and
nat_arith so the refit has its corpus.

CLI: ix shard --backend aiur --max-ram G; ix profile --backend/--top
(per-metric block leaderboards with display names) and ix profile sweep
(env-wide closure cost sweep: per-root feature CSV + feasibility,
min-root-per-hot-block, and diversity reports).
prove --ixe --ixes with no --shard now runs the whole partition through
one EnvHandle + one AiurSystem instead of a fire-and-forget loop.
Progress persists in the keyed cache ~/.ix/cache/shard-proofs/ (one
entry per claim digest, holding the proof's store address): an entry is
written only after the proof VERIFIES and binds to the shard's
reconstructed CheckEnv claim digest, so a crash or OOM costs only the
in-flight shard and re-running the same command resumes. Cached entries
are re-verified on resume — the digest pins the claim, not the circuit
version. The digest is manifest-independent, so manifests sharing a
shard share its proof.

Pending shards prove heaviest-predicted-RAM first (from the packer's
costs sidecar): if the RAM model's blind spot is going to breach a box
it is one of the heaviest shards, and proving those first surfaces the
failure in the opening minutes instead of hours in. --jobs N proves
shards concurrently (default 1 — each prove peaks at the shard's full
predicted RAM).

aiur: ExecError::InvalidIOKey now names the channel and key, so a
witness that under-ships identifies the constant whose bytes are
missing instead of failing generically.
aiur-shard bench backend: profile the env's touch graph, pack at a
pinned machine-independent budget (aiurShardBudgetGb = 250, the
deliverable prove box — a PR row and its baseline must describe the
same partition problem), upload planner metrics (shards,
pred-floor-ram, union-bytes) on tight 5% upper bands (profile worker
interleaving wobbles touch sets ±2 shards, so no exact pins), then
execute the three heaviest-predicted shards through the native kernel
and aggregate count-neutral heavy-execute-time / heavy-peak-rss.
ix check gains --json/--json-name so a shard check self-reports its
bench row with the env parse excluded from the timed window. Init is
registered as a benchmark env — it is the shard pipeline's deliverable
partition, so its planner trend line matters on its own.

Tests/Ix/Kernel/ShardPipeline.lean: end-to-end regression over a small
multi-shard env — profile → pack (disjoint cover) → every shard checks
→ batched prove reaches the composed verdict → second run resumes from
the shard-proofs cache with zero pending. Runs under a scratch cache
root, never the global ~/.ix/cache; wired into the ixvm suite.
…s preserved

ix shard --rebudget K --manifest M.ixes --max-ram G --backend aiur packs
only shard K's blocks at the child budget (in the original cut-coherent
order); every other shard's block list is reproduced verbatim — asserted,
because ShardManifest::build is deterministic in (profile, assignment)
and that identity is exactly what keeps the untouched shards' claim
digests, and therefore their ~/.ix/cache/shard-proofs entries, valid.
The first child keeps id K, the rest append; the aggregation tree's leaf
K becomes a left-leaning chain over the children. The costs sidecar is
recomputed for every row (identical for untouched shards — the
faulted-set union is a function of the owned set alone), so heaviest-
first prove ordering picks up the children's predictions.

This is the recourse when the RAM model's blind spot packs an outlier
shard past what the box can prove: a global repack at a lower budget
shifts boundaries from early in the pack order and forfeits most of the
proof cache, while a rebudget costs exactly the split shard.

Ported from the sb/sp1-shard-agg experiment (where a 148-predicted shard
measured ~248 GiB); simplified for the owned-only manifest — the
promotion-inheritance step is gone because the witness ships the full
closure and a child can never miss an IO key.
…resources fit

Replaces the placeholder coefficients (fit on the pre-addr-first eager
kernel) with a model fit entirely on this kernel, restructured around the
prover's actual work unit.

Stage 1 predicts a run's total FFT cost from profile features:

  fft = 2.599e9 + 188.6*nlogn(faulted_bytes) + 71.27*nlogn(subst)
                + 6548*nlogn(def_eq)

def_eq is load-bearing: it appears in every acceptable-tail fit, pricing
the definitional-equality-dense shards that under the old model measured
up to 25% over predicted RAM (reproduced on this kernel before the
refit: the nat-heaviest Init shard proved at 219 GiB against a 175
prediction). nat_arith adds no explanatory power at shard scale on this
corpus and stays a persisted-but-unmodeled feature; the exact path below
covers its tail.

Stage 2 maps FFT cost linearly to resources — physically grounded
(committed LDE volume is proportional to FFT work):

  prove_secs = 6.18  + 2.0425 * BFFT      RSS_gib = 13.59 + 2.3507 * BFFT

exposed both composed (plan-time, from features) and as *_for_fft
accessors, so a measured FFT cost from an execute-mode stats dump gives
an exact-height prediction — the cheap pre-prove gate for any residual
outlier, which can then be `--rebudget` before burning a prove.

Corpus and fit quality (2026-08-03, this box):
- stage 1: exact FFT of all 34 Init shards at a 250 GiB pack (execute
  stats dumps). MAPE 5.0%, worst under -18.5%.
- stage 2: 13 measured Init shard proves spanning 21-81 BFFT. RSS
  max|err| 9.9%, wall 12.2%. Composed end-to-end on the proved set:
  RAM within 12.4%.
- generalization: 60 InitStd shards the fit never saw. Stage-1 MAPE
  5.7%, worst under -15.9% — inside the tail bound.
- execute models refit on the same sweep (wall tracks def_eq alone,
  MAPE 9.6%; RSS bytes+def_eq, 3.7%).

AIUR_RAM_USABLE_FRAC rises 0.70 -> 0.75: the composed worst-case
under-prediction (stage-1 tail x stage-2 tail ~ -25%) inverts to 0.797,
leaving the difference as OS/variance margin — headroom that is now
derived from measurement instead of absorbing an unpriced blind spot.

Validation, end to end: repacking Init at 250 GiB yields 32 shards
(stale model: 34, one of which measured unprovable at a projected ~277
GiB); under the new pack no shard's exact-FFT-mapped RAM exceeds the
budget, and the heaviest (88.2 BFFT) proved green at 227.2 GiB measured
vs 220.9 mapped (+2.9%).

Also fixed: both greedy pack loops' tentative cap-test cost accumulated
only bytes+hb before testing — harmless when RAM ignored subst/def_eq,
an under-test under any model with reduction terms. The ShardPipeline
E2E now cuts its tiny env with the fixed-count min-cut partitioner: the
composed model base (~19.7 GiB) exceeds anything a ~200-constant env can
fill, so a RAM budget either degenerates to singletons or one shard;
the claim layer is partition-agnostic and the RAM packer keeps its own
unit coverage.
The witness IOBuffer materialized a shard's full bfs_closure eagerly,
8x-expanded (one G per byte), before the kernel executed a single
query — ~2.5 GiB per Init shard buffer, ~30 GiB at Mathlib scale, per
in-flight shard. The kernel's laziness was circuit-side only: shipped-
but-unfaulted bytes cost zero rows, but the host always paid for the
whole closure.

IOBuffer gains an optional IOFaultSource backing consulted on a
get_info miss: EnvFaultSource resolves ch 2/3/4 keys against the
shared mmap'd env and materializes entries on first fault, so host
witness RAM scales with the FAULTED set — what the check actually
touches — instead of the shipped closure. Soundness-neutral: faulted
bytes are blake3-verified against their content-addressed key exactly
like eagerly-seeded ones, and the idx/len wires are unconstrained
witness either way. Trace generation reads through a frozen lookup
(get_info_frozen): it runs circuits in parallel over a shared buffer
and never needs to fault — execution already materialized every entry
it read.

EnvHandle holds Arc<Env> so a backing outlives any handle borrow. The
claim-channel seeding (ch 0/1) is factored into
seed_shard_check_env_claim, shared by the eager and lazy builders, and
all four with-env check/prove FFI paths now use the lazy variants.
Shard boundaries come from executing the env's check schedule through
the codegen'd Aiur kernel — thin-frontier CheckEnv claims, the exact
unit a prover pays for — with a running cost readout, replacing the
counter-prediction path for Aiur. `ix shard env.ixe` runs the scan by
default; `--backend zisk` chains profile + guest-cost pack; the
.ixprof packer is unchanged for Zisk.

- Two-term cut: a shard ends when RAM-per-fft * fft + record bytes
  reaches the budget's usable headroom — both resources a prove
  consumes.
- One RAM mechanism: a per-worker record-byte cap
  ((0.70*box - 10)/workers, 8 GiB autoscale floor; --workers 1 grants
  the whole allowance), enforced inside execution via thread-local
  accounting in QueryMap::insert and codegen'd
  record_budget_exceeded() polls (ExecError::RecordBudgetExceeded).
  A trip mid-segment is an early cut; a block over the cap alone is
  skipped and named. Fleet RAM is workers x cap by construction — no
  reactive control.
- True costs: every shard is re-priced with one cold CheckEnv — the
  claim its prove executes — and the merge pass consolidates on true
  costs to a fixpoint.
- Manifest v2: tagged per-shard cost (Unknown / ProfileHeartbeats /
  ZiskCostUnits / AiurFft, raw fft units), version-checked in both the
  Rust and Lean parsers; ix prove orders heaviest-first from the
  manifest; the costs CSV sidecar is report-only.
- Parallel scan over chunked schedule with work stealing;
  --fail-fast (default) / --no-fail-fast uniform across
  ix shard / check / prove, skipped blocks inventoried in failed.csv;
  IX_SCAN_LIMIT_BLOCKS slice knob for reproducers.
- aiur-shard bench backend gains whole-env rows: scan-time, peak-rss,
  shards, max-shard-fft, and fft-cost in raw fft units so env totals
  join the existing per-constant plot.

Validated at 250 GiB: Init 40 shards / 1,719 BFFT total (152 s, peak
91 GiB); InitStd 76 / 3,330 (255 s, 127 GiB); Lean 108 / 5,086 (383 s,
123 GiB); zero skipped blocks; deterministic partitions; prove spot
check 74.8 GiB real vs 76.1 predicted.
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