Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions .claude/blackboard.md
Original file line number Diff line number Diff line change
@@ -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.
Comment thread
AdaWorldAPI marked this conversation as resolved.
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
Expand Down
Loading