Skip to content

Blake3 circuit optimizations + Aiur inlined function calls#497

Closed
arthurpaulino wants to merge 4 commits into
mainfrom
ap/blake3-distill
Closed

Blake3 circuit optimizations + Aiur inlined function calls#497
arthurpaulino wants to merge 4 commits into
mainfrom
ap/blake3-distill

Conversation

@arthurpaulino

Copy link
Copy Markdown
Member

Distills the blake3 work from ap/aiur-hash-width onto main: the @-inlining
compiler feature plus three blake3 circuit optimizations. Excludes the
chain-rotr gadget generalization (blake3 only needs the existing fixed
u8_chain_rotr4/u8_chain_rotr7 ops), the keccak/sha256 work, and the bench
harness changes.

Commits

  1. Aiur: inlined function calls (@fn(args))

    Replaces Source.Term.app's unconstrained : Bool with a CallMode
    (normal | unconstrained | inlined). @fn(args) marks an inlined call:
    Toplevel.inlineCalls, run first in compile, splices the callee's body
    into the caller. An inlined call compiles to no separate circuit and no
    call interface — the callee's work joins the caller's straight-line
    circuit. The mode only exists in the Source stage; evaluation is unchanged.

  2. Aiur blake3: extract G-function word-ops into @-inlined helpers

    blake3_g_function was ~100 lines of inlined byte arithmetic. Rewritten
    as its eight-step dataflow over @-inlined word ops: ByteStream's
    existing u32_add / u32_xor, plus four right-rotations
    u32_rotr16/u32_rotr8 (byte moves) and u32_rotr12/u32_rotr7
    (byte move + chained gadget), added to ByteStream next to the other
    unsigned-int ops. Still one circuit, byte-identical work; FFT unchanged.

  3. blake3: fuse the G-function's 3-word adds into one carry pass

    u32_add3 (ByteStream) accumulates each byte column with chained u8_add
    and folds the 0/1 carry bits with a free field add: 11 u8_adds vs 14 for
    two chained adds. G-function lookups 63 → 57, width 224 → 206.

  4. blake3: self-recursive compress rounds

    blake3_compress_inner_j recurses on the round index, permuting the
    message half between rounds and returning after round 6, so
    blake3_compress pays one 128-output call site instead of seven
    (1118 → 338 columns). Includes the regenerated Aiur kernel
    (lake exe ix codegen) and the 56 re-pinned kernel-check FFT costs.

Results

lake exe ix check Nat.add_comm (blake3 drives every kernel-check's hashing):

main this branch delta
Total width 35,705 34,964 −2.1%
Total FFT cost 53,748,034 52,451,949 −2.4%

All 56 pinned kernel-check FFT costs re-measured from a full
lake test -- --ignored ixvm run (suite green, codegen/bytecode parity
included).

Replace `Source.Term.app`'s `unconstrained : Bool` with a `CallMode`
(normal | unconstrained | inlined). `@fn(args)` marks an inlined call:
`Toplevel.inlineCalls`, run first in `compile`, splices the callee's
body into the caller — each input Local bound to its argument via a
let, the (recursively inline-expanded) body as the value. Inlining is
forbidden for callees that are transitively inline-recursive.

An inlined call compiles to no separate circuit and no call interface
(no lookup, no output columns): the callee's work joins the caller's
straight-line circuit. This lets a hot inner routine be written once
and expanded into a single wide circuit, instead of paying the
per-call state-rematerialization and the taller callee-circuit height.

The mode only exists in the Source stage — `inlineCalls` eliminates
`.inlined` before typechecking, and Check maps normal->false /
unconstrained->true into the existing downstream bool. Interpreters
match the mode as `_` (an inlined call executes identically to a
normal one), so evaluation is unchanged.

The pass expands bottom-up over the inline-call DAG (Kahn topo order),
so it is fuel-free and total. At each inline site the callee's inputs
and body are alpha-renamed to fresh `inl#N` locals before argument
binding, so nested `@`-calls cannot capture caller bindings; spliced
let-chains are hoisted out of strict argument positions (array
elements, operator operands, app arguments), which lowering would
otherwise reject. Also drops a `simp` arg in `compile_stages_of_ok`
that the final pipeline shape no longer needs.
blake3_g_function was ~100 lines of inlined byte arithmetic: six
little-endian carry chains, the add/xor/rotate quarter-round steps
spelled out per byte. Rewrite it as its eight-step dataflow over
`@`-inlined word ops: ByteStream's existing u32_add / u32_xor, plus
the four right-rotations u32_rotr16/u32_rotr8 (byte moves) and
u32_rotr12/u32_rotr7 (byte move + chained gadget), added to
ByteStream next to the other unsigned-int ops. Still one circuit,
byte-identical work.

Probe (1 hash, 2048 bytes, non-periodic LCG): 6,575,392 FFT,
unchanged. `aiur-hashes` blake3 cases green (execute/claim/prove-verify).
Each `blake3_g_function` does two 3-word additions (a + b + x and
a + b + y) as chained `u32_add`s. `u32_add3` accumulates each byte
column with chained `u8_add` and folds the 0/1 carry bits with a free
field add: 11 u8_adds vs 14 for two chained adds. Column carry is
floor(sum/256) <= 2.

blake3_g_function lookups 63 -> 57, width 224 -> 206; blake3_bench total
FFT 6,575,392 -> 6,214,420 (-5.5%). Digests verified against the Rust
reference across chunk/block-boundary sizes.
blake3_compress_inner_j recurses on the round index, permuting the
message half between rounds and returning after round 6, so
blake3_compress pays one 128-output call site instead of seven
(1118 -> 338 columns).

Regenerate the Aiur kernel (`lake exe ix codegen`) and re-pin all 56
kernel-check FFT costs from a full `lake test -- --ignored ixvm` run
(all green). Nat.add_comm: 53,748,034 -> 52,451,949 total FFT vs main.
@arthurpaulino

Copy link
Copy Markdown
Member Author

!benchmark aiur-recursive

@argument-ci-bot

Copy link
Copy Markdown

!benchmark — main vs 6c04bae

backends: aiur-recursive · envs: InitStd · set: primary · shard: 0

aiur-recursive · InitStd — main from: bencher @ 45092d8

proof recursive-prove-time (main) recursive-prove-time (PR) Δ% recursive-peak-ram (main) recursive-peak-ram (PR) Δ% recursive-proof-size (main) recursive-proof-size (PR) Δ% recursive-verify-time (main) recursive-verify-time (PR) Δ% recursive-execute-time (main) recursive-execute-time (PR) Δ% recursive-fft-cost (main) recursive-fft-cost (PR) Δ% prove-time (main) prove-time (PR) Δ% proof-size (main) proof-size (PR) Δ% verify-time (main) verify-time (PR) Δ% peak-ram (main) peak-ram (PR) Δ%
factorial-q100-b2 30.975 s 31.242 s +0.9% 74.67 GiB 74.35 GiB -0.4% 15.84 MiB 15.31 MiB -3.3% 🟢 112.0 ms 105.9 ms -5.5% (1.06× faster) 🟢 8.987 s 8.967 s -0.2% 17.86B 17.97B +0.6% 197.9 ms 240.2 ms +21.4% (1.21× slower) ⚠️ 848.61 KiB 848.61 KiB +0.0% 6.4 ms 6.4 ms +0.3% 413.64 MiB 416.13 MiB +0.6%
square-q100-b1 22.404 s 23.096 s +3.1% ⚠️ 43.55 GiB 43.16 GiB -0.9% 15.76 MiB 15.23 MiB -3.3% 🟢 95.6 ms 104.1 ms +9.0% (1.09× slower) ⚠️ 7.605 s 8.125 s +6.8% (1.07× slower) ⚠️ 15.36B 15.43B +0.4% 170.0 ms 198.1 ms +16.5% (1.17× slower) ⚠️ 782.05 KiB 782.05 KiB +0.0% 5.5 ms 5.6 ms +2.2% 309.19 MiB 326.14 MiB +5.5% (1.05× larger) ⚠️

2 proofs · 2 with regressions · 2 with improvements (|Δ| > 3.0% on any metric).

Workflow logs

@samuelburnham

Copy link
Copy Markdown
Member

!benchmark

@argument-ci-bot

argument-ci-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

!benchmark — main vs 6c04bae

backends: aiur=prove · envs: InitStd · set: primary · shard: 0

aiur · InitStd · prove — main from: base run @ a75cb04 (not on bencher)

constant prove-time (main) prove-time (PR) Δ% throughput (const/s) (main) throughput (const/s) (PR) Δ% peak-ram (main) peak-ram (PR) Δ% execute-time (main) execute-time (PR) Δ% verify-time (main) verify-time (PR) Δ% proof-size (main) proof-size (PR) Δ% fft-cost (main) fft-cost (PR) Δ%
Array.extract_append 55.513 s 58.356 s +5.1% (1.05× slower) ⚠️ 30.730 29.230 -4.9% (1.05× slower) ⚠️ 92.52 GiB 91.62 GiB -1.0% 12.978 s 13.023 s +0.3% 227.5 ms 247.0 ms +8.6% (1.09× slower) ⚠️ 34.43 MiB 33.90 MiB -1.5% 39.96B 39.62B -0.8%
ByteArray.utf8DecodeChar?_utf8EncodeChar_append 57.297 s 58.354 s +1.8% 50.440 49.530 -1.8% 100.44 GiB 98.60 GiB -1.8% 11.966 s 12.300 s +2.8% 238.4 ms 223.0 ms -6.4% (1.07× faster) 🟢 34.43 MiB 33.90 MiB -1.5% 42.29B 41.52B -1.8%
Char.ofOrdinal_le_of_le 45.008 s 46.057 s +2.3% 63.590 62.140 -2.3% 75.26 GiB 73.92 GiB -1.8% 9.153 s 9.108 s -0.5% 226.7 ms 239.9 ms +5.8% (1.06× slower) ⚠️ 34.43 MiB 33.90 MiB -1.5% 32.54B 31.97B -1.8%
Vector.extract_append._proof_2 32.424 s 33.817 s +4.3% ⚠️ 44.440 42.610 -4.1% ⚠️ 53.19 GiB 52.67 GiB -1.0% 6.991 s 7.149 s +2.3% 215.7 ms 232.2 ms +7.6% (1.08× slower) ⚠️ 34.35 MiB 33.82 MiB -1.5% 23.29B 23.05B -1.0%
_private.Init.Data.Range.Polymorphic.SInt.0.Int64.instRxcHasSize_eq 27.034 s 27.426 s +1.4% 72.980 71.940 -1.4% 48.20 GiB 47.26 GiB -1.9% 4.431 s 4.559 s +2.9% 213.8 ms 241.2 ms +12.8% (1.13× slower) ⚠️ 34.35 MiB 33.82 MiB -1.5% 17.35B 17.01B -2.0%
String.split 25.902 s 26.440 s +2.1% 75.240 73.710 -2.0% 46.56 GiB 45.67 GiB -1.9% 4.150 s 4.193 s +1.0% 214.8 ms 230.9 ms +7.5% (1.07× slower) ⚠️ 34.35 MiB 33.82 MiB -1.5% 15.89B 15.59B -1.9%
List.mergeSort 17.432 s 18.209 s +4.5% ⚠️ 91.670 87.760 -4.3% ⚠️ 29.50 GiB 29.16 GiB -1.1% 2.804 s 2.882 s +2.8% 211.4 ms 243.6 ms +15.2% (1.15× slower) ⚠️ 34.35 MiB 33.82 MiB -1.5% 11.58B 11.34B -2.0%
Vector.append 5.316 s 5.740 s +8.0% (1.08× slower) ⚠️ 106.470 98.600 -7.4% (1.08× slower) ⚠️ 8.77 GiB 8.67 GiB -1.2% 535.8 ms 537.7 ms +0.4% 221.5 ms 213.5 ms -3.6% 🟢 34.12 MiB 33.59 MiB -1.5% 2.58B 2.54B -1.9%
Nat.gcd_comm 4.355 s 4.542 s +4.3% ⚠️ 95.290 91.360 -4.1% ⚠️ 6.89 GiB 6.94 GiB +0.7% 354.3 ms 369.1 ms +4.2% ⚠️ 215.0 ms 223.3 ms +3.9% ⚠️ 34.12 MiB 33.59 MiB -1.5% 1.75B 1.71B -2.4%
String.append 2.949 s 3.265 s +10.7% (1.11× slower) ⚠️ 119.700 108.130 -9.7% (1.11× slower) ⚠️ 4.38 GiB 4.19 GiB -4.2% 🟢 188.7 ms 198.9 ms +5.4% (1.05× slower) ⚠️ 210.8 ms 214.4 ms +1.7% 34.05 MiB 33.52 MiB -1.5% 963.43M 941.35M -2.3%
Int.gcd 2.424 s 2.487 s +2.6% 94.490 92.060 -2.6% 3.10 GiB 3.01 GiB -3.1% 🟢 130.0 ms 128.5 ms -1.1% 207.8 ms 220.8 ms +6.3% (1.06× slower) ⚠️ 34.05 MiB 33.52 MiB -1.5% 604.12M 590.24M -2.3%
Nat.sub_le_of_le_add 2.251 s 2.380 s +5.8% (1.06× slower) ⚠️ 84.410 79.820 -5.4% (1.06× slower) ⚠️ 2.85 GiB 2.80 GiB -1.6% 98.4 ms 98.4 ms -0.0% 208.1 ms 217.7 ms +4.6% ⚠️ 34.05 MiB 33.52 MiB -1.5% 509.28M 497.81M -2.3%
Nat.add_comm 1.098 s 1.236 s +12.5% (1.12× slower) ⚠️ 46.430 41.280 -11.1% (1.12× slower) ⚠️ 1.27 GiB 1.27 GiB +0.1% 13.2 ms 13.7 ms +4.4% ⚠️ 204.1 ms 224.7 ms +10.1% (1.10× slower) ⚠️ 33.91 MiB 33.39 MiB -1.6% 53.75M 52.45M -2.4%
Std.Tactic.BVDecide.BVExpr.bitblast.goCache_Inv_of_Inv._mutual OOM OOM n/a OOM OOM n/a OOM OOM n/a 1m 21.1s 1m 22.2s +1.3% OOM OOM n/a OOM OOM n/a 260.46B 257.02B -1.3%

14 constants · 12 with regressions · 4 with improvements (|Δ| > 3.0% on any metric).

Workflow logs

@arthurpaulino
arthurpaulino deleted the ap/blake3-distill branch July 17, 2026 23:15
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