|
| 1 | +//! One question over the whole 64×64 permutation field, answered by one fold. |
| 2 | +//! |
| 3 | +//! The expression is `f = ternlog(0x96)` (a XOR b XOR c) over a population of |
| 4 | +//! `N` 64-byte blocks. Each question is asked of the WHOLE field — all 4096 |
| 5 | +//! cells of a `PermTable12` — and has one answer. Two executions: |
| 6 | +//! |
| 7 | +//! - **per-cell**: 4096 standalone folds. Each composes its cell's map, |
| 8 | +//! shuffles the operands into that coordinate system, evaluates, and adds its |
| 9 | +//! answer to the total. |
| 10 | +//! - **hot fold**: the data and the accumulator stay resident, and the field |
| 11 | +//! streams through the one running fold as relations. Nothing is constructed |
| 12 | +//! per cell. A shared basis disappears under an invariant terminal. A |
| 13 | +//! relative basis or an output-coordinate mask stays in the fold through the |
| 14 | +//! field's exposure (a 64×64 kernel built from the two 64-entry tables), |
| 15 | +//! never as per-cell work. |
| 16 | +//! |
| 17 | +//! Ordered Keep is the one terminal that owes per-cell output. It is included |
| 18 | +//! to show where the per-cell cost genuinely remains. |
| 19 | +//! |
| 20 | +//! Every hot-fold answer is asserted equal to the per-cell answer. The |
| 21 | +//! counters count what this probe itself does: `evals` is the number of |
| 22 | +//! expression/predicate evaluations inside the fold, which is the incremental |
| 23 | +//! work that remains. |
| 24 | +//! |
| 25 | +//! ```sh |
| 26 | +//! cargo run --release --example perm_field_probe |
| 27 | +//! ``` |
| 28 | +
|
| 29 | +use ndarray::hpc::perm::{LaneOp, LaneTernlog, Perm64, PermBatch, PermTable12, Schedule}; |
| 30 | +use std::time::Instant; |
| 31 | + |
| 32 | +const N: usize = 64; |
| 33 | +const CELLS: u16 = 4096; |
| 34 | +type Block = [u8; 64]; |
| 35 | + |
| 36 | +#[derive(Default, Clone, Copy)] |
| 37 | +struct Counters { |
| 38 | + compositions: u64, |
| 39 | + shuffles: u64, |
| 40 | + evals: u64, |
| 41 | + bytes_written: u64, |
| 42 | +} |
| 43 | + |
| 44 | +struct Run<'a> { |
| 45 | + t: &'a PermTable12, |
| 46 | + c: Counters, |
| 47 | +} |
| 48 | + |
| 49 | +impl Run<'_> { |
| 50 | + fn compose(&mut self, code: u16) -> Perm64 { |
| 51 | + self.c.compositions += 1; |
| 52 | + self.t.for_code(code) |
| 53 | + } |
| 54 | + fn shuffle(&mut self, p: Perm64, b: &Block) -> Block { |
| 55 | + self.c.shuffles += 1; |
| 56 | + self.c.bytes_written += 64; |
| 57 | + p.materialize(b) |
| 58 | + } |
| 59 | + fn f(&mut self, a: &Block, b: &Block, c: &Block) -> Block { |
| 60 | + self.c.evals += 64; |
| 61 | + LaneTernlog(0x96).apply([a, b, c]) |
| 62 | + } |
| 63 | +} |
| 64 | + |
| 65 | +fn nonzero(v: &Block) -> u64 { |
| 66 | + v.iter().filter(|&&x| x != 0).count() as u64 |
| 67 | +} |
| 68 | + |
| 69 | +fn splitmix(s: &mut u64) -> u64 { |
| 70 | + *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15); |
| 71 | + let mut z = *s; |
| 72 | + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); |
| 73 | + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); |
| 74 | + z ^ (z >> 31) |
| 75 | +} |
| 76 | + |
| 77 | +fn shuffled(seed: u64) -> Perm64 { |
| 78 | + let mut s = seed; |
| 79 | + let mut idx: [u8; 64] = core::array::from_fn(|i| i as u8); |
| 80 | + for i in (1..64).rev() { |
| 81 | + idx.swap(i, (splitmix(&mut s) % (i as u64 + 1)) as usize); |
| 82 | + } |
| 83 | + Perm64::from_indices(idx).unwrap() |
| 84 | +} |
| 85 | + |
| 86 | +/// Sparse-ish bytes, so Count and Any are not saturated. |
| 87 | +fn population(seed: u64) -> Vec<Block> { |
| 88 | + let mut s = seed; |
| 89 | + (0..N) |
| 90 | + .map(|_| { |
| 91 | + core::array::from_fn(|_| { |
| 92 | + if splitmix(&mut s).is_multiple_of(3) { |
| 93 | + (splitmix(&mut s) & 0xFF) as u8 |
| 94 | + } else { |
| 95 | + 0 |
| 96 | + } |
| 97 | + }) |
| 98 | + }) |
| 99 | + .collect() |
| 100 | +} |
| 101 | + |
| 102 | +struct Row { |
| 103 | + name: &'static str, |
| 104 | + c: Counters, |
| 105 | + micros: f64, |
| 106 | +} |
| 107 | + |
| 108 | +fn run<T>(t: &PermTable12, name: &'static str, body: impl FnOnce(&mut Run) -> T) -> (T, Row) { |
| 109 | + let mut r = Run { |
| 110 | + t, |
| 111 | + c: Counters::default(), |
| 112 | + }; |
| 113 | + let start = Instant::now(); |
| 114 | + let out = body(&mut r); |
| 115 | + let micros = start.elapsed().as_secs_f64() * 1e6; |
| 116 | + (out, Row { name, c: r.c, micros }) |
| 117 | +} |
| 118 | + |
| 119 | +fn report(case: &str, answer: &str, rows: &[Row]) { |
| 120 | + println!("\n## {case}\nanswer: {answer} (all executions equal, asserted)"); |
| 121 | + println!( |
| 122 | + "{:<10} {:>12} {:>10} {:>12} {:>13} {:>11}", |
| 123 | + "execution", "compositions", "shuffles", "evals", "bytes_written", "µs" |
| 124 | + ); |
| 125 | + for r in rows { |
| 126 | + println!( |
| 127 | + "{:<10} {:>12} {:>10} {:>12} {:>13} {:>11.1}", |
| 128 | + r.name, r.c.compositions, r.c.shuffles, r.c.evals, r.c.bytes_written, r.micros |
| 129 | + ); |
| 130 | + } |
| 131 | +} |
| 132 | + |
| 133 | +fn main() { |
| 134 | + let table = PermTable12::new(core::array::from_fn(|i| shuffled(100 + i as u64))); |
| 135 | + let (a, b, c) = (population(1), population(2), population(3)); |
| 136 | + println!("field: 4096 cells; population: {N} blocks per operand"); |
| 137 | + |
| 138 | + // ---- 1. Shared basis, Count over the field --------------------------- |
| 139 | + // Σ over cells of Count(f(P·a, P·b, P·c)). |
| 140 | + { |
| 141 | + let (per_cell, r1) = run(&table, "per-cell", |r| { |
| 142 | + let mut total = 0u64; |
| 143 | + for code in 0..CELLS { |
| 144 | + let p = r.compose(code); |
| 145 | + for i in 0..N { |
| 146 | + let (pa, pb, pc) = (r.shuffle(p, &a[i]), r.shuffle(p, &b[i]), r.shuffle(p, &c[i])); |
| 147 | + total += nonzero(&r.f(&pa, &pb, &pc)); |
| 148 | + } |
| 149 | + } |
| 150 | + total |
| 151 | + }); |
| 152 | + let (hot, r2) = run(&table, "hot fold", |r| { |
| 153 | + // Equivariance moves P outside f, and Count removes it: every cell |
| 154 | + // contributes the same amount. One pass, then scale. |
| 155 | + let once: u64 = (0..N).map(|i| nonzero(&r.f(&a[i], &b[i], &c[i]))).sum(); |
| 156 | + once * CELLS as u64 |
| 157 | + }); |
| 158 | + assert_eq!(hot, per_cell); |
| 159 | + assert_eq!(r2.c.compositions + r2.c.shuffles, 0); |
| 160 | + report("shared basis, field-wide Count", &hot.to_string(), &[r1, r2]); |
| 161 | + } |
| 162 | + |
| 163 | + // ---- 2. Shared basis, Any over the field (stops at the first hit) ----- |
| 164 | + { |
| 165 | + let (per_cell, r1) = run(&table, "per-cell", |r| { |
| 166 | + for code in 0..CELLS { |
| 167 | + let p = r.compose(code); |
| 168 | + for i in 0..N { |
| 169 | + let (pa, pb, pc) = (r.shuffle(p, &a[i]), r.shuffle(p, &b[i]), r.shuffle(p, &c[i])); |
| 170 | + if nonzero(&r.f(&pa, &pb, &pc)) > 0 { |
| 171 | + return true; |
| 172 | + } |
| 173 | + } |
| 174 | + } |
| 175 | + false |
| 176 | + }); |
| 177 | + let (hot, r2) = run(&table, "hot fold", |r| (0..N).any(|i| nonzero(&r.f(&a[i], &b[i], &c[i])) > 0)); |
| 178 | + assert_eq!(hot, per_cell); |
| 179 | + report("shared basis, field-wide Any (early exit)", &hot.to_string(), &[r1, r2]); |
| 180 | + } |
| 181 | + |
| 182 | + // ---- 3. Different bases, Count over the field ------------------------- |
| 183 | + // a is seen through P(cell); b and c through one fixed Q. The relation |
| 184 | + // R = Q relative to P changes with the cell and survives Count, so it |
| 185 | + // stays in the fold as the re-based field's exposure. |
| 186 | + { |
| 187 | + let q = table.for_code(0x5A5); |
| 188 | + let (per_cell, r1) = run(&table, "per-cell", |r| { |
| 189 | + let mut total = 0u64; |
| 190 | + for code in 0..CELLS { |
| 191 | + let p = r.compose(code); |
| 192 | + for i in 0..N { |
| 193 | + let (pa, qb, qc) = (r.shuffle(p, &a[i]), r.shuffle(q, &b[i]), r.shuffle(q, &c[i])); |
| 194 | + total += nonzero(&r.f(&pa, &qb, &qc)); |
| 195 | + } |
| 196 | + } |
| 197 | + total |
| 198 | + }); |
| 199 | + let (hot, r2) = run(&table, "hot fold", |r| { |
| 200 | + let e = r.t.relative_field(q).exposure(); |
| 201 | + r.c.compositions += 128; // building the two re-based 64-entry tables |
| 202 | + let mut total = 0u64; |
| 203 | + for i in 0..N { |
| 204 | + let (ab, bb, cb) = (&a[i], &b[i], &c[i]); |
| 205 | + let mut evals = 0u64; |
| 206 | + total += e.count_where(|lane, src| { |
| 207 | + evals += 1; |
| 208 | + ab[lane] ^ bb[src] ^ cb[src] != 0 |
| 209 | + }); |
| 210 | + r.c.evals += evals; |
| 211 | + } |
| 212 | + total |
| 213 | + }); |
| 214 | + assert_eq!(hot, per_cell); |
| 215 | + report("different bases (P per cell, fixed Q), field-wide Count", &hot.to_string(), &[r1, r2]); |
| 216 | + } |
| 217 | + |
| 218 | + // ---- 4. Mask fixed in OUTPUT coordinates, Count over the field --------- |
| 219 | + // Σ over cells of Count(M ∧ P·f(a,b,c)). M does not move with P; it takes |
| 220 | + // part in the fold through the exposure instead of per-cell transport. |
| 221 | + { |
| 222 | + let masks: Vec<u64> = { |
| 223 | + let mut s = 77; |
| 224 | + (0..N).map(|_| splitmix(&mut s)).collect() |
| 225 | + }; |
| 226 | + let (per_cell, r1) = run(&table, "per-cell", |r| { |
| 227 | + let mut total = 0u64; |
| 228 | + for code in 0..CELLS { |
| 229 | + let p = r.compose(code); |
| 230 | + for i in 0..N { |
| 231 | + let (pa, pb, pc) = (r.shuffle(p, &a[i]), r.shuffle(p, &b[i]), r.shuffle(p, &c[i])); |
| 232 | + let v = r.f(&pa, &pb, &pc); |
| 233 | + total += (0..64) |
| 234 | + .filter(|&l| masks[i] >> l & 1 == 1 && v[l] != 0) |
| 235 | + .count() as u64; |
| 236 | + } |
| 237 | + } |
| 238 | + total |
| 239 | + }); |
| 240 | + let (hot, r2) = run(&table, "hot fold", |r| { |
| 241 | + let e = r.t.exposure(); |
| 242 | + let mut total = 0u64; |
| 243 | + for i in 0..N { |
| 244 | + let v = r.f(&a[i], &b[i], &c[i]); |
| 245 | + let m = masks[i]; |
| 246 | + let mut evals = 0u64; |
| 247 | + total += e.count_where(|lane, src| { |
| 248 | + evals += 1; |
| 249 | + m >> lane & 1 == 1 && v[src] != 0 |
| 250 | + }); |
| 251 | + r.c.evals += evals; |
| 252 | + } |
| 253 | + total |
| 254 | + }); |
| 255 | + assert_eq!(hot, per_cell); |
| 256 | + report("mask fixed in output coordinates, field-wide Count", &hot.to_string(), &[r1, r2]); |
| 257 | + } |
| 258 | + |
| 259 | + // ---- 5. Ordered Keep: the one terminal that owes per-cell output ------ |
| 260 | + { |
| 261 | + let (per_cell, r1) = run(&table, "per-cell", |r| { |
| 262 | + let mut out = Vec::with_capacity(CELLS as usize * N); |
| 263 | + for code in 0..CELLS { |
| 264 | + let p = r.compose(code); |
| 265 | + for i in 0..N { |
| 266 | + let (pa, pb, pc) = (r.shuffle(p, &a[i]), r.shuffle(p, &b[i]), r.shuffle(p, &c[i])); |
| 267 | + let v = r.f(&pa, &pb, &pc); |
| 268 | + out.push(v); |
| 269 | + r.c.bytes_written += 64; |
| 270 | + } |
| 271 | + } |
| 272 | + out |
| 273 | + }); |
| 274 | + let (hot, r2) = run(&table, "hot fold", |r| { |
| 275 | + // Evaluate once in the shared basis; each cell is owed only its |
| 276 | + // coordinates of the result. |
| 277 | + let v: Vec<Block> = (0..N).map(|i| r.f(&a[i], &b[i], &c[i])).collect(); |
| 278 | + let mut out = Vec::with_capacity(CELLS as usize * N); |
| 279 | + for code in 0..CELLS { |
| 280 | + let p = r.compose(code); |
| 281 | + for blk in &v { |
| 282 | + out.push(r.shuffle(p, blk)); |
| 283 | + } |
| 284 | + } |
| 285 | + out |
| 286 | + }); |
| 287 | + assert_eq!(hot, per_cell); |
| 288 | + report("shared basis, ordered Keep (per-cell output owed)", "4096 × N blocks", &[r1, r2]); |
| 289 | + } |
| 290 | + |
| 291 | + // ---- 6. Duplicate-heavy request stream, ordered Keep ------------------ |
| 292 | + { |
| 293 | + let mut s = 5; |
| 294 | + let stream: Vec<u16> = (0..20_000) |
| 295 | + .map(|_| (splitmix(&mut s) % 300) as u16 * 13 % CELLS) |
| 296 | + .collect(); |
| 297 | + let src = LaneTernlog(0x96).apply([&a[0], &b[0], &c[0]]); |
| 298 | + let (per_request, r1) = run(&table, "per-cell", |r| { |
| 299 | + stream |
| 300 | + .iter() |
| 301 | + .map(|&code| { |
| 302 | + let p = r.compose(code); |
| 303 | + r.shuffle(p, &src) |
| 304 | + }) |
| 305 | + .collect::<Vec<Block>>() |
| 306 | + }); |
| 307 | + let (dedup, r2) = run(&table, "dedup", |r| { |
| 308 | + let composed = PermBatch::new(&stream, Schedule::Deduplicate).compose(r.t); |
| 309 | + r.c.compositions += composed.compositions() as u64; |
| 310 | + let mut out = vec![[0u8; 64]; stream.len()]; |
| 311 | + composed.materialize_into(&src, &mut out); |
| 312 | + r.c.shuffles += out.len() as u64; |
| 313 | + r.c.bytes_written += out.len() as u64 * 64; |
| 314 | + out |
| 315 | + }); |
| 316 | + assert_eq!(dedup, per_request, "multiplicity and order must survive deduplication"); |
| 317 | + let distinct = PermBatch::new(&stream, Schedule::Deduplicate) |
| 318 | + .field() |
| 319 | + .unwrap() |
| 320 | + .len(); |
| 321 | + report( |
| 322 | + &format!("{} requests over {distinct} distinct cells, ordered Keep", stream.len()), |
| 323 | + "one block per request, in request order", |
| 324 | + &[r1, r2], |
| 325 | + ); |
| 326 | + } |
| 327 | +} |
0 commit comments