Skip to content

IxVM kernel: canonical level normalization + FFT-cost pinning#447

Merged
arthurpaulino merged 2 commits into
mainfrom
ap/level-leq-normalize
Jun 17, 2026
Merged

IxVM kernel: canonical level normalization + FFT-cost pinning#447
arthurpaulino merged 2 commits into
mainfrom
ap/level-leq-normalize

Conversation

@arthurpaulino

@arthurpaulino arthurpaulino commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

Two commits on level-normalization:

  1. IxVM: canonical level normalization for level_equal /
    level_leq.
    Replaces the recursive Level.leq mirror (two-way
    param-substitution split per Max / IMax — exponential in unresolved
    universe params) with a port of Rust's Géran canonical-form machinery
    from src/ix/kernel/level.rs::normalize_level / norm_level_eq /
    norm_level_le. level_equal and level_leq now normalize-and-compare;
    the substitution helpers (level_subst_reduce, level_has_param,
    level_any_param) are deleted.

  2. Tests: pin FFT cost per kernel-check target. Adds
    expectedFftCost : Option Nat to AiurTestCase; runTestCase asserts
    the rounded Aiur.computeStats.totalFftCost matches exactly. Pins all
    41 entries in Tests/Ix/IxVM.lean::kernelCheckEntries.

Motivation

The recursive Level.leq split was exponential in the number of params and
each branch materialized freshly substituted levels. On
_private.…SInt.0.Int16.instRxcHasSize_eq the level family was 93% of the
record at 2^31 ops (level_subst_reduce 60.8M + level_leq 53.2M +
level_max 30.4M entries…), entered through Inductive's ctor-field
universe constraint (level_leq), not def-eq's level_equal.

The same algorithmic gap blocks several stdlib targets in the in-tree
ixvm test suite. Beyond the fix itself, the existing suite has no
per-target FFT-cost floor: a kernel change can silently shift cost on any
pinned constant without review.

What changed

Kernel (Ix/IxVM/Kernel/Levels.lean)

  • Canonical form: path-sorted list of entries (path, const, vars) where
    path is the sorted list of param indices conditioning an imax
    chain, const the max constant contribution, vars the
    idx-sorted (param, offset) contributions.
  • normalize_aux walks the level with imax-path conditioning,
    delegating IMax-shape cases to normalize_imax_dispatch.
  • Phase-2 nl_subsumption_walk drops contributions dominated by another
    entry whose path is a subset. Called once at the end of
    level_normalize (with the same list as both rem and snapshot).
  • nl_le / nl_eq operate on canonical forms; covers-split soundness
    fix matches level.rs.
  • level_normalize is keyed on the level alone, so each distinct level
    normalizes once per run; equality / leq on canonical forms is cheap.

Tests

  • Tests/Aiur/Common.lean: new expectedFftCost : Option Nat field on
    AiurTestCase. runTestCase consumes queryCounts, computes
    Aiur.computeStats, and asserts the rounded totalFftCost matches
    exactly. Adds Ix.Aiur.Statistics to the imports.
  • Tests/Ix/IxVM.lean: kernelCheckNames : List String
    kernelCheckEntries : List (String × Nat). kernelChecks now sets
    expectedFftCost := some expected per entry.

Results

Unlocked targets

Before the kernel commit these typechecks OOM'd (process killed under
14 GB ulimit). After: complete with the FFT cost reported by
lake exe check <target>.

Target Before After (FFT cost)
Trans.mk OOM 2.91M
Array.append_assoc OOM 3.94G
Vector.append OOM 4.02G
Vector.extract_append OOM 63.76G

No regression on previously-passing targets

8-target sample, build clean both sides:

Target Before After
Nat.add_comm 56.05M 56.08M
Nat.add 13.34M 13.34M
Nat.decEq 71.92M 71.92M
Nat.decLe 209.64M 209.64M
Nat.sub_le_of_le_add 567.58M 567.58M

Differences are sub-0.1%, attributable to the inlined-nl_subsumption
micro-cleanup (one fewer memoized dispatch per level_normalize call).

Pinned suite coverage

41 kernel-check constants now assert exact rounded FFT cost: the 38 prior
kernelCheckNames entries (Stdlib, IxVMPrim, IxVMInd, edge-case
prelude) plus 3 newly-unlocked targets (Trans.mk,
Array.append_assoc, Vector.append). Vector.extract_append is
deliberately omitted (too heavy for CI; verified manually).

How to update a pinned cost

If a future kernel change legitimately shifts FFT cost on a pinned
target, the test failure message contains the new value:

× ∃: FFT cost matches for Kernel check Trans.mk: expected 2911629, got 2911635

Paste the got N value back into kernelCheckEntries. No tooling
beyond lake test -- --ignored ixvm is required.

Test plan

  • lake build check — clean
  • lake build IxTests — clean
  • lake test -- --ignored ixvm — 41 FFT-cost pins pass, no
    regressions in the rest of the suite
  • lake exe check Trans.mk — 2.91 M FFT, passes
  • lake exe check Vector.append — 4.02 G FFT, passes
  • lake exe check Array.append_assoc — 3.94 G FFT, passes
  • lake exe check Vector.extract_append — 63.76 G FFT, passes
  • lake exe check Nat.add_comm — 56.08 M FFT, no regression

@arthurpaulino arthurpaulino force-pushed the ap/level-leq-normalize branch from 439dd16 to 7821fcb Compare June 17, 2026 15:58
Port the Rust kernel's canonical-form level machinery (level.rs
normalize_level / norm_level_eq / norm_level_le, itself a line-by-line
port of Lean4Lean's Level.Normalize with the covers-split soundness
fix): normalize_aux with imax-path conditioning, phase-2 subsumption,
and structural/dominance comparison on canonical forms. level_equal and
level_leq now normalize-and-compare; the previous recursive Level.leq
mirror with its two-way param-substitution split per Max/IMax — and its
helpers level_subst_reduce / level_has_param / level_any_param — is
deleted. level_normalize is keyed on the level alone, so each distinct
level normalizes once per run.

The split was exponential in the number of params and every branch
materialized freshly substituted levels. On
_private.…SInt.0.Int16.instRxcHasSize_eq the level family was 93% of
the record at 2^31 ops (level_subst_reduce 60.8M + level_leq 53.2M +
level_max 30.4M entries…), entered through Inductive's ctor-field
universe constraint (level_leq), not def-eq's level_equal.

Measured: Int16.instRxcHasSize_eq goes from NON-COMPLETING (killed at
178 GB RSS after 21 min, growth not converging) to 2m23s / 17.9 GB /
295.7B FFT. Int8 completes in 21s. The bench suite is unaffected (its
level comparisons hit the structural fast path). 297 ixvm tests pass.
@arthurpaulino arthurpaulino force-pushed the ap/level-leq-normalize branch from 7821fcb to 95c3695 Compare June 17, 2026 20:17
@arthurpaulino arthurpaulino changed the title IxVM kernel: port Rust's Géran normalize for level_leq + pin per-target FFT cost IxVM kernel: canonical level normalization + FFT-cost pinning Jun 17, 2026
## Summary

Adds `expectedFftCost : Option Nat` to `AiurTestCase`. When set,
`runTestCase` asserts the rounded `Aiur.computeStats.totalFftCost`
matches exactly. Converts `Tests/Ix/IxVM.lean::kernelCheckNames` to
`kernelCheckEntries : List (String × Nat)` and pins all 41 entries.

## Motivation

Without per-circuit cost pinning, kernel changes that silently shift
FFT cost can land without review. Pinning forces every cost change to
be acknowledged in the test source — failures report `expected X, got
Y` so the new value can be pasted back.

## Coverage

Pins the 38 prior `kernelCheckNames` entries (Stdlib, `IxVMPrim`,
`IxVMInd`, edge-case prelude) plus 3 newly-unlocked targets from the
`level_leq` Géran-normalize fix:

- `Trans.mk`              — 2,732,504
- `Array.append_assoc`    — 3,938,369,542
- `Vector.append`         — 4,023,063,255

`Vector.extract_append` deliberately omitted (too heavy for the suite).
@arthurpaulino arthurpaulino force-pushed the ap/level-leq-normalize branch from 95c3695 to 31a35f0 Compare June 17, 2026 20:32
@arthurpaulino arthurpaulino merged commit 59a4183 into main Jun 17, 2026
14 checks passed
@arthurpaulino arthurpaulino deleted the ap/level-leq-normalize branch June 17, 2026 21:02
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.

3 participants