Corgi columnar lowerings - #831
Merged
Merged
Conversation
Ports the B4 benchmark and its samply target from fable/work, where they were written but never landed: list build with arithmetic, flatmap explosion, variant tag + case + fold per element, then a tiny min reduce (16 keys, so the reduce is not the subject). A closed-form oracle checks vec, corgi checks against vec, and a hand-written native twin checks against the oracle. The program is chosen to sit entirely on `apply_ops`' row-wise fallback path, which is what it measures. On master-next today (arm64, mimalloc): n=1m native 1.08s vec 6.56s (6.08x nat) corgi 5.96s (5.52x nat, 0.91x vec) n=4m native 4.45s vec 32.45s (7.29x nat) corgi 25.70s (5.77x nat, 0.79x vec) Profile at n=1m: apply_ops is 55.9% of the run, of which ir::eval 27.1%, from_updates 18.0%, into_updates 5.5% — the untranscode/eval/re-transcode round trip is 50.6% of total. Value::clone (12.8%) plus drop_in_place<Value> (10.9%) is 23.7% spent on DValue trees that exist only for the fallback. corgi::eval_graph draws zero samples: every map declines. `Term::List` has no lowering, and the decline propagates up through Fold/Inject/If/Case, so the general Case lowering from #811 never engages on this program. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YH7iq9JoXmf7ATpq1gZaQS
A homogeneous `list(e0, .., ek)` becomes k columns and a length-k list per row
through corgi's existing kernel matrix, with no per-row work and no new corgi
op: `Enlist` each element (a length-1 lane per row), `Iota` a per-row [0..k)
tag list, `Weave` interleaves the lanes in field order into List<Sum{X x k}>,
and `MapList(Unwrap)` strips the now-homogeneous sum. Empty and heterogeneous
literals decline (Weave needs a lane, Unwrap needs the lanes to join) and rows
handle them.
`infer_term_shape` gains the matching arm, which is what makes `fold` over a
literal work: the Fold lowering reads the list's shape to find its element
shape, so without it a `fold(list(..), ..)` declined even though both halves
were already written.
That is the keystone property in general -- a `list(..)` subterm anywhere made
the WHOLE projection fall back to rows, which is why ast's case and fold
lowerings (landed in #811) had never once engaged. B4 at n=1m: eval_graph goes
from 0 samples to 59, and corgi 0.91x -> 0.85x vec.
`compilable` still answers false for `List`, like `Case`: it is the shape-free
gate for join-INLINE projections and cannot judge homogeneity. Its doc said
these were unwritten lowerings; it now says what the gate is actually for.
Gate 14/14, 29 lib tests, B4 checked against its oracle at every size.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YH7iq9JoXmf7ATpq1gZaQS
The `If` lowering refused any sum-shaped result, on the grounds that merging sum columns committing different lanes tripped an offset bug in the pinned engine's lane merge. The comment said "revisit at the next corgi pin bump". The bump happened: #817 moved the pin from c4626fc to cb26fbd, whose sole commit IS the fix -- "gather_lanes: commit every lane any source does, not source 0's (#10)", with a regression test for the exact differing-arity case. `Op::Select` is implemented by `gather_lanes`, so the gate has been dead weight since Aug 11. It was not a cheap gate. `if(c, Fwd(x), Bwd(y))` is the shape of every conditional constructor, so a `case` over one fell back to rows -- which, together with the list-literal gap, is why B4's whole compute chain ran row-wise. B4 (n, corgi vs vec, checked against the oracle at each size): 100k 0.94x -> 0.54x 1m 0.91x -> 0.59x 4m 0.79x -> 0.54x Against the compiled-DD twin, 5.5-5.8x -> 3.6-3.9x. Profile at n=1m, share of total: apply_ops 55.9% -> 32.6%, ir::eval 27.1% -> 0.5%, from_updates 18.0% -> 11.8%, into_updates 5.5% -> 2.2%, eval_graph 0% -> 13.7%. The untranscode/eval/retranscode round trip falls from 50.6% to 14.5%, and what remains is flatmap, which is still row-wise. Gate 14/14 (adt, case_ops, sum_ops, sum_skew, sum_skew_compiled and tour all exercise sums), 29 lib tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YH7iq9JoXmf7ATpq1gZaQS
`compile_flatmap` lowers the list term over [key, val] to a corgi List column,
declining when it will not lower or is not list-shaped (the explode needs real
bounds, where `ir::eval` would accept any List value it happened to produce).
The backend then explodes that column WITHOUT moving any element: the list's
flat element storage already IS the new value column, and each row's span in
the bounds yields both the within-row position DDIR pairs with it and a repeat
map that carries key/time/diff across. No per-row eval, no transcode. The
row-wise path moves to `apply_flatmap_rows` and stays the fallback.
B4 (n, corgi vs vec, checked against the oracle at each size):
100k 0.54x -> 0.42x 1m 0.59x -> 0.48x 4m 0.54x -> 0.45x
Against the compiled-DD twin, 3.6-3.9x -> 3.0-3.2x.
That finishes the row-wise fallback on this program. Profile at n=1m, share of
total, from where this branch started:
apply_ops 55.9% -> 17.2% ir::eval 27.1% -> ~0%
eval_graph 0% -> 16.0% from_updates 18.0% -> 1.2%
into_updates 5.5% -> ~0%
apply_ops is now essentially eval_graph alone, and the remaining `from_updates`
is the `ToCorgi` import unary -- the I/O boundary, which is where transcoding
belongs. `Value::clone` + `drop_in_place<Value>` fall from 23.7% to 2.0%.
Gate 14/14, with unnest and tour verified to drive the columnar path rather
than the fallback (instrumented run: zero fallbacks across the gate).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YH7iq9JoXmf7ATpq1gZaQS
…lace `enter_at`'s key and val columns are the identity -- only times change -- but the row-wise path untranscoded the WHOLE container to get at one integer per row. `compile_scalar` now lowers the delay field to a `U64` column (declining when it is not `Prim`-shaped, since the delay has no per-row reading then), and the times are adjusted in place. The delta never has to be built: joining `Product(0, PointStamp([0,..,delay]))` is coordinate-wise `max` at index `level-1` and identity elsewhere, because 0 is u64's minimum. `PointStamp::new` re-strips the trailing minimums the resize can add, so a zero delay leaves the representation canonical. `compile_flatmap`/`compile_scalar`/`compile_predicate` now share one `compile_over_kv` -- they differed only in the shape they demand of the result. As sized earlier this is coverage, not speed: enter_at's input collection changes once per epoch, so it is not per-iteration hot. Verified engaged rather than assumed -- an instrumented run shows the gate taking the columnar branch twice (scc.ddp's two enter_at sites) and the fallback zero times. scc agrees with vec beyond the gate's tiny inputs: 500/1500 -> 1319/-34 on both, and 5000/15000 -> 13281/-39 on both (corgi 348ms vs vec 410ms). Gate 14/14, 29 lib tests, 10 explain tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YH7iq9JoXmf7ATpq1gZaQS
`LinearOp::LiftIter` is synthesized by the explanation rewrite -- every `$host:` export is a LiftIter Linear, and it panics if it appears in a user program -- so an explained program is the only thing that exercises it. tests/explain.rs ran on `vec` alone, leaving the entire explain surface unexercised on corgi. These tests render the SAME rewritten dataflow on both backends and require every export to agree, which is the coverage a columnar LiftIter needs to be safe to write. Seven agree: reach, tc, flatmap, collect, a depth-1 `enter_at` loop, a depth-1 `min` loop, and an SCC-shaped program (depth-2 nesting, min, three joins, filtered feedback). Three do not, and the divergence PREDATES this branch -- reproduced at 78d75b0 in a scratch worktree, so it is neither the list work nor the columnar enter_at. On an explained program whose iterative feedback is NEGATED, corgi reports a strict SUBSET of vec's `demand:input0` (2 rows of 10 on the small scc instance). Isolated to the negation alone: `corgi_agrees_on_explained_scc_one_scope` and `explained_scc_one_scope_negated` differ in exactly one line (`var trim = trim_fwd | filter(..)` vs `var trim = trim_fwd - edges`) and only the negated one fails. Ruled out: `enter_at` (the depth-1 delay loop agrees, and forcing the row-wise enter_at path changes nothing), `min`, and depth-2 nesting on its own. The three are `#[ignore]`d with that reason rather than deleted, and the one-line pair is kept as the minimal reproducer. Note the plain (unexplained) programs do NOT diverge -- the corgi gate's scc passes, and scc at 5000/15000 matches vec exactly -- so the bug needs both the negated feedback and the rewrite. `query_rows` is factored out of `demand_for_queries` for reuse. Explain suite: 17 passed, 7 ignored (3 new known-divergence + 4 pre-existing sweeps). Gate 14/14, 29 lib tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YH7iq9JoXmf7ATpq1gZaQS
The inverse of EnterAt. `LiftIter` reads each row's iteration coordinate out of its time and appends it to the value, so keys, times and diffs are untouched and no term is compiled -- which makes this path TOTAL. There is no gate and no fallback, unlike every other columnar op here. The empty product is where the two representations part company, and it is not a corner case: DDIR unit IS `Tuple([])`, which `append_iter` extends to `Tuple([iter])`, but columnar it arrives as `CValue::Unit`, not an empty `Prod`. Emitting `Prod([Unit, iter])` would be a silent one-field-too-many divergence from `backend::vec`, so `Unit` maps to `Prod([iter])`. The explain suite hits that branch 38 times (and the `Prod` branch 128) at the previous commit's coverage, so this is checked rather than argued. `append_iter` goes with it -- corgi has no remaining row-wise caller, and `backend::vec` keeps its own. Explain 17 passed / 7 ignored, gate 14/14, 29 lib tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YH7iq9JoXmf7ATpq1gZaQS
Nothing ever required the two backends' hash to be a PARTICULAR function -- only
that they agree, since `hash` is an observable program value that lands in keys
and gets exported. Rust makes no cross-run promise for `Hash` either. So DDIR's
`hash` becomes corgi's structural hash, and `ir::eval` follows corgi rather than
the reverse: `ir::structural_hash` is a row-at-a-time transcription of
`corgi::hash`'s fold, salts and all, pinned by five tests that hash the same
values through both paths (scalars, tuples, units, lists, variants, nesting).
`ir::eval` no longer calls `hash_u64`, which goes back to being what it is --
the benchmark row generator's mixer, unrelated to the language.
The lowering is `Op::Hash` over the arguments as one tuple, `Shr(1)` for the
sign bit, `Rem` by the bound. The bound guard needs no `Select`: `Rem`'s total
`x % 0 = x` gives `bound == 0` the identity, and a NEGATIVE bound reads as a u64
at or above 2^63 -- above the shifted hash -- so it is the identity too. Both
match `ir::eval`'s `if bound > 0` exactly. `compilable` can admit `hash`
shape-free, since `Op::Hash` folds whatever structure it is handed.
REQUIRES corgi's `BinOp::Rem` (~/Projects/WIP/corgi, branch `ddir-rem`, commit
2221578, NOT pushed). The pin here is a local path so this branch is buildable
and testable; it must become a rev before this merges.
The two gates keyed to hash being unlowerable are gone, per Frank: a test that
asserts a thing we intend to fix is the bug.
* `join_fallback.ddp` used `hash` purely as an unlowerable term. It now uses a
`case`, which drives the SAME path for a permanent reason: `compilable` is
the shape-free gate, it runs before any container exists, and it will always
decline shape-dependent terms. Verified still driving it (instrumented: one
ROWFALL join, and no project fallback after, so the rebased projection
compiles columnar).
* `sum_skew.ddp` and `sum_skew_compiled.ddp` were the same program twice, one
wrapped in `hash` to force the row-wise path. They now compile identically,
so they are one program: the surviving `sum_skew.ddp` keeps the property
(differing Sum arity reconciled through ⊥ lanes) and the accounting of both
derivations. Gate 14 -> 13.
Audit: instrumenting every fallback and running the whole suite leaves exactly
two, both correct declines rather than gaps -- a `filter` whose `Case` arms have
genuinely conflicting shapes (Prim vs Prod), and a `project` whose `Case` arm is
a `Fold` whose step reads `Bound(2)`, past the closed body's env. No term now
falls back for want of a lowering.
B4 unmoved at 0.50x vec. 34 lib, 13 gate, 17 explain.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YH7iq9JoXmf7ATpq1gZaQS
`frankmcsherry/wip` @ 2221578 ("Integer Rem, with a total zero divisor"),
branched off the previously pinned cb26fbd. Replaces the local-path pin the
previous commit used to be testable before the push.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YH7iq9JoXmf7ATpq1gZaQS
`Distinct` and `Min` selected the values they present with `d > 0`. DD's `reduce` presents every value whose accumulation is non-zero, negatives included, and `backend::vec` -- the correctness reference -- acts on exactly what DD hands it: its `Distinct` pushes `1` without looking at the diffs at all, and its `Min` takes `vals.iter().map(|(v, _)| v).min()` over all of them. So corgi silently dropped any key whose values all accumulated negative, and could pick a different minimum in a bracket that mixed signs. Only a negated collection produces a negative accumulation, and in the six canonical programs the reducers only ever see non-negative data -- which is why this survived until the explain rewrite, whose demand dataflow subtracts. `Count` already tested the SUM (`c > 0`, matching vec) and `Collect` already emitted per bracket with `d.max(0)` items; both were right and are untouched. Found by bisection on operator-level traces of the two backends: the wave front was `n165 = n164 | Distinct` in the explain scope, whose input agreed exactly on both backends at that time -- one value accumulating to -1 -- and whose output had the row on vec and not on corgi. Everything previously reported about this bug (the demand loop stalling at iteration 2, a join yielding multiplicity 1 instead of 3, the nested clone diverging while the forward clone did not) was downstream of that one row. The three `#[ignore]`d divergence tests from the explain-on-corgi commit now pass and are un-ignored, including the one-line minimal reproducer pair. Ruled out along the way and left as a pinned invariant: `ColTimes`'s columnar time order matches the owned `Ord` (property test over mixed-length PointStamps, `[3]` vs `[3, 1]` and friends) -- the chunk layer sorts and merges by the former while every other layer reasons with the latter. 35 lib, 13 gate, 20 explain, plus all 4 heavy sweeps (fuzz, scc-100, join-partner-time, demand-excess). B4 0.47x vec, scc 5000/15000 identical to vec (13281/-39) at 369ms vs 408ms. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YH7iq9JoXmf7ATpq1gZaQS
`interactive/examples` holds three Rust files, all generic drivers, plus DDIR programs. The B4 harness added two more binaries, each carrying its DDIR program as a string literal -- exactly what the convention exists to prevent. `corgi_ast_prof` was wholly redundant: `ddir` already loads a `.ddp`, takes `--backend=vec|corgi`, synthesizes rows and reports timings, so profiling is `samply record -- ddir --backend=corgi programs/ast.ddp ..` with no new binary. `corgi_ast_compute` duplicated that too; what was genuinely its own -- a closed-form oracle and a hand-written compiled-DD twin -- is a benchmarking concern, and compiled baselines already live in `diagnostics/examples`, not here. Both go; a benchmarking pass can reintroduce a baseline deliberately. The program itself is worth keeping and is now `examples/programs/ast.ddp`, where `ddir` can run it and the gate can cover it (14 programs). It is the only one exercising list intro, columnar flatmap, `case` over an `if`-selected constructor and `fold` together, and since a `list(..)` subterm anywhere makes the whole projection fall back to rows, a regression in one shows up as the others going row-wise too. Its input contract (non-negative, `$0[0] - $1[0] + 32768` non-negative) is written down in the file. The corgi-vs-vec claim stays reproducible from the tree without the harness: `ddir --backend=vec|corgi programs/ast.ddp 2 32768 200000 200000 1` gives vec 4.01s vs corgi 1.86s (0.46x), matching the 0.47-0.49x the deleted harness measured at n=1m. What is not reproducible without a baseline binary is the ratio to compiled DD. Branch diff drops from +856/-146 to +658/-146. 35 lib, 14 gate, 20 explain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YH7iq9JoXmf7ATpq1gZaQS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Further improve the lowering to Corgi columnar operations, avoiding row-at-a-time operation.