Skip to content

Commit 000b077

Browse files
authored
Merge pull request #406 from AdaWorldAPI/ccr-45a41fb4-0j3i0l
D-SIMD-MICRO-MACRO-1 step 1: packed microtile oracle (lab fixture)
2 parents 7ce1562 + 97d8cdd commit 000b077

6 files changed

Lines changed: 968 additions & 0 deletions

File tree

‎.claude/blackboard.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4010,3 +4010,18 @@ laws, ExtentUnsupported, row bounds) and WITNESS in OrderedLaneWitness; no new t
40104010
- PR A #402 (draft): `half_add_mask` / `full_add_mask` in `simd_masking_ops`, re-exported; vectorized `pow2n_from_int` (vsexp 4x, vserf 2.4x on AVX-512); `U32x16` `Shl`/`Shr<Self>` added to AVX2, AVX, NEON, wasm, nightly.
40114011
- Lab: `Abacus<BITS>` with transactional refusal in `examples/u9_domino`; cost equal to eager F. PR B and PR C deferred: no consumer, planes already cheap with existing words. Report: `.claude/knowledge/binary-abacus-polyfill-probe.md`.
40124012
- Loose end: NEON `simd_exp_f32` has ~47 scalar fcmp/csel (pre-existing, select/compare path); AVX2 mask packing for `select` still lane by lane.
4013+
4014+
## 2026-10-30 — D-SIMD-MICRO-MACRO-1 step 1: packed microtile oracle (lab only)
4015+
4016+
- `examples/microtile/` + `tests/microtile.rs`: 128-bit tiles of `5×U24/I24` (+8 metadata
4017+
bits) and `14×U9/I9` (+2 spare), field-isolated add/sub/carry/borrow/signed overflow,
4018+
intra-tile stencil, exact unsigned+signed fold; u128, SoA `U64x8`, widened `U32x16`,
4019+
bit-plane (`full_add_mask`) realizations, all against one unpacked oracle.
4020+
- 9 tests green on AVX-512, AVX2, AVX, v2, NEON (qemu), nightly. Disable runs D1–D13 red
4021+
except D6 (redundant mask, removed).
4022+
- Cost gate: no single winner. U24x5 → widened for add and fold; U9x14 → packed SoA for
4023+
L2-resident add, bit planes for the fold. u128 alone never wins (not vectorized).
4024+
- Found: NEON/wasm `U32x16` lacks `&`, `|`, `-`, `!`, `+=`, `from_slice`, `copy_to_slice`.
4025+
Recorded, not fixed.
4026+
- Open: steps 2–4 (KPEA method binding, Thought-0 delta reuse, interlacing). H13 stays
4027+
deferred. Report: `.claude/knowledge/microtile-probe.md`.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# D-SIMD-MICRO-MACRO-1, step 1: the packed microtile oracle
2+
3+
Lab fixture only: `examples/microtile/` (realizations + cost gate) and
4+
`tests/microtile.rs` (exactness). No public type, no backend change, no
5+
production caller. Steps 2+ (binding to a KPEA method, Thought-0 delta reuse,
6+
interlacing) are not started.
7+
8+
## What the microtile is
9+
10+
One 128-bit tile carries several isolated arithmetic fields:
11+
12+
| layout | fields | payload | other bits |
13+
|---|---|---|---|
14+
| `U24x5` | 5 × 24 bit (`U24`/`I24`) | 120 bits | 8 metadata bits, 120..128 |
15+
| `U9x14` | 14 × 9 bit (`U9`/`I9`) | 126 bits | 2 spare bits, 126..128 |
16+
17+
Operations: field-isolated wrapping add and sub, per-field unsigned carry and
18+
borrow out, per-field signed overflow, an intra-tile stencil
19+
`f[j-1] + 2 f[j] + f[j+1]`, and an exact fold (unsigned and signed per-field
20+
sums over many tiles). Every result keeps the left operand's metadata.
21+
22+
Isolation is the SWAR law: clear each field's top bit in the addends so a
23+
carry stops at its own top bit, then fix that bit with
24+
`(a ^ b) & HIGH`; for sub, set each top bit of `a` so a borrow stops there.
25+
Signed fold: `Σ sext(f) = Σ f - 2^W · #(top bit set)`, so one pass yields
26+
both sums.
27+
28+
## Four realizations, one oracle
29+
30+
The oracle unpacks every field and does the arithmetic in `u64`/`i64`.
31+
32+
1. **u128**: the tile as one 128-bit integer.
33+
2. **SoA `U64x8`**: 8 tiles per op, the tile split into a low and a high
34+
64-bit plane. The fields that straddle bit 64 (`U24` field 2, `U9` field
35+
7) take an explicit carry/borrow out of bit 63, computed with lane-wise
36+
logic only (no shuffle).
37+
3. **Widened `U32x16`**: field-major `u32` lanes. The fold flushes to `u64`
38+
every `floor((2^32-1)/(2^W-1))` steps (256 for `U24`).
39+
4. **Bit planes**: one bit per tile, 64 tiles per word, add by ripple of
40+
`simd::full_add_mask`, fold by `Σ_k 2^k popcount(plane_k)`.
41+
42+
Exactness: 9 tests over random tiles plus every pairing of edge field values
43+
(0, 1, sign bit - 1, sign bit, max - 1, max), the two straddling fields'
44+
cross-word carry and borrow, 8192 maximal `U24` values in one `u32` lane, and
45+
metadata against an all-ones right operand. Green on AVX-512, AVX2,
46+
AVX-without-AVX2 (qemu SandyBridge), x86-64-v2 (qemu Nehalem), NEON (qemu
47+
aarch64) and nightly `core::simd`.
48+
49+
Disable runs, each red: top bit not cleared in add (D1) or set in sub (D8),
50+
no carry (D2) or borrow (D7) across bit 64, metadata dropped (D3), signed
51+
fold without the top-bit correction (D4) and its plane form (D9), widened
52+
fold never flushing (D5), carry-out as `a & b` only (D10), straddling field
53+
read from the low word only (D11), fold tail skipped (D12), widened add
54+
unmasked (D13). D6 (an extra mask on the stencil's left shift) stayed green:
55+
`add` already masks its right operand, so the mask was removed rather than
56+
kept as a guard that does not bind.
57+
58+
## Cost gate
59+
60+
`cargo run --release --example microtile --features std`, median ns per tile
61+
over 31 runs, three runs (two AVX-512 v4, one AVX2 v3) on a 4-core host.
62+
4096 tiles = 64 KB per operand (L2-resident); 65536 tiles = 1 MB.
63+
64+
| op | layout | u128 | SoA U64x8 | widened | planes |
65+
|---|---|---|---|---|---|
66+
| add, 4K | U24x5 | 2.00–2.10 | 0.95–1.73 | **0.69–0.71** | 1.87–1.93 |
67+
| add, 4K | U9x14 | 2.03–2.10 | **1.08–1.66** | 1.92–1.97 | 1.83–2.03 |
68+
| add, 64K | U24x5 | 2.29–2.31* | 1.94–3.16 | 2.18–2.35 | **1.88–2.07** |
69+
| add, 64K | U9x14 | 2.04–2.41 | 1.54–2.81 | 6.19–6.39 | **1.87–2.06** |
70+
| fold, 4K | U24x5 | — | 0.60–1.01 | **0.29–0.30** | 0.85–0.90 |
71+
| fold, 4K | U9x14 | — | 2.71–3.60 | 0.76–1.20 | **0.73–1.42** |
72+
| fold, 64K | U24x5 | — | 0.53–0.93 | **0.28–0.31** | 0.75–0.82 |
73+
| fold, 64K | U9x14 | — | 2.81–3.22 | 1.85–2.16 | **0.60–0.76** |
74+
75+
\* one outlier at 4.19 excluded.
76+
77+
Storage: packed and planes 16 B/tile; widened 20 B/tile (`U24x5`) and
78+
56 B/tile (`U9x14`).
79+
80+
Read:
81+
82+
- **No realization wins everything, so packed is not assumed.** Wide fields
83+
(`U24x5`) favour widened lanes for both add and fold: only 25% more bytes,
84+
and one native add per field. Narrow fields (`U9x14`) favour packed SoA for
85+
cache-resident add and bit planes for the fold; widened costs 3.5× the
86+
bytes and is 3× slower once the data leaves L2.
87+
- **The single u128 is never the fast form.** It runs at about 2 ns/tile
88+
whatever the layout: rustc does not vectorize the two-word add. The packed
89+
win comes from the SoA split, which puts 8 tiles in each register.
90+
- **At 1 MB every add is about 2 ns/tile** except widened `U9x14`: the
91+
operation is bandwidth-bound and the bytes per tile decide.
92+
- **Bit planes earn their place on folds, not adds.** A narrow-field fold is
93+
W popcounts per 64 tiles; their add needs W full adders and is no faster
94+
than packed.
95+
- Unexplained: SoA add is faster on AVX2 (0.95–1.08 at 4K) than on AVX-512
96+
(1.61–1.73). Not diagnosed.
97+
98+
## Findings outside the fixture
99+
100+
- **NEON and wasm `U32x16` lack most of the facade:** no `&`, `|`, `-`,
101+
`!`, `+=`, `from_slice` or `copy_to_slice`; they implement `+`, `^`, the
102+
two shifts and `from_array`/`to_array`. AVX-512, AVX2, scalar and nightly
103+
have the full set. The fixture works around it (mask by `<< k >> k`, load
104+
through `from_array`), so this is recorded, not fixed here. It is a real
105+
parity gap for any consumer that builds on `U32x16`.
106+
107+
## What this does not show yet
108+
109+
- No consumer: the gates for binding to a KPEA mask/count method (step 2),
110+
Thought-0 invariant reuse across thought deltas (step 3) and interlacing
111+
(step 4) are open. The fold here is the candidate primitive for step 2.
112+
- Pack/unpack between layouts is untimed; a method that converts would pay it.
113+
- Only x86 was timed; NEON and the other tiers ran under emulation for
114+
exactness only.

‎Cargo.toml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ name = "u9_domino"
9090
path = "examples/u9_domino/main.rs"
9191
required-features = ["std"]
9292

93+
[[example]]
94+
name = "microtile"
95+
path = "examples/microtile/main.rs"
96+
required-features = ["std"]
97+
9398
[[example]]
9499
name = "cpu_guard_probe"
95100
required-features = ["std"]

‎examples/microtile/main.rs‎

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
//! D-SIMD-MICRO-MACRO-1 step 1 cost gate: field-isolated add and exact fold
2+
//! over 64K microtiles, in four realizations. Every realization is checked
3+
//! against the unpacked oracle before it is timed.
4+
//! `cargo run --release --example microtile --features std`.
5+
6+
// The tests exercise sub and the stencil; the bench times add and fold only.
7+
#[allow(dead_code)]
8+
mod tile;
9+
10+
use std::hint::black_box;
11+
use std::time::Instant;
12+
use tile::*;
13+
14+
fn tier() -> &'static str {
15+
if cfg!(target_feature = "avx512bw") {
16+
"x86_64 AVX-512"
17+
} else if cfg!(target_feature = "avx2") {
18+
"x86_64 AVX2"
19+
} else if cfg!(target_arch = "aarch64") {
20+
"aarch64 NEON"
21+
} else {
22+
"other"
23+
}
24+
}
25+
26+
fn xorshift(s: &mut u64) -> u64 {
27+
*s ^= *s << 13;
28+
*s ^= *s >> 7;
29+
*s ^= *s << 17;
30+
*s
31+
}
32+
33+
/// Median ns per tile over 31 runs.
34+
fn time(name: &str, tiles: usize, base: Option<f64>, f: &mut dyn FnMut() -> u64) -> f64 {
35+
black_box(f());
36+
let mut t: Vec<f64> = (0..31)
37+
.map(|_| {
38+
let t0 = Instant::now();
39+
black_box(f());
40+
t0.elapsed().as_nanos() as f64 / tiles as f64
41+
})
42+
.collect();
43+
t.sort_by(f64::total_cmp);
44+
let med = t[15];
45+
let rel = base
46+
.map(|b| format!(" {:6.2}x", med / b))
47+
.unwrap_or_default();
48+
println!(" {name:<44} {med:8.3} ns/tile{rel}");
49+
med
50+
}
51+
52+
fn run<P: Packing>(label: &str, tiles: usize) {
53+
let mut s = 0x9E37_79B9_7F4A_7C15u64;
54+
let a: Vec<u128> = (0..tiles)
55+
.map(|_| (xorshift(&mut s) as u128) << 64 | xorshift(&mut s) as u128)
56+
.collect();
57+
let b: Vec<u128> = (0..tiles)
58+
.map(|_| (xorshift(&mut s) as u128) << 64 | xorshift(&mut s) as u128)
59+
.collect();
60+
let ((alo, ahi), (blo, bhi)) = (split(&a), split(&b));
61+
let (sa, sb) = (Soa { lo: &alo, hi: &ahi }, Soa { lo: &blo, hi: &bhi });
62+
let (wa, wb) = (widen::<P>(&a), widen::<P>(&b));
63+
let (pa, pb) = (to_planes::<P>(&a), to_planes::<P>(&b));
64+
let (n, w) = (P::N as usize, P::W as usize);
65+
66+
// Exactness before timing.
67+
let want: Vec<u128> = a
68+
.iter()
69+
.zip(&b)
70+
.map(|(&x, &y)| oracle_add::<P>(x, y).0)
71+
.collect();
72+
let mut out128 = vec![0u128; tiles];
73+
for t in 0..tiles {
74+
out128[t] = add::<P>(a[t], b[t]);
75+
}
76+
assert_eq!(out128, want, "{label} u128 add");
77+
let (mut ol, mut oh) = (vec![0u64; tiles], vec![0u64; tiles]);
78+
soa_add_slices::<P>(&sa, &sb, &mut ol, &mut oh);
79+
assert!((0..tiles).all(|t| (ol[t] as u128 | (oh[t] as u128) << 64) == want[t]), "{label} soa add");
80+
let mut ow = vec![vec![0u32; tiles]; n];
81+
for j in 0..n {
82+
widened_add::<P>(&wa[j], &wb[j], &mut ow[j]);
83+
assert!((0..tiles).all(|t| ow[j][t] == get::<P>(want[t], j as u32)), "{label} widened add");
84+
}
85+
let (fu, fs) = oracle_fold::<P>(&a);
86+
assert_eq!(soa_fold::<P>(&sa), (fu.clone(), fs.clone()), "{label} soa fold");
87+
for j in 0..n {
88+
assert_eq!(widened_fold::<P>(&wa[j]), fu[j], "{label} widened fold");
89+
assert_eq!(planes_fold(&pa[j], w), (fu[j], fs[j]), "{label} planes fold");
90+
}
91+
92+
println!("{label}: {n} fields x {w} bits, {tiles} tiles, all realizations exact");
93+
println!(" storage: packed 16 B/tile, widened {} B/tile, planes 16 B/tile", 4 * n);
94+
let base = time("add u128 (one 128-bit word)", tiles, None, &mut || {
95+
for t in 0..tiles {
96+
out128[t] = add::<P>(a[t], b[t]);
97+
}
98+
out128[tiles - 1] as u64
99+
});
100+
time("add SoA U64x8 (8 tiles per op)", tiles, Some(base), &mut || {
101+
soa_add_slices::<P>(&sa, &sb, &mut ol, &mut oh);
102+
ol[tiles - 1]
103+
});
104+
time("add widened U32x16 (one field per lane)", tiles, Some(base), &mut || {
105+
for j in 0..n {
106+
widened_add::<P>(&wa[j], &wb[j], &mut ow[j]);
107+
}
108+
ow[0][tiles - 1] as u64
109+
});
110+
let mut op = vec![0u64; w];
111+
time("add bit planes, full_add_mask ripple", tiles, Some(base), &mut || {
112+
let mut acc = 0;
113+
for j in 0..n {
114+
for (ak, bk) in pa[j].chunks_exact(w).zip(pb[j].chunks_exact(w)) {
115+
acc ^= planes_add(ak, bk, &mut op);
116+
}
117+
}
118+
acc
119+
});
120+
let base = time("fold SoA U64x8 (unsigned + signed)", tiles, None, &mut || soa_fold::<P>(&sa).0[0]);
121+
time("fold widened U32x16 (unsigned)", tiles, Some(base), &mut || {
122+
(0..n).map(|j| widened_fold::<P>(&wa[j])).sum()
123+
});
124+
time("fold bit planes, popcount (unsigned + signed)", tiles, Some(base), &mut || {
125+
(0..n).map(|j| planes_fold(&pa[j], w).0).sum()
126+
});
127+
}
128+
129+
fn main() {
130+
println!("D-SIMD-MICRO-MACRO-1 step 1, {}", tier());
131+
for tiles in [1 << 12, 1 << 16] {
132+
run::<U24x5>("U24x5", tiles);
133+
run::<U9x14>("U9x14", tiles);
134+
}
135+
}

0 commit comments

Comments
 (0)