Skip to content

feat(fields): fast KoalaBear quartic extension and coset interpolation - #399

Draft
mitschabaude-bot wants to merge 111 commits into
Verified-zkEVM:mainfrom
mitschabaude-bot:koalabear-fast-ext4
Draft

mitschabaude-bot wants to merge 111 commits into
Verified-zkEVM:mainfrom
mitschabaude-bot:koalabear-fast-ext4

Conversation

@mitschabaude-bot

@mitschabaude-bot mitschabaude-bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Depends on #395, which depends on #390. This branch starts at #395's tip. GitHub cannot use a fork branch as an upstream base, so this draft targets main and its diff includes both earlier PRs. Review only the new changes.

A fast, fully proved degree-4 extension of KoalaBear, KoalaBear[X] / (X^4 - 3): the same field as the spec KoalaBear.Ext4 and Plonky3's BinomialExtensionField<KoalaBear, 4>. On top of it, a proved coset interpolation at an extension point, benchmarked against Plonky3's stock interpolate_coset.

  • KoalaBear.Fast.Ext4 holds four Montgomery coefficients. toSpec maps it into the spec field; toSpec_add, toSpec_mul, toSpec_inv, toSpec_div and toSpec_ofBase prove agreement operation by operation, and specEquiv is the ring isomorphism.
  • Multiplication folds 3 into the left factors, so each output coefficient is a sum of four products with a single Montgomery reduction. The new Montgomery.Native32.mulAdd4 does this for every 32-bit Montgomery field: four products stay below 2 · p · 2^32, so one conditional subtraction brings them into reduce's range.
  • Inversion uses the norm to the base field: with σ : X ↦ -X, a · σ(a) = g₀ + g₁ X² and a⁻¹ = σ(a) (g₀ - g₁ X²) / (g₀² - 3 g₁²), one base-field inversion. The norm of a nonzero element is nonzero because σ is an injective ring endomorphism of the quotient field and 3 is a quadratic non-residue mod p.
  • Codec: the four coefficient encodings, identical to the spec field's encoding (toBytes_toSpec), with round trip through ByteCodec.

Results

The existing fields-extension-koalabear-ext4-mul and -inv benchmark groups now time both types on the same operands; the harness checks that both produce the same checksum. Per call, including the harness's operand lookup and the result allocation:

Operation Spec KoalaBear.Ext4 Fast KoalaBear.Fast.Ext4
Multiplication 7.33 µs 28.1 ns
Inversion 913 µs (Fermat) 161 ns (norm)

The tests check known answers computed with Plonky3 (p3-field 0.4.2) for a product and two inverses, field laws, agreement with the spec field, and the codec.

Coset interpolation

KoalaBear.Fast.interpolateCoset is Plonky3's interpolate_coset for KoalaBear and its quartic extension: given a row-major 2^logN × width matrix whose columns hold the values of polynomials on the coset s · ⟨ω⟩, it evaluates every column's polynomial at a point z of the extension, by (zⁿ - sⁿ) / (n sⁿ) · ∑ᵢ xᵢ / (z - xᵢ) · p(xᵢ).

  • NTT.Domain.aeval_eq_cosetBarycentric proves that formula for any field extension E / F: the coset's nodal polynomial is Xⁿ - sⁿ, so each barycentric weight is xᵢ / (n sⁿ).
  • List.batchInv is Montgomery's batch inversion, proved correct for inputs without zeros and transferable along maps preserving *, ⁻¹ and 1, so it runs on the fast extension, which has no Field instance.
  • toSpec_interpolateCoset proves every output equal to aeval z pⱼ for the column polynomials pⱼ.
  • --suite interpolate runs it against p3_interpolation::interpolate_coset 0.4.2 on the same fixtures; full output digests agree before timing, from single rows to 2²⁰ × 16.

KoalaBear.Fast.interpolateCosetPacked is the fast version. It takes the matrix as Montgomery words in a ByteArray, the layout of Plonky3's RowMajorMatrix, and interpolateCosetPacked_eq proves it equal to the reference for every point off the coset.

  • Base-field weights. For t = z₀ - x, (z - x)⁻¹ = (b₀ + t b₁ + t² b₂ + t³) / N(t), with extension constants bₖ that depend on z only and N(t) the norm of z - x. So each row's weight is four base-field values x tᵏ / N, only base-field norms need inverting, and a column sum is ∑ₖ Sₖ bₖ for four base-field sums. That is about twelve base-field multiplications per row.
  • Blocks of 512 rows. A forward pass stores each row's node, t², norm and prefix product, one inversion per block inverts the product, and a backward pass overwrites each row with its weights.
  • Lazy sums. Column sums accumulate unreduced 64-bit products with one conditional subtraction each (Montgomery.Native32.LazyAcc) and one Montgomery reduction per block. Four columns share each row's weights.
  • Tasks. Eight row ranges run in parallel, with the caller taking the lowest.

It reads and writes words with the packed NTT's backported ByteArray word accessors (lean4#14053) and adds no extern of its own. The benchmark suite also checks its full outputs against the reference and Plonky3 before timing.

Five rounds of --suite interpolate on eight workers at 130c36a, milliseconds per call:

Rows Columns Lean packed Lean reference Plonky3
2^12 1 0.08 0.68 0.20
2^12 16 0.09 1.76 0.15
2^16 1 0.30 11.92 0.88
2^16 16 0.47 27.97 0.64
2^20 1 3.26 248.35 8.08
2^20 16 4.84 523.12 8.80

graikos and others added 30 commits September 29, 2026 07:33
…ocks

Fold the 64x64 product's middle terms without carry bookkeeping so clang emits
one widening multiply for squarings too; `mul x x` inlined into `square` used
to compile to a four-imul chain. Run the Fermat inverse and the pow ladder on
unreduced words (`mulLazy`, `squareNLazy`, `invLazy`, `powLazy`) with a single
canonicalization at the end, and make `squareN` tail recursive. Correctness
rests on word-level cast lemmas (`foldUInt128Lazy_cast`, `mulLazy_cast`,
`squareNLazy_cast`, `invLazy_cast`, `powLazy_cast`).

A/B against master on the same core: inv 311 -> 167 ns, pow 99 -> 74 ns,
mul and add unchanged.
`conditionalSubtract` was a plain def, so every KoalaBear and BabyBear `add`
paid an out-of-line call. A/B: add latency 844 -> 606 ps, throughput
876 -> 166 ps.
…nreduced words

`univariate-dense-goldilocks` now runs the native-word carrier beside ZMod like
the other fast fields, with rows named in the shared `-fast` scheme. Add tests
that unreduced `ofUInt64` words compare and multiply as their residues.
Compute the canonical sum as `x - (p - y)`: the subtraction borrows exactly
when `x + y < p`, so one borrow-selected correction by `2^32 - 1` yields the
reduced result, three dependent instructions instead of a carry check plus a
separate compare against `p`. `addRaw_toNat` carries the bound and the cast
lemma; the carry-based reducer and its lemmas are removed.

A/B against master on the same core: add latency 1021 -> 616 ps, throughput
381 -> 230 ps; mul, inv and pow unchanged.
# Conflicts:
#	bench/CompPolyBench/Fields/Arith.lean
… the Goldilocks fold

Two changes to the 128-bit fold behind every fast Goldilocks multiply. The borrow
of the low word against the top limb has probability about 2^-32, but a Lean `if`
compiled to a select on the critical path; moving that arm into a `@[noinline]`
function forces a predicted branch. The middle term `hi_lo * (2^32 - 1)` compiled
to a three-cycle `imul` inside loops (clang holds the constant in a register); it
is now `(hi <<< 32) - hi_lo`, two one-cycle operations. `shiftLeft32_sub_low` and
`foldTail_cast` carry the proofs; the fold lemma cases on the borrow.

A/B against master on the same core: mul latency 3077 -> 2485 ps, throughput
815 -> 739 ps, inv 314 -> 136 ns, pow 101 -> 64 ns; add unchanged.
One-sentence docstrings, `2 ^ k` literals instead of decimals, a `_toNat`
bridge for the product's high word, implicit bounds on the `addRaw` lemmas,
camelCase limb names, and README rows for the Goldilocks files.
Idle worker cores take a few hundred milliseconds of load before the
frequency governor raises their clocks; the 50 ms warmup often ended
before that. The NTT suite now uses the large budget (200 ms warmup,
50 samples) in Lean and Rust; the Rust harness gains Budget presets
matching Lean's.
KoalaBear.Fast.Ext4 is KoalaBear[X]/(X^4 - 3) over Montgomery
coefficients, proved isomorphic to the spec KoalaBear.Ext4 operation by
operation. Multiplication reduces each output coefficient once
(Native32.mulAdd4); inversion uses the norm to the base field, with one
base-field inversion. The byte codec matches the spec field's, and
Plonky3 known answers are tested. Multiplication 28 ns and inversion
161 ns, against 7.3 us and 913 us for the spec field.
aeval_eq_cosetBarycentric proves Plonky3's interpolate_coset formula:
a polynomial of degree below n evaluates at an extension-field point z
outside the coset s·H as (zⁿ - sⁿ)/(n sⁿ) · Σ xᵢ/(z - xᵢ) · p(xᵢ). The
coset's nodal polynomial is Xⁿ - sⁿ, so each barycentric weight is
xᵢ/(n sⁿ). List.batchInv is Montgomery's trick, proved correct for
inputs without zeros and transferable along maps preserving *, ⁻¹, 1.
KoalaBear.Fast.interpolateCoset evaluates each column of a row-major
matrix of coset values at an extension-field point, like Plonky3's
interpolate_coset: coset nodes by prefix products, one batch inversion
for the weights x_i/(z - x_i), one dot product per column and a final
scale by (zⁿ - sⁿ)/(n sⁿ). toSpec_interpolateCoset proves every output
equal to aeval z of the column's polynomial.
Adds --suite interpolate: CompPolyInterpolateBench times the proved
reference KoalaBear.Fast.interpolateCoset, and the Rust crate calls
p3_interpolation::interpolate_coset 0.4.2 on the same fixtures (root,
shift, two alternating extension points, row-major matrix). Full output
digests agree before timing. The sequential reference is 3-58x slower
than parallel Plonky3 on eight workers.
@mitschabaude-bot mitschabaude-bot changed the title feat(fields): fast KoalaBear quartic extension feat(fields): fast KoalaBear quartic extension and coset interpolation Oct 7, 2026
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

🤖 PR Summary

ℹ️ Some sections were omitted to fit GitHub's comment size limit.

Failed to generate AI summary. Please check the per-file summaries and statistics below.


Statistics

Metric Count
📝 Files Changed 137
✅ Lines Added 21900
❌ Lines Removed 1197

Lean Declarations

✏️ Removed: 35 declaration(s)

CompPoly/Fields/BLS12_377/Fast.lean (1)

  • instance instMont64x8Field : Mont64x8Field BLS12_377.scalarFieldSize where

CompPoly/Fields/BLS12_381/Fast.lean (1)

  • instance instMont64x8Field : Mont64x8Field BLS12_381.scalarFieldSize where

CompPoly/Fields/BN254/Fast.lean (1)

  • instance instMont64x8Field : Mont64x8Field BN254.scalarFieldSize where

CompPoly/Fields/Goldilocks/Fast.lean (2)

  • private theorem mul_assoc_field (x y z : Field) : (x * y) * z = x * (y * z)
  • private theorem pow_succ (x : Field) (n : Nat) : pow x (n + 1) = pow x n * x

CompPoly/Fields/Goldilocks/FastDefs.lean (1)

  • def reduceAddWithCarryRaw (lo : UInt64) (carry : Bool) : UInt64

CompPoly/Fields/Goldilocks/FastReduction.lean (6)

  • theorem addWithCarry_bound (x y : UInt64)
  • theorem addWithCarry_value (x y : UInt64) :
  • theorem reduceAddWithCarryRaw_cast (lo : UInt64) (carry : Bool)
  • theorem reduceAddWithCarryRaw_lt (lo : UInt64) (carry : Bool)
  • theorem wideMul_high_nat
  • theorem wideMul_high_toNat

CompPoly/Fields/Montgomery/Native64x8.lean (1)

  • private theorem carry_top_zero {S A B Q c : ℕ} (h : S + 2 ^ 256 * c = A + B)

CompPoly/Fields/Montgomery/Native64x8Field.lean (1)

  • theorem two_mul_q_lt : 2 * P.modulusLimbs.toNat < 2 ^ 256

CompPoly/Fields/Pasta/Fast.lean (1)

  • instance instMont64x8Field : Mont64x8Field Vesta.baseFieldSize where

CompPoly/Fields/Secp256k1.lean (7)

  • abbrev BaseField
  • abbrev ScalarField
  • def BASE_FIELD_CARD : Nat
  • def SCALAR_FIELD_CARD : Nat
  • example :
  • theorem BaseField_is_prime : Nat.Prime BASE_FIELD_CARD
  • theorem ScalarField_is_prime : Nat.Prime SCALAR_FIELD_CARD

bench/CompPolyBench/Fields/Binary/Tower.lean (7)

  • @[inline] private def towerSampler {E : Type} (xs : Array E) (one : E) : Nat → E × E
  • @[specialize] private def runTowerGroup (groupKey title method : String)
  • def checksumConcreteBt128 (x : ConcreteBTField 7) : Nat
  • def checksumFastBT128 (x : Fast.FastBT128) : Nat
  • private def runTowerInv (preset : BenchPreset) (gen : StdGen) :
  • private def runTowerMul (preset : BenchPreset) (gen : StdGen) :
  • private def towerShape : String

bench/CompPolyBench/Fields/Binary/Tower/Scalar.lean (5)

  • private def runTowerInv64 (preset : BenchPreset) (gen : StdGen) :
  • private def runTowerMul64 (preset : BenchPreset) (gen : StdGen) :
  • private def runTowerMul8 (preset : BenchPreset) (gen : StdGen) :
  • private def towerWordPool (bits : Nat) (gen : StdGen) : Array UInt64 × StdGen
  • private def towerWordRep (suffix representation field : String) (pool : Array UInt64) :

bench/CompPolyBench/Fields/Inputs.lean (1)

  • @[inline] def sinkMont64x8 {modulus : Nat} [Montgomery.Native64x8.Mont64x8Field modulus]
✏️ Added: 1152 declaration(s)

CompPoly/Data/Array/Involution.lean (9)

  • @[inline] def permuteInvolution (order : Array Nat) (a : Array α) : Array α
  • @[inline] private def involutionStep (f : Nat → Nat) (a : Array α) (i : Nat) : Array α
  • @[simp] private theorem size_involutionStep (f : Nat → Nat) (a : Array α) (i : Nat) :
  • @[simp] theorem size_permuteInvolution (order : Array Nat) (a : Array α) :
  • private theorem getD_swap (a : Array α) (i j k : Nat) (d : α)
  • private theorem permuteInvolution_eq_fold (order : Array Nat) (a : Array α) :
  • private theorem prefix_invariant (f : Nat → Nat) (a : Array α) (d : α)
  • private theorem size_fold (f : Nat → Nat) (is : List Nat) (a : Array α) :
  • theorem permuteInvolution_eq_map (order : Array Nat) (a : Array α) (d : α)

CompPoly/Data/ByteArray/Pack.lean (12)

  • @[simp] theorem size_setUInt32LE! (a : ByteArray) (off : Nat) (v : UInt32) :
  • @[simp] theorem size_usetUInt32LE (a : ByteArray) (i : USize) (v : UInt32) (h) :
  • def getUInt32LE : (a : @& ByteArray) → (i : @& Nat) → (h : i + 4 ≤ a.size
  • def getUInt32LE! (a : @& ByteArray) (off : @& Nat) : UInt32
  • def setUInt32LE : (a : ByteArray) → (i : @& Nat) → (v : UInt32) →
  • def setUInt32LE! (a : ByteArray) (off : @& Nat) (v : UInt32) : ByteArray
  • def ugetUInt32LE : (a : @& ByteArray) → (i : USize) →
  • def usetUInt32LE : (a : ByteArray) → (i : USize) → (v : UInt32) →
  • theorem get!_eq_getElem! (a : ByteArray) (i : Nat) : a.get! i = a[i]!
  • theorem getElem!_setUInt32LE!_of_outside (a : ByteArray) (o j : Nat) (v : UInt32)
  • theorem getUInt32LE!_setUInt32LE!_of_disjoint (a : ByteArray) (o₁ o₂ : Nat) (v : UInt32)
  • theorem getUInt32LE!_setUInt32LE!_self (a : ByteArray) (off : Nat) (v : UInt32)

CompPoly/Data/ByteArray/Packed.lean (75)

  • @[inline, reducible] def ofWord32 {R : Type u} (toWord : R → UInt32) (ofWord : UInt32 → R)
  • @[inline] def getAt (b : ByteArray) (k : USize) (hk : k.toNat < b.size / width R)
  • @[inline] def loadWord (b : ByteArray) (i : USize) (h : i.toNat + 4 ≤ b.size)
  • @[inline] def setAt (b : ByteArray) (k : USize) (x : R) (hk : k.toNat < b.size / width R)
  • @[inline] def setIf (n : Nat) (hn : n < USize.size) (b : ByteArray) (k : USize) (x : R) :
  • @[inline] def storeWord (b : ByteArray) (i : USize) (v : UInt32) (h : i.toNat + 4 ≤ b.size)
  • @[simp] theorem decode_encode (a : Array R) : decode (encode a) = a
  • @[simp] theorem encode_empty : encode (#[] : Array R) = ByteArray.empty
  • @[simp] theorem size_alloc (n : Nat) : (alloc R n).size = width R * n
  • @[simp] theorem size_alloc_div (n : Nat) : (alloc R n).size / width R = n
  • @[simp] theorem size_encode (a : Array R) : (encode a).size = width R * a.size
  • @[simp] theorem size_encode_uint32 (W : Array UInt32) : (encode W).size = 4 * W.size
  • @[simp] theorem size_setAt (b : ByteArray) (k : USize) (x : R) (hk) (hu) :
  • @[simp] theorem size_uset (b : ByteArray) (i : USize) (v : UInt8) (h : i.toNat < b.size) :
  • @[simp] theorem size_zeros (n : Nat) : (zeros n).size = n
  • @[simp] theorem width_uint32 : width UInt32 = 4
  • def alloc (R : Type u) [PackedRepr R] (n : Nat) : ByteArray
  • def bytesAt (b : ByteArray) (off w : Nat) : Vector UInt8 w
  • def decode (b : ByteArray) : Array R
  • def encode (a : Array R) : ByteArray
  • def fillZeros (target : Nat) : Nat → ByteArray → ByteArray
  • def get (b : ByteArray) (j : Nat) : R
  • def ofWordBytes (v : Vector UInt8 4) : UInt32
  • def replaceAt (b : ByteArray) (off : Nat) (values : ByteArray) : ByteArray
  • def replicate (n : Nat) (x : R) : ByteArray
  • def replicateGo (n : Nat) (block acc : ByteArray) : ByteArray
  • def wordBytes (v : UInt32) : Vector UInt8 4
  • def zeroBlock : ByteArray
  • def zeros (n : Nat) : ByteArray
  • private theorem drop_flatMap (l : List R) (i : Nat) :
  • private theorem length_flatMap (l : List R) :
  • private theorem take_flatMap (l : List R) (i : Nat) :
  • theorem bytesAt_encode (a : Array R) (i : Nat) (hi : i < a.size) :
  • theorem copySlice_encode (a o : Array R) (i c : Nat) :
  • theorem copySlice_replicate (k m len : Nat) (hl : len ≤ k) :
  • theorem encode_append (a b : Array R) : encode (a ++ b) = encode a ++ encode b
  • theorem encode_extract (a : Array R) (first last : Nat) :
  • theorem encode_ofWord32 {R : Type u} (toWord : R → UInt32) (ofWord : UInt32 → R)
  • theorem fillZeros_replicate (target : Nat) :
  • theorem get!_replaceAt (b v : ByteArray) (off i : Nat) (h : off + v.size ≤ b.size) :
  • theorem getAt_eq (b : ByteArray) (k : USize) (hk) (hu) :
  • theorem getElem_decode (b : ByteArray) (i : Nat) (hi : i < (decode (R
  • theorem getElem_decode_eq_get (b : ByteArray) (j : Nat) (hj : j < (decode (R
  • theorem getUInt32LE!_encode (W : Array UInt32) (k : Nat) :
  • theorem getUInt32LE!_eq_ofWordBytes (b : ByteArray) (off : Nat) (h : off + 4 ≤ b.size) :
  • theorem get_encode (a : Array R) (j : Nat) (hj : j < a.size) : get (encode a) j = a[j]
  • theorem get_setAt (b : ByteArray) (k : USize) (x : R) (hk) (hu) (j : Nat) :
  • theorem loadWord_eq (b : ByteArray) (i : USize) (h hu) :
  • theorem ofWordBytes_wordBytes (v : UInt32) : ofWordBytes (wordBytes v) = v
  • theorem read_encode (a : Array R) (off : USize) (i : Nat) (ho : off.toNat = width R * i)
  • theorem replaceAt_empty (b : ByteArray) (off : Nat) (h : off ≤ b.size) :
  • theorem replaceAt_encode (a v : Array R) (i : Nat) :
  • theorem replaceAt_encode_uint32 (W V : Array UInt32) (i : Nat) :
  • theorem replaceAt_replaceAt (b p q : ByteArray) (off : Nat)
  • theorem replaceAt_self (b : ByteArray) (off w : Nat) (h : off + w ≤ b.size) :
  • theorem replicateGo_encode (x : R) : ∀ (n m a : Nat),
  • theorem replicate_eq (n : Nat) (x : R) : replicate n x = encode (Array.replicate n x)
  • theorem setAt_encode (a : Array R) (k : USize) (x : R) (hk) (hu) :
  • theorem setIf_encode (a : Array R) (k : USize) (x : R) (hn : width R * a.size < USize.size) :
  • theorem setIf_encode_of_eq (a : Array R) (k : USize) (x : R) (m : Nat) (hm : m < USize.size)
  • theorem setUInt32LE!_encode (W : Array UInt32) (k : Nat) (v : UInt32) :
  • theorem setUInt32LE!_eq_replaceAt (b : ByteArray) (off : Nat) (v : UInt32)
  • theorem setUInt32LE!_eq_replaceAt_encode (b : ByteArray) (off : Nat) (v : UInt32)
  • theorem set_eq_splice (a : Array R) (i : Nat) (x : R) (hi : i < a.size) :
  • theorem size_decode (b : ByteArray) : (decode (R
  • theorem size_replaceAt (b values : ByteArray) (off : Nat) (h : off + values.size ≤ b.size) :
  • theorem slot_toNat (k : USize) (n : Nat) (hk : k.toNat < n / width R) (hu : n < USize.size) :
  • theorem storeWord_eq (b : ByteArray) (i : USize) (v : UInt32) (h hu) :
  • theorem toNat_add_byte (i : USize) (n d : Nat) (h : i.toNat + 4 ≤ n) (hu : n < USize.size)
  • theorem toUInt8_shiftRight (v : UInt32) (s : UInt32) (k : Nat) (hs : s.toNat = 8 * k)
  • theorem uget_eq_get! (b : ByteArray) (i : USize) (h : i.toNat < b.size) :
  • theorem uset_eq_set! (b : ByteArray) (i : USize) (v : UInt8) (h : i.toNat < b.size) :
  • theorem wordBytes_ofWordBytes (v : Vector UInt8 4) : wordBytes (ofWordBytes v) = v
  • theorem write_encode (a : Array R) (off : USize) (i : Nat) (x : R)
  • theorem zeros_eq (n : Nat) : zeros n = ⟨Array.replicate n 0⟩

CompPoly/Data/Bytes/UInt32.lean (2)

  • @[simp] theorem ByteCodec.width_uint32 : ByteCodec.width UInt32 = 4
  • theorem ByteCodec.toBytes_uint32 (x : UInt32) :

CompPoly/Data/List/BatchInv.lean (15)

  • @[simp] theorem length_batchInv [Inv K] [One K] (xs : List K) :
  • def batchInv [Inv K] [One K] (xs : List K) : List K
  • def batchInvGo : List K → List K → K → List K → List K
  • def prefixProducts (q : K) : List K → List K
  • def productFrom (q : K) : List K → K
  • theorem batchInvGo_map (hmul : ∀ a b, f (a * b) = f a * f b) (xs ps : List K) (acc : K)
  • theorem batchInvGo_spec_reverse (r : List K) (hl : ∀ x ∈ r, x ≠ 0) (q : K) (hq : q ≠ 0)
  • theorem batchInv_eq (xs : List K) (hxs : ∀ x ∈ xs, x ≠ 0) : batchInv xs = xs.map (·⁻¹)
  • theorem batchInv_map [Inv K] [One K] [Inv L] [One L] (hmul : ∀ a b, f (a * b) = f a * f b)
  • theorem length_batchInvGo (xs ps : List K) (hl : xs.length = ps.length) (acc : K)
  • theorem length_prefixProducts (q : K) (l : List K) :
  • theorem prefixProducts_append_singleton (q : K) (l : List K) (x : K) :
  • theorem prefixProducts_map (hmul : ∀ a b, f (a * b) = f a * f b) (q : K) (l : List K) :
  • theorem productFrom_eq (q : K) (l : List K) : productFrom q l = q * l.prod
  • theorem productFrom_map (hmul : ∀ a b, f (a * b) = f a * f b) (q : K) (l : List K) :

CompPoly/Fields/BLS12_377/Fast.lean (1)

  • instance instMont64x4Field : Mont64x4Field BLS12_377.scalarFieldSize where

CompPoly/Fields/BLS12_381/Fast.lean (1)

  • instance instMont64x4Field : Mont64x4Field BLS12_381.scalarFieldSize where

CompPoly/Fields/BN254/Fast.lean (1)

  • instance instMont64x4Field : Mont64x4Field BN254.scalarFieldSize where

CompPoly/Fields/Goldilocks/FastDefs.lean (7)

  • def addRaw (x y : UInt64) : UInt64
  • def foldUInt128Borrow (lo hiHi hiLo : UInt64) : UInt64
  • def foldUInt128Lazy (lo hi : UInt64) : UInt64
  • def invLazy (x : UInt64) : UInt64
  • def mulLazy (x y : UInt64) : UInt64
  • def powLazy (acc x : UInt64) (n : Nat) : UInt64
  • def squareNLazy (x : UInt64) : Nat → UInt64

CompPoly/Fields/Goldilocks/FastReduction.lean (12)

  • private theorem foldTail_cast (lo hiHi hiLo t0 : UInt64) (hhiLo : hiLo.toNat < 2 ^ 32)
  • private theorem wideMul_hi_nat (p00 p01 p10 p11 : Nat) :
  • theorem addRaw_cast {x y : UInt64} (hx : x.toNat < Goldilocks.fieldSize)
  • theorem addRaw_lt {x y : UInt64} (hx : x.toNat < Goldilocks.fieldSize)
  • theorem addRaw_toNat {x y : UInt64} (hx : x.toNat < Goldilocks.fieldSize)
  • theorem foldUInt128Lazy_cast (lo hi : UInt64) :
  • theorem invLazy_cast (x : UInt64) :
  • theorem mulLazy_cast (x y : UInt64) :
  • theorem powLazy_cast (acc x : UInt64) (n : Nat) :
  • theorem shiftLeft32_sub_low (hi : UInt64) :
  • theorem squareNLazy_cast (x : UInt64) (n : Nat) :
  • theorem wideMul_snd_toNat (x y : UInt64) :

CompPoly/Fields/KoalaBear/FastExt4.lean (44)

  • @[inline] def g0 (a : Ext4) : Field
  • @[inline] def g1 (a : Ext4) : Field
  • @[inline] def inv (a : Ext4) : Ext4
  • @[inline] def mul (a b : Ext4) : Ext4
  • @[inline] def norm (a : Ext4) : Field
  • @[inline] def ofBase (x : Field) : Ext4
  • @[inline] def smul (x : Field) (a : Ext4) : Ext4
  • @[inline] def triple (x : Field) : Field
  • @[simp] theorem specEquiv_apply (a : Ext4) : specEquiv a = toSpec a
  • @[simp] theorem toSpec_add (a b : Ext4) : toSpec (a + b) = toSpec a + toSpec b
  • @[simp] theorem toSpec_div (a b : Ext4) : toSpec (a / b) = toSpec a / toSpec b
  • @[simp] theorem toSpec_mul (a b : Ext4) : toSpec (a * b) = toSpec a * toSpec b
  • @[simp] theorem toSpec_neg (a : Ext4) : toSpec (-a) = -toSpec a
  • @[simp] theorem toSpec_ofBase (x : Field) :
  • @[simp] theorem toSpec_one : toSpec 1 = 1

…and 1002 more not listed.

✏️ Affected: 21 declaration(s) (line number changed)
  • theorem addLimbs_toNat (a b : Limbs8) (ha : a.Bounded) (hb : b.Bounded) : in CompPoly/Fields/Montgomery/Native64x8.lean moved from L278 to L319
  • theorem add_bounded (q a b : Limbs8) : (add q a b).Bounded in CompPoly/Fields/Montgomery/Native64x8.lean moved from L372 to L422
  • @[inline] def add (q a b : Limbs8) : Limbs8 in CompPoly/Fields/Montgomery/Native64x8Defs.lean moved from L149 to L178
  • @[specialize] def gcdFinalChunks (q : Limbs8) (negInv : UInt64) (finalRounds : Nat) in CompPoly/Fields/Montgomery/Native64x8InvDefs.lean moved from L194 to L215
  • @[specialize] def gcdInvCandidate (modulus : Nat) [P : GcdData modulus] (q : Limbs8) in CompPoly/Fields/Montgomery/Native64x8InvDefs.lean moved from L207 to L230
  • @[specialize] def gcdMainLoop (q : Limbs8) (negInv : UInt64) (rounds : Nat) (a u b v : Limbs8) in CompPoly/Fields/Montgomery/Native64x8InvDefs.lean moved from L174 to L193
  • def towerScalarTasks : List BenchTask in bench/CompPolyBench/Fields/Binary/Tower/Scalar.lean moved from L93 to L101
  • private def runScalarInv (modulus : Nat) [Mont64x4Field modulus] [GcdData modulus] in bench/CompPolyBench/Fields/Montgomery.lean moved from L32 to L33
  • private def a : Limbs4 in tests/CompPolyTests/Fields/Pasta/Fast.lean moved from L31 to L31
  • private def b : Limbs4 in tests/CompPolyTests/Fields/Pasta/Fast.lean moved from L35 to L34
  • private def montA : Limbs4 in tests/CompPolyTests/Fields/Pasta/Fast.lean moved from L39 to L37
  • private def montAB : Limbs4 in tests/CompPolyTests/Fields/Pasta/Fast.lean moved from L47 to L43
  • private def montB : Limbs4 in tests/CompPolyTests/Fields/Pasta/Fast.lean moved from L43 to L40
  • private def negInv : UInt64 in tests/CompPolyTests/Fields/Pasta/Fast.lean moved from L25 to L25
  • private def pMontA : Limbs4 in tests/CompPolyTests/Fields/Pasta/Fast.lean moved from L84 to L79
  • private def pMontAB : Limbs4 in tests/CompPolyTests/Fields/Pasta/Fast.lean moved from L92 to L85
  • private def pMontB : Limbs4 in tests/CompPolyTests/Fields/Pasta/Fast.lean moved from L88 to L82
  • private def pNegInv : UInt64 in tests/CompPolyTests/Fields/Pasta/Fast.lean moved from L82 to L77
  • private def pq : Limbs4 in tests/CompPolyTests/Fields/Pasta/Fast.lean moved from L80 to L75
  • private def q : Limbs4 in tests/CompPolyTests/Fields/Pasta/Fast.lean moved from L23 to L23
  • private def rMod : Limbs4 in tests/CompPolyTests/Fields/Pasta/Fast.lean moved from L27 to L27

sorry Tracking

  • No sorrys were added, removed, or affected.

Coverage Notes

  • AI file summarization partially analyzed 2 file(s) because their individual diffs exceeded the per-file size budget. Statistics and Lean signal tracking still cover the full PR.
Partially Analyzed Files
  • CompPoly/Univariate/NTTFast/ColumnsCorrectness.lean (+1499/-0)
  • docs/wiki/benchmarking.md (+476/-4)
* Additional-instructions analysis was skipped because the full diff exceeded the analysis size budget, and partial results would be misleading.

Last updated: 2026-10-13 02:52 UTC.

…the reference

`interpolateCosetPacked` takes Plonky3's row-major matrix of Montgomery words in a `ByteArray`:

- 1/(z - x) = (b0 + t b1 + t^2 b2 + t^3) / N(t) for t = z0 - x, so each row's weight is four
  base-field values and only the base-field norms need inverting;
- blocks of 256 rows: a forward pass stores nodes, t^2, norms and prefix products, one
  inversion per block, and a backward pass overwrites each row with its weights;
- column sums accumulate unreduced 64-bit products with one conditional subtraction each and one
  Montgomery reduction per block, four columns sharing each row's weights;
- row ranges run on parallel tasks, the caller taking the lowest.

`interpolateCosetPacked_eq` proves the result equal to `interpolateCoset` on the decoded words for
every point off the coset; the native smoke test compares the two on 38 shapes.
Interleaved runs over 2^12..2^20 rows and 1..16 columns: 512 rows beat 256 by up to 8% and
1024 by up to 20%; only 2^16 x 16 prefers 128. Eight tasks stay ahead of sixteen, which
oversubscribe the cores by 1.4x.
Fixtures hold canonical values; the packed path reads Montgomery words, so the runner packs the
decoded matrix before timing, as Plonky3 builds its matrix from the same values.
…rray word accessors

The packed NTT now reads and writes words with `ByteArray.ugetUInt32LE` and
`ByteArray.usetUInt32LE` from lean4#14053, backported in `CompPoly.Data.ByteArray.Pack` with the
same names, signatures, Lean definitions and lemmas. `Native.readRaw` and the `Native.storeWords`
extern are gone: `storeWords` is a Lean batch of word stores, appends grow the buffer with
`ByteArray.copySlice`, and checked reads are `readAt`. `storeWords_eq` and the read lemmas prove
them equal to the previous storage semantics, so the refinement proofs carry over.

The backport's C differs from the PR's: loads and stores use a fixed-size `memcpy`, and a store
states that the array is still unshared, so a run of stores keeps one sharing check. With the
PR's byte-shift C the million-point transform took 79 ms instead of 23 ms; without the
assumption, 23 ms instead of 13.

Range checks run on machine words; `storeWords` reads the append offset before the growth
consumes the buffer, which would otherwise copy it on every batch.
The binary split tree, the sliced pair tree, the transpose and the reversed leaves now store
each 16-word batch at a cursor into a buffer pre-sized with zeros, instead of growing the
output by appending. Zero buffers are filled by appending copies of a separate 4 KiB zero
block into a reserved destination, so the destination stays exclusive.
The transpose extends its output by one zero block right before each tile overwrites it, so
the zeros are still in cache. encode overwrites a zero buffer word by word instead of appending
one word at a time; this restores the small transforms that end in an encode.
Port the packed coset interpolation from the removed readRaw extern to the
backported ByteArray.getUInt32LE! accessor.
storeWords loses its append mode, which only push, push16 and the old
generate used. generate now overwrites a zero buffer word by word, and
encode is generate over the field array. Removed: push, push16, zeroBytes,
the natural-index write16, Storage.wordAt and lemmas no longer referenced.
The native smoke test keeps only the accessor checks: storeWords and read
are Lean definitions proved against Storage.pack.
# Conflicts:
#	bench/README.md
#	tests/CompPolyNativeSmoke.lean
interpolateCosetPacked is Lean over the backported word accessors and is
proved equal to the reference by interpolateCosetPacked_eq; the accessors'
inline C keeps its compiled checks. The removed checks spawned tasks, whose
1 GiB stack reservations exceeded the smoke test's 4 GiB address-space limit.
PackedRepr R stores every element in width bytes, with toBytes/ofBytes
as the specification and fast read/write at byte offsets proved equal to
it; PackedRepr.encode/decode view a ByteArray as an Array R. UInt32 and
every Native32 FastField are one-word instances on the backported
ByteArray word accessors.

The packed NTT's Storage.pack and packFields become PackedRepr.encode at
UInt32 and at the field, and the executable encoder is
Native.encodeFields. The column-parallel NTT drops Word32Repr and the
byte-by-byte ByteWords layer for PackedRepr.getAt/setAt with element-count
invariants, so it is generic over the element width. Zero buffers are
ByteArray.zeros. Data/Bytes/Words.lean and Packed/Storage.lean are gone.
Move the packed interpolation proofs to PackedRepr.encode.
CompPoly is extern-free by design; only the packed NTT kernels opt into the
backported ByteArray word accessors. The one-word PackedRepr instance read
and wrote through those accessors, which put them under the extern-free
column-parallel NTT too. It now loads and stores the four bytes with
ByteArray.uget/uset, proved equal to the accessors' Lean definitions, and
the column-parallel NTT runs at its speed before the PackedRepr refactor.
Plan.forward/inverse took field arrays through a separate input path:
field-array split kernels, their models and refinement proofs, and a
field-input task tree. Consumers hold packed words (PackedRepr.encode, or
Native.encodeFields and Native.unpack for fast conversion), so the plan now
exposes only forwardPacked/inversePacked. The packed decode path shared by
small and custom-depth transforms is Native.runDecoded.

The benchmark's field-array variant goes with the API; packed-io remains.
NaturalPlan.forwardPacked/inversePacked take and return packed elements
(PackedRepr), like the packed plan; forwardPacked_eq and inversePacked_eq
prove forwardPacked (encode a) = encode (forward a) and the same for the
inverse.

Even sizes from 2^16 run Columns.transformPacked: column tasks decode their
own columns from the packed buffer, and sixteen tasks each interleave a
contiguous range of rows into packed words, joined by one copy. The proofs
reuse the array transform: a generic simulation lemma over the interleave
loop relates packed stores to array stores and offset chunks to slices of
the whole interleave. Other sizes unpack with Columns.decodeAll, run the
array transform and pack with Columns.encodeAll.

PackedRepr gains replicate (filled by doubling copies) and setIf, a guarded
store whose machine-size bound is proved once per buffer. The benchmark's
variants are now packed and externless, both with packed words in and out.
The column transform now handles odd sizes: a leaf finishes with the radix-two
layer (lastPair), which commutes with cutting the array into leaves. So every
size from 2^15 with more than one worker takes the column path, and smaller
transforms or a single worker run the sequential transform as one leaf. The
segment path for odd sizes (Parallel.chunksTask) and the inverse pass lists it
needed are gone.

Odd sizes on several workers get 2-4x faster (2^19 on 8 workers: 17.7 ms to
4.2 ms); other shapes are unchanged. The NTT suite validates 2^15 and 2^17
against Plonky3.
The packed plan now runs the extern-free plan's pipeline on packed
words: from 2^15 elements and with more than one worker, column tasks
gather their columns and run the top two radix-four passes with
`inner16`, sixteen leaf tasks finish with `leafNatural`, and
`naturalLeaves` interleaves; otherwise one leaf. `Packed/Columns.lean`
holds the pipeline, `Packed/ColumnsCorrectness.lean` its proof, which
reuses the extern-free column and leaf lemmas.

The split tree, slice tree, tiled decoder, `Plan.defaultDepth` and the
in-stage normalization they needed are deleted. The spec files that
remain are merged into `Packed/DifSpec.lean`. Both plans use sixteen
column tasks at every worker count, which keeps a task's rows and
twiddles in L2 with four workers.

Forward, ms, previous packed plan → this: CPUs 0-7 2^12 0.079 → 0.020,
2^16 0.410 → 0.246, 2^20 2.53 → 2.42; CPUs 0-3 2^18 0.687 → 0.672,
2^20 3.54 → 3.23; CPU 0 2^20 10.8 → 7.69.
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