Repository navigation
D-SIMD-MICRO-MACRO-1 step 1: packed microtile oracle (lab fixture) #406
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+968
−0
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| } | ||
| 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); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the release cost gate, every bit-plane block writes its sum into the same
opscratch buffer, but that buffer is never read; only the carry masks reachblack_box. Becauseplanes_addis 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 throughblack_boxinside the timed closure.Useful? React with 👍 / 👎.