diff --git a/.claude/blackboard.md b/.claude/blackboard.md index 6c539dce..0b82496c 100644 --- a/.claude/blackboard.md +++ b/.claude/blackboard.md @@ -1,3 +1,163 @@ +## 2026-09-16 (18) — the tail OPTIMISATION IS INERT ON EVERY POWER-OF-TWO POPULATION >= 512 ROWS, including the 4096-row tile + +Established before writing the rewrite (15)-(17) argued for, and it changes +whether that rewrite is worth doing at all. No code; this entry is the gate. + +### The arithmetic + +A mask over `n` rows is `ceil(n/64)` words (`lgj-abi`'s `mask_words_for`, and +this file's own `mask_words_for`). The facade's algebra ops walk `U64x8`, so a +TAIL exists only when `words % 8 != 0`. For a power of two, `words = 2^(k-6)`, +and `2^(k-6) % 8 == 0` for every `k >= 9`. So: + +| rows | words | tail | tail SHARE of the op | what the REWRITE would WIN | +|---:|---:|---:|---:|---:| +| 64 | 1 | 1 | 100 % | 67 % | +| 128 | 2 | 2 | 100 % | 67 % | +| 256 | 4 | 4 | 100 % | 67 % | +| **512** | **8** | **0** | **none** | **none** | +| **4096** | **64** | **0** | **none** | **none** | +| 10 000 | 157 | 5 | 73 % | 49 % | +| 65 536 | 1024 | 0 | **none** | **none** | +| 1 000 000 | 15 625 | 1 | 2.6 % | **1.7 %** | +| 16 777 216 | 262 144 | 0 | **none** | **none** | + +⊘ **Two corrections, both codex on #316, both to this table and its headline.** + +**(a) The headline dropped its own condition.** It read "inert on every +power-of-two population". False below 512 rows: 64/128/256 rows are 1/2/4 +words, so `words % 8 != 0` and the tail is the WHOLE op. The derivation right +above the table says `k >= 9` and the prose below said "at or above 512 rows" — +the TITLE is what lost it, which is the fourth level of this session's one +defect class: after a label outrunning its accumulator, a claim outrunning its +evidence and a measurement outrunning its regime, **a headline outrunning the +derivation directly beneath it.** + +**(b) A SHARE is not a WIN, and this entry conflated them.** The 16 ns is the +padded tail's share of the current op; the rewrite does not remove it, it +replaces it with #315's measured fixed-step tail of **5.33 ns**. So the win is +`(16 − 5.33) / current`, and every share figure overstated it by exactly +`16 / 10.67 = 1.5×`. At a million rows: current `1953 × 0.31 + 16 ≈ 621 ns`, +win `10.67 ns ≈ 1.7 %` — not 2.6 %. + +**Every power-of-two population at or above 512 rows has NO TAIL AT ALL**, and +this workspace's shapes are overwhelmingly powers of two — the 4096-row tile, +the 64-word mask, the 512-byte node, the 4096-centroid codebook. + +### What this does to (15)-(17) + +Those entries are not wrong; they are **scoped narrower than they read.** The +probe swept `base = 8` and `base = 64` WORDS — 512 and 4096 rows — plus `k` of +1..7, i.e. exactly the small end, and its "7 of 8 mask sizes have a tail" is a +statement about ARBITRARY sizes. The consumers' sizes are not arbitrary. + +So the honest reading of the merged measurement: + +- "the padded tail is routinely larger than the work it trails" — true **at + 8-71 words**, which is 512-4544 rows. +- On a million-row table the same tail is **2.6 %**. +- On the canonical 4096-row tile it is **0 %** — the op has no tail to pay for. + +### Consequence: the 11-function rewrite is NOT justified yet + +It would touch a hot facade across six realizations to win 0 % on the canonical +shape, ~2.6 % on a large arbitrary one, and a lot on sizes like 10 000 rows. +Whether that last case is real is a CONSUMER question nobody has asked yet: +which populations actually reach `mask_and` and friends, at what row counts. + +**The gate for step (b) is therefore not "a quieter machine" (what I told the +reviewer on #315) — it is a census of real consumer population sizes.** If they +are powers of two, the work is inert and should not be done; if arbitrary row +counts dominate, it is worth ~2.6 % there and much more on small masks. + +### And ternlog specifically points the other way + +For `mask_ternlog` the padded form is **ONE** instruction — a full-width +`vpternlogq zmm` over the zero-padded register. The fixed-step peel measured in +(16) needs **three** logic ops at 4-lane plus more at 2-lane plus a scalar +step. So for ternlog the padded tail may well be the CHEAPER shape, which is +the opposite of the direction (15) proposed for it, and is an independent +reason not to sweep all 11 sites uniformly. + +### The rule this is an instance of + +(15)-(17) were each about a label outrunning its measurement. This one is one +level up: **a measurement outrunning its REGIME.** The numbers were correct at +the sizes probed and the conclusion was stated without them. Before a kernel +optimisation lands, ask what the consumer's actual shapes are — a 3x win on a +size nobody runs is worth nothing, and the cheapest way to find that out is +arithmetic on the size formula, not another benchmark. + +### The consumer census (18) called for — run, and it closes the question + +Done read-only against `lance-graph-java`'s `lgj-abi`, the binding consumer of +the facade's mask algebra. + +**1. There is NO tiling of the population.** `lgj_pattern_open(n_rows, …)` / +`lgj_rowstore_open(n_rows, …)` take the row count directly and a mask spans the +WHOLE population (`abi.rs::mask_words_for` = `n_rows.div_ceil(64)`). The only +"tile" in that crate is the 12-byte facet register, not the rows. So the padded +tail is paid ONCE per op and amortizes over the entire body — the favourable +direction for the status quo. + +**2. Every committed population size is blind to the tail**, and in a +bimodal way that is worth seeing laid out: + +| rows | words | can it see a tail? | +|---:|---:|---| +| 4, 8, 10, 16, 32, 64 | 1 | **all tail** — not one full group | +| 70, 100, 128 | 2 | **all tail** | +| 200 | 4 | **all tail** | +| 500 | 8 | none | +| 1000, 1024 | 16 | none | +| 4096 | 64 | none | +| 64000 | 1000 | none | + +The small sizes are correctness fixtures where the op IS its tail and speed is +irrelevant; every size large enough to have a body has **zero tail**. So no +committed test or bench in the consumer can ever exercise the regime the +optimisation targets — which is exactly why #315's probe had to choose +`base = 8` and `64` words to see it at all. + +**3. Where it does bite** — reported as the WIN the rewrite would deliver, not +the tail's share, per the correction above (padded 16 ns replaced by #315's +measured fixed-step 5.33 ns, against a body of `groups × 0.31 ns`): + +| population | tail share | REWRITE WIN | +|---:|---:|---:| +| 5 000 rows | 85 % | 57 % | +| 50 000 | 35 % | 23 % | +| 100 000 | 21 % | 14 % | +| ~325 000 | 7.5 % | **5 %** | +| ~502 000 | 5 % | 3.4 % | +| 1 000 000 | 2.6 % | **1.7 %** | +| ~1 735 000 | 1.5 % | **1 %** | +| any power of two ≥ 512 | 0 % | **0 %** | + +So the crossovers move in with the correction: the win falls under **5 %** above +~325 k rows and under **1 %** above ~1.74 M, where the share figures put those +thresholds at ~502 k and ~2.6 M. + +### Verdict on step (b): DO NOT BUILD IT + +The optimisation is worth something only for arbitrary populations in roughly +the **5 k – 325 k row** band (where the WIN, not the share, clears 5 %). Outside it: under 512 rows the op is all tail and +untimed, above ~2.6 M rows it is under 1 %, and on every power of two it is +exactly zero. Against that it would touch a hot facade at 11 sites across six +realizations, and make `mask_ternlog` WORSE (its padded form is one full-width +`vpternlogq`; the peel is three ops plus more). + +**So step (b) is closed as measured-not-worth-doing, not deferred.** Reopening +it needs a named consumer workload whose populations sit in that band — not +another kernel benchmark. + +Cross-ref: (15) the trip-count finding and the codegen witnesses stand +unchanged — `U64x4 &` really is bit-identical to a hand-written loop, fixed +peels really are packed on aarch64, the `avx512vl` gate really is unnecessary. +What is withdrawn is only the implied priority of acting on them. + +--- + ## 2026-09-16 (17) — ⊘ CORRECTS (16) twice: the unit was OBSERVATIONS not widths, and "lower bound" was one claim too many Both caught by coderabbit on the same PR (#315), both are errors (16) introduced