diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02e9952683..ce18a5a523 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,7 +93,14 @@ jobs: # giving us up to ~13 minutes of retry headroom before declaring the # registry unreachable. Mirrored in the `mutants` job below. workspace-test: - runs-on: [self-hosted, X64, Linux, clean-room] + # PMAT-3138 (#3138): arch-neutral -- intel, yoga OR gx10. Measured 2026-09-12 on + # gx10 with this image and env, --no-fail-fast: step 1 (59k tests) 10.6 min vs + # 34.5 on intel, step 2 (102 tree readers) 6.7 vs 27.4, the over-the-cap + # cargo check 84 s; test EXECUTION runs 3-4x faster on the idle 20-core box. + # The four aarch64-only reds that pin found are fixed in the same PR (an x86 + # example, a shared-B GEMM defect, a wall-clock ratio moved to bench-gates, a + # lane-count tolerance). pr-review-receipt keeps X64 until #3132. + runs-on: [self-hosted, Linux, clean-room] # 100, not 85. The step below sets `timeout-minutes: 75`, but "Set up runner" # (image pull) has measured at 20 minutes, so 20 + 75 = 95 > 85 and the JOB # timeout always fired first -- producing a bare "The operation was canceled" @@ -623,6 +630,18 @@ jobs: set -euo pipefail read -ra targets <<< "$TARGETS" printf 'tree-reader targets: %s\n' "${#targets[@]}" + # PMAT-1098 67-F2: build ONLY the packages the targets name. `--workspace + # --lib --tests` built every test binary of every member to run 41 of + # them: run 34680214617 attempt 1 compiled 421 crates after step 1 had + # compiled 703, linked 686 binaries it then skipped, and spent 27.4 min + # on 17 min of tests. A token is crate:--lib[:MOD] | crate:--bins | + # crate:--test:NAME, so the package is the first field. The three + # GPU/SIMD crates are excluded by the registry guard already + # (check_tree_reader_tests.sh); filtered here again so a registry + # drift can never pull them into a clean-room run. + pkgs="$(printf '%s\n' "${targets[@]}" | cut -d: -f1 | sort -u \ + | grep -vxE 'aprender-(gpu|cuda-edge|compute)' | sed 's/^/-p /' | tr '\n' ' ')" + printf 'packages built: %s\n' "$(printf '%s' "$pkgs" | grep -o -- '-p ' | wc -l)" EXPR="$(bash scripts/ci_test_tier.sh --filterset "$TARGETS")" printf 'nextest filterset (%s clause(s)):\n' "$(printf '%s' "$EXPR" | tr '|' '\n' | wc -l)" printf '%s\n' "$EXPR" | tr '|' '\n' | sed 's/^ *//; s/ *$//; s/^/ + /' @@ -643,7 +662,7 @@ jobs: -e CARGO_PROFILE_DEV_DEBUG=line-tables-only \ -e CARGO_TERM_COLOR=never \ "$IMAGE" \ - cargo nextest run --profile ci --workspace --lib --tests --exclude aprender-gpu --exclude aprender-cuda-edge --exclude aprender-compute -E "$EXPR" + cargo nextest run --profile ci $pkgs --lib --tests -E "$EXPR" # BSE-17 quick tier, part 3 (PMAT-1098 67-E2, #3084 — rule (i)): the # selection was OVER THE CAP (touched crates + their direct reverse # dependents exceed gate_touched_crates.sh's CAP=3). That used to escalate diff --git a/crates/aprender-compute/Cargo.toml b/crates/aprender-compute/Cargo.toml index ee23f56d39..387d88c083 100644 --- a/crates/aprender-compute/Cargo.toml +++ b/crates/aprender-compute/Cargo.toml @@ -115,6 +115,10 @@ matrixmultiply = "0.3" # Direct matrixmultiply crate benchmark (engine behind n [features] default = [] parallel = ["rayon"] +# Opt-in wall-clock gates (speedup ratios, throughput floors). Never part of a +# required check: scripts/check_no_timing_in_required.sh -- they belong to the +# nightly bench lane. A-013 (NEON >= 2x scalar) lives here. +bench-gates = [] gpu = ["wgpu", "pollster", "bytemuck", "futures-intrusive"] # GPU for WASM (WebGPU) - uses wasm-bindgen-futures instead of pollster gpu-wasm = ["wgpu", "bytemuck", "futures-intrusive", "wasm-bindgen-futures", "wasm-bindgen", "web-sys"] diff --git a/crates/aprender-compute/src/blis/parallel.rs b/crates/aprender-compute/src/blis/parallel.rs index 19e56f930c..5335010da4 100644 --- a/crates/aprender-compute/src/blis/parallel.rs +++ b/crates/aprender-compute/src/blis/parallel.rs @@ -188,51 +188,32 @@ pub fn gemm_blis_parallel_shared_b( return Err(TruenoError::InvalidInput("Dimension mismatch".to_string())); } - // For small problems, use single-thread path let flops = m * n * k; - if flops < 8_000_000 { - return gemm_blis(m, n, k, a, b, c, None); - } - - // Require AVX-512 for the 8×32 microkernel - #[cfg(target_arch = "x86_64")] - if !std::arch::is_x86_feature_detected!("avx512f") { + if !shared_b_path_available(flops) { return gemm_blis(m, n, k, a, b, c, None); } - let phys_cores = num_cpus::get_physical(); - let max_threads = if flops < 64_000_000 { - 2.min(phys_cores) - } else if flops < 512_000_000 { - 4.min(phys_cores) - } else if flops < 4_000_000_000 { - // Shared-B means less L3 pressure per thread, so we can potentially - // use more threads than the per-thread-B path. Try phys_cores/2. - (phys_cores / 2).max(8).min(phys_cores) - } else { - (phys_cores / 2).max(8).min(phys_cores) - }; - + let num_threads = shared_b_thread_count(flops).min(rayon::current_num_threads()); let blk = super::cache_topology::blocking_8x32(); - let mr = blk.mr; // 8 - let nr = blk.nr; // 32 - let mc = blk.mc.min(m); - let nc = blk.nc.min(n); - let kc = blk.kc; + let geo = SharedBGeometry { + mr: blk.mr, // 8 + nr: blk.nr, // 32 + mc: blk.mc.min(m), + nc: blk.nc.min(n), + kc: blk.kc, + }; // Shared packed B: one allocation for the largest B panel - let b_panels = (nc + nr - 1) / nr; - let packed_b_size = b_panels * nr * kc; - let mut packed_b = vec![0.0f32; packed_b_size]; + let b_panels = geo.nc.div_ceil(geo.nr); + let mut packed_b = vec![0.0f32; b_panels * geo.nr * geo.kc]; let c_ptr = c.as_mut_ptr() as usize; - let num_threads = max_threads.min(rayon::current_num_threads()); - for jc in (0..n).step_by(nc) { - let nc_block = nc.min(n - jc); + for jc in (0..n).step_by(geo.nc) { + let nc_block = geo.nc.min(n - jc); - for pc in (0..k).step_by(kc) { - let kc_block = kc.min(k - pc); + for pc in (0..k).step_by(geo.kc) { + let kc_block = geo.kc.min(k - pc); // Pack B ONCE (sequential) — shared by all threads super::compute::pack_b_block_generic( @@ -242,96 +223,30 @@ pub fn gemm_blis_parallel_shared_b( jc, kc_block, nc_block, - nr, + geo.nr, &mut packed_b, ); - let shared_b: &[f32] = &packed_b; + let block = SharedBBlock { + a, + k, + n, + c_ptr, + jc, + pc, + nc_block, + kc_block, + geo: &geo, + shared_b: &packed_b, + }; // Parallel ic loop: each thread gets a slice of M - let m_per_thread = ((m + num_threads - 1) / num_threads + mr - 1) / mr * mr; + let m_per_thread = m.div_ceil(num_threads).div_ceil(geo.mr) * geo.mr; (0..num_threads).into_par_iter().for_each(|tid| { let ic_start = tid * m_per_thread; - if ic_start >= m { - return; - } - let ic_end = (ic_start + m_per_thread).min(m); - - // Thread-local packed A — reuse across (jc, pc) iterations - // via thread_local! to avoid heap allocation per iteration. - thread_local! { - static TL_A: std::cell::RefCell> = - const { std::cell::RefCell::new(Vec::new()) }; + if ic_start < m { + block.run_slice(ic_start, (ic_start + m_per_thread).min(m), m_per_thread); } - TL_A.with(|tl| { - let a_panels = (m_per_thread + mr - 1) / mr; - let needed = a_panels * mr * kc_block; - let mut packed_a = tl.borrow_mut(); - if packed_a.len() < needed { - packed_a.resize(needed, 0.0); - } - - let panels_n = (nc_block + nr - 1) / nr; - - for ic in (ic_start..ic_end).step_by(mc) { - let mc_block = mc.min(ic_end - ic); - - super::packing::pack_a_block( - a, - k, - ic, - pc, - mc_block, - kc_block, - &mut packed_a, - ); - - let panels_m = (mc_block + mr - 1) / mr; - - for ir_panel in 0..panels_m { - let ir = ir_panel * mr; - let mr_block = mr.min(mc_block - ir); - - for jr_panel in 0..panels_n { - let jr = jr_panel * nr; - let nr_block = nr.min(nc_block - jr); - - let a_panel = &packed_a[ir_panel * mr * kc_block..]; - let b_panel = &shared_b[jr_panel * nr * kc_block..]; - - if mr_block == 8 && nr_block == 32 { - #[cfg(target_arch = "x86_64")] - unsafe { - super::compute::avx512_microkernel_8x32_rowmajor( - kc_block, - a_panel.as_ptr(), - b_panel.as_ptr(), - (c_ptr as *mut f32).add((ic + ir) * n + (jc + jr)), - n, - ); - } - } else { - // Scalar fallback for edge tiles - for ir_local in 0..mr_block { - for jr_local in 0..nr_block { - let mut sum = 0.0f32; - for p in 0..kc_block { - sum += a_panel[p * mr + ir_local] - * b_panel[p * nr + jr_local]; - } - unsafe { - let c = c_ptr as *mut f32; - *c.add( - (ic + ir + ir_local) * n + (jc + jr + jr_local), - ) += sum; - } - } - } - } - } - } - } - }); // TL_A.with }); } } @@ -339,6 +254,186 @@ pub fn gemm_blis_parallel_shared_b( Ok(()) } +/// Whether the shared-B path may run at all: big enough to pay for the +/// packing, and on a target that has the 8×32 microkernel. +/// +/// FALSIFY-SHARED-B-001 on aarch64 (gx10, 2026-09-12): the AVX-512 check used +/// to exist only under `cfg(target_arch = "x86_64")`, and the microkernel call +/// in the tile loop is `cfg(x86_64)` inside an `if` with no other arm — so on +/// every other target the full 8×32 tiles were silently SKIPPED and only the +/// edge tiles were computed: max diff 39.2 against the reference at 256³ (the +/// 100×96 row passed only because it sits under the 8M-flop cut). The shared-B +/// path is AVX-512-only by construction; everything else takes the same plain +/// BLIS path a no-AVX-512 x86 box takes. +#[cfg(feature = "parallel")] +fn shared_b_path_available(flops: usize) -> bool { + if flops < 8_000_000 { + return false; + } + #[cfg(target_arch = "x86_64")] + { + std::arch::is_x86_feature_detected!("avx512f") + } + #[cfg(not(target_arch = "x86_64"))] + { + false + } +} + +/// Thread budget by problem size. Shared-B means less L3 pressure per thread +/// than the per-thread-B path, so the large tiers use phys_cores/2 (≥ 8). +#[cfg(feature = "parallel")] +fn shared_b_thread_count(flops: usize) -> usize { + let phys_cores = num_cpus::get_physical(); + if flops < 64_000_000 { + 2.min(phys_cores) + } else if flops < 512_000_000 { + 4.min(phys_cores) + } else { + (phys_cores / 2).max(8).min(phys_cores) + } +} + +/// The 8×32 blocking the shared-B path packs for. +#[cfg(feature = "parallel")] +struct SharedBGeometry { + mr: usize, + nr: usize, + mc: usize, + nc: usize, + kc: usize, +} + +/// One (jc, pc) block: B is packed once and read by every thread; each thread +/// packs its own A slice and writes a disjoint row range of C. +#[cfg(feature = "parallel")] +struct SharedBBlock<'a> { + a: &'a [f32], + k: usize, + n: usize, + /// `*mut f32` of C as usize so the block is `Sync`; every write lands in + /// this thread's own row range (see `run_slice`). + c_ptr: usize, + jc: usize, + pc: usize, + nc_block: usize, + kc_block: usize, + geo: &'a SharedBGeometry, + shared_b: &'a [f32], +} + +#[cfg(feature = "parallel")] +impl SharedBBlock<'_> { + /// This thread's M-slice `[ic_start, ic_end)`: pack A per mc block into a + /// thread-local buffer (reused across (jc, pc) iterations — no allocation + /// per iteration) and run the panel loop over it. + fn run_slice(&self, ic_start: usize, ic_end: usize, m_per_thread: usize) { + thread_local! { + static TL_A: std::cell::RefCell> = + const { std::cell::RefCell::new(Vec::new()) }; + } + TL_A.with(|tl| { + let geo = self.geo; + let needed = m_per_thread.div_ceil(geo.mr) * geo.mr * self.kc_block; + let mut packed_a = tl.borrow_mut(); + if packed_a.len() < needed { + packed_a.resize(needed, 0.0); + } + + for ic in (ic_start..ic_end).step_by(geo.mc) { + let mc_block = geo.mc.min(ic_end - ic); + super::packing::pack_a_block( + self.a, + self.k, + ic, + self.pc, + mc_block, + self.kc_block, + &mut packed_a, + ); + self.run_panels(&packed_a, ic, mc_block); + } + }); + } + + /// Every (mr × nr) tile of one packed-A block against the shared B. + fn run_panels(&self, packed_a: &[f32], ic: usize, mc_block: usize) { + let geo = self.geo; + let panels_n = self.nc_block.div_ceil(geo.nr); + for ir_panel in 0..mc_block.div_ceil(geo.mr) { + let ir = ir_panel * geo.mr; + let mr_block = geo.mr.min(mc_block - ir); + for jr_panel in 0..panels_n { + let jr = jr_panel * geo.nr; + let nr_block = geo.nr.min(self.nc_block - jr); + let a_panel = &packed_a[ir_panel * geo.mr * self.kc_block..]; + let b_panel = &self.shared_b[jr_panel * geo.nr * self.kc_block..]; + self.tile(a_panel, b_panel, ic + ir, self.jc + jr, mr_block, nr_block); + } + } + } + + /// One tile at C[row.., col..]: the AVX-512 microkernel for a full 8×32, + /// the scalar loop for every edge tile (and for every full tile on a + /// target without the microkernel — reachable only if the path guard is + /// ever widened). + fn tile( + &self, + a_panel: &[f32], + b_panel: &[f32], + row: usize, + col: usize, + mr_block: usize, + nr_block: usize, + ) { + #[cfg(target_arch = "x86_64")] + if mr_block == 8 && nr_block == 32 { + // SAFETY: the path guard proved avx512f; a_panel/b_panel hold + // kc_block × 8 and kc_block × 32 packed floats; (row, col) is + // inside this thread's disjoint row range of C, which outlives + // the block. + unsafe { + super::compute::avx512_microkernel_8x32_rowmajor( + self.kc_block, + a_panel.as_ptr(), + b_panel.as_ptr(), + (self.c_ptr as *mut f32).add(row * self.n + col), + self.n, + ); + } + return; + } + self.scalar_tile(a_panel, b_panel, row, col, mr_block, nr_block); + } + + /// Scalar fallback for edge tiles. + fn scalar_tile( + &self, + a_panel: &[f32], + b_panel: &[f32], + row: usize, + col: usize, + mr_block: usize, + nr_block: usize, + ) { + let geo = self.geo; + for ir_local in 0..mr_block { + for jr_local in 0..nr_block { + let mut sum = 0.0f32; + for p in 0..self.kc_block { + sum += a_panel[p * geo.mr + ir_local] * b_panel[p * geo.nr + jr_local]; + } + // SAFETY: (row + ir_local, col + jr_local) is inside this + // thread's disjoint row range of C (see `run_slice`). + unsafe { + let c = self.c_ptr as *mut f32; + *c.add((row + ir_local) * self.n + (col + jr_local)) += sum; + } + } + } + } +} + /// Non-parallel fallback #[cfg(not(feature = "parallel"))] pub fn gemm_blis_parallel( diff --git a/crates/aprender-compute/tests/falsification_tests/section_a.rs b/crates/aprender-compute/tests/falsification_tests/section_a.rs index c1a3f0bcb9..756c82fb27 100644 --- a/crates/aprender-compute/tests/falsification_tests/section_a.rs +++ b/crates/aprender-compute/tests/falsification_tests/section_a.rs @@ -266,8 +266,14 @@ fn test_a004_gpu_tolerance() { } /// A-013: NEON provides >= 2x speedup over Scalar on ARM64 +/// +/// A wall-clock RATIO, so it is a `bench-gates` test and never part of a required +/// check (scripts/check_no_timing_in_required.sh). It was live on every aarch64 +/// run and vacuous on x86: gx10 measured 0.87x on 2026-09-12 because LLVM +/// autovectorises the "scalar" `iter().zip().map().collect()` baseline -- the +/// ratio measures the optimiser, not NEON. Measured, not asserted, at PR time. #[test] -#[cfg(target_arch = "aarch64")] +#[cfg(all(target_arch = "aarch64", feature = "bench-gates"))] fn test_a013_neon_speedup() { use std::time::Instant; @@ -295,9 +301,10 @@ fn test_a013_neon_speedup() { assert!(speedup >= 2.0, "A-013 FALSIFIED: NEON speedup {} is less than 2x", speedup); } -/// A-013: NEON speedup test placeholder for non-ARM64 +/// A-013: NEON speedup test placeholder for non-ARM64 and for aarch64 without +/// `bench-gates` (the ratio itself is an opt-in bench gate, see above) #[test] -#[cfg(not(target_arch = "aarch64"))] +#[cfg(not(all(target_arch = "aarch64", feature = "bench-gates")))] fn test_a013_neon_speedup_placeholder() { // NEON is ARM64-only, test passes trivially on other architectures // This ensures the claim number exists for tracking purposes diff --git a/crates/aprender-compute/tests/fma_correctness_f017.rs b/crates/aprender-compute/tests/fma_correctness_f017.rs index bfe2bc051a..81e44d443a 100644 --- a/crates/aprender-compute/tests/fma_correctness_f017.rs +++ b/crates/aprender-compute/tests/fma_correctness_f017.rs @@ -283,9 +283,16 @@ fn f022_fma_dot_product_accuracy() { let relative_error = (dot - expected).abs() / expected; - // FMA-accelerated dot product should have low error + // FMA-accelerated dot product should have low error. The bound is the + // blocked-accumulation model, not one box's lane count: 10^4 terms of + // ~0.01 summed in f32 (ulp 7.6e-6 near 100) with k independent + // accumulators carry at most n * eps / (2k) relative error -- 7.5e-5 for + // k = 4 (NEON f32x4, the narrowest SIMD path), 1.9e-5 measured on gx10; + // a scalar sequential sum can reach 3e-4 and still fails. The old 1e-5 + // held only with >= 8 accumulators (AVX2/AVX-512) and was RED on aarch64 + // (2026-09-12). assert!( - relative_error < 1e-5, + relative_error < 1e-4, "F022 FALSIFIED: dot product error too large: {} (expected {}, got {})", relative_error, expected, diff --git a/crates/aprender-serve/examples/bench_simd_dot.rs b/crates/aprender-serve/examples/bench_simd_dot.rs index e6acb33ab1..73d2ab5839 100644 --- a/crates/aprender-serve/examples/bench_simd_dot.rs +++ b/crates/aprender-serve/examples/bench_simd_dot.rs @@ -2,202 +2,222 @@ #![allow(unsafe_op_in_unsafe_fn)] #![allow(clippy::needless_range_loop, clippy::cast_ptr_alignment)] -use std::arch::x86_64::*; -use std::time::Instant; - -const ITERATIONS: usize = 100_000; -const DIM: usize = 2048; // Typical hidden dim -const Q4_BLOCK_SIZE: usize = 32; -const Q4_BLOCK_BYTES: usize = 18; // 2 bytes scale + 16 bytes quants - -fn has_avx_vnni() -> bool { - let result = unsafe { __cpuid_count(7, 1) }; - (result.eax & (1 << 4)) != 0 -} - -#[target_feature(enable = "avx2", enable = "fma")] -unsafe fn dot_avx2(q4_data: &[u8], q8_scales: &[f32], q8_quants: &[i8]) -> f32 { - let num_blocks = DIM / Q4_BLOCK_SIZE; - let mut acc = _mm256_setzero_ps(); - let offset = _mm256_set1_epi8(8); - let low_mask = _mm256_set1_epi8(0x0F); - - for block_idx in 0..num_blocks { - let q4_ptr = q4_data.as_ptr().add(block_idx * Q4_BLOCK_BYTES); - let q8_ptr = q8_quants.as_ptr().add(block_idx * Q4_BLOCK_SIZE); - - // Read Q4 scale (f16 stored as 2 bytes) - let scale_bytes = std::ptr::read_unaligned(q4_ptr.cast::()); - let q4_scale = half::f16::from_bits(scale_bytes).to_f32(); - - // Load Q4 nibbles and unpack - let q4_packed = _mm_loadu_si128(q4_ptr.add(2).cast::<__m128i>()); - let q4_lo = _mm256_and_si256(_mm256_cvtepu8_epi16(q4_packed), low_mask); - let q4_hi = _mm256_and_si256(_mm256_cvtepu8_epi16(_mm_srli_epi16(q4_packed, 4)), low_mask); - - // Interleave lo/hi to get full 32 values, then subtract offset - let q4_vals = _mm256_sub_epi8(_mm256_packus_epi16(q4_lo, q4_hi), offset); - - // Load Q8 values - let q8_vals = _mm256_loadu_si256(q8_ptr.cast::<__m256i>()); - - // maddubs: pairs of (u8 × i8) -> i16, then horizontal add pairs - let products = _mm256_maddubs_epi16( - _mm256_sign_epi8(q4_vals, q8_vals), - _mm256_sign_epi8(q8_vals, q8_vals), - ); - - // madd with 1s to sum pairs of i16 -> i32 - let sums = _mm256_madd_epi16(products, _mm256_set1_epi16(1)); - - // Convert to float and accumulate with scale - let scale_vec = _mm256_set1_ps(q4_scale * q8_scales[block_idx]); - acc = _mm256_fmadd_ps(_mm256_cvtepi32_ps(sums), scale_vec, acc); +// The kernels are AVX2/FMA/AVX-VNNI: `std::arch::x86_64` plus inline `asm!` with +// ymm registers. On any other target the example still builds and runs (G3.EX: every +// example must build AND run before a tag) and says so -- it does not pretend to bench. +#[cfg(target_arch = "x86_64")] +mod x86 { + use std::arch::x86_64::*; + use std::time::Instant; + + const ITERATIONS: usize = 100_000; + const DIM: usize = 2048; // Typical hidden dim + const Q4_BLOCK_SIZE: usize = 32; + const Q4_BLOCK_BYTES: usize = 18; // 2 bytes scale + 16 bytes quants + + fn has_avx_vnni() -> bool { + let result = unsafe { __cpuid_count(7, 1) }; + (result.eax & (1 << 4)) != 0 } - // Horizontal sum - let hi = _mm256_extractf128_ps(acc, 1); - let lo = _mm256_castps256_ps128(acc); - let sum128 = _mm_add_ps(lo, hi); - let sum64 = _mm_add_ps(sum128, _mm_movehl_ps(sum128, sum128)); - let sum32 = _mm_add_ss(sum64, _mm_shuffle_ps(sum64, sum64, 1)); - _mm_cvtss_f32(sum32) -} + #[target_feature(enable = "avx2", enable = "fma")] + unsafe fn dot_avx2(q4_data: &[u8], q8_scales: &[f32], q8_quants: &[i8]) -> f32 { + let num_blocks = DIM / Q4_BLOCK_SIZE; + let mut acc = _mm256_setzero_ps(); + let offset = _mm256_set1_epi8(8); + let low_mask = _mm256_set1_epi8(0x0F); + + for block_idx in 0..num_blocks { + let q4_ptr = q4_data.as_ptr().add(block_idx * Q4_BLOCK_BYTES); + let q8_ptr = q8_quants.as_ptr().add(block_idx * Q4_BLOCK_SIZE); + + // Read Q4 scale (f16 stored as 2 bytes) + let scale_bytes = std::ptr::read_unaligned(q4_ptr.cast::()); + let q4_scale = half::f16::from_bits(scale_bytes).to_f32(); + + // Load Q4 nibbles and unpack + let q4_packed = _mm_loadu_si128(q4_ptr.add(2).cast::<__m128i>()); + let q4_lo = _mm256_and_si256(_mm256_cvtepu8_epi16(q4_packed), low_mask); + let q4_hi = + _mm256_and_si256(_mm256_cvtepu8_epi16(_mm_srli_epi16(q4_packed, 4)), low_mask); + + // Interleave lo/hi to get full 32 values, then subtract offset + let q4_vals = _mm256_sub_epi8(_mm256_packus_epi16(q4_lo, q4_hi), offset); + + // Load Q8 values + let q8_vals = _mm256_loadu_si256(q8_ptr.cast::<__m256i>()); + + // maddubs: pairs of (u8 × i8) -> i16, then horizontal add pairs + let products = _mm256_maddubs_epi16( + _mm256_sign_epi8(q4_vals, q8_vals), + _mm256_sign_epi8(q8_vals, q8_vals), + ); + + // madd with 1s to sum pairs of i16 -> i32 + let sums = _mm256_madd_epi16(products, _mm256_set1_epi16(1)); + + // Convert to float and accumulate with scale + let scale_vec = _mm256_set1_ps(q4_scale * q8_scales[block_idx]); + acc = _mm256_fmadd_ps(_mm256_cvtepi32_ps(sums), scale_vec, acc); + } -#[target_feature(enable = "avx2", enable = "fma")] -unsafe fn dot_avx_vnni(q4_data: &[u8], q8_scales: &[f32], q8_quants: &[i8]) -> f32 { - use std::arch::asm; - - let num_blocks = DIM / Q4_BLOCK_SIZE; - let mut acc = _mm256_setzero_ps(); - let offset = _mm256_set1_epi8(8); - let low_mask = _mm256_set1_epi8(0x0F); - - for block_idx in 0..num_blocks { - let q4_ptr = q4_data.as_ptr().add(block_idx * Q4_BLOCK_BYTES); - let q8_ptr = q8_quants.as_ptr().add(block_idx * Q4_BLOCK_SIZE); - - let scale_bytes = std::ptr::read_unaligned(q4_ptr.cast::()); - let q4_scale = half::f16::from_bits(scale_bytes).to_f32(); - - let q4_packed = _mm_loadu_si128(q4_ptr.add(2).cast::<__m128i>()); - let q4_lo = _mm256_and_si256(_mm256_cvtepu8_epi16(q4_packed), low_mask); - let q4_hi = _mm256_and_si256(_mm256_cvtepu8_epi16(_mm_srli_epi16(q4_packed, 4)), low_mask); - let q4_vals = _mm256_sub_epi8(_mm256_packus_epi16(q4_lo, q4_hi), offset); - - // Make unsigned by adding 8 back - let q4_unsigned = _mm256_add_epi8(q4_vals, offset); - let q8_vals = _mm256_loadu_si256(q8_ptr.cast::<__m256i>()); - - // Use vpdpbusd: u8 × i8 -> i32 accumulate (VEX-encoded for AVX-VNNI) - let mut int_acc = _mm256_setzero_si256(); - asm!( - // VEX.256.66.0F38.W0 50 /r - VPDPBUSD ymm1, ymm2, ymm3/m256 - ".byte 0xc4, 0xe2, 0x6d, 0x50, 0xc1", // vpdpbusd ymm0, ymm2, ymm1 - inout("ymm0") int_acc, - in("ymm1") q8_vals, - in("ymm2") q4_unsigned, - options(pure, nomem, nostack), - ); - - // Subtract bias: 8 * sum(q8) per lane - simplified for benchmark - let scale_vec = _mm256_set1_ps(q4_scale * q8_scales[block_idx]); - acc = _mm256_fmadd_ps(_mm256_cvtepi32_ps(int_acc), scale_vec, acc); + // Horizontal sum + let hi = _mm256_extractf128_ps(acc, 1); + let lo = _mm256_castps256_ps128(acc); + let sum128 = _mm_add_ps(lo, hi); + let sum64 = _mm_add_ps(sum128, _mm_movehl_ps(sum128, sum128)); + let sum32 = _mm_add_ss(sum64, _mm_shuffle_ps(sum64, sum64, 1)); + _mm_cvtss_f32(sum32) } - let hi = _mm256_extractf128_ps(acc, 1); - let lo = _mm256_castps256_ps128(acc); - let sum128 = _mm_add_ps(lo, hi); - let sum64 = _mm_add_ps(sum128, _mm_movehl_ps(sum128, sum128)); - let sum32 = _mm_add_ss(sum64, _mm_shuffle_ps(sum64, sum64, 1)); - _mm_cvtss_f32(sum32) -} + #[target_feature(enable = "avx2", enable = "fma")] + unsafe fn dot_avx_vnni(q4_data: &[u8], q8_scales: &[f32], q8_quants: &[i8]) -> f32 { + use std::arch::asm; + + let num_blocks = DIM / Q4_BLOCK_SIZE; + let mut acc = _mm256_setzero_ps(); + let offset = _mm256_set1_epi8(8); + let low_mask = _mm256_set1_epi8(0x0F); + + for block_idx in 0..num_blocks { + let q4_ptr = q4_data.as_ptr().add(block_idx * Q4_BLOCK_BYTES); + let q8_ptr = q8_quants.as_ptr().add(block_idx * Q4_BLOCK_SIZE); + + let scale_bytes = std::ptr::read_unaligned(q4_ptr.cast::()); + let q4_scale = half::f16::from_bits(scale_bytes).to_f32(); + + let q4_packed = _mm_loadu_si128(q4_ptr.add(2).cast::<__m128i>()); + let q4_lo = _mm256_and_si256(_mm256_cvtepu8_epi16(q4_packed), low_mask); + let q4_hi = + _mm256_and_si256(_mm256_cvtepu8_epi16(_mm_srli_epi16(q4_packed, 4)), low_mask); + let q4_vals = _mm256_sub_epi8(_mm256_packus_epi16(q4_lo, q4_hi), offset); + + // Make unsigned by adding 8 back + let q4_unsigned = _mm256_add_epi8(q4_vals, offset); + let q8_vals = _mm256_loadu_si256(q8_ptr.cast::<__m256i>()); + + // Use vpdpbusd: u8 × i8 -> i32 accumulate (VEX-encoded for AVX-VNNI) + let mut int_acc = _mm256_setzero_si256(); + asm!( + // VEX.256.66.0F38.W0 50 /r - VPDPBUSD ymm1, ymm2, ymm3/m256 + ".byte 0xc4, 0xe2, 0x6d, 0x50, 0xc1", // vpdpbusd ymm0, ymm2, ymm1 + inout("ymm0") int_acc, + in("ymm1") q8_vals, + in("ymm2") q4_unsigned, + options(pure, nomem, nostack), + ); + + // Subtract bias: 8 * sum(q8) per lane - simplified for benchmark + let scale_vec = _mm256_set1_ps(q4_scale * q8_scales[block_idx]); + acc = _mm256_fmadd_ps(_mm256_cvtepi32_ps(int_acc), scale_vec, acc); + } -fn main() { - println!("=== SIMD Dot Product Benchmark ===\n"); - println!("CPU: Intel Core Ultra 7 155H"); - println!("AVX-VNNI available: {}", has_avx_vnni()); - println!("Dimension: {}", DIM); - println!("Iterations: {}\n", ITERATIONS); - - // Setup test data - let num_blocks = DIM / Q4_BLOCK_SIZE; - let mut q4_data = vec![0u8; num_blocks * Q4_BLOCK_BYTES]; - let q8_scales = vec![0.1f32; num_blocks]; - let mut q8_quants = vec![0i8; DIM]; - - // Fill with pseudo-random data - for (i, b) in q4_data.iter_mut().enumerate() { - *b = ((i * 17 + 3) % 256) as u8; - } - for (i, q) in q8_quants.iter_mut().enumerate() { - *q = (((i * 13 + 7) % 256) as i8).wrapping_sub(64); + let hi = _mm256_extractf128_ps(acc, 1); + let lo = _mm256_castps256_ps128(acc); + let sum128 = _mm_add_ps(lo, hi); + let sum64 = _mm_add_ps(sum128, _mm_movehl_ps(sum128, sum128)); + let sum32 = _mm_add_ss(sum64, _mm_shuffle_ps(sum64, sum64, 1)); + _mm_cvtss_f32(sum32) } - // Warmup - for _ in 0..1000 { - unsafe { - let _ = dot_avx2(&q4_data, &q8_scales, &q8_quants); + pub fn main() { + println!("=== SIMD Dot Product Benchmark ===\n"); + println!("CPU: Intel Core Ultra 7 155H"); + println!("AVX-VNNI available: {}", has_avx_vnni()); + println!("Dimension: {}", DIM); + println!("Iterations: {}\n", ITERATIONS); + + // Setup test data + let num_blocks = DIM / Q4_BLOCK_SIZE; + let mut q4_data = vec![0u8; num_blocks * Q4_BLOCK_BYTES]; + let q8_scales = vec![0.1f32; num_blocks]; + let mut q8_quants = vec![0i8; DIM]; + + // Fill with pseudo-random data + for (i, b) in q4_data.iter_mut().enumerate() { + *b = ((i * 17 + 3) % 256) as u8; } - } - - // Benchmark AVX2 - let start = Instant::now(); - let mut result_avx2 = 0.0f32; - for _ in 0..ITERATIONS { - unsafe { - result_avx2 = dot_avx2(&q4_data, &q8_scales, &q8_quants); + for (i, q) in q8_quants.iter_mut().enumerate() { + *q = (((i * 13 + 7) % 256) as i8).wrapping_sub(64); } - } - let avx2_time = start.elapsed(); - // Benchmark AVX-VNNI (only if available) - let vnni_time; - let result_vnni; - if has_avx_vnni() { // Warmup for _ in 0..1000 { unsafe { - let _ = dot_avx_vnni(&q4_data, &q8_scales, &q8_quants); + let _ = dot_avx2(&q4_data, &q8_scales, &q8_quants); } } + // Benchmark AVX2 let start = Instant::now(); - let mut r = 0.0f32; + let mut result_avx2 = 0.0f32; for _ in 0..ITERATIONS { unsafe { - r = dot_avx_vnni(&q4_data, &q8_scales, &q8_quants); + result_avx2 = dot_avx2(&q4_data, &q8_scales, &q8_quants); } } - vnni_time = Some(start.elapsed()); - result_vnni = Some(r); - } else { - vnni_time = None; - result_vnni = None; - } + let avx2_time = start.elapsed(); + + // Benchmark AVX-VNNI (only if available) + let vnni_time; + let result_vnni; + if has_avx_vnni() { + // Warmup + for _ in 0..1000 { + unsafe { + let _ = dot_avx_vnni(&q4_data, &q8_scales, &q8_quants); + } + } - // Results - let avx2_ns = avx2_time.as_nanos() as f64 / ITERATIONS as f64; - println!("AVX2 (maddubs+madd):"); - println!(" Time: {:.1} ns/dot", avx2_ns); - println!(" Result: {:.4}", result_avx2); - - if let (Some(vt), Some(rv)) = (vnni_time, result_vnni) { - let vnni_ns = vt.as_nanos() as f64 / ITERATIONS as f64; - let speedup = avx2_ns / vnni_ns; - println!("\nAVX-VNNI (vpdpbusd):"); - println!(" Time: {:.1} ns/dot", vnni_ns); - println!(" Result: {:.4}", rv); - println!("\nSpeedup: {:.2}x", speedup); - - if speedup > 1.1 { - println!("✓ AVX-VNNI is faster - consider enabling in quantize.rs"); - } else if speedup < 0.9 { - println!("✗ AVX-VNNI is slower - keep AVX2 path"); + let start = Instant::now(); + let mut r = 0.0f32; + for _ in 0..ITERATIONS { + unsafe { + r = dot_avx_vnni(&q4_data, &q8_scales, &q8_quants); + } + } + vnni_time = Some(start.elapsed()); + result_vnni = Some(r); } else { - println!("≈ Similar performance - AVX2 path is fine"); + vnni_time = None; + result_vnni = None; + } + + // Results + let avx2_ns = avx2_time.as_nanos() as f64 / ITERATIONS as f64; + println!("AVX2 (maddubs+madd):"); + println!(" Time: {:.1} ns/dot", avx2_ns); + println!(" Result: {:.4}", result_avx2); + + if let (Some(vt), Some(rv)) = (vnni_time, result_vnni) { + let vnni_ns = vt.as_nanos() as f64 / ITERATIONS as f64; + let speedup = avx2_ns / vnni_ns; + println!("\nAVX-VNNI (vpdpbusd):"); + println!(" Time: {:.1} ns/dot", vnni_ns); + println!(" Result: {:.4}", rv); + println!("\nSpeedup: {:.2}x", speedup); + + if speedup > 1.1 { + println!("✓ AVX-VNNI is faster - consider enabling in quantize.rs"); + } else if speedup < 0.9 { + println!("✗ AVX-VNNI is slower - keep AVX2 path"); + } else { + println!("≈ Similar performance - AVX2 path is fine"); + } + } else { + println!("\nAVX-VNNI: Not available on this CPU"); } - } else { - println!("\nAVX-VNNI: Not available on this CPU"); } } + +#[cfg(target_arch = "x86_64")] +fn main() { + x86::main(); +} + +#[cfg(not(target_arch = "x86_64"))] +fn main() { + eprintln!( + "bench_simd_dot: x86_64-only kernels (AVX2/FMA/AVX-VNNI); nothing to run on this target" + ); +} diff --git a/docs/audits/impl-PMAT-3138-receipt.md b/docs/audits/impl-PMAT-3138-receipt.md new file mode 100644 index 0000000000..1c9ca8b41f --- /dev/null +++ b/docs/audits/impl-PMAT-3138-receipt.md @@ -0,0 +1,87 @@ +--- +status: in-flight +merged: "[U] — opens against main after train #3127 lands; this line is amended with the squash sha" +ticket: PMAT-3138 +row: 67-fleet (follow-up to #3104; 06x-release-schedule.md §2 E/F) +issue: 3138 +epic: 3078 +model: "orchestrator claude-fable-5-1 (direct; no worker dispatched — the measurement ran on gx10 under setsid, the fixes are four files + ci.yml)" +tokens_used: "orchestrator [U] (not instrumented)" +wall_clock_s: "08:20Z–09:10Z wall for measurement + fixes ≈ 3000 s; the gx10 sweeps themselves: 499 + 638 + 84 + 833 + 25 + 36 s" +orch_model: claude-fable-5-1 +orch_class: orchestration +orch_decision: "direct — one operator-priority ticket, the measurement is a shell harness on gx10 and the fixes are four files + ci.yml; no worker or lane would have been cheaper than the round-trips it saved" +fable_binding: true +quota_age_h: 0 +quota_mark: A +k_measured_at_set: 4 +--- +# impl-PMAT-3138 — workspace-test on ARM64: gx10 runs the quick tier 3–4× faster than intel; four aarch64-only reds fixed; the tree-reader step builds 20 packages, not 686 binaries (#3138) + +Receipt for the comparison in the title: `evidence/ci/arm64-workspace-test-2026-09-12/gx10-summary.txt` (per-step rc and seconds on gx10) and `evidence/ci/arm64-workspace-test-2026-09-12/gx10-tiers.txt` (the Starting/Summary lines of every tier), against intel's run 34680214617 attempt 1 (step timestamps in the Verification table below). Measured, not a target. + +## Identity +ticket PMAT-3138 · kind code+ci · operator instruction 2026-09-12 08:20Z: "STOP the entire build system, and unclog gx10, then continue". gx10 read 0 % busy while PRs queued on intel because every long job pinned `X64`. + +## What lands +- `.github/workflows/ci.yml` `workspace-test`: `runs-on: [self-hosted, Linux, clean-room]` (was `X64`). The tree-reader step derives `-p` from its 102 targets (20 packages) instead of `--workspace --lib --tests` (686 test binaries linked to run 41). `pr-review-receipt` keeps `X64` (#3132). +- `crates/aprender-serve/examples/bench_simd_dot.rs`: the AVX2/FMA/AVX-VNNI kernels move into a `#[cfg(target_arch = "x86_64")] mod x86`; other targets get a `main` that says so. `cargo check --workspace --all-targets --locked` was RED on aarch64 (`the feature named avx2 is not valid for this target`, ×4). +- `crates/aprender-compute/src/blis/parallel.rs`: `gemm_blis_parallel_shared_b` — the AVX-512 guard was `cfg(x86_64)`-only and the microkernel call sits in a `cfg(x86_64)` `if` with no other arm, so on aarch64 every full 8×32 tile was skipped (FALSIFY-SHARED-B-001: max diff 39.2 at 256³; 100×96 passed only under the 8 M-flop cut). Non-x86 takes the plain BLIS path; the scalar arm covers a full tile wherever the microkernel is absent. Decomposed into `shared_b_path_available`, `shared_b_thread_count`, `SharedBBlock::{run_slice, run_panels, tile, scalar_tile}` — the pre-commit complexity gate (cyclomatic 30 / cognitive 25) blocks any edit to the 175-line original. Not on any inference path: the only caller is `examples/blis_benchmark.rs`. +- `crates/aprender-compute/Cargo.toml` + `tests/falsification_tests/section_a.rs`: A-013 (NEON ≥ 2× scalar) behind a new `bench-gates` feature; the placeholder covers every other configuration. It is a wall-clock ratio, x86-vacuous, and 0.87× on gx10 because LLVM autovectorises the "scalar" baseline (`iter().zip().map().collect()`). +- `crates/aprender-compute/tests/fma_correctness_f017.rs` F022: relative-error bound 1e-5 → 1e-4, derived (`n·eps/(2k)`, k = 4 NEON lanes → 7.5e-5; measured 1.9e-5 on gx10; a scalar sequential sum ≈ 3e-4 still fails). +- `docs/roadmaps/roadmap.yaml`: PMAT-3138 (minted from #3138). + +routes: + ph0 class=orchestration route=self w=11.11 basis=first-run[U] # STOP: cancel 5 runs, rerun one job, inventory gx10 + ph1 class=measure route=self w=11.11 basis=first-run[U] # arm-sweep.sh / -2 / -3 / -5 on gx10 (setsid, container = CI image) + ph2 class=impl route=self w=11.11 basis=first-run[U] # four aarch64 fixes + ci.yml scoping + runs-on + ph3 class=verify route=self w=11.11 basis=first-run[U] # x86 (AVX-512 box) + gx10 re-verification, guards, lint + +verification: + cmd="cargo fmt --all -- --check" claimed_exit=0 rerun_exit=0 log_path=evidence/ci/arm64-workspace-test-2026-09-12/fmt.log sha256=ac8582433c5eba13abd955caf91e8d716ce92cd77971a68eb4cf3cae698d171a + cmd="bash scripts/check_runner_labels.sh && bash scripts/check_no_timing_in_required.sh" claimed_exit=0 rerun_exit=0 (both rc 0) log_path=evidence/ci/arm64-workspace-test-2026-09-12/guards.log sha256=42fdf9e8118584215862df453732ef614b4f04a2bd5dca46bb9842c36e76f0fc + cmd="cargo nextest run --profile ci --no-fail-fast -p aprender-compute --lib --tests --features parallel -E 'test(/blis::/) | test(f022_fma) | test(a013)' [x86_64, avx512f]" claimed_exit=0 rerun_exit=0 (306 passed) log_path=evidence/ci/arm64-workspace-test-2026-09-12/x86-compute-tests.log sha256=26bd469fb2b01c7c44336309db6caa79bd30fe0a4ca44a62e7773e89117d7b87 + cmd="cargo clippy -p aprender-compute --lib --features parallel -- -D warnings [x86_64]" claimed_exit=0 rerun_exit=0 log_path=evidence/ci/arm64-workspace-test-2026-09-12/x86-clippy.log sha256=d4c756ad12014c9e82f8acbca2c2e85974215a709739f74da1689b86a5470de7 + cmd="cargo check -p aprender-serve --example bench_simd_dot [x86_64]" claimed_exit=0 rerun_exit=0 log_path=evidence/ci/arm64-workspace-test-2026-09-12/x86-bench-simd-dot-check.log sha256=a97f10e12a61a6a2a1e5752a0d8cc7b62087bb367986f0645b95c7268199c9d5 + cmd="gx10: cargo nextest run --profile ci --no-fail-fast -p aprender-compute --lib --tests --features parallel -E 'test(shared_b) | test(f022_fma) | test(a013)' [aarch64, after the fixes]" claimed_exit=0 rerun_exit=0 (4 passed; the same four were 3 FAIL + 1 vacuous before) log_path=evidence/ci/arm64-workspace-test-2026-09-12/gx10-fix-verify.txt sha256=1338e624cb462932fa676c4803130f45dbd0252c7be950359f10acedf5823990 + cmd="gx10: cargo check --workspace --all-targets --locked [aarch64, after the bench_simd_dot gate; was rc 101]" claimed_exit=0 rerun_exit=0 (check2_rc=0 in 84 s) log_path=evidence/ci/arm64-workspace-test-2026-09-12/gx10-check2-tail.txt sha256=2935c8ccae0da02aa20eb83cbc5caef19dbba856fe8148c443025a22cabc7d54 + cmd="gx10: quick step 1 / step 2 / FULL lib / GPU crates / compute lib — the Starting/Summary lines of every tier" claimed_exit=0 rerun_exit=n/a (measurement; 59,359/59,362 pre-fix, 10,325/10,325, 82,085/82,085, rc 0, rc 0) log_path=evidence/ci/arm64-workspace-test-2026-09-12/gx10-tiers.txt sha256=727c3c726c5565ec03ba4e24ffae3fc2034e13ebacbaebbf5057b8baff7249fd + cmd="gx10: arm-sweep summary (tier decision, per-step rc and seconds)" claimed_exit=0 rerun_exit=n/a (measurement) log_path=evidence/ci/arm64-workspace-test-2026-09-12/gx10-summary.txt sha256=b4285607a7f912c546065cbb777c8edc0f8e6b9489707a74287a3b07151f5efa + +## Verification +Measurement harness: `gx10:~/eph-work/arm-sweep{,-2,-3,-5}.sh`, results `gx10:~/eph-work/arm-sweep/20260912T082818Z/summary.txt` — same `localhost:5000/sovereign-ci:stable` image and env block as ci.yml, train tree a6148ab72, `--no-fail-fast`, `CARGO_BUILD_JOBS=12` (CI uses 8). +| check | intel (run 34680214617 attempt 1) | gx10 | +|---|---|---| +| quick step 1, 9 selected crates | 34.5 min, 59,590/59,590 (tests 1603 s) | 638 s, 59,359/59,362 (tests 532 s) — the 3 reds below | +| quick step 2, 102 tree-reader targets | 27.4 min, 10,329/10,329 (tests 1016 s) | 402 s, 10,325/10,325 (tests 246 s) | +| `cargo check --workspace --all-targets --locked` | cancelled at 1.5 min | rc 101 (bench_simd_dot) → rc 0 in 84 s after the gate | +| FULL: workspace `--lib` −3 crates | ~60 min (nightly) | 833 s, 82,085/82,085 | +| FULL: `-p aprender-gpu -p aprender-cuda-edge --lib` | — | 25 s, rc 0 | +| FULL: `cargo test -p aprender-compute --lib` | — | 36 s, rc 0 | +| the four fixes, `-p aprender-compute --lib --tests --features parallel -E 'test(shared_b) \| test(f022_fma) \| test(a013)'` | 4/4 on this AVX-512 box (48 cores) | 4/4 (`fix-verify.txt`, rc 0) | +| `cargo nextest run -p aprender-compute --lib --features parallel -E 'test(/blis::/)'` after the decomposition | 304/304 | — | +| `cargo clippy -p aprender-compute --lib --features parallel -- -D warnings` | clean | — | +| `cargo run -p aprender-serve --example bench_simd_dot` | (x86: benches) | rc 0, prints the x86-only notice | +| `scripts/check_runner_labels.sh`, `scripts/check_no_timing_in_required.sh`, `cargo fmt --all -- --check` | rc 0 | — | +| tree-reader `-p` derivation dry-run on the train's touched set | 102 targets → 20 packages | — | + +## Mutation (RED, then GREEN) +- bench_simd_dot: `check.txt` RED (4 errors) → `check2.txt` rc 0 after the gate. Reverting the module gate restores the 4 errors on aarch64. +- shared-B GEMM: `step1.txt` RED (max diff 39.2, 3 tries) → `fix-verify.txt` 4/4. On x86 the same 304 blis tests are green before and after (the x86 path is unchanged by construction: same microkernel, same blocking). +- A-013: `step1.txt` RED (0.87× < 2×) → with `bench-gates` off the ratio is not compiled; with it on the ratio is measured and RED on gx10 — which is the finding, not a defect to hide. +- F022: `step1.txt` RED (1.9e-5 > 1e-5) → GREEN at 1e-4. A scalar sequential sum's ≈3e-4 stays RED under the new bound, so the bound still excludes an outcome. +- tree-reader `-p` scoping: `[U]` until the PR's first run — its step-2 log must show `packages built: 20` and far fewer `Compiling` lines than 421. + +## Jidoka +- The first draft of this PR "fixed" step 1 by adding an env block it already had (a `grep -v '-e '` in my own view had hidden it). Re-read the raw block before diagnosing; the actual waste was step 2's `--workspace`. +- The first verification run on gx10 (`blis-adhoc.txt`, 161/161) proved nothing: without `--features parallel` the shared-B tests are not compiled. Feature unification in the 9-crate CI selection is what compiles them there. +- `tar … | ssh 'tar xf - && … &'` extracted nothing: the trailing `&` backgrounded the whole chain and tar read `/dev/null`. Copy with scp, launch in a second command. +- The Bash tool is zsh: `read -ra` and `${PIPESTATUS}` fail silently; the derivation dry-run runs under `bash -c`. + +## Gaps +- The arm64 `sovereign-ci:stable` image runs as uid 0: 17 root-owned per-PR target dirs (100 GB) on gx10 that the host reclaim step cannot delete — chowned by hand today; the image needs `USER 1000` (separate infra change, not in this PR). +- `pr-review-receipt` stays `X64` until #3132 (reject-76-drop on arm64). +- The queue-run `estimatedTimeToMerge` and the 80/80/50 packing targets are unreachable while every PR sits behind one serialized train; the 0.68 DIRTY wave after #3127 is what fills three boxes. + +## Estimates +basis: first-run[U] — no prior measured row for kind=ci+code on repo=aprender at this shape (measurement + fixes + refactor on two boxes). diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index 88ce2bd46a..0e822f019d 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -17169,6 +17169,26 @@ roadmap: labels: - kind:triage notes: null +- id: PMAT-3138 + github_issue: 3138 + item_type: task + title: 'workspace-test on ARM64: route arch-neutral after the four aarch64-only reds; scope the tree-reader step to its packages' + status: planned + priority: high + assigned_to: null + created: 2026-09-12T08:54:56Z + updated: 2026-09-12T08:54:56Z + spec: null + acceptance_criteria: [] + phases: [] + subtasks: [] + estimated_effort: null + labels: + - ci + - fleet + - gx10 + notes: null + - id: PMAT-3146 github_issue: 3146 item_type: task diff --git a/evidence/ci/arm64-workspace-test-2026-09-12/gx10-check2-tail.txt b/evidence/ci/arm64-workspace-test-2026-09-12/gx10-check2-tail.txt new file mode 100644 index 0000000000..66a0f33910 --- /dev/null +++ b/evidence/ci/arm64-workspace-test-2026-09-12/gx10-check2-tail.txt @@ -0,0 +1,40 @@ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: unused import: `super::super::ulp::assert_ulp_eq` + --> crates/aprender-contracts/src/kernels/rope.rs:179:9 + | +179 | use super::super::ulp::assert_ulp_eq; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: unused import: `super::super::ulp::assert_ulp_eq` + --> crates/aprender-contracts/src/kernels/swiglu.rs:121:9 + | +121 | use super::super::ulp::assert_ulp_eq; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: unused import: `super::super::ulp::assert_ulp_eq` + --> crates/aprender-contracts/src/kernels/linear.rs:186:9 + | +186 | use super::super::ulp::assert_ulp_eq; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: unused import: `crate::kernels::ulp::assert_ulp_eq` + --> crates/aprender-contracts/src/kernels/adamw.rs:160:9 + | +160 | use crate::kernels::ulp::assert_ulp_eq; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: unused import: `crate::kernels::ulp::assert_ulp_eq` + --> crates/aprender-contracts/src/kernels/cma_es.rs:163:9 + | +163 | use crate::kernels::ulp::assert_ulp_eq; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: unused import: `crate::kernels::ulp::assert_ulp_eq` + --> crates/aprender-contracts/src/kernels/gated_delta_net.rs:179:9 + | +179 | use crate::kernels::ulp::assert_ulp_eq; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: `aprender-contracts` (lib test) generated 13 warnings (run `cargo fix --lib -p aprender-contracts --tests` to apply 13 suggestions) + Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 13s diff --git a/evidence/ci/arm64-workspace-test-2026-09-12/gx10-fix-verify.txt b/evidence/ci/arm64-workspace-test-2026-09-12/gx10-fix-verify.txt new file mode 100644 index 0000000000..5cfa9c6576 --- /dev/null +++ b/evidence/ci/arm64-workspace-test-2026-09-12/gx10-fix-verify.txt @@ -0,0 +1,93 @@ +info: syncing channel updates for 1.93.0-aarch64-unknown-linux-gnu +info: latest update on 2026-01-22 for version 1.93.0 (254b59607 2026-01-19) +info: downloading 5 components + Compiling aprender-compute v0.66.0 (/workspace/crates/aprender-compute) + Compiling aprender-core v0.66.0 (/workspace/crates/aprender-core) + Compiling aprender-simulate v0.66.0 (/workspace/crates/aprender-simulate) +warning: aprender-compute@0.66.0: provable-contracts binding.yaml not found at /workspace/crates/aprender-compute/../../../provable-contracts/contracts/trueno/binding.yaml; CONTRACT_* env vars will not be set (CI/crates.io build) +warning: aprender-core@0.66.0: provable-contracts binding.yaml not found at /workspace/crates/aprender-core/../../../provable-contracts/contracts/aprender/binding.yaml; CONTRACT_* env vars will not be set (CI/crates.io build) +warning: aprender-simulate@0.66.0: [contract] Assertions: 2 preconditions, 1 postconditions from YAML +warning: unused variable: `mr_block` + --> crates/aprender-compute/src/blis/compute.rs:94:5 + | +94 | mr_block: usize, + | ^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_mr_block` + | + = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default + +warning: unused variable: `nr_block` + --> crates/aprender-compute/src/blis/compute.rs:95:5 + | +95 | nr_block: usize, + | ^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_nr_block` + +warning: unused variable: `backend` + --> crates/aprender-compute/src/brick/quant_ops/mod.rs:219:43 + | +219 | fn execute(&self, input: Self::Input, backend: Backend) -> Result { + | ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_backend` + +warning: unused variable: `backend` + --> crates/aprender-compute/src/brick/quant_ops/mod.rs:318:43 + | +318 | fn execute(&self, input: Self::Input, backend: Backend) -> Result { + | ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_backend` + +warning: `aprender-compute` (lib) generated 4 warnings (run `cargo fix --lib -p aprender-compute` to apply 4 suggestions) +warning: unused imports: `OperationType`, `select_backend_for_operation`, and `select_best_available_backend` + --> crates/aprender-compute/tests/falsification_tests/section_a.rs:5:5 + | +5 | select_backend_for_operation, select_best_available_backend, Backend, OperationType, Vector, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default + +warning: unused import: `Backend` + --> crates/aprender-compute/tests/falsification_tests/section_e.rs:5:45 + | +5 | use trueno::{select_best_available_backend, Backend, Vector}; + | ^^^^^^^ + +warning: `aprender-compute` (test "falsification_tests") generated 2 warnings (run `cargo fix --test "falsification_tests" -p aprender-compute` to apply 2 suggestions) +warning: unused import: `super::super::*` + --> crates/aprender-compute/src/backends/q4k/tests_core/avx2.rs:3:5 + | +3 | use super::super::*; + | ^^^^^^^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default + +warning: unused import: `crate::blis::*` + --> crates/aprender-compute/src/blis/tests/numerical/asm_coverage/asm_microkernel.rs:1:5 + | +1 | use crate::blis::*; + | ^^^^^^^^^^^^^^ + +warning: unused variable: `backend` + --> crates/aprender-compute/src/eigen/tests.rs:231:9 + | +231 | let backend = eigen.backend(); + | ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_backend` + +warning: unused variable: `expected` + --> crates/aprender-compute/src/matrix/ops/linear/tests/parallel.rs:35:9 + | +35 | let expected = m_scalar.matvec(&v).unwrap(); + | ^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_expected` + +warning: function `assert_activation_in_range` is never used + --> crates/aprender-compute/src/vector/ops/activations/tests/mod.rs:32:4 + | +32 | fn assert_activation_in_range( + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default + +warning: `aprender-compute` (lib test) generated 9 warnings (4 duplicates) (run `cargo fix --lib -p aprender-compute --tests` to apply 4 suggestions) + Finished `test` profile [unoptimized + debuginfo] target(s) in 21.03s +──────────── + Nextest run ID d7108cc1-cfe2-4880-a1ce-b65b1cf294d2 with nextest profile: ci + Starting 4 tests across 17 binaries (3739 tests skipped) +──────────── + Summary [ 0.340s] 4 tests run: 4 passed, 3739 skipped +rc=0 diff --git a/evidence/ci/arm64-workspace-test-2026-09-12/gx10-summary.txt b/evidence/ci/arm64-workspace-test-2026-09-12/gx10-summary.txt new file mode 100644 index 0000000000..e813ae1834 --- /dev/null +++ b/evidence/ci/arm64-workspace-test-2026-09-12/gx10-summary.txt @@ -0,0 +1,59 @@ +start=20260912T082818Z head=a6148ab72 host=gx10-a5b5 +touched=134 +tier_rc=0 +tier=quick +check_workspace=1 +reason=pull_request: selection of 32 crate(s) exceeds cap (3) -> full workspace check -- rule (i): the touched crate(s) run their tests and ONE cargo check --workspace --all-targets covers every reverse dependent at compile level; plus 102 tree-reader target(s) from scripts/tree_reader_tests.txt +targets_n=102 +clauses=101 +nextest_rc=0 nextest_secs=402 + Summary [ 245.864s] 10325 tests run: 10325 passed (1 slow), 54949 skipped +check_rc=101 check_secs=97 + 3 warning: unused variable: `backend` + 3 warning: unused import: `aprender::prelude` + 2 error: the feature named `fma` is not valid for this target + 2 error: the feature named `avx2` is not valid for this target + 1 warning: unused variable: `v` + 1 warning: unused variable: `nr_block` + 1 warning: unused variable: `mr_block` + 1 warning: unused variable: `marker_with_page` + 1 warning: unused variable: `expected` + 1 warning: unused variable: `d_model` + 1 warning: unused return value of `embeddings::BertEmbeddings::forward` that must be used + 1 warning: unused import: `super::super::*` + 1 warning: unused import: `std::path::Path` + 1 warning: unused import: `std::arch::is_x86_feature_detected` + 1 warning: unused imports: `QuantType` and `quantize` + 1 warning: unused imports: `OperationType`, `select_backend_for_operation`, and `select_best_available_backend` + 1 warning: unused import: `crate::nn::Module` + 1 warning: unused import: `crate::format::quantize::BLOCK_SIZE` + 1 warning: unused import: `crate::blis::*` + 1 warning: unused import: `Backend` +end=20260912T083700Z total_secs=499 +DONE +step1_crates=9 +step1_rc=100 step1_secs=638 + TRY 1 FAIL [ 0.142s] (───────────) aprender-compute blis::tests::validate_and_parallel::test_gemm_parallel_shared_b_256 + TRY 2 FAIL [ 0.143s] (───────────) aprender-compute blis::tests::validate_and_parallel::test_gemm_parallel_shared_b_256 + TRY 3 FAIL [ 0.143s] ( 8696/59362) aprender-compute blis::tests::validate_and_parallel::test_gemm_parallel_shared_b_256 + TRY 1 FAIL [ 0.029s] (───────────) aprender-compute::falsification_tests section_a::test_a013_neon_speedup + TRY 2 FAIL [ 0.027s] (───────────) aprender-compute::falsification_tests section_a::test_a013_neon_speedup + TRY 3 FAIL [ 0.019s] (11614/59362) aprender-compute::falsification_tests section_a::test_a013_neon_speedup + TRY 1 FAIL [ 0.003s] (───────────) aprender-compute::fma_correctness_f017 f022_fma_dot_product_accuracy + TRY 2 FAIL [ 0.004s] (───────────) aprender-compute::fma_correctness_f017 f022_fma_dot_product_accuracy + TRY 3 FAIL [ 0.003s] (11687/59362) aprender-compute::fma_correctness_f017 f022_fma_dot_product_accuracy + Summary [ 532.040s] 59362 tests run: 59359 passed (5 slow), 3 failed, 251 skipped + TRY 3 FAIL [ 0.143s] ( 8696/59362) aprender-compute blis::tests::validate_and_parallel::test_gemm_parallel_shared_b_256 + TRY 3 FAIL [ 0.019s] (11614/59362) aprender-compute::falsification_tests section_a::test_a013_neon_speedup + TRY 3 FAIL [ 0.003s] (11687/59362) aprender-compute::fma_correctness_f017 f022_fma_dot_product_accuracy +DONE2 +check2_rc=0 check2_secs=84 (bench_simd_dot gated) +example_run_rc=0: bench_simd_dot: x86_64-only kernels (AVX2/FMA/AVX-VNNI); nothing to run on this target +DONE3 +full_rc=0 full_secs=833 + 1 Summary [ 781.465s] 82085 tests run: 82085 passed (5 slow), 130 skipped +gpu_rc=0 gpu_secs=25 + 1 Summary [ 0.701s] 664 tests run: 664 passed, 0 skipped +compute_rc=0 compute_secs=36 + 1 test result: ok. 3326 passed; 0 failed; 4 ignored; 0 measured; 0 filtered out; finished in 1.51s +DONE5 diff --git a/evidence/ci/arm64-workspace-test-2026-09-12/gx10-tiers.txt b/evidence/ci/arm64-workspace-test-2026-09-12/gx10-tiers.txt new file mode 100644 index 0000000000..7a0436f6f7 --- /dev/null +++ b/evidence/ci/arm64-workspace-test-2026-09-12/gx10-tiers.txt @@ -0,0 +1,21 @@ +/home/noah/eph-work/arm-sweep/latest/step1.txt: Starting 59362 tests across 423 binaries (251 tests skipped) +/home/noah/eph-work/arm-sweep/latest/step1.txt: TRY 1 FAIL [ 0.142s] (───────────) aprender-compute blis::tests::validate_and_parallel::test_gemm_parallel_shared_b_256 +/home/noah/eph-work/arm-sweep/latest/step1.txt: TRY 2 FAIL [ 0.143s] (───────────) aprender-compute blis::tests::validate_and_parallel::test_gemm_parallel_shared_b_256 +/home/noah/eph-work/arm-sweep/latest/step1.txt: TRY 3 FAIL [ 0.143s] ( 8696/59362) aprender-compute blis::tests::validate_and_parallel::test_gemm_parallel_shared_b_256 +/home/noah/eph-work/arm-sweep/latest/step1.txt: TRY 1 FAIL [ 0.029s] (───────────) aprender-compute::falsification_tests section_a::test_a013_neon_speedup +/home/noah/eph-work/arm-sweep/latest/step1.txt: TRY 2 FAIL [ 0.027s] (───────────) aprender-compute::falsification_tests section_a::test_a013_neon_speedup +/home/noah/eph-work/arm-sweep/latest/step1.txt: TRY 3 FAIL [ 0.019s] (11614/59362) aprender-compute::falsification_tests section_a::test_a013_neon_speedup +/home/noah/eph-work/arm-sweep/latest/step1.txt: TRY 1 FAIL [ 0.003s] (───────────) aprender-compute::fma_correctness_f017 f022_fma_dot_product_accuracy +/home/noah/eph-work/arm-sweep/latest/step1.txt: TRY 2 FAIL [ 0.004s] (───────────) aprender-compute::fma_correctness_f017 f022_fma_dot_product_accuracy +/home/noah/eph-work/arm-sweep/latest/step1.txt: TRY 3 FAIL [ 0.003s] (11687/59362) aprender-compute::fma_correctness_f017 f022_fma_dot_product_accuracy +/home/noah/eph-work/arm-sweep/latest/step1.txt: Summary [ 532.040s] 59362 tests run: 59359 passed (5 slow), 3 failed, 251 skipped +/home/noah/eph-work/arm-sweep/latest/step1.txt: TRY 3 FAIL [ 0.143s] ( 8696/59362) aprender-compute blis::tests::validate_and_parallel::test_gemm_parallel_shared_b_256 +/home/noah/eph-work/arm-sweep/latest/step1.txt: TRY 3 FAIL [ 0.019s] (11614/59362) aprender-compute::falsification_tests section_a::test_a013_neon_speedup +/home/noah/eph-work/arm-sweep/latest/step1.txt: TRY 3 FAIL [ 0.003s] (11687/59362) aprender-compute::fma_correctness_f017 f022_fma_dot_product_accuracy +/home/noah/eph-work/arm-sweep/latest/nextest.txt: Starting 10325 tests across 41 binaries (54949 tests and 686 binaries skipped) +/home/noah/eph-work/arm-sweep/latest/nextest.txt: Summary [ 245.864s] 10325 tests run: 10325 passed (1 slow), 54949 skipped +/home/noah/eph-work/arm-sweep/latest/full.txt: Starting 82085 tests across 73 binaries (130 tests skipped) +/home/noah/eph-work/arm-sweep/latest/full.txt: Summary [ 781.465s] 82085 tests run: 82085 passed (5 slow), 130 skipped +/home/noah/eph-work/arm-sweep/latest/gpu.txt: Starting 664 tests across 2 binaries +/home/noah/eph-work/arm-sweep/latest/gpu.txt: Summary [ 0.701s] 664 tests run: 664 passed, 0 skipped +test result: ok. 3326 passed; 0 failed; 4 ignored; 0 measured; 0 filtered out; finished in 1.51s