Skip to content

Commit 2fedea4

Browse files
committed
crepe-plan-probe: the relalg Or limit is a setting, 2k-1 slots
An Or of k atoms takes 2k - 1 scratch slots, not one per atom: each atom and each OR gets a slot of its own, and no slot is reused inside a fold. MAX_SLOTS = 8 is an r2il setting ("sized with headroom"); mask-risc addresses 65,536 slots. A larger arena, or slot reuse inside the fold, would lift the 4-center limit. The comment, the plan and the board entry now say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VncKoyG6JkpsoqhqTGTv3f
1 parent 42505f1 commit 2fedea4

3 files changed

Lines changed: 12 additions & 8 deletions

File tree

‎.claude/board/entries/2026-10-20-crepe-rule-optimizer-spike.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@
1919
- Dynamic MeTTa rules need crepe's analysis as a library: crepe is a proc-macro crate (follow-up A, `crepe-core` split).
2020
- Semijoin reduction built (`Options::semijoin`): laid-out rows 36 at every size instead of growing with n; preparation 3.47 ms → 0.28 ms at 50,000. The scan stays O(n).
2121
- ndarray for the row filter, measured (`examples/row_filter.rs`): over the row-major store the strided compare is slower than the scalar test at every needle count, so it is not wired; over a contiguous column it is 5–6 µs per needle at 50,000 rows and beats the scalar test up to 64 needles; at 128 needles two runs disagree because of benchmark noise (the scalar time moved 1.8× between runs), so that crossover is not pinned (re-measured after the #1632 Codex fix). Next step: a column-major relation store.
22-
- Stage 3 over Lance 13 / lancedb 0.40 built (`lance_store`): each side read under a relalg address mask with `with_row_addr_prefilter`; every masked read scans exactly its mask (P1–P4 green). The relalg membership mask holds 4 centers at most (one scratch slot per `eq` atom, `MAX_SLOTS = 8`); above that a scalar test builds it.
22+
- Stage 3 over Lance 13 / lancedb 0.40 built (`lance_store`): each side read under a relalg address mask with `with_row_addr_prefilter`; every masked read scans exactly its mask (P1–P4 green). The relalg membership mask holds 4 centers at most: `k` centers take `2k - 1` scratch slots (no slot reuse inside a fold), and `MAX_SLOTS = 8` is an r2il setting, not a hard limit (mask-risc addresses 65,536). Above that a scalar test builds it.
2323
- Arm B built (`addressed`): lanes in key order, attested with `SealedFacetLane::attest_sorted`, read only at the `seek_from` ranges of the demanded keys. At 50,000 rows it scans `3k` rows where Arm A scans at least 150,000. It is 2.5–12× faster than Arm A up to 1% of centers and pays back its 7 ms attest after 2–3 queries. From 10% of centers up a full scan wins, and with all centers Arm B never breaks even. Open: the selectivity crossover between 1% and 10%; multi-fragment lanes (an append currently forces a compaction); a second lane for demands on the second column.

‎.claude/plans/crepe-rule-optimizer-spike-v1.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ DECISION (operator, 2026-10-20): "ja, bau Stufe 3 mit Lance 13 und CI-Workflow"
118118
The relations are stored column-major in Lance: one table per relation, columns `c0`, `c1` (`UInt32`), written through lancedb 0.40 and read through lance 13. Preparation becomes H14d's stages 2 and 3 per side:
119119

120120
1. Read the one column the address mask needs (the restriction column, or the key column for the semijoin), with `_rowaddr`.
121-
2. Build the address mask `R` through relalg over that column as a `U32` lane: `eq_u32(col, k)` for a restriction, `Or` of `eq_u32(key, c)` for the semijoin centers (lowered by mask-risc onto `ndarray::simd::eq_u32_to_mask`). Above 4 centers the mask is built by the scalar membership test instead: each `eq` atom of the `Or` takes one scratch slot and a fold may use at most 8 (`lance_graph_r2il::MAX_SLOTS`), so 5 centers fail with `ScratchBufferTooSmall`. This is relalg's ceiling, not the §2c speed crossover, which is ndarray's row filter.
121+
2. Build the address mask `R` through relalg over that column as a `U32` lane: `eq_u32(col, k)` for a restriction, `Or` of `eq_u32(key, c)` for the semijoin centers (lowered by mask-risc onto `ndarray::simd::eq_u32_to_mask`). Above 4 centers the mask is built by the scalar membership test instead. relalg lowers `Or(a1..ak)` left-associatively, and every atom and every `OR` takes a scratch slot of its own (no reuse inside a fold), so `k` centers take `2k - 1` slots. One fold may use `lance_graph_r2il::MAX_SLOTS = 8`, a setting the crate calls "sized with headroom", so 5 centers (9 slots) fail with `ScratchBufferTooSmall`. mask-risc addresses up to 65,536 slots. Either a larger arena or slot reuse inside the fold (an `Or` chain needs only the accumulator and the current atom) would lift the limit; both are r2il/relalg changes. This is not the §2c speed crossover, which is ndarray's row filter.
122122
3. Read the side's columns with `with_row_addr_prefilter(R)`.
123123
4. Hand those rows to `JoinTable::prepare_star`, as the in-memory path does.
124124

‎crates/crepe-plan-probe/src/lance_store.rs‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,16 @@ use crate::native::{PreparedStar, StarPlan, prepare_from_rows};
3030
/// Up to this many semijoin centers, the address mask is an `Or` of `eq_u32`
3131
/// atoms through relalg; above it, the scalar membership test builds it.
3232
///
33-
/// The limit is relalg's, not a speed crossover: each `eq` atom of the `Or`
34-
/// holds its own scratch slot, and one fold may use at most
35-
/// `lance_graph_r2il::MAX_SLOTS` (8) of them. Measured: 4 centers fit, 5
36-
/// fail with `ScratchBufferTooSmall { need_words: 145, have_words: 129 }` at
37-
/// 1,000 rows. (The 64-needle crossover in plan §2c is ndarray's row filter,
38-
/// a different mechanism.)
33+
/// The limit comes from a setting, not from the algebra and not from a speed
34+
/// crossover. relalg lowers `Or(a1..ak)` left-associatively, and each atom
35+
/// and each `OR` takes a scratch slot of its own (slots are not reused
36+
/// inside a fold), so `k` centers take `2k - 1` slots. One fold may use
37+
/// `lance_graph_r2il::MAX_SLOTS` (8, "sized with headroom") of them. Measured:
38+
/// 4 centers fit (7 slots); 5 need 9 and fail with
39+
/// `ScratchBufferTooSmall { need_words: 145, have_words: 129 }` at 1,000 rows.
40+
/// mask-risc itself addresses up to 65,536 slots. A larger arena, or slot
41+
/// reuse in the fold, lifts this. (The 64-needle crossover in plan §2c is
42+
/// ndarray's row filter, a different mechanism.)
3943
pub const SEMIJOIN_ATOMS_MAX: usize = 4;
4044

4145
#[derive(Debug)]

0 commit comments

Comments
 (0)