Skip to content
Merged
Show file tree
Hide file tree
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
15 changes: 15 additions & 0 deletions .claude/blackboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -4010,3 +4010,18 @@ laws, ExtentUnsupported, row bounds) and WITNESS in OrderedLaneWitness; no new t
- 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.
- 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`.
- 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.

## 2026-10-30 — D-SIMD-MICRO-MACRO-1 step 1: packed microtile oracle (lab only)

- `examples/microtile/` + `tests/microtile.rs`: 128-bit tiles of `5×U24/I24` (+8 metadata
bits) and `14×U9/I9` (+2 spare), field-isolated add/sub/carry/borrow/signed overflow,
intra-tile stencil, exact unsigned+signed fold; u128, SoA `U64x8`, widened `U32x16`,
bit-plane (`full_add_mask`) realizations, all against one unpacked oracle.
- 9 tests green on AVX-512, AVX2, AVX, v2, NEON (qemu), nightly. Disable runs D1–D13 red
except D6 (redundant mask, removed).
- Cost gate: no single winner. U24x5 → widened for add and fold; U9x14 → packed SoA for
L2-resident add, bit planes for the fold. u128 alone never wins (not vectorized).
- Found: NEON/wasm `U32x16` lacks `&`, `|`, `-`, `!`, `+=`, `from_slice`, `copy_to_slice`.
Recorded, not fixed.
- Open: steps 2–4 (KPEA method binding, Thought-0 delta reuse, interlacing). H13 stays
deferred. Report: `.claude/knowledge/microtile-probe.md`.
114 changes: 114 additions & 0 deletions .claude/knowledge/microtile-probe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# D-SIMD-MICRO-MACRO-1, step 1: the packed microtile oracle

Lab fixture only: `examples/microtile/` (realizations + cost gate) and
`tests/microtile.rs` (exactness). No public type, no backend change, no
production caller. Steps 2+ (binding to a KPEA method, Thought-0 delta reuse,
interlacing) are not started.

## What the microtile is

One 128-bit tile carries several isolated arithmetic fields:

| layout | fields | payload | other bits |
|---|---|---|---|
| `U24x5` | 5 × 24 bit (`U24`/`I24`) | 120 bits | 8 metadata bits, 120..128 |
| `U9x14` | 14 × 9 bit (`U9`/`I9`) | 126 bits | 2 spare bits, 126..128 |

Operations: field-isolated wrapping add and sub, per-field unsigned carry and
borrow out, per-field signed overflow, an intra-tile stencil
`f[j-1] + 2 f[j] + f[j+1]`, and an exact fold (unsigned and signed per-field
sums over many tiles). Every result keeps the left operand's metadata.

Isolation is the SWAR law: clear each field's top bit in the addends so a
carry stops at its own top bit, then fix that bit with
`(a ^ b) & HIGH`; for sub, set each top bit of `a` so a borrow stops there.
Signed fold: `Σ sext(f) = Σ f - 2^W · #(top bit set)`, so one pass yields
both sums.

## Four realizations, one oracle

The oracle unpacks every field and does the arithmetic in `u64`/`i64`.

1. **u128**: the tile as one 128-bit integer.
2. **SoA `U64x8`**: 8 tiles per op, the tile split into a low and a high
64-bit plane. The fields that straddle bit 64 (`U24` field 2, `U9` field
7) take an explicit carry/borrow out of bit 63, computed with lane-wise
logic only (no shuffle).
3. **Widened `U32x16`**: field-major `u32` lanes. The fold flushes to `u64`
every `floor((2^32-1)/(2^W-1))` steps (256 for `U24`).
4. **Bit planes**: one bit per tile, 64 tiles per word, add by ripple of
`simd::full_add_mask`, fold by `Σ_k 2^k popcount(plane_k)`.

Exactness: 9 tests over random tiles plus every pairing of edge field values
(0, 1, sign bit - 1, sign bit, max - 1, max), the two straddling fields'
cross-word carry and borrow, 8192 maximal `U24` values in one `u32` lane, and
metadata against an all-ones right operand. Green on AVX-512, AVX2,
AVX-without-AVX2 (qemu SandyBridge), x86-64-v2 (qemu Nehalem), NEON (qemu
aarch64) and nightly `core::simd`.

Disable runs, each red: top bit not cleared in add (D1) or set in sub (D8),
no carry (D2) or borrow (D7) across bit 64, metadata dropped (D3), signed
fold without the top-bit correction (D4) and its plane form (D9), widened
fold never flushing (D5), carry-out as `a & b` only (D10), straddling field
read from the low word only (D11), fold tail skipped (D12), widened add
unmasked (D13). D6 (an extra mask on the stencil's left shift) stayed green:
`add` already masks its right operand, so the mask was removed rather than
kept as a guard that does not bind.

## Cost gate

`cargo run --release --example microtile --features std`, median ns per tile
over 31 runs, three runs (two AVX-512 v4, one AVX2 v3) on a 4-core host.
4096 tiles = 64 KB per operand (L2-resident); 65536 tiles = 1 MB.

| op | layout | u128 | SoA U64x8 | widened | planes |
|---|---|---|---|---|---|
| add, 4K | U24x5 | 2.00–2.10 | 0.95–1.73 | **0.69–0.71** | 1.87–1.93 |
| add, 4K | U9x14 | 2.03–2.10 | **1.08–1.66** | 1.92–1.97 | 1.83–2.03 |
| add, 64K | U24x5 | 2.29–2.31* | 1.94–3.16 | 2.18–2.35 | **1.88–2.07** |
| add, 64K | U9x14 | 2.04–2.41 | 1.54–2.81 | 6.19–6.39 | **1.87–2.06** |
| fold, 4K | U24x5 | — | 0.60–1.01 | **0.29–0.30** | 0.85–0.90 |
| fold, 4K | U9x14 | — | 2.71–3.60 | 0.76–1.20 | **0.73–1.42** |
| fold, 64K | U24x5 | — | 0.53–0.93 | **0.28–0.31** | 0.75–0.82 |
| fold, 64K | U9x14 | — | 2.81–3.22 | 1.85–2.16 | **0.60–0.76** |

\* one outlier at 4.19 excluded.

Storage: packed and planes 16 B/tile; widened 20 B/tile (`U24x5`) and
56 B/tile (`U9x14`).

Read:

- **No realization wins everything, so packed is not assumed.** Wide fields
(`U24x5`) favour widened lanes for both add and fold: only 25% more bytes,
and one native add per field. Narrow fields (`U9x14`) favour packed SoA for
cache-resident add and bit planes for the fold; widened costs 3.5× the
bytes and is 3× slower once the data leaves L2.
- **The single u128 is never the fast form.** It runs at about 2 ns/tile
whatever the layout: rustc does not vectorize the two-word add. The packed
win comes from the SoA split, which puts 8 tiles in each register.
- **At 1 MB every add is about 2 ns/tile** except widened `U9x14`: the
operation is bandwidth-bound and the bytes per tile decide.
- **Bit planes earn their place on folds, not adds.** A narrow-field fold is
W popcounts per 64 tiles; their add needs W full adders and is no faster
than packed.
- Unexplained: SoA add is faster on AVX2 (0.95–1.08 at 4K) than on AVX-512
(1.61–1.73). Not diagnosed.

## Findings outside the fixture

- **NEON and wasm `U32x16` lack most of the facade:** no `&`, `|`, `-`,
`!`, `+=`, `from_slice` or `copy_to_slice`; they implement `+`, `^`, the
two shifts and `from_array`/`to_array`. AVX-512, AVX2, scalar and nightly
have the full set. The fixture works around it (mask by `<< k >> k`, load
through `from_array`), so this is recorded, not fixed here. It is a real
parity gap for any consumer that builds on `U32x16`.

## What this does not show yet

- No consumer: the gates for binding to a KPEA mask/count method (step 2),
Thought-0 invariant reuse across thought deltas (step 3) and interlacing
(step 4) are open. The fold here is the candidate primitive for step 2.
- Pack/unpack between layouts is untimed; a method that converts would pay it.
- Only x86 was timed; NEON and the other tiers ran under emulation for
exactness only.
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ name = "u9_domino"
path = "examples/u9_domino/main.rs"
required-features = ["std"]

[[example]]
name = "microtile"
path = "examples/microtile/main.rs"
required-features = ["std"]

[[example]]
name = "cpu_guard_probe"
required-features = ["std"]
Expand Down
135 changes: 135 additions & 0 deletions examples/microtile/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
//! D-SIMD-MICRO-MACRO-1 step 1 cost gate: field-isolated add and exact fold
//! over 64K microtiles, in four realizations. Every realization is checked
//! against the unpacked oracle before it is timed.
//! `cargo run --release --example microtile --features std`.

// The tests exercise sub and the stencil; the bench times add and fold only.
#[allow(dead_code)]
mod tile;

use std::hint::black_box;
use std::time::Instant;
use tile::*;

fn tier() -> &'static str {
if cfg!(target_feature = "avx512bw") {
"x86_64 AVX-512"
} else if cfg!(target_feature = "avx2") {
"x86_64 AVX2"
} else if cfg!(target_arch = "aarch64") {
"aarch64 NEON"
} else {
"other"
}
}

fn xorshift(s: &mut u64) -> u64 {
*s ^= *s << 13;
*s ^= *s >> 7;
*s ^= *s << 17;
*s
}

/// Median ns per tile over 31 runs.
fn time(name: &str, tiles: usize, base: Option<f64>, f: &mut dyn FnMut() -> u64) -> f64 {
black_box(f());
let mut t: Vec<f64> = (0..31)
.map(|_| {
let t0 = Instant::now();
black_box(f());
t0.elapsed().as_nanos() as f64 / tiles as f64
})
.collect();
t.sort_by(f64::total_cmp);
let med = t[15];
let rel = base
.map(|b| format!(" {:6.2}x", med / b))
.unwrap_or_default();
println!(" {name:<44} {med:8.3} ns/tile{rel}");
med
}

fn run<P: Packing>(label: &str, tiles: usize) {
let mut s = 0x9E37_79B9_7F4A_7C15u64;
let a: Vec<u128> = (0..tiles)
.map(|_| (xorshift(&mut s) as u128) << 64 | xorshift(&mut s) as u128)
.collect();
let b: Vec<u128> = (0..tiles)
.map(|_| (xorshift(&mut s) as u128) << 64 | xorshift(&mut s) as u128)
.collect();
let ((alo, ahi), (blo, bhi)) = (split(&a), split(&b));
let (sa, sb) = (Soa { lo: &alo, hi: &ahi }, Soa { lo: &blo, hi: &bhi });
let (wa, wb) = (widen::<P>(&a), widen::<P>(&b));
let (pa, pb) = (to_planes::<P>(&a), to_planes::<P>(&b));
let (n, w) = (P::N as usize, P::W as usize);

// Exactness before timing.
let want: Vec<u128> = a
.iter()
.zip(&b)
.map(|(&x, &y)| oracle_add::<P>(x, y).0)
.collect();
let mut out128 = vec![0u128; tiles];
for t in 0..tiles {
out128[t] = add::<P>(a[t], b[t]);
}
assert_eq!(out128, want, "{label} u128 add");
let (mut ol, mut oh) = (vec![0u64; tiles], vec![0u64; tiles]);
soa_add_slices::<P>(&sa, &sb, &mut ol, &mut oh);
assert!((0..tiles).all(|t| (ol[t] as u128 | (oh[t] as u128) << 64) == want[t]), "{label} soa add");
let mut ow = vec![vec![0u32; tiles]; n];
for j in 0..n {
widened_add::<P>(&wa[j], &wb[j], &mut ow[j]);
assert!((0..tiles).all(|t| ow[j][t] == get::<P>(want[t], j as u32)), "{label} widened add");
}
let (fu, fs) = oracle_fold::<P>(&a);
assert_eq!(soa_fold::<P>(&sa), (fu.clone(), fs.clone()), "{label} soa fold");
for j in 0..n {
assert_eq!(widened_fold::<P>(&wa[j]), fu[j], "{label} widened fold");
assert_eq!(planes_fold(&pa[j], w), (fu[j], fs[j]), "{label} planes fold");
}

println!("{label}: {n} fields x {w} bits, {tiles} tiles, all realizations exact");
println!(" storage: packed 16 B/tile, widened {} B/tile, planes 16 B/tile", 4 * n);
let base = time("add u128 (one 128-bit word)", tiles, None, &mut || {
for t in 0..tiles {
out128[t] = add::<P>(a[t], b[t]);
}
out128[tiles - 1] as u64
});
time("add SoA U64x8 (8 tiles per op)", tiles, Some(base), &mut || {
soa_add_slices::<P>(&sa, &sb, &mut ol, &mut oh);
ol[tiles - 1]
});
time("add widened U32x16 (one field per lane)", tiles, Some(base), &mut || {
for j in 0..n {
widened_add::<P>(&wa[j], &wb[j], &mut ow[j]);
}
ow[0][tiles - 1] as u64
});
let mut op = vec![0u64; w];
time("add bit planes, full_add_mask ripple", tiles, Some(base), &mut || {
let mut acc = 0;
for j in 0..n {
for (ak, bk) in pa[j].chunks_exact(w).zip(pb[j].chunks_exact(w)) {
acc ^= planes_add(ak, bk, &mut op);
Comment on lines +114 to +115

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Materialize and consume the bit-plane addition results

In the release cost gate, every bit-plane block writes its sum into the same op scratch buffer, but that buffer is never read; only the carry masks reach black_box. Because planes_add is inlined, the optimizer can eliminate the sum calculations and stores, leaving a carry-only benchmark. Even without that optimization, this path avoids writing a complete result dataset, unlike the other three addition paths. This makes the reported addition timings and bandwidth conclusions incomparable. Store each block's sum in a full output buffer and pass that output through black_box inside the timed closure.

Useful? React with 👍 / 👎.

}
}
acc
});
let base = time("fold SoA U64x8 (unsigned + signed)", tiles, None, &mut || soa_fold::<P>(&sa).0[0]);
time("fold widened U32x16 (unsigned)", tiles, Some(base), &mut || {
(0..n).map(|j| widened_fold::<P>(&wa[j])).sum()
});
time("fold bit planes, popcount (unsigned + signed)", tiles, Some(base), &mut || {
(0..n).map(|j| planes_fold(&pa[j], w).0).sum()
});
}

fn main() {
println!("D-SIMD-MICRO-MACRO-1 step 1, {}", tier());
for tiles in [1 << 12, 1 << 16] {
run::<U24x5>("U24x5", tiles);
run::<U9x14>("U9x14", tiles);
}
}
Loading
Loading