Skip to content

Commit 40a71ad

Browse files
committed
masking-ops: G8 review round -- carving, feature bits, tail, sixth backend
Five review findings on #317, each verified in the tree before fixing, plus two facts the verification turned up that change the gate. - "each tier byte" was wrong about the unit. `RailSpec::v3_facet` (src/hpc/clam_v3.rs) walks six sequential (u8:u8) levels at key offsets 4..16 with stride 2, so a tier is TWO bytes, one per axis. The u64 operand at 8..16 is 4 tiers x 2 bytes = 8 bytes = 16 nibbles. States the carving, operand width and pair-versus-axis grouping up front, as both reviewers asked. - vpshufb at zmm width needs AVX-512BW, not AVX-512CD. The entry gated the whole sequence on CD alone. Now names both bits and says a CD-without-BW target needs a different byte reversal. - "no padding and no tail descent" was a claim about the OPERAND stated as though it covered the population. Eight u64 tails tile a zmm, but a row count not divisible by eight still leaves a column remainder, as the slice-level U64x8 ops handle with pad_tail. No multiple-of-eight input contract is stated or intended; an implementation keeps a remainder path and tests non-multiple-of-eight lengths. - The falsifier said six realizations while the list named five. The sixth is the nightly core::simd arm (src/simd_nightly/), now named. - MD040: the depth fence had no language. Two findings from reading clam_v3.rs that the reviews did not reach, and that matter more than any of the above: - RailSpec carries TWO carvings and its doc records a measurement between them. The interleaved X:Y pair reading (v3_facet, 6 levels, stride 2) fits only 44.25% of paths on the medcare bake; the contiguous per-axis slab (RailSpec::slab, 12 levels, stride 1) fits 99.62% in twelve levels. That inverts G8's gate: on the slab carving a contiguous u64 IS one axis, the mixed-axis caveat evaporates, and a plain lzcnt(bswap) is exactly right; on the pair carving it is only a mixed-axis tiebreaker. So the consumer gate must name the CARVING as well as the call site, and the measured split says the slab is the likelier target. Do not build against the pair reading because it happens to be the zero-fallback default. - Levels grow by STACKING, not widening: a class needing more than six levels chains a second register, explicitly contemplated as the edge lane at 16..32, running depth 0..=12 over two registers. So a G8 column op over a stacked pair is two chained operands, never one wider one, which is also why no u128 variant is proposed. Docs only. Still named, not built. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HScwwezRdMxFfTs3WLG19d
1 parent ab5f347 commit 40a71ad

1 file changed

Lines changed: 46 additions & 9 deletions

File tree

‎.claude/knowledge/masking-ops-state.md‎

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,13 @@ session does not "fix" it.
202202

203203
**G8 — `lzcnt_bswap_u64_to_u8` (tree-depth column) — NAMED 2026-09-17, not
204204
built.** The V3 facet's tail (bytes 8..16 = tiers 2–5, one aligned `u64` at a
205-
compile-time offset — a PEEK, not a gather) is a **tree path**: each tier byte
205+
compile-time offset — a PEEK, not a gather) is a **tree path**. State the
206+
carving before the op, because "tier" and "byte" are not the same unit: the
207+
V3 facet is `classid(4) | payload(12)`, and `RailSpec::v3_facet`
208+
(`src/hpc/clam_v3.rs`) walks **six sequential `(u8:u8)` levels at key offsets
209+
`4..16`, stride 2** — so a tier is TWO bytes, one per axis, not one byte. The
210+
`u64` operand at `8..16` therefore covers tiers 2–5, i.e. **4 tiers × 2 bytes
211+
= 8 bytes = 16 nibbles**. Each *byte* within a tier
206212
is a **16-ary nibble hierarchy** — OGAR canon's *"1 hex digit = 1 nibble = 1
207213
level of the 16-ary tree (`FAN_OUT=16`)"* — high nibble coarse. The metric on
208214
a tree path is depth-of-divergence, which at that granularity on a `u64` is
@@ -233,22 +239,33 @@ facet is little-endian (tier 2 at the LOW byte of the tail `u64`) but the
233239
hierarchy inside a byte is MSB-coarse. `tzcnt` gets byte order right and
234240
nibble order wrong; `lzcnt` the reverse. One `bswap` reconciles them:
235241

236-
```
242+
```text
237243
depth_nibbles = lzcnt(bswap(a ^ b)) >> 2 // 0..16, stepless, no branch
238244
```
239245

240246
That is three scalar instructions (`xor`, `bswap`, `lzcnt`) or, as a column,
241-
`vpshufb` (byte reverse) + `vplzcntq` (AVX-512CD) over 8 rows per zmm with
242-
**no padding and no tail descent** — eight tails tile a zmm exactly, which is
247+
`vpshufb` (byte reverse, **AVX-512BW** at zmm width) + `vplzcntq`
248+
(**AVX-512CD**) over 8 rows per zmm — BOTH feature bits, not CD alone; a
249+
target with CD but not BW needs a different byte reversal. Per 8-row group
250+
there is **no operand padding and no tail descent** — eight tails tile a zmm
251+
exactly, which is
243252
the whole reason the `u64` width is the right one here (and consistent with
244253
blackboard (18): the tail optimisation is inert on power-of-two populations
245254
because there is no tail; stripping HEEL/HIP makes the operand *start*
246-
power-of-two). Consumer: basin-local similarity in lance-graph (`FacetCascade`
255+
power-of-two). **That is a claim about the OPERAND, not the population:** a
256+
row count not divisible by 8 still leaves a column remainder, exactly as the
257+
slice-level `U64x8` ops handle with `pad_tail`. No multiple-of-eight input
258+
contract is stated or intended, so an implementation keeps a remainder path
259+
and tests non-multiple-of-eight lengths. What is eliminated is the
260+
*within-operand* padding a narrower width would need, not the last partial
261+
group. Consumer: basin-local similarity in lance-graph (`FacetCascade`
247262
tail) and the CAKES nearest-ranking on `NiblePath` (lance-graph
248263
`ISS-NIBLEPATH-FOLD-IS-CARRIER-2-UNMASKED`, the packed-`u64` carrier where the
249264
same `lzcnt` IS the fold). Realizations: `vplzcntq` on avx512cd; avx2 has no
250265
vector lzcnt — polyfill via the float-exponent trick or a scalar peel (measure
251-
which); NEON has `clz` on 32-bit lanes (two halves + select); wasm/scalar flat.
266+
which); NEON has `clz` on 32-bit lanes (two halves + select); wasm/scalar flat;
267+
and the **nightly** `core::simd` arm (`src/simd_nightly/`) — six in total,
268+
which is the count the falsifier below means.
252269
Pre-registered falsifier: the column op must equal the scalar
253270
`(a ^ b).swap_bytes().leading_zeros() >> 2` on every row of a 64k fixture at
254271
all six realizations, AND a disable-run without the `bswap` must fail on a
@@ -283,9 +300,29 @@ hierarchy, the `lo` chain the orthogonal one"*), not coarse and fine of one.
283300
So raw-tail depth is a **mixed-axis** metric: still a valid monotone
284301
tie-breaker, NOT a depth in either hierarchy. A consumer that wants one
285302
axis needs a per-chain fold (the existing `hi_distance` / `lo_distance`) or a
286-
deinterleave before the `lzcnt`. Whether the mixed-axis reading is what the
287-
basin-local-similarity consumer actually wants is now part of the gate, not
288-
an assumption. Gate
303+
deinterleave before the `lzcnt`.
304+
305+
**And `clam_v3.rs` already answers which carving the real bake wants — it is
306+
not the pair reading.** `RailSpec` carries TWO carvings, and the doc comment
307+
records a measurement against them: the interleaved `X:Y` pair reading
308+
(`v3_facet`, 6 levels, stride 2) *"fits only 44.25 % of paths"* on the
309+
medcare bake, while the contiguous per-axis slab (`RailSpec::slab`, 12 levels,
310+
stride 1) *"fits 99.62 % in twelve levels"*. That inverts the gate for G8: on
311+
the **slab** carving a contiguous `u64` IS one axis, the mixed-axis caveat
312+
above evaporates, and a plain `lzcnt(bswap(·))` is exactly right. On the
313+
**pair** carving it is a mixed-axis tiebreaker. So G8's consumer gate must
314+
name the CARVING as well as the call site, and the measured 99.62 %/44.25 %
315+
split says the slab is the likelier target. Do not build against the pair
316+
reading on the strength of it being the zero-fallback default.
317+
318+
**Stacking, not widening, is how the levels grow.** The same module: *"a class
319+
that needs more than six levels does not widen a byte — it stacks a second
320+
register, e.g. into the edge lane, and chains it (`RailSpec::stacked`). Depth
321+
then runs 0..=12 over two registers, same hole rule, same arithmetic."* The
322+
edge lane at `16..32` is explicitly contemplated as that continuation register
323+
(*"`16..32` may be a continuation register (if the spec says so)"*). A G8
324+
column op over a stacked pair is therefore two operands chained, never one
325+
wider one — which is also why no `u128` variant of G8 is proposed. Gate
289326
before building: one named consumer call site that ranks by depth — the same
290327
count rule G5 carries.
291328

0 commit comments

Comments
 (0)