Skip to content

Commit 4170e58

Browse files
committed
simd: three data-indexed mask primitives — gather, scatter-or, keyed group-sum
`mask_gather_u32(src, src_rows, index, out)`: bit i of out = src[index[i]], an out-of-range address reads FALSE (zero-fallback). The foreign-key semijoin: a predicate mask over table B gathered onto table A through A's fk lane, no index vector, no pair relation. `mask_scatter_or_u32(src, index, out, out_rows)`: for every set bit i of src, set bit index[i] of out. The one-to-many hop (docs that have a selected line). Cost ∝ popcount(src). `masked_group_sum_i32(mask, keys, values, out)`: out[keys[i]] += values[i] over the mask, ONE pass, all groups; a key past out.len() is not a group. This is the terminal a categorical GROUP BY … SUM lowers to — and it is NOT `masked_strided_group_sum`, which sums the byte-groups of one V3 register into one scalar, carries no key, and has zero callers. All three are deliberately scalar bit-walks: data-indexed permutations and scatter-adds do not vector-load, the same reasoning `masked_strided_group_sum` already records. 26 unit tests (tails at 67/130, out-of-range dropped, garbage-prefilled buffers come back tail-zero), parity check group 13 against naive references with out-of-range addresses mixed in, gather's range guard disable-verified red (2 tests + parity 0xD00) then green. Consumer: lance-graph-mask-risc `Gather` / `ScatterOr` / `GroupSum`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXUahz73MZxtxWcfpHp9dG
1 parent e1ef350 commit 4170e58

4 files changed

Lines changed: 724 additions & 9 deletions

File tree

‎.claude/blackboard.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## 2026-09-21 (20) — three data-indexed mask primitives: gather / scatter-or / keyed group-sum
2+
3+
Added `mask_gather_u32`, `mask_scatter_or_u32`, `masked_group_sum_i32` to `simd_masking_ops.rs` + the `simd::` facade.
4+
All three are deliberately scalar bit-walks — permutations/scatters indexed by `index`/`keys` data, not a fixed stride, so none of this crate's backends can vector-load them (same shape as `masked_strided_group_sum`, which says so in its own doc).
5+
`masked_strided_group_sum` is NOT a keyed group-by and never was — it sums one record's own byte-groups into a single scalar with no key at all; zero callers of either are affected by this addition.
6+
Parity: `check_gather_scatter_group` (0xDxx) in `crates/simd-masking-parity`, against naive per-element references, disable-verified red-then-green.
7+
Consumer: lance-graph-mask-risc `Gather`/`ScatterOr`/`GroupSum` (landing next).
8+
19
## 2026-09-17 (19) — G8 named: a tree-depth column (`lzcnt(bswap(x)) >> 2`) is the missing primitive for basin-local ranking; popcount is only its tie-break
210

311
Filed, not built. Full text in `masking-ops-state.md` § OUTLOOK G8 and the

‎crates/simd-masking-parity/src/lib.rs‎

Lines changed: 116 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121
//! predicates (mask-risc `Pred { under }`, D-MRX-0), `0xBxx` `mask_set_range`
2222
//! (the range WRITE, N1), `0xCxx` the unsigned `u8` / `u64` compare→mask
2323
//! family (`eq`/`ne`/`gt`/`ge`/`lt`/`le`, N2/N3 — built earlier but never
24-
//! exercised by this program until now). `main.rs` (native / qemu) and
24+
//! exercised by this program until now), `0xDxx` the data-indexed
25+
//! permutation/scatter family (`mask_gather_u32`/`mask_scatter_or_u32`/
26+
//! `masked_group_sum_i32`, for lance-graph-mask-risc's Gather/ScatterOr/
27+
//! GroupSum verbs). `main.rs` (native / qemu) and
2528
//! `selfcheck()` (the wasm cdylib export, driven by `run.mjs`) both call
2629
//! [`run`].
2730
@@ -30,16 +33,16 @@ use ndarray::simd::{
3033
eq_u64_to_mask, eq_u8_to_mask, ge_i32_to_mask, ge_i32_to_mask_under, ge_u64_to_mask, ge_u8_to_mask, gt_i32_to_mask,
3134
gt_i32_to_mask_under, gt_u64_to_mask, gt_u8_to_mask, le_i32_to_mask, le_i32_to_mask_under, le_u64_to_mask,
3235
le_u8_to_mask, lt_i32_to_mask, lt_i32_to_mask_under, lt_u64_to_mask, lt_u8_to_mask, mask_all, mask_and,
33-
mask_and_assign, mask_andnot, mask_andnot_assign, mask_any, mask_not, mask_not_assign, mask_or, mask_or_assign,
34-
mask_set_range, mask_shift_morton, mask_ternlog, mask_ternlog_assign, mask_xor, mask_xor_assign, masked_max_i32,
35-
masked_min_i32, masked_strided_group_sum, masked_sum_i32, ne_i32_to_mask, ne_i32_to_mask_under, ne_u32_to_mask,
36-
ne_u32_to_mask_under, ne_u64_to_mask, ne_u8_to_mask, ternary_match_strided_to_mask, ternary_match_u32_to_mask,
37-
ternary_match_u32_to_mask_under, ternary_match_u64_to_mask, ternary_match_u64_to_mask_under, ternlog, I32x16,
38-
MortonDir, U32x16, U64x8,
36+
mask_and_assign, mask_andnot, mask_andnot_assign, mask_any, mask_gather_u32, mask_not, mask_not_assign, mask_or,
37+
mask_or_assign, mask_scatter_or_u32, mask_set_range, mask_shift_morton, mask_ternlog, mask_ternlog_assign,
38+
mask_xor, mask_xor_assign, masked_group_sum_i32, masked_max_i32, masked_min_i32, masked_strided_group_sum,
39+
masked_sum_i32, ne_i32_to_mask, ne_i32_to_mask_under, ne_u32_to_mask, ne_u32_to_mask_under, ne_u64_to_mask,
40+
ne_u8_to_mask, ternary_match_strided_to_mask, ternary_match_u32_to_mask, ternary_match_u32_to_mask_under,
41+
ternary_match_u64_to_mask, ternary_match_u64_to_mask_under, ternlog, I32x16, MortonDir, U32x16, U64x8,
3942
};
4043

4144
/// Number of check groups [`run`] executes (for the log line only).
42-
pub const CHECKS: usize = 12;
45+
pub const CHECKS: usize = 13;
4346

4447
/// The wasm export: identical to [`run`], `extern "C"` so `run.mjs` can call it.
4548
#[no_mangle]
@@ -52,7 +55,7 @@ pub fn run() -> u32 {
5255
let groups: [fn() -> Result<(), u32>; CHECKS] = [
5356
check_ternlog_all_tables, check_u64x8_algebra, check_i32x16_compare, check_predicates_to_mask,
5457
check_mask_algebra, check_care_match, check_masked_reductions, check_blend, check_morton_shift,
55-
check_predicates_under, check_set_range, check_unsigned_compare_to_mask,
58+
check_predicates_under, check_set_range, check_unsigned_compare_to_mask, check_gather_scatter_group,
5659
];
5760
for g in groups {
5861
if let Err(code) = g() {
@@ -1119,3 +1122,107 @@ fn check_unsigned_compare_to_mask() -> Result<(), u32> {
11191122
}
11201123
Ok(())
11211124
}
1125+
1126+
// ── 0xDxx: mask_gather_u32 / mask_scatter_or_u32 / masked_group_sum_i32 —
1127+
// the data-indexed permutation/scatter family (lance-graph-mask-risc's
1128+
// Gather/ScatterOr/GroupSum verbs). Every reference below is a plain `for`
1129+
// loop indexed by the SAME data (`index`/`keys`) the primitive under test
1130+
// reads, never a call back into the primitive itself.
1131+
1132+
fn check_gather_scatter_group() -> Result<(), u32> {
1133+
let mut rng = SplitMix64(0xD000_0000_0008);
1134+
for &n in &LENS {
1135+
let nw = words_for(n);
1136+
1137+
// ── mask_gather_u32 ──────────────────────────────────────────────
1138+
let src_rows = 40usize;
1139+
let src_words = words_for(src_rows);
1140+
let src: Vec<u64> = (0..src_words).map(|_| rng.next()).collect();
1141+
// Every third address is deliberately out of range, so the
1142+
// "false by contract" arm is exercised, not merely plausible.
1143+
let index: Vec<u32> = (0..n)
1144+
.map(|i| {
1145+
if i % 3 == 0 {
1146+
(src_rows as u64 + 5 + i as u64) as u32
1147+
} else {
1148+
(rng.next() % src_rows as u64) as u32
1149+
}
1150+
})
1151+
.collect();
1152+
let out_len = nw + 1; // dirty, over-long
1153+
let mut out = vec![u64::MAX; out_len];
1154+
mask_gather_u32(&src, src_rows, &index, &mut out);
1155+
let want = reference_mask(n, out_len, |i| {
1156+
let idx = index[i] as usize;
1157+
idx < src_rows && (src[idx / 64] >> (idx % 64)) & 1 == 1
1158+
});
1159+
if out != want {
1160+
return Err(0xD00);
1161+
}
1162+
1163+
// ── mask_scatter_or_u32 ──────────────────────────────────────────
1164+
let out_rows = 50usize;
1165+
let out_words_count = words_for(out_rows);
1166+
let src_bits: Vec<u64> = (0..nw).map(|_| rng.next()).collect();
1167+
// Every fourth target is deliberately out of range.
1168+
let idx2: Vec<u32> = (0..n)
1169+
.map(|i| {
1170+
if i % 4 == 0 {
1171+
(out_rows as u64 + 7 + i as u64) as u32
1172+
} else {
1173+
(rng.next() % out_rows as u64) as u32
1174+
}
1175+
})
1176+
.collect();
1177+
let out_len2 = out_words_count + 1; // dirty, over-long
1178+
let mut out2 = vec![u64::MAX; out_len2];
1179+
mask_scatter_or_u32(&src_bits, &idx2, &mut out2, out_rows);
1180+
let mut want2 = vec![false; out_rows];
1181+
for i in 0..n {
1182+
if (src_bits[i / 64] >> (i % 64)) & 1 == 1 {
1183+
let t = idx2[i] as usize;
1184+
if t < out_rows {
1185+
want2[t] = true;
1186+
}
1187+
}
1188+
}
1189+
let want2_words = reference_mask(out_rows, out_len2, |t| want2[t]);
1190+
if out2 != want2_words {
1191+
return Err(0xD10);
1192+
}
1193+
1194+
// ── masked_group_sum_i32 ─────────────────────────────────────────
1195+
let n_groups = 12usize;
1196+
let mask_bits: Vec<u64> = (0..nw).map(|_| rng.next()).collect();
1197+
// Every fifth key is deliberately out of range.
1198+
let keys: Vec<u32> = (0..n)
1199+
.map(|i| {
1200+
if i % 5 == 0 {
1201+
(n_groups as u64 + 3 + i as u64) as u32
1202+
} else {
1203+
(rng.next() % n_groups as u64) as u32
1204+
}
1205+
})
1206+
.collect();
1207+
let values = i32_values(n, &mut rng);
1208+
let mut group_out = vec![-1i64; n_groups + 1]; // garbage + one unreferenced slot
1209+
masked_group_sum_i32(&mask_bits, &keys, &values, &mut group_out);
1210+
let mut want_group = vec![0i64; n_groups];
1211+
for i in 0..n {
1212+
if (mask_bits[i / 64] >> (i % 64)) & 1 == 1 {
1213+
let k = keys[i] as usize;
1214+
if k < n_groups {
1215+
want_group[k] = want_group[k].wrapping_add(values[i] as i64);
1216+
}
1217+
}
1218+
}
1219+
if group_out[..n_groups] != want_group[..] {
1220+
return Err(0xD20);
1221+
}
1222+
// The unreferenced slot must be zeroed, not left as garbage.
1223+
if group_out[n_groups] != 0 {
1224+
return Err(0xD21);
1225+
}
1226+
}
1227+
Ok(())
1228+
}

‎src/simd.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,16 +813,19 @@ pub use crate::simd_masking_ops::{
813813
mask_andnot,
814814
mask_andnot_assign,
815815
mask_any,
816+
mask_gather_u32,
816817
mask_not,
817818
mask_not_assign,
818819
mask_or,
819820
mask_or_assign,
821+
mask_scatter_or_u32,
820822
mask_set_range,
821823
mask_shift_morton,
822824
mask_ternlog,
823825
mask_ternlog_assign,
824826
mask_xor,
825827
mask_xor_assign,
828+
masked_group_sum_i32,
826829
masked_max_i32,
827830
masked_min_i32,
828831
masked_strided_group_sum,

0 commit comments

Comments
 (0)