From 05bfea7a30a313c89885cc4a9e7ea554f7f34908 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 08:01:00 +0000 Subject: [PATCH 1/6] simd: re-export bf16_tile_gemm_16x16 through the ndarray::simd polyfill (W1a) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AMX ladder comment at simd.rs:577 promises "the whole AMX ladder through ndarray::simd::* (W1a) without dipping into crate::hpc directly" — but the BF16 16×16 tile GEMM was only reachable via ndarray::hpc::bf16_tile_gemm, forcing consumers (lance-graph symbiont Domino POC) to violate the "all SIMD from ndarray::simd" rule. Add the re-export next to amx_available/matmul_i8_to_i32. bf16_tile_gemm_16x16 self-gates on amx_available() (the one-time arch_prctl(158) XTILEDATA grant per AMX_GOTCHAS.md Gotcha 4), using AMX TDPBF16PS when present and the F32x16 FMA fallback otherwise — safe on any CPU. std-gated like the rest of the AMX surface. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01CcpLeEC3XK8Eye53GKBVvi --- src/simd.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/simd.rs b/src/simd.rs index 6541fd85..c828f4e1 100644 --- a/src/simd.rs +++ b/src/simd.rs @@ -579,6 +579,12 @@ pub use crate::hpc::heel_f64x8::cosine_f32_to_f64_simd; // exposes the runtime tier check for reporting. #[cfg(feature = "std")] pub use crate::hpc::amx_matmul::{amx_available, matmul_i8_to_i32}; +// BF16 16×16 tile GEMM — AMX `TDPBF16PS` when `amx_available()`, `F32x16` FMA +// fallback otherwise. Same W1a rule: reach it via `ndarray::simd::*`, never +// `crate::hpc` directly (the one-time `arch_prctl(158)` XTILEDATA grant is gated +// inside `amx_available()`, which this dispatcher calls before any tile op). +#[cfg(feature = "std")] +pub use crate::hpc::bf16_tile_gemm::bf16_tile_gemm_16x16; // CPU-generation detection (cached): SPR / EMR / GNR / Sierra Forest. Lets a // consumer report which silicon a run landed on and distinguish "no AMX // silicon" from "AMX present but not OS-enabled" — both surface via `amx_report`. From 058b04ab66366f5b8e9031c8952bf52a9d8a2ab4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 08:26:33 +0000 Subject: [PATCH 2/6] =?UTF-8?q?AMX=5FGOTCHAS:=20Gotcha=2014=20=E2=80=94=20?= =?UTF-8?q?a=20guest=20can=20DENY=20AMX=20outright=20(env=20gate,=20not=20?= =?UTF-8?q?a=20bug)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Breadcrumb so no future session reads one amx_available()==false as "AMX broken." Probe all FOUR gates (not just CPUID): CPUID masked-to-0, XCR0 b17/b18 tile-state, arch_prctl(158) REQ_XCOMP_PERM (-EOPNOTSUPP = kernel refuses, no byte-call forces it), GET_XCOMP_PERM mask. Observed once: all four deny -> genuinely unavailable, detect_amx() correctly false, consumers run the AVX-512/F32x16 fallback. Same binary prints [AMX TDPBF16PS] where arch_prctl returns 0 + XCR0 bits set. Placed mid-doc (before Hardware tiers/Files) on purpose — out of the head/tail zone a skim touches — so it informs without mis-scenting the file as "no AMX." Cross-ref lance-graph EPIPHANIES E-DOMINO-SOA-ORCHESTRATION-GREEN. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01CcpLeEC3XK8Eye53GKBVvi --- .claude/AMX_GOTCHAS.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/.claude/AMX_GOTCHAS.md b/.claude/AMX_GOTCHAS.md index 79cd0110..b934f66d 100644 --- a/.claude/AMX_GOTCHAS.md +++ b/.claude/AMX_GOTCHAS.md @@ -222,6 +222,35 @@ once is correct even under rayon. `cpu_model()` is cached the same way. --- +## Gotcha 14: a guest can DENY AMX outright — environment gate, NOT a code/ISA bug + +> Cross-check breadcrumb (2026-06-20). Buried mid-doc on purpose (away from the +> head/tail a skim touches) so NO session concludes "AMX is broken" from one +> `amx_available()==false`. ISA fixes 1-13 above are correct and verified. + +Some virtualised guests refuse AMX even when every ISA fix above is right. To +tell "AMX off by provisioning" from "our code is wrong", probe all FOUR gates +directly (not just CPUID) — each a few lines of stable asm (Gotcha 4/5 has them): + +- `__cpuid_count(7,0).edx` b24/b25/b22 (TILE/INT8/BF16) — hypervisor may MASK to + 0 (a generic model name like `Xeon @ 2.80GHz` is the tell). +- `_xgetbv(0)` XCR0 b17/b18 (TILECFG/TILEDATA) — if **0**, the OS never enabled + tile XSTATE; AMX cannot run, full stop. +- `arch_prctl(158, ARCH_REQ_XCOMP_PERM, XTILEDATA=18)` — `-95 / -EOPNOTSUPP` + means the kernel REFUSES the grant; **no byte-call can force it**. +- `arch_prctl(158, ARCH_GET_XCOMP_PERM, &mask)` — XTILEDATA (b18) must be in mask. + +Observed once on one provisioning (kernel 6.18.5 guest): CPUID=0 **and** XCR0 +b17/b18=0 **and** REQ=`-EOPNOTSUPP` → genuinely unavailable. `detect_amx()` +correctly returns `false`; consumers run the AVX-512 / `F32x16` fallback (correct, +NaN-clean). The SAME binary prints `[AMX TDPBF16PS]` the moment it lands on a +guest where `arch_prctl` returns 0 and XCR0 bits are set (e.g. the Emerald Rapids +host this file's header was verified on). **`false` here ≠ broken — it's an +environment gate above the code.** Full write-up + a ~25-line four-gate probe: +lance-graph `.claude/board/EPIPHANIES.md` → `E-DOMINO-SOA-ORCHESTRATION-GREEN`. + +--- + ## Hardware tiers ``` From e1d54442d8b968450d297b8866fb9b358ffcb451 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 22:50:20 +0000 Subject: [PATCH 3/6] chore(toolchain): pin 1.97.1 + restructure the comment so it cannot go stale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ecosystem realignment: lance-graph, OGAR, woa-rs and MedCare-rs moved to 1.97.1 in the lance-9 / DataFusion-54 sweep (lance-graph b2b08b07); this repo was left behind. The comment is restructured per lance-graph #916: it no longer restates the pinned version (that is what makes these files go stale — a bump edits channel and leaves the prose behind) and carries an append-only bump log instead. Where the old comment asserted cross-repo alignment that had since become false, the claim is corrected in place rather than deleted. Verified: cargo check green under 1.97.1. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01CcpLeEC3XK8Eye53GKBVvi --- rust-toolchain.toml | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index e7ebd394..4ead399e 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,10 +1,21 @@ [toolchain] -channel = "1.95.0" -# Pinned to 1.95.0 (2026-04-14). Aligns ndarray with the rest of the -# AdaWorldAPI stack: bevy (already on 1.95 for edition 2024) and -# lance-graph (bumped in parallel). 1.95 introduced one new clippy lint -# that fires on this codebase (`clippy::manual_checked_ops` on -# `impl_owned_array.rs::into_scalar`); that's fixed in this same commit. -# Never auto-track `stable` — bump explicitly when a future version is -# reviewed and the workspace clippy passes clean. +channel = "1.97.1" +# The pinned version is the `channel` line ABOVE — deliberately not restated in +# this comment. Restating it is how these files go stale: a bump edits `channel` +# and leaves the prose behind (lance-graph's copy was wrong twice for exactly +# that reason before it was restructured this way). +# +# Bump log (append one line per bump): +# → 1.95.0 2026-04-14. Aligned with bevy (edition 2024) and lance-graph, +# bumped in parallel. 1.95 added `clippy::manual_checked_ops`, +# which fired on `impl_owned_array.rs::into_scalar`; fixed in the +# same commit. +# → 1.97.1 2026-08-10. Ecosystem realignment: lance-graph, OGAR, woa-rs and +# MedCare-rs were already on 1.97.1 (the lance-9 / DataFusion-54 +# sweep, lance-graph b2b08b07). ndarray has no lance/arrow/ +# datafusion dependency of its own, so this is a compiler-only +# alignment — nothing in the dependency graph moves with it. +# +# Never auto-track `stable` — bump explicitly when a future version is reviewed +# and the workspace clippy passes clean. components = ["clippy", "rustfmt"] From cd31bb13ee2632b481cf0587a0147756e9413bfc Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 19:38:31 +0000 Subject: [PATCH 4/6] =?UTF-8?q?probe(examples):=20geostrophic=20stencil=20?= =?UTF-8?q?via=20int8=5Ftile=5Fgemm=5F16x16=20=E2=80=94=20real=20ERA5,=20u?= =?UTF-8?q?8=20substrate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator: "Was ist mit Domino.rs" -> "Go ahead" on the falsifiable step I proposed — route the geostrophic finite-difference stencil through the AMX int8 tile-GEMM path domino.rs (lance-graph) tile-batches, and diff against the numpy reference from the session's voxel_chess_probe.py. The stencil-as-matrix mapping onto ONE AMX tile: A[16,K=64] u8 — 16 independent latitude rows x 64 quantized mean_sea_level_pressure samples per row B[K=64,N=16] i8 — 16 columns, each an EXACT {-1,0,+1} centered-difference stencil at a distinct longitude tap (no quantization error in the operator, only in the data it multiplies) C[16,16] i32 = A . B via int8_tile_gemm_16x16 — the SAFE dispatching wrapper (AMX TDPBUSD when available, scalar fallback otherwise; the crate's own fallback_matches_scalar_ reference_k64 already proves those two paths bit-exact) Checked BEFORE speccing anything: this container has no AMX_TILE/AMX_INT8 (/proc/cpuinfo). int8_gemm_amx_tiled has NO runtime AMX check ("production builds skip those for performance -- callers must runtime-check amx_available() themselves") and would execute raw TDPBUSD on this host, faulting. int8_tile_gemm_16x16 is the correct entry point precisely because it dispatches. Measured, not assumed: amx_available() = false, so this run exercised the scalar fallback -- reported explicitly in the output, not silently glossed over. Real data, real provenance: WeatherBench2 ERA5 6h 0.25deg, mean_sea_level_pressure, t=91246 (2021-06-15 12Z, the SAME timestep as voxel_chess_probe.py), 16 rows spanning 52.25N..59.75N around the NH storm that probe's own E6 Rankine measurement centered on (55.75N, 334.50E), 64-wide longitude window centered on that storm. 4096-byte fixture, include_bytes!'d next to the example (life.rs/life.txt precedent), regenerable against the recorded store/time_index/indices. Four pre-registered bars, all measured (not assumed) and all PASS: BAR-1 corr(d_est, d_true) = 0.9985 (bar >= 0.98 -- fires correctly) BAR-2 max|d_est-d_true| = 10.58 Pa (bar <= 11.86 Pa -- quant bound, real headroom, not padding) BAR-3 std(d_true) = 91.62 Pa (bar > 50 Pa -- non-trivial sample, the silence guard) DISABLE-RUN (sign-flipped stencil): corr = -0.9985 (bar <= -0.90) -- proves the harness can actually detect a broken stencil, not just print a number that happens to look right. Gates: my file rustfmt-clean, zero clippy findings attributed to examples/geostrophic_stencil.rs. NOT touched: src/iterators/mod.rs, which already has 4 pre-existing clippy errors on the clean tree (verified via git stash before/after) -- left alone per this session's own scope discipline, not swept. Honest scope statement, restated in the file's own doc comment: this validates the stencil-as-GEMM numerics and the u8-quantization pipeline on THIS host's scalar-fallback path, not AMX TDPBUSD throughput -- that requires Sapphire-Rapids-class hardware, which this container lacks. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CcpLeEC3XK8Eye53GKBVvi --- Cargo.toml | 4 + examples/geostrophic_stencil.bin | Bin 0 -> 4096 bytes examples/geostrophic_stencil.rs | 226 +++++++++++++++++++++++++++++++ 3 files changed, 230 insertions(+) create mode 100644 examples/geostrophic_stencil.bin create mode 100644 examples/geostrophic_stencil.rs diff --git a/Cargo.toml b/Cargo.toml index 8098710b..494c828a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,10 @@ required-features = ["std"] name = "amx_rb_probe" required-features = ["std"] +[[example]] +name = "geostrophic_stencil" +required-features = ["std"] + # Morton cascade probe imports `ndarray::simd` (std-gated). [[example]] name = "morton_cascade_probe" diff --git a/examples/geostrophic_stencil.bin b/examples/geostrophic_stencil.bin new file mode 100644 index 0000000000000000000000000000000000000000..d488756d7f5f95b3dc96aa7ad49c51b75cc28641 GIT binary patch literal 4096 zcmXX}X;@X|8b#}BIWaq+0-DvW3`9Y}0npOrEptQ>1;Jb1+bpPv0}2k+lbMKsfQpFK z&CFyL2ZnDt*`(lrA`YmPX}3oi!~qBHI?uj8&U5zOXYaGWwcfSX`+k{g4SKz6kU7nu zB+$Uy+hA8`e&5nKU#oGyT%+fg8WkHf>SHykXK8c@)<_}M!GAMdMRvqQDJLw3qwIB!Q!i`ma?+Sh%t5sZDP;<$wh`Yd1<&~kImGMBb>urMtZtJi`a@#C zS&7v@O7yCfm{KZ{pD(fPTM6-v#PB^5Szq&*-;O+gxL3kypTrX0Q&1$~eo*38xkLl` zX#JW0RVz_@Q6hmHHZ@Clwo9BQ2SvRU5(g=i`zbsORya@mx95=acetiy3S&P}IFZ3M zf2pvdNMT5oLclKy1$8_h$y$3FjDEr(?V84;A2fRJ)tI|cW6B2_V<%~xctIonX^pV! z3U`fyB8MeObJyH4=+2k(<9H z(jJk|9>m;V;qc2A{$5k9wMEA&f5Nk0x;`;MX^Y~YV*-Z*_`Wozbs4?>r zYhI~wZl^|KqK5xmji3>%w~NN@zZ5EtDn#$5mRRdUbID(j!kE4a5sz7?8xmVjT3XKG z8r%NEdTpbwH%p8njt5I6lHZdEi(&1;B%CMlo>wG>g|LRBh>xE;BPG@ZOH_^{2d_zt zoJM?eBvvipe=m|avzpj95j(kbp*B3IiMBHmQ~#7m=NepG6w0W9fI#Z|b%o>f-+pSp z&qoT8*$TP)6vA2e{U;Sdepm3itKjk!J=|_#-%7m~Y@_xQsPE|-ZG$zOJ84X7pneWf z-)RbRA@NgNNrQRZjqhur{_5%DA0*t$B%)aBEt%xw6Ka~6H@_*dW2!_AafQ4rG2crf z)0tSGmYCI%wRMmPeJpUPUEp8`iKNctpgW)KDbdb*O9!*QK^7m&$wm1GyiRQ#%%EoI zl~8ISfE+BR4r)3oOzmUkO*?(qFVez4liDA?Q=zFuA@e8J>@u;mD!6vhNbRqYenVsN z_sq848mm@m%$cn*&rf3%e_!z%y;8~N$ls25g|IN{XDD;dUEwk_Xvr0cD?ifL2bfd4 zsrikpb%I16);Vhe>+QpHV)1=~>%J*)vO&P-gusO#1x70Y2O-d2A>bwj>Wx6yF@Yhq z0+Ck*mNp7(CMR~jb`Fv_9LVgLOy1s-Q1r-V`aabEFgLQNJR^fqY1Q%MF z{@{HcPgY20p6{U6T3YE@*8YIK2CVxEYI7lVIwstzS$;pOzK+)f`Vd!inLzj#0`6%7 z+3N+oRtj8MEO0x{`X0t}`LV$H41x4L0z1kD4xJQm{!?JflgvX;iR09Q-9PshN|Z1g z+Py?eKeegY52UMv7J3Fe2qr_efGIi8qNFYg^xAFd<};&)CKPy#*8R9 z%Y0|AwD-AOW?x#gLd;l&Mo;FzLwdeWBBzA4-omx8F5BObXnaZHZWoFDH!S>VUkMCf zCr~tBAT?CrA#r~*d~zUEirJs zM89_|pEVhE5ci5hTzQD9}7w zzA(f7dlIo_)8EC+w`%&}PkMinK{RKdTb&GA>4nouBbQk? zWSvIso9qV@$lZ$?Mcp;7HWE8~r+x16rzaf9b!r5;Vb95NQ@BVSdLE}AnM-zm9%0X0 z!pyR-KhBLa5bGHCJ8NG}E|kDFX2N2AVh&R$-wU*!5vXHM)^ww$2TMGfBr%7YvS*zG zXQxcgKdE<_x4l_!zVqO0a?dqXZKu~OsC{O8;1irlpCN`2`g)E=W4uOWipK6e8dXx` zzJj zv9udGBo~j#g1}(i=f1`n_FtTL*DBQRvikYWOA3z6 zs#H&n(ZLqx8C-ANW{r#A(9f)A3NgpEaDMA%ko~MdbFjgp*#;hQ1`VeS78V=0r5p5H zYTy`74uTB4`WsB`XfXQ^&Irdf(n>XQay0xn54*8XL~tLllQ~gMuIsw+I(-&j$6mpH zS;*Y+qfe$VGuOVwS!Wx2;5-MXO}@#DYgES!J1131|Y51}y-D52!=MnqkK+(68~O<#}x))Ck$eLHz>Zx8g{iI(9?z)Lu~LG%j2mw_@A>OSlAG`$A*wp8>*Js z(2IP;PP8F(s11&NZ8%7t3Yy5pMT@_vGVU3_vOJTu-oTfd>iIVJkP!woTuZ}PzAwOF z3G*dupg}n`*w>XcAXkk~8Qgr*AeR`5sLwGxp2qCm-G$F_|JjeY7LwNo^l6>1!KP8n zWokEIhNZ^?AF?(piTzVPn`N;2YlGnj_+Q`i**XJ39p~ORNFr9J{>0*ALwSe|XQ$cF UG~b5gSQ~1SY`FC~ab?@^Kb6fac>n+a literal 0 HcmV?d00001 diff --git a/examples/geostrophic_stencil.rs b/examples/geostrophic_stencil.rs new file mode 100644 index 00000000..7d24d670 --- /dev/null +++ b/examples/geostrophic_stencil.rs @@ -0,0 +1,226 @@ +//! Geostrophic finite-difference stencil, computed as an int8 tile-GEMM — +//! the mechanism `domino.rs` tile-batches, exercised on real ERA5 pressure +//! data through the crate's own safe dispatching wrapper. +//! +//! Session context: `symbiont/domino.rs` (lance-graph) proves 16 SoA boards +//! batch into ONE AMX 16×16 tile GEMM; this probe asks whether the same +//! 16×16×64 shape can compute a REAL finite-difference stencil (the leading +//! term of geostrophic balance, `du/dy` via centered differences) after the +//! input has been quantized to the shipped u8-palette convention, and how +//! close the GEMM-recovered value lands to the unquantized f64 reference. +//! +//! # The mapping onto one AMX tile +//! - `A[16, K=64]` (u8): 16 independent latitude rows × 64 quantized +//! pressure samples per row (a `mean_sea_level_pressure` window). +//! - `B[K=64, N=16]` (i8): 16 columns, each an EXACT `{-1, 0, +1}` centered- +//! difference stencil at a distinct longitude tap — no quantization error +//! in the operator itself, only in the DATA it multiplies. +//! - `C[16, 16]` (i32) = `A · B`, computed via +//! [`int8_tile_gemm_16x16`] — the SAFE dispatching wrapper (AMX `TDPBUSD` +//! when available, scalar fallback otherwise; the two paths are proven +//! bit-exact by the crate's own `fallback_matches_scalar_reference_k64`). +//! +//! `C[i,j] = idx(p[row_i, tap_j+1]) − idx(p[row_i, tap_j−1])`, an INTEGER +//! bucket-index difference. Dequantized by the shared bucket width, that is +//! an estimate of the true finite difference `p[j+1] − p[j−1]` in Pa. +//! +//! # Data provenance (`geostrophic_stencil.bin`, 4096 bytes, `include_bytes!`) +//! WeatherBench2 ERA5 6-hourly 0.25° reanalysis, +//! `gs://weatherbench2/datasets/era5/1959-2022-6h-1440x721.zarr`, +//! `mean_sea_level_pressure`, time index 91246 (2021-06-15 12Z — the SAME +//! timestep as the session's `voxel_chess_probe.py`). 16 rows spanning +//! 52.25°N .. 59.75°N (0.5° spacing) around the NH low that probe's own E6 +//! Rankine-vortex measurement centered on (55.75°N, 334.50°E); a 64-wide +//! longitude window (326.5° .. 342.5°E) centered on that storm. Raw f32, +//! row-major, regenerable from `probes/weather-p1/` in the `lance-graph` +//! sibling repo against the same store/time_index/indices recorded here. +//! +//! # What this DOES and does NOT prove +//! `int8_tile_gemm_16x16` is the SAFE wrapper — it runtime-dispatches to +//! AMX `TDPBUSD` when `amx_available()`, scalar otherwise. This host's +//! actual path is reported at the top of the run. On a non-AMX host this +//! validates the stencil-as-GEMM NUMERICS and the u8-quantization pipeline, +//! not AMX instruction throughput — the crate's own bit-exact fallback +//! parity test is what licenses treating those as interchangeable. +//! +//! Pre-registered bars (checked, not assumed): +//! BAR-1 (can-it-fire, meaningful): Pearson corr(d_est, d_true) ≥ 0.98 +//! across all 256 cells — proves the mechanism differentiates with +//! the right sign and scale, not merely "some correlation". +//! BAR-2 (quantization bound, per-cell): |d_est − d_true| ≤ 1.01×bucket +//! width for EVERY cell — two independent half-bucket errors. +//! BAR-3 (silence guard / non-triviality): std(d_true) > 50 Pa — the +//! sample must carry real synoptic-scale variation, or a mechanism +//! that produces near-zero everywhere would trivially "pass" BAR-2. +//! DISABLE-RUN (falsifier self-check): re-run with the stencil SIGN +//! FLIPPED. If this does not invert the correlation to ≤ −0.9, the +//! harness cannot actually detect a broken stencil and BAR-1 is +//! decoration — this run's own defect-injection proof. +//! +//! cargo run --release --example geostrophic_stencil + +use ndarray::hpc::int8_tile_gemm::int8_tile_gemm_16x16; +use ndarray::simd::amx_available; + +const RAW: &[u8] = include_bytes!("geostrophic_stencil.bin"); +const M: usize = 16; // latitude rows +const K: usize = 64; // longitude samples per row +const N: usize = 16; // output stencil taps + +/// Longitude index (within the 64-wide window) of each of the 16 output +/// taps: 4, 7, 10, ..., 49 — evenly spaced, every one safely inside +/// `[1, K-2]` so both `tap-1` and `tap+1` stay in-window. +const TAPS: [usize; N] = { + let mut t = [0usize; N]; + let mut j = 0; + while j < N { + t[j] = 4 + 3 * j; + j += 1; + } + t +}; + +fn load_rows() -> [[f32; K]; M] { + assert_eq!(RAW.len(), M * K * 4, "fixture size mismatch"); + let mut rows = [[0f32; K]; M]; + for i in 0..M { + for k in 0..K { + let off = (i * K + k) * 4; + rows[i][k] = f32::from_le_bytes(RAW[off..off + 4].try_into().unwrap()); + } + } + rows +} + +/// Linear quantization over the fixture's own min/max — the same bucketing +/// formula the shipped `helix::quantize::RollingFloor` uses (a fixed +/// percentile window would barely differ at this sample size, so the +/// simpler min/max convention is used here and stated as such). +fn quantize(rows: &[[f32; K]; M]) -> ([[u8; K]; M], f32, f32) { + let (mut lo, mut hi) = (f32::MAX, f32::MIN); + for row in rows { + for &v in row { + lo = lo.min(v); + hi = hi.max(v); + } + } + let mut q = [[0u8; K]; M]; + for i in 0..M { + for k in 0..K { + let t = (rows[i][k] - lo) / (hi - lo); + q[i][k] = (t * 256.0).clamp(0.0, 255.0) as u8; + } + } + (q, lo, hi) +} + +/// Build the `K x N` centered-difference stencil: column `j` has `+1` at +/// `TAPS[j]+1`, and `sign * -1` at `TAPS[j]-1` (sign flips for the +/// disable-run), zero elsewhere. +fn build_stencil(sign: i8) -> [i8; K * N] { + let mut b = [0i8; K * N]; + for (j, &tap) in TAPS.iter().enumerate() { + b[(tap - 1) * N + j] = -sign; + b[(tap + 1) * N + j] = sign; + } + b +} + +fn run_gemm(a_u8: &[[u8; K]; M], b_i8: &[i8; K * N]) -> [i32; M * N] { + let a_flat: Vec = a_u8.iter().flatten().copied().collect(); + let mut c = [0i32; M * N]; + int8_tile_gemm_16x16(&a_flat, b_i8, &mut c, K); + c +} + +fn pearson(xs: &[f64], ys: &[f64]) -> f64 { + let n = xs.len() as f64; + let mx = xs.iter().sum::() / n; + let my = ys.iter().sum::() / n; + let (mut cov, mut vx, mut vy) = (0.0, 0.0, 0.0); + for (x, y) in xs.iter().zip(ys) { + cov += (x - mx) * (y - my); + vx += (x - mx).powi(2); + vy += (y - my).powi(2); + } + cov / (vx.sqrt() * vy.sqrt()) +} + +fn main() { + let rows = load_rows(); + let (a_u8, lo, hi) = quantize(&rows); + let bucket_width = (hi - lo) as f64 / 256.0; + + println!("== geostrophic_stencil: int8 tile-GEMM stencil, real ERA5 data ==\n"); + println!( + " amx_available() = {} -> execution path: {}", + amx_available(), + if amx_available() { + "AMX TDPBUSD" + } else { + "scalar fallback (int8_tile_gemm_16x16's own dispatch)" + } + ); + println!(" fixture: p range [{lo:.1}, {hi:.1}] Pa, bucket width {bucket_width:.4} Pa\n"); + + // True (unquantized) finite differences, for reference. + let mut d_true = [[0f64; N]; M]; + for i in 0..M { + for (j, &tap) in TAPS.iter().enumerate() { + d_true[i][j] = (rows[i][tap + 1] - rows[i][tap - 1]) as f64; + } + } + let flat_true: Vec = d_true.iter().flatten().copied().collect(); + let std_true = { + let m = flat_true.iter().sum::() / flat_true.len() as f64; + (flat_true.iter().map(|v| (v - m).powi(2)).sum::() / flat_true.len() as f64).sqrt() + }; + + // ---- CORRECT stencil ---- + let b_correct = build_stencil(1); + let c_correct = run_gemm(&a_u8, &b_correct); + let flat_est: Vec = c_correct.iter().map(|&v| v as f64 * bucket_width).collect(); + let corr = pearson(&flat_est, &flat_true); + let max_err = flat_est + .iter() + .zip(&flat_true) + .map(|(e, t)| (e - t).abs()) + .fold(0.0_f64, f64::max); + + println!(" BAR-1 corr(d_est, d_true) = {corr:.4} (bar: >= 0.98)"); + println!(" BAR-2 max |d_est - d_true| = {max_err:.4} Pa (bar: <= {:.4} Pa)", 1.01 * bucket_width); + println!(" BAR-3 std(d_true) = {std_true:.2} Pa (bar: > 50 Pa)"); + + // ---- DISABLE-RUN: sign-flipped stencil (the falsifier self-check) ---- + let b_flipped = build_stencil(-1); + let c_flipped = run_gemm(&a_u8, &b_flipped); + let flat_flip: Vec = c_flipped.iter().map(|&v| v as f64 * bucket_width).collect(); + let corr_flip = pearson(&flat_flip, &flat_true); + println!("\n DISABLE-RUN corr(d_est_signflipped, d_true) = {corr_flip:.4} (bar: <= -0.90)"); + + let bar1 = corr >= 0.98; + let bar2 = max_err <= 1.01 * bucket_width; + let bar3 = std_true > 50.0; + let disable_ok = corr_flip <= -0.90; + + println!("\n VERDICT:"); + println!(" BAR-1 (fires correctly) ....... {}", if bar1 { "PASS" } else { "FAIL" }); + println!(" BAR-2 (quantization bound) .... {}", if bar2 { "PASS" } else { "FAIL" }); + println!(" BAR-3 (non-trivial sample) .... {}", if bar3 { "PASS" } else { "FAIL" }); + println!( + " disable-run (can detect a broken stencil) . {}", + if disable_ok { + "PASS" + } else { + "FAIL — harness is blind" + } + ); + + if !(bar1 && bar2 && bar3 && disable_ok) { + eprintln!("\n ONE OR MORE BARS FAILED — see above."); + std::process::exit(1); + } + println!("\n All bars satisfied. The geostrophic centered-difference stencil"); + println!(" survives u8 quantization + int8_tile_gemm_16x16 on this host's"); + println!(" {} path.", if amx_available() { "AMX" } else { "scalar-fallback" }); +} From 81cd5d2693a4f29b38edaaf280af3167d2b402fa Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 19:39:02 +0000 Subject: [PATCH 5/6] docs(blackboard): record the geostrophic_stencil probe Per CLAUDE.md's own rule -- write decisions to the blackboard, not just to chat. Cross-repo context, the AMX-vs-scalar decision and its reasoning, and the loose end (no AMX host available in this container to validate the TDPBUSD path itself) recorded so a future session doesn't re-derive them. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CcpLeEC3XK8Eye53GKBVvi --- .claude/blackboard.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.claude/blackboard.md b/.claude/blackboard.md index 9a6d263f..1526d8d1 100644 --- a/.claude/blackboard.md +++ b/.claude/blackboard.md @@ -220,3 +220,28 @@ index was transiently stale this session (returned empty for `simd_int_ops.rs`, `vnni_gemm.rs`, `bf16_gemm_f32`); Bash ground-truth confirmed all present. Orchestrator should `cargo fmt`/`clippy`/`test` centrally (edits were edit-only, no compile performed here). + +[PROBE] `examples/geostrophic_stencil.rs` (2026-08-11) — real ERA5 pressure +data through `int8_tile_gemm_16x16`, testing whether a physics finite- +difference stencil survives u8 quantization + the shipped tile-GEMM. Cross- +repo context: `lance-graph`'s `symbiont/domino.rs` tile-batches 16 SoA +boards into one AMX 16x16 GEMM; this asks whether the SAME shape can carry +a real geostrophic stencil, not just BF16 board state. 4 pre-registered +bars, all measured PASS (corr 0.9985 vs bar 0.98; max err 10.58 Pa vs bar +11.86 Pa; std 91.62 Pa vs bar 50 Pa non-triviality guard; sign-flip +disable-run inverts to -0.9985, proving the harness can detect a broken +stencil). Full provenance (WB2 store, t=91246, lat/lon indices) in the +fixture's doc comment; regenerable, not just re-runnable. + [DECISION] Used `int8_tile_gemm_16x16` (the safe dispatching wrapper), + NOT `int8_gemm_amx_tiled` — the latter's own doc says "production + builds skip [the AMX check] for performance; callers must runtime-check + amx_available() themselves", and this container has no AMX_TILE/ + AMX_INT8 (checked /proc/cpuinfo before writing any code). Calling the + unchecked function here would have executed raw TDPBUSD on non-AMX + silicon and faulted. + [LOOSE END] `amx_available() = false` on this host — the run exercised + ONLY the scalar fallback path, not AMX TDPBUSD throughput. The crate's + own `fallback_matches_scalar_reference_k64` licenses treating the two + as numerically interchangeable, but no session on this container can + measure the AMX path's actual behavior. Needs a Sapphire-Rapids-class + (or newer) host to close. From 882f456c7a99d18ea5d247577870c03628edcefd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 22:17:08 +0000 Subject: [PATCH 6/6] Fix the merge regression that broke every compile job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit My 2026-08-11 merge of origin/master kept BOTH sides of the simd.rs re-export block, which introduced a duplicate `bf16_tile_gemm_16x16` name: once unaliased from `hpc::bf16_tile_gemm` (this branch's W1a re-export) and once from `simd_ops` (master's canonical polyfill export). The collision is a hard compile error, which is why clippy, tests, native-backend, tier4-avx512, wasm-simd, neon-qemu and hpc-stream-parallel all went red at once on 921bd7a. Resolution: drop this branch's unaliased re-export and keep master's deliberate two-name split — `bf16_tile_gemm_16x16` (the pure polyfill kernel, from simd_ops) and `bf16_tile_gemm_16x16_amx` (the tile-dispatching wrapper, from hpc). Master's arrangement already satisfies the W1a "reach it via ndarray::simd::*" intent that this branch's export was written for, so nothing is lost. No tile-GEMM function is added or removed by this branch — `int8_tile_gemm_16x16` (used by the geostrophic example) has been on master since 58200fb, and no 16x32 tile GEMM exists in the crate; the only 16x32 symbols are SIMD register types (I16x32/U16x32) in simd_nightly, which are lane widths, not tile kernels. 16x16 is AMX's native TDPBUSD accumulator geometry, not a choice. Also in this commit: - Restore the newline my merge script ate between the `_amx` re-export and the CPU-generation comment (this was the `format/stable` failure). - Correct the arch_prctl wording in AMX_GOTCHAS Gotcha 15: 158 is the x86_64 SYS_arch_prctl syscall NUMBER, not an operation argument, so both diagnostics now read `syscall(SYS_arch_prctl /* 158 */, ARCH_{REQ,GET}_XCOMP_PERM, ...)`. Raised by CodeRabbit on #275 and correct on the merits. Gates, run locally on the merged tree: `cargo fmt --check` clean, `cargo check --lib --features std` clean, `cargo clippy --lib --features std` no errors, and `cargo run --example geostrophic_stencil` green with all 4 pre-registered bars PASS on the scalar-fallback path. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01CcpLeEC3XK8Eye53GKBVvi --- .claude/AMX_GOTCHAS.md | 6 ++++-- src/simd.rs | 14 +++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.claude/AMX_GOTCHAS.md b/.claude/AMX_GOTCHAS.md index 23f4d222..0481b6cb 100644 --- a/.claude/AMX_GOTCHAS.md +++ b/.claude/AMX_GOTCHAS.md @@ -281,9 +281,11 @@ directly (not just CPUID) — each a few lines of stable asm (Gotcha 4/5 has the 0 (a generic model name like `Xeon @ 2.80GHz` is the tell). - `_xgetbv(0)` XCR0 b17/b18 (TILECFG/TILEDATA) — if **0**, the OS never enabled tile XSTATE; AMX cannot run, full stop. -- `arch_prctl(158, ARCH_REQ_XCOMP_PERM, XTILEDATA=18)` — `-95 / -EOPNOTSUPP` +- `syscall(SYS_arch_prctl /* 158 */, ARCH_REQ_XCOMP_PERM, XFEATURE_XTILEDATA=18)` + — `-95 / -EOPNOTSUPP` means the kernel REFUSES the grant; **no byte-call can force it**. -- `arch_prctl(158, ARCH_GET_XCOMP_PERM, &mask)` — XTILEDATA (b18) must be in mask. +- `syscall(SYS_arch_prctl /* 158 */, ARCH_GET_XCOMP_PERM, &mask)` — XTILEDATA (b18) + must be in mask. Observed once on one provisioning (kernel 6.18.5 guest): CPUID=0 **and** XCR0 b17/b18=0 **and** REQ=`-EOPNOTSUPP` → genuinely unavailable. `detect_amx()` diff --git a/src/simd.rs b/src/simd.rs index 5d6628ad..662dbfcf 100644 --- a/src/simd.rs +++ b/src/simd.rs @@ -645,12 +645,11 @@ pub use crate::hpc::cascade; // `backend::gemm_bf16` (portable scalar / NEON / wasm-SIMD paths). #[cfg(all(feature = "std", target_arch = "x86_64"))] pub use crate::hpc::amx_matmul::{amx_available, matmul_i8_to_i32}; -// BF16 16×16 tile GEMM — AMX `TDPBF16PS` when `amx_available()`, `F32x16` FMA -// fallback otherwise. Same W1a rule: reach it via `ndarray::simd::*`, never -// `crate::hpc` directly (the one-time `arch_prctl(158)` XTILEDATA grant is gated -// inside `amx_available()`, which this dispatcher calls before any tile op). -#[cfg(feature = "std")] -pub use crate::hpc::bf16_tile_gemm::bf16_tile_gemm_16x16; +// (The BF16 tile GEMM this branch re-exported unaliased is already surfaced +// twice below — `bf16_tile_gemm_16x16` from `simd_ops` (the pure polyfill +// kernel) and `bf16_tile_gemm_16x16_amx` from `hpc` (the tile-dispatching +// wrapper). A third, unaliased `hpc` re-export collided with the `simd_ops` +// name; dropped at the 2026-08-11 merge in favour of master's two-name split.) // Runtime-dispatch trampolines (`simd_runtime`, feature = "runtime-dispatch") // surfaced through the canonical `ndarray::simd::*` namespace — the W1a // consumer invariant is "all SIMD from `ndarray::simd`", so consumers that @@ -682,7 +681,8 @@ pub use crate::simd_runtime::matmul_i8_to_i32; #[cfg(all(feature = "std", target_arch = "x86_64"))] pub use crate::hpc::bf16_tile_gemm::{ bf16_tile_gemm_16x16 as bf16_tile_gemm_16x16_amx, bf16_tile_gemm_16x16_packed, bf16_tile_gemm_tier, PackedBf16B, -};// CPU-generation detection (cached): SPR / EMR / GNR / Sierra Forest. Lets a +}; +// CPU-generation detection (cached): SPR / EMR / GNR / Sierra Forest. Lets a // consumer report which silicon a run landed on and distinguish "no AMX // silicon" from "AMX present but not OS-enabled" — both surface via `amx_report`. #[cfg(target_arch = "x86_64")]