Skip to content

Commit 368e01d

Browse files
committed
review: gate gridlake_field_tile to x86_64 + use canonical simd surface (#233, codex P2)
The example imported the x86_64-only hpc::amx_matmul / hpc::bf16_tile_gemm paths under required-features = ["std"], so a cross-target `cargo check --examples` would fail on non-x86 with unresolved imports. Fixes: - Wrap the whole probe in `#[cfg(target_arch = "x86_64")] mod amx { ... }` with a `#[cfg(not(target_arch = "x86_64"))]` fallback `main` that prints the x86-only requirement. Non-x86 builds now compile (and skip) cleanly. - Switch to the canonical `ndarray::simd::*` surface (per the W1a consumer contract — consumers import from `ndarray::simd`, not raw `hpc::*`): `amx_available`, `bf16_tile_gemm_16x16_amx` (the TDPBF16PS tile wrapper, aliased to the body's name), `bf16_tile_gemm_tier` — all arch-gated re-exports. Behaviour unchanged on x86: still runs the real AMX TDPBF16PS tile op (amx_available = true), 0.095% / 0.401% Frobenius, bit-exact on integer operands. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MLBnPuScZy6w9di2QEjsXM
1 parent 89bbcf8 commit 368e01d

1 file changed

Lines changed: 190 additions & 170 deletions

File tree

examples/gridlake_field_tile.rs

Lines changed: 190 additions & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -30,195 +30,204 @@
3030
//!
3131
//! Run: `cargo run --release --example gridlake_field_tile --features std`
3232
33-
use ndarray::hpc::amx_matmul::amx_available;
34-
use ndarray::hpc::bf16_tile_gemm::{bf16_tile_gemm_16x16, bf16_tile_gemm_tier};
35-
use ndarray::hpc::quantized::{f32_to_bf16_rounded, BF16};
36-
37-
const M: usize = 16; // tile edge — the AMX tile / gridlake quadrant
38-
const KPAD: usize = 32; // bf16_tile_gemm requires K a multiple of 32; pad 16→32 with zeros
39-
40-
fn mix(mut z: u64) -> u64 {
41-
z = z.wrapping_add(0x9E37_79B9_7F4A_7C15);
42-
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
43-
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
44-
z ^ (z >> 31)
45-
}
46-
47-
/// Pack a row-major `M×KPAD` (or `KPAD×M`) f32 matrix into the `&[u16]` bf16
48-
/// operand the tile GEMM consumes (RNE, matching `VCVTNEPS2BF16`).
49-
fn to_bf16(src: &[f32]) -> Vec<u16> {
50-
let mut b = vec![BF16(0); src.len()];
51-
f32_to_bf16_rounded(src, &mut b);
52-
b.iter().map(|x| x.0).collect()
53-
}
33+
// The AMX / BF16-tile path is x86_64-only: `hpc::amx_matmul` and
34+
// `hpc::bf16_tile_gemm` are `#[cfg(target_arch = "x86_64")]`, and the canonical
35+
// `ndarray::simd::*` re-exports of the tile ladder are arch-gated to match. Wrap
36+
// the whole probe in an x86 module with a non-x86 fallback `main`, so a
37+
// cross-target `cargo check --examples` does not hit unresolved imports.
38+
#[cfg(target_arch = "x86_64")]
39+
mod amx {
40+
// Canonical public surface (`ndarray::simd::*`, per the W1a consumer contract),
41+
// not the raw `hpc::*` paths. `bf16_tile_gemm_16x16_amx` is the tile-dispatching
42+
// TDPBF16PS wrapper (aliased to the name the body already uses).
43+
use ndarray::hpc::quantized::{f32_to_bf16_rounded, BF16};
44+
use ndarray::simd::{amx_available, bf16_tile_gemm_16x16_amx as bf16_tile_gemm_16x16, bf16_tile_gemm_tier};
45+
46+
const M: usize = 16; // tile edge — the AMX tile / gridlake quadrant
47+
const KPAD: usize = 32; // bf16_tile_gemm requires K a multiple of 32; pad 16→32 with zeros
48+
49+
fn mix(mut z: u64) -> u64 {
50+
z = z.wrapping_add(0x9E37_79B9_7F4A_7C15);
51+
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
52+
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
53+
z ^ (z >> 31)
54+
}
5455

55-
/// `C[16×16] = A[16×16] · B[16×16]` via the SHIPPED BF16 tile op. A/B are padded
56-
/// on K from 16→32 with zeros (the pad contributes nothing), so this is one
57-
/// `bf16_tile_gemm_16x16` — one `TDPBF16PS` tile op on the AMX tier.
58-
fn tile_matmul(a: &[f32; M * M], b: &[f32; M * M]) -> [f32; M * M] {
59-
let mut a_pad = [0.0f32; M * KPAD];
60-
let mut b_pad = [0.0f32; KPAD * M];
61-
for i in 0..M {
62-
for k in 0..M {
63-
a_pad[i * KPAD + k] = a[i * M + k]; // A[16×32], cols 16..31 = 0
64-
b_pad[k * M + i] = b[k * M + i]; // B[32×16], rows 16..31 = 0 (already)
65-
}
56+
/// Pack a row-major `M×KPAD` (or `KPAD×M`) f32 matrix into the `&[u16]` bf16
57+
/// operand the tile GEMM consumes (RNE, matching `VCVTNEPS2BF16`).
58+
fn to_bf16(src: &[f32]) -> Vec<u16> {
59+
let mut b = vec![BF16(0); src.len()];
60+
f32_to_bf16_rounded(src, &mut b);
61+
b.iter().map(|x| x.0).collect()
6662
}
67-
let (ab, bb) = (to_bf16(&a_pad), to_bf16(&b_pad));
68-
let mut c = vec![0.0f32; M * M];
69-
bf16_tile_gemm_16x16(&ab, &bb, &mut c, KPAD);
70-
let mut out = [0.0f32; M * M];
71-
out.copy_from_slice(&c);
72-
out
73-
}
7463

75-
/// Direct `f64` reference GEMM `C = A·B` (16×16).
76-
fn direct_matmul(a: &[f32; M * M], b: &[f32; M * M]) -> [f64; M * M] {
77-
let mut c = [0.0f64; M * M];
78-
for i in 0..M {
79-
for j in 0..M {
80-
let mut s = 0.0f64;
64+
/// `C[16×16] = A[16×16] · B[16×16]` via the SHIPPED BF16 tile op. A/B are padded
65+
/// on K from 16→32 with zeros (the pad contributes nothing), so this is one
66+
/// `bf16_tile_gemm_16x16` — one `TDPBF16PS` tile op on the AMX tier.
67+
fn tile_matmul(a: &[f32; M * M], b: &[f32; M * M]) -> [f32; M * M] {
68+
let mut a_pad = [0.0f32; M * KPAD];
69+
let mut b_pad = [0.0f32; KPAD * M];
70+
for i in 0..M {
8171
for k in 0..M {
82-
s += a[i * M + k] as f64 * b[k * M + j] as f64;
72+
a_pad[i * KPAD + k] = a[i * M + k]; // A[16×32], cols 16..31 = 0
73+
b_pad[k * M + i] = b[k * M + i]; // B[32×16], rows 16..31 = 0 (already)
8374
}
84-
c[i * M + j] = s;
8575
}
76+
let (ab, bb) = (to_bf16(&a_pad), to_bf16(&b_pad));
77+
let mut c = vec![0.0f32; M * M];
78+
bf16_tile_gemm_16x16(&ab, &bb, &mut c, KPAD);
79+
let mut out = [0.0f32; M * M];
80+
out.copy_from_slice(&c);
81+
out
8682
}
87-
c
88-
}
8983

90-
fn transpose(a: &[f32; M * M]) -> [f32; M * M] {
91-
let mut t = [0.0f32; M * M];
92-
for i in 0..M {
93-
for j in 0..M {
94-
t[j * M + i] = a[i * M + j];
84+
/// Direct `f64` reference GEMM `C = A·B` (16×16).
85+
fn direct_matmul(a: &[f32; M * M], b: &[f32; M * M]) -> [f64; M * M] {
86+
let mut c = [0.0f64; M * M];
87+
for i in 0..M {
88+
for j in 0..M {
89+
let mut s = 0.0f64;
90+
for k in 0..M {
91+
s += a[i * M + k] as f64 * b[k * M + j] as f64;
92+
}
93+
c[i * M + j] = s;
94+
}
9595
}
96+
c
9697
}
97-
t
98-
}
9998

100-
/// `(max_abs_err, frobenius_relative_err)` of a tile-GEMM result vs the f64
101-
/// direct. Frobenius-relative `‖tile − direct‖_F / ‖direct‖_F` is the honest
102-
/// aggregate precision — a per-element max-relative blows up on the near-zero
103-
/// entries that a cancellation-heavy product (e.g. `Mᵀ·Σ·M`) naturally has,
104-
/// which measures the cancellation, not the kernel.
105-
fn errors(tile: &[f32; M * M], direct: &[f64; M * M]) -> (f64, f64) {
106-
let mut max_abs = 0.0f64;
107-
let mut num = 0.0f64; // ‖E‖_F²
108-
let mut den = 0.0f64; // ‖direct‖_F²
109-
for i in 0..M * M {
110-
let e = tile[i] as f64 - direct[i];
111-
max_abs = max_abs.max(e.abs());
112-
num += e * e;
113-
den += direct[i] * direct[i];
99+
fn transpose(a: &[f32; M * M]) -> [f32; M * M] {
100+
let mut t = [0.0f32; M * M];
101+
for i in 0..M {
102+
for j in 0..M {
103+
t[j * M + i] = a[i * M + j];
104+
}
105+
}
106+
t
114107
}
115-
(max_abs, (num / den.max(1e-12)).sqrt())
116-
}
117108

118-
fn main() {
119-
println!("Gridlake field interdependency = BF16 16×16 AMX tile op — measured");
120-
println!(" tile M=16 (the AMX tile; 64×64 gridlake = 4×4 = 16 of these), K padded 16→32");
121-
println!(
122-
" shipped kernel tier on THIS host: {} (amx_available = {})\n",
123-
bf16_tile_gemm_tier(),
124-
amx_available()
125-
);
126-
127-
// ── field tile X: a correlated gridlake patch (smooth → strong interdependency)
128-
let mut x = [0.0f32; M * M];
129-
for r in 0..M {
130-
for c in 0..M {
131-
x[r * M + c] = 4.0 * (r as f32 * 0.4).sin() * (c as f32 * 0.35).cos() + 0.5 * (r as f32 - c as f32);
109+
/// `(max_abs_err, frobenius_relative_err)` of a tile-GEMM result vs the f64
110+
/// direct. Frobenius-relative `‖tile − direct‖_F / ‖direct‖_F` is the honest
111+
/// aggregate precision — a per-element max-relative blows up on the near-zero
112+
/// entries that a cancellation-heavy product (e.g. `Mᵀ·Σ·M`) naturally has,
113+
/// which measures the cancellation, not the kernel.
114+
fn errors(tile: &[f32; M * M], direct: &[f64; M * M]) -> (f64, f64) {
115+
let mut max_abs = 0.0f64;
116+
let mut num = 0.0f64; // ‖E‖_F²
117+
let mut den = 0.0f64; // ‖direct‖_F²
118+
for i in 0..M * M {
119+
let e = tile[i] as f64 - direct[i];
120+
max_abs = max_abs.max(e.abs());
121+
num += e * e;
122+
den += direct[i] * direct[i];
132123
}
124+
(max_abs, (num / den.max(1e-12)).sqrt())
133125
}
134126

135-
// ── (1) intercorrelation C = Xᵀ·X — the inter-cell coupling as a field op ──
136-
let xt = transpose(&x);
137-
let corr_tile = tile_matmul(&xt, &x);
138-
let corr_direct = direct_matmul(&xt, &x);
139-
let (ca, cr) = errors(&corr_tile, &corr_direct);
140-
println!(" (1) intercorrelation C = Xᵀ·X (one tile op)");
141-
println!(" max_abs_err={ca:.4} frobenius_rel_err={:.3}% (BF16-precision class)", cr * 100.0);
142-
143-
// ── (2) covariance sandwich Σ' = Mᵀ·Σ·M — the splat EWA projection shape ──
144-
// Σ = a symmetric PSD covariance (Xᵀ·X); M = a coupling/projection matrix.
145-
let sigma_f: [f32; M * M] = {
146-
let mut s = [0.0f32; M * M];
147-
for i in 0..M * M {
148-
s[i] = corr_direct[i] as f32;
127+
pub fn run() {
128+
println!("Gridlake field interdependency = BF16 16×16 AMX tile op — measured");
129+
println!(" tile M=16 (the AMX tile; 64×64 gridlake = 4×4 = 16 of these), K padded 16→32");
130+
println!(
131+
" shipped kernel tier on THIS host: {} (amx_available = {})\n",
132+
bf16_tile_gemm_tier(),
133+
amx_available()
134+
);
135+
136+
// ── field tile X: a correlated gridlake patch (smooth → strong interdependency)
137+
let mut x = [0.0f32; M * M];
138+
for r in 0..M {
139+
for c in 0..M {
140+
x[r * M + c] = 4.0 * (r as f32 * 0.4).sin() * (c as f32 * 0.35).cos() + 0.5 * (r as f32 - c as f32);
141+
}
149142
}
150-
s
151-
};
152-
let mut m_proj = [0.0f32; M * M];
153-
for r in 0..M {
154-
for c in 0..M {
155-
m_proj[r * M + c] = if r == c {
156-
1.0
157-
} else {
158-
0.15 * (r as f32 - c as f32).signum()
159-
};
143+
144+
// ── (1) intercorrelation C = Xᵀ·X — the inter-cell coupling as a field op ──
145+
let xt = transpose(&x);
146+
let corr_tile = tile_matmul(&xt, &x);
147+
let corr_direct = direct_matmul(&xt, &x);
148+
let (ca, cr) = errors(&corr_tile, &corr_direct);
149+
println!(" (1) intercorrelation C = Xᵀ·X (one tile op)");
150+
println!(" max_abs_err={ca:.4} frobenius_rel_err={:.3}% (BF16-precision class)", cr * 100.0);
151+
152+
// ── (2) covariance sandwich Σ' = Mᵀ·Σ·M — the splat EWA projection shape ──
153+
// Σ = a symmetric PSD covariance (Xᵀ·X); M = a coupling/projection matrix.
154+
let sigma_f: [f32; M * M] = {
155+
let mut s = [0.0f32; M * M];
156+
for i in 0..M * M {
157+
s[i] = corr_direct[i] as f32;
158+
}
159+
s
160+
};
161+
let mut m_proj = [0.0f32; M * M];
162+
for r in 0..M {
163+
for c in 0..M {
164+
m_proj[r * M + c] = if r == c {
165+
1.0
166+
} else {
167+
0.15 * (r as f32 - c as f32).signum()
168+
};
169+
}
160170
}
161-
}
162-
// tile path: T = Σ·M, then Σ' = Mᵀ·T (two tile ops — the sandwich)
163-
let t_tile = tile_matmul(&sigma_f, &m_proj);
164-
let mt = transpose(&m_proj);
165-
let sand_tile = tile_matmul(&mt, &t_tile);
166-
// direct reference: Mᵀ·Σ·M in f64
167-
let t_direct = {
168-
let mut td = [0.0f32; M * M];
169-
let d = direct_matmul(&sigma_f, &m_proj);
171+
// tile path: T = Σ·M, then Σ' = Mᵀ·T (two tile ops — the sandwich)
172+
let t_tile = tile_matmul(&sigma_f, &m_proj);
173+
let mt = transpose(&m_proj);
174+
let sand_tile = tile_matmul(&mt, &t_tile);
175+
// direct reference: Mᵀ·Σ·M in f64
176+
let t_direct = {
177+
let mut td = [0.0f32; M * M];
178+
let d = direct_matmul(&sigma_f, &m_proj);
179+
for i in 0..M * M {
180+
td[i] = d[i] as f32;
181+
}
182+
td
183+
};
184+
let sand_direct = direct_matmul(&mt, &t_direct);
185+
let (sa, sr) = errors(&sand_tile, &sand_direct);
186+
println!(" (2) covariance sandwich Σ' = Mᵀ·Σ·M (two tile ops = the splat EWA projection)");
187+
println!(
188+
" max_abs_err={sa:.4} frobenius_rel_err={:.3}% (== hpc::splat3d::sandwich_x16 shape;",
189+
sr * 100.0
190+
);
191+
println!(" per-entry max-rel is meaningless here — Mᵀ·Σ·M cancels to near-zero entries)");
192+
193+
// ── (3) bit-exactness on bf16-exact integer operands (module guarantee) ──
194+
let mut ai = [0.0f32; M * M];
195+
let mut bi = [0.0f32; M * M];
170196
for i in 0..M * M {
171-
td[i] = d[i] as f32;
197+
ai[i] = ((mix(i as u64) % 16) as f32) - 8.0; // small ints, bf16-exact
198+
bi[i] = ((mix(i as u64 ^ 0x5555) % 16) as f32) - 8.0;
172199
}
173-
td
174-
};
175-
let sand_direct = direct_matmul(&mt, &t_direct);
176-
let (sa, sr) = errors(&sand_tile, &sand_direct);
177-
println!(" (2) covariance sandwich Σ' = Mᵀ·Σ·M (two tile ops = the splat EWA projection)");
178-
println!(
179-
" max_abs_err={sa:.4} frobenius_rel_err={:.3}% (== hpc::splat3d::sandwich_x16 shape;",
180-
sr * 100.0
181-
);
182-
println!(" per-entry max-rel is meaningless here — Mᵀ·Σ·M cancels to near-zero entries)");
183-
184-
// ── (3) bit-exactness on bf16-exact integer operands (module guarantee) ──
185-
let mut ai = [0.0f32; M * M];
186-
let mut bi = [0.0f32; M * M];
187-
for i in 0..M * M {
188-
ai[i] = ((mix(i as u64) % 16) as f32) - 8.0; // small ints, bf16-exact
189-
bi[i] = ((mix(i as u64 ^ 0x5555) % 16) as f32) - 8.0;
190-
}
191-
let int_tile = tile_matmul(&ai, &bi);
192-
let int_direct = direct_matmul(&ai, &bi);
193-
let bit_exact = (0..M * M).all(|i| int_tile[i] as f64 == int_direct[i]);
194-
println!(" (3) bit-exact on bf16-exact integer operands (|Σ| < 2^24): {bit_exact}");
195-
196-
// ── (4) throughput of the 16×16 tile op on this host ──
197-
let iters = 200_000usize;
198-
let t0 = std::time::Instant::now();
199-
let mut acc = 0.0f32;
200-
for it in 0..iters {
201-
// vary the operand slightly so nothing is optimized away
202-
let mut xv = x;
203-
xv[0] += (it & 0x7) as f32 * 0.01;
204-
let c = tile_matmul(&xv, &m_proj);
205-
acc += c[0] + c[M * M - 1];
206-
}
207-
let dt = t0.elapsed().as_secs_f64();
208-
let tiles_s = iters as f64 / dt;
209-
println!(
210-
" (4) throughput: {iters} tile ops in {:.1} ms → {:.2} M tile-ops/s (checksum {acc:.1})",
211-
dt * 1000.0,
212-
tiles_s / 1e6
213-
);
214-
215-
// hard gate: BF16-precision class (rel err small) + integer bit-exactness
216-
assert!(cr < 0.05, "intercorrelation rel err too high for BF16 class");
217-
assert!(sr < 0.05, "sandwich rel err too high for BF16 class");
218-
assert!(bit_exact, "bf16-exact integer tile op is NOT bit-exact vs direct");
219-
220-
println!(
221-
"\n MEASURED CONCLUSION: YES — a gridlake field interdependency reduces to a\n\
200+
let int_tile = tile_matmul(&ai, &bi);
201+
let int_direct = direct_matmul(&ai, &bi);
202+
let bit_exact = (0..M * M).all(|i| int_tile[i] as f64 == int_direct[i]);
203+
println!(" (3) bit-exact on bf16-exact integer operands (|Σ| < 2^24): {bit_exact}");
204+
205+
// ── (4) throughput of the 16×16 tile op on this host ──
206+
let iters = 200_000usize;
207+
let t0 = std::time::Instant::now();
208+
let mut acc = 0.0f32;
209+
for it in 0..iters {
210+
// vary the operand slightly so nothing is optimized away
211+
let mut xv = x;
212+
xv[0] += (it & 0x7) as f32 * 0.01;
213+
let c = tile_matmul(&xv, &m_proj);
214+
acc += c[0] + c[M * M - 1];
215+
}
216+
let dt = t0.elapsed().as_secs_f64();
217+
let tiles_s = iters as f64 / dt;
218+
println!(
219+
" (4) throughput: {iters} tile ops in {:.1} ms → {:.2} M tile-ops/s (checksum {acc:.1})",
220+
dt * 1000.0,
221+
tiles_s / 1e6
222+
);
223+
224+
// hard gate: BF16-precision class (rel err small) + integer bit-exactness
225+
assert!(cr < 0.05, "intercorrelation rel err too high for BF16 class");
226+
assert!(sr < 0.05, "sandwich rel err too high for BF16 class");
227+
assert!(bit_exact, "bf16-exact integer tile op is NOT bit-exact vs direct");
228+
229+
println!(
230+
"\n MEASURED CONCLUSION: YES — a gridlake field interdependency reduces to a\n\
222231
\x20 BF16 16×16 tile op (one `bf16_tile_gemm_16x16`, i.e. one `TDPBF16PS` on\n\
223232
\x20 the AMX tier), matching the direct f64 reference to BF16 precision and\n\
224233
\x20 BIT-EXACT for bf16-exact integer operands. It IS the same primitive as:\n\
@@ -228,5 +237,16 @@ fn main() {
228237
\x20 Interdependency / intercorrelation / perturbation / transform / covariance\n\
229238
\x20 projection are ONE op: the 16×16 BF16 tile GEMM. The 64×64 gridlake is\n\
230239
\x20 4×4 = 16 of these tiles."
231-
);
240+
);
241+
}
242+
} // mod amx
243+
244+
#[cfg(target_arch = "x86_64")]
245+
fn main() {
246+
amx::run();
247+
}
248+
249+
#[cfg(not(target_arch = "x86_64"))]
250+
fn main() {
251+
eprintln!("gridlake_field_tile requires x86_64 (AMX TDPBF16PS / AVX-512 VDPBF16PS BF16 tile GEMM).");
232252
}

0 commit comments

Comments
 (0)