Files
T
brenden 682afacd30 Add native self-hosted instance connection to fluxer_desktop
Trimmed monorepo checkout (fluxer_desktop + packages/voice_engine_v2 +
tools/ci) with a "Connect to a Different Server" menu item and popout
that lets the desktop app switch to any self-hosted Fluxer instance,
plus fixes for well-known discovery on single-domain self-hosted
deployments and a false-positive ERR_ABORTED on same-origin client
redirects during the switch. Defaults to chat.fluxr.chat and uses an
isolated userData directory from the official build.
2026-07-01 18:22:43 -04:00

705 lines
26 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! WASM kernel microbenches. Run on wasm32 only.
//!
//! RUSTFLAGS='-C target-feature=+simd128' \
//! CARGO_TARGET_WASM32_WASIP1_RUNNER='wasmtime --env RUST_TEST_NOCAPTURE=1 --' \
//! cargo bench --release --target wasm32-wasip1 -p tract-linalg --bench wasm
//!
//! Re-run with `+simd128,+relaxed-simd` to compare baseline mul+add against
//! the FMA emit driven by the `madd_f32x4!` macro in `linalg/src/wasm.rs`.
#[cfg(not(target_arch = "wasm32"))]
fn main() {
eprintln!("this bench only runs on wasm32 targets — skipping on host");
}
#[cfg(target_arch = "wasm32")]
fn main() {
let target = if cfg!(target_feature = "relaxed-simd") {
"+simd128,+relaxed-simd (FMA)"
} else {
"+simd128 only (mul+add)"
};
eprintln!("=== WASM 8x8 GEMM microbench ({target}) ===");
bench_8x8::run();
eprintln!();
eprintln!("=== Isolated 32x1 GEMV microbench ({target}) ===");
bench_32x1::run();
eprintln!();
eprintln!("=== Isolated 16x1 GEMV microbench ({target}) ===");
bench_16x1::run();
eprintln!();
eprintln!("=== int8 (i8->i32) 4x4 GEMM: wasm SIMD vs generic scalar ({target}) ===");
bench_i8_4x4::run();
#[cfg(target_feature = "relaxed-simd")]
{
eprintln!();
eprintln!("=== int8 relaxed-dot prototype: relaxed_dot vs widening (4x4 tile) ===");
bench_relaxed_dot::run();
}
#[cfg(not(target_feature = "relaxed-simd"))]
eprintln!("\n(int8 relaxed-dot prototype skipped — rebuild with +relaxed-simd)");
}
#[cfg(target_arch = "wasm32")]
mod bench_8x8 {
//! Microbench: time `wasm_f32_8x8` (the GEMM kernel for N>=2) at shapes
//! relevant to DFN3, transformer FFN, and CNN→GEMM workloads.
use std::time::Instant;
use tract_data::internal::*;
use tract_linalg::mmm::{AsInputValue, FusedSpec};
fn run_one(
kernel: &dyn tract_linalg::mmm::MatMatMul,
m: usize,
k: usize,
n: usize,
iters: usize,
) -> f64 {
let packing = &kernel.packings()[0];
let a = Tensor::zero::<f32>(&[m, k]).unwrap();
let pa = packing.0.prepare_one(&a, 1, 0).unwrap();
let b = Tensor::zero::<f32>(&[k, n]).unwrap();
let pb = packing.1.prepare_one(&b, 0, 1).unwrap();
let mut c = Tensor::zero::<f32>(&[m, n]).unwrap();
for _ in 0..50 {
unsafe {
kernel
.run(
m,
n,
&[
FusedSpec::AddMatMul {
a: AsInputValue::Borrowed(&*pa),
b: AsInputValue::Borrowed(&*pb),
packing: 0,
},
FusedSpec::Store(kernel.c_view(Some(0), Some(1)).wrap(&c.view_mut())),
],
)
.unwrap();
}
}
let t0 = Instant::now();
for _ in 0..iters {
unsafe {
kernel
.run(
m,
n,
&[
FusedSpec::AddMatMul {
a: AsInputValue::Borrowed(&*pa),
b: AsInputValue::Borrowed(&*pb),
packing: 0,
},
FusedSpec::Store(kernel.c_view(Some(0), Some(1)).wrap(&c.view_mut())),
],
)
.unwrap();
}
}
let elapsed = t0.elapsed();
elapsed.as_secs_f64() / iters as f64 * 1e9
}
fn pick(name: &str) -> Box<dyn tract_linalg::mmm::MatMatMul> {
let mut ops = tract_linalg::generic();
tract_linalg::wasm::plug(&mut ops);
for impl_ in ops.mmm_impls() {
if impl_.name() == name {
return impl_.clone();
}
}
panic!("kernel {name} not registered")
}
fn bench_shape(label: &str, m: usize, k: usize, n: usize, iters: usize) {
let k88 = pick("wasm_f32_8x8");
let ns = run_one(&*k88, m, k, n, iters);
let m_tiles = m.div_ceil(8);
let n_tiles = n.div_ceil(8);
let total_tiles = m_tiles * n_tiles;
let per_tile_ns = ns / total_tiles as f64;
eprintln!(
"{label} (m={m} k={k} n={n}, iters={iters}): {ns:.0} ns/call \
({total_tiles} 8x8 tiles, {per_tile_ns:.1} ns/tile)"
);
}
pub fn run() {
// DFN3 N>1 GEMM case (the primary 8x8 hit on DFN3).
bench_shape("DFN3-style m=64 k=64 n=8", 64, 64, 8, 50_000);
// Larger N — typical batched/transformer GEMM.
bench_shape("m=64 k=64 n=64", 64, 64, 64, 10_000);
bench_shape("m=128 k=128 n=8", 128, 128, 8, 20_000);
bench_shape("m=128 k=128 n=64", 128, 128, 64, 5_000);
bench_shape("m=256 k=256 n=8", 256, 256, 8, 5_000);
bench_shape("m=256 k=256 n=64", 256, 256, 64, 1_000);
// Whisper-tiny FFN-ish (large K, small N).
bench_shape("m=384 k=1536 n=8", 384, 1536, 8, 1_000);
}
}
#[cfg(target_arch = "wasm32")]
mod bench_32x1 {
//! Isolated, statistics-aware microbench for `wasm_f32_32x1` to investigate
//! the apparent regression at M=100/256 in `microbench_dispatch_gemv`. That
//! bench loops all 4 GEMV kernels back-to-back at every shape, biasing the
//! later-running kernel (32x1) with cache contention and thermal buildup.
//! This module benches 32x1 alone, with min-of-N reporting across
//! repetitions to expose variance honestly.
use std::time::Instant;
use tract_data::internal::*;
use tract_linalg::mmm::{AsInputValue, FusedSpec};
fn run_one(kernel: &dyn tract_linalg::mmm::MatMatMul, m: usize, k: usize, iters: usize) -> f64 {
let packing = &kernel.packings()[0];
let a = Tensor::zero::<f32>(&[m, k]).unwrap();
let pa = packing.0.prepare_one(&a, 1, 0).unwrap();
let b = Tensor::zero::<f32>(&[k, 1]).unwrap();
let pb = packing.1.prepare_one(&b, 0, 1).unwrap();
let mut c = Tensor::zero::<f32>(&[m, 1]).unwrap();
// Generous warmup — 200 calls primes the JIT and hot caches.
for _ in 0..200 {
unsafe {
kernel
.run(
m,
1,
&[
FusedSpec::AddMatMul {
a: AsInputValue::Borrowed(&*pa),
b: AsInputValue::Borrowed(&*pb),
packing: 0,
},
FusedSpec::Store(kernel.c_view(Some(0), Some(0)).wrap(&c.view_mut())),
],
)
.unwrap();
}
}
let t0 = Instant::now();
for _ in 0..iters {
unsafe {
kernel
.run(
m,
1,
&[
FusedSpec::AddMatMul {
a: AsInputValue::Borrowed(&*pa),
b: AsInputValue::Borrowed(&*pb),
packing: 0,
},
FusedSpec::Store(kernel.c_view(Some(0), Some(0)).wrap(&c.view_mut())),
],
)
.unwrap();
}
}
let elapsed = t0.elapsed();
elapsed.as_secs_f64() / iters as f64 * 1e9
}
fn pick(name: &str) -> Box<dyn tract_linalg::mmm::MatMatMul> {
let mut ops = tract_linalg::generic();
tract_linalg::wasm::plug(&mut ops);
for impl_ in ops.mmm_impls() {
if impl_.name() == name {
return impl_.clone();
}
}
panic!("kernel {name} not registered")
}
fn bench_min_of_n(label: &str, m: usize, k: usize, iters: usize, repetitions: usize) {
let kernel = pick("wasm_f32_32x1");
let mut samples: Vec<f64> = Vec::with_capacity(repetitions);
for _ in 0..repetitions {
samples.push(run_one(&*kernel, m, k, iters));
}
samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
let min = samples[0];
let median = samples[samples.len() / 2];
let max = samples[samples.len() - 1];
let pct_spread = (max - min) / min * 100.0;
eprintln!(
"{label} (m={m} k={k}, {iters} iters × {repetitions} reps): \
min={min:.0} median={median:.0} max={max:.0} ns/call (spread {pct_spread:.0}%)"
);
}
pub fn run() {
// Suspect shapes from microbench_dispatch_gemv (apparent regression):
bench_min_of_n("M=100 k=256", 100, 256, 10_000, 10);
bench_min_of_n("M=256 k=256", 256, 256, 5_000, 10);
bench_min_of_n("M=256 k=512", 256, 512, 2_000, 10);
// Reference shapes (showed clean speedup before):
bench_min_of_n("M=24 k=256", 24, 256, 30_000, 10);
bench_min_of_n("M=64 k=96", 64, 96, 20_000, 10);
}
}
#[cfg(target_arch = "wasm32")]
mod bench_16x1 {
//! Isolated 16x1 GEMV microbench — same methodology as bench_32x1.
//! 16x1 has 4 SIMD accumulators per K-step, which under +relaxed-simd
//! exposes the destructive-fmla accumulator recurrence (4-cycle latency
//! throttling throughput to 1 FMA/cycle even though Apple Silicon pipes
//! can do 4). Used to validate that the fix in linalg/src/wasm.rs (which
//! routes 16x1 through `madd_f32x4_nofma!` to use separate mul+add)
//! recovers the regression PR #2199 missed.
use std::time::Instant;
use tract_data::internal::*;
use tract_linalg::mmm::{AsInputValue, FusedSpec};
fn run_one(kernel: &dyn tract_linalg::mmm::MatMatMul, m: usize, k: usize, iters: usize) -> f64 {
let packing = &kernel.packings()[0];
let a = Tensor::zero::<f32>(&[m, k]).unwrap();
let pa = packing.0.prepare_one(&a, 1, 0).unwrap();
let b = Tensor::zero::<f32>(&[k, 1]).unwrap();
let pb = packing.1.prepare_one(&b, 0, 1).unwrap();
let mut c = Tensor::zero::<f32>(&[m, 1]).unwrap();
for _ in 0..200 {
unsafe {
kernel
.run(
m,
1,
&[
FusedSpec::AddMatMul {
a: AsInputValue::Borrowed(&*pa),
b: AsInputValue::Borrowed(&*pb),
packing: 0,
},
FusedSpec::Store(kernel.c_view(Some(0), Some(0)).wrap(&c.view_mut())),
],
)
.unwrap();
}
}
let t0 = Instant::now();
for _ in 0..iters {
unsafe {
kernel
.run(
m,
1,
&[
FusedSpec::AddMatMul {
a: AsInputValue::Borrowed(&*pa),
b: AsInputValue::Borrowed(&*pb),
packing: 0,
},
FusedSpec::Store(kernel.c_view(Some(0), Some(0)).wrap(&c.view_mut())),
],
)
.unwrap();
}
}
let elapsed = t0.elapsed();
elapsed.as_secs_f64() / iters as f64 * 1e9
}
fn pick(name: &str) -> Box<dyn tract_linalg::mmm::MatMatMul> {
let mut ops = tract_linalg::generic();
tract_linalg::wasm::plug(&mut ops);
for impl_ in ops.mmm_impls() {
if impl_.name() == name {
return impl_.clone();
}
}
panic!("kernel {name} not registered")
}
fn bench_min_of_n(label: &str, m: usize, k: usize, iters: usize, repetitions: usize) {
let kernel = pick("wasm_f32_16x1");
let mut samples: Vec<f64> = Vec::with_capacity(repetitions);
for _ in 0..repetitions {
samples.push(run_one(&*kernel, m, k, iters));
}
samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
let min = samples[0];
let median = samples[samples.len() / 2];
let max = samples[samples.len() - 1];
let pct_spread = (max - min) / min * 100.0;
eprintln!(
"{label} (m={m} k={k}, {iters} iters × {repetitions} reps): \
min={min:.0} median={median:.0} max={max:.0} ns/call (spread {pct_spread:.0}%)"
);
}
pub fn run() {
// 16x1's natural band per plug()'s mmv_f32 closure: M ∈ 9..=16
bench_min_of_n("M=9 k=256", 9, 256, 30_000, 10);
bench_min_of_n("M=12 k=256", 12, 256, 30_000, 10);
bench_min_of_n("M=16 k=96", 16, 96, 30_000, 10);
bench_min_of_n("M=16 k=256", 16, 256, 20_000, 10);
bench_min_of_n("M=16 k=512", 16, 512, 10_000, 10);
bench_min_of_n("M=16 k=1024", 16, 1024, 5_000, 10);
}
}
#[cfg(target_arch = "wasm32")]
mod bench_i8_4x4 {
//! int8 (i8->i32) GEMM microbench: the new SIMD `wasm_i32_4x4` vs the scalar
//! `generic_i32_4x4` fallback. Both kernels expose the *identical* i8i8
//! PackedI8K4 packing (packing index 1), the same 4x4 tile and i32
//! accumulator — so the ratio is a clean read on what the SIMD
//! widening-extmul AddMatMul buys over the generic scalar loop. min-of-N
//! reporting per kernel to keep the variance honest.
use std::time::Instant;
use tract_data::internal::*;
use tract_linalg::mmm::{AsInputValue, FusedSpec, MatMatMul};
// i8i8 packing slot is index 1 on both generic_i32_4x4 and wasm_i32_4x4.
const I8I8: usize = 1;
fn run_one(kernel: &dyn MatMatMul, m: usize, k: usize, n: usize, iters: usize) -> f64 {
let packing = &kernel.packings()[I8I8];
let a = Tensor::zero::<i8>(&[m, k]).unwrap();
let pa = packing.0.prepare_one(&a, 1, 0).unwrap();
let b = Tensor::zero::<i8>(&[k, n]).unwrap();
let pb = packing.1.prepare_one(&b, 0, 1).unwrap();
let mut c = Tensor::zero::<i32>(&[m, n]).unwrap();
// Warmup: prime the JIT and hot caches.
for _ in 0..50 {
unsafe {
kernel
.run(
m,
n,
&[
FusedSpec::AddMatMul {
a: AsInputValue::Borrowed(&*pa),
b: AsInputValue::Borrowed(&*pb),
packing: I8I8,
},
FusedSpec::Store(kernel.c_view(Some(0), Some(1)).wrap(&c.view_mut())),
],
)
.unwrap();
}
}
let t0 = Instant::now();
for _ in 0..iters {
unsafe {
kernel
.run(
m,
n,
&[
FusedSpec::AddMatMul {
a: AsInputValue::Borrowed(&*pa),
b: AsInputValue::Borrowed(&*pb),
packing: I8I8,
},
FusedSpec::Store(kernel.c_view(Some(0), Some(1)).wrap(&c.view_mut())),
],
)
.unwrap();
}
}
let elapsed = t0.elapsed();
elapsed.as_secs_f64() / iters as f64 * 1e9
}
fn pick(name: &str) -> Box<dyn MatMatMul> {
let mut ops = tract_linalg::generic();
tract_linalg::wasm::plug(&mut ops);
for impl_ in ops.mmm_impls() {
if impl_.name() == name {
return impl_.clone();
}
}
panic!("kernel {name} not registered")
}
fn min_of_n(
kernel: &dyn MatMatMul,
m: usize,
k: usize,
n: usize,
iters: usize,
reps: usize,
) -> f64 {
let mut samples: Vec<f64> = (0..reps).map(|_| run_one(kernel, m, k, n, iters)).collect();
samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
samples[0]
}
fn bench(label: &str, m: usize, k: usize, n: usize, iters: usize, reps: usize) {
let wasm = pick("wasm_i32_4x4");
let generic = pick("generic_i32_4x4");
let w = min_of_n(&*wasm, m, k, n, iters, reps);
let g = min_of_n(&*generic, m, k, n, iters, reps);
let tiles = m.div_ceil(4) * n.div_ceil(4);
eprintln!(
"{label} (m={m} k={k} n={n}, {iters} iters × {reps} reps): \
wasm={w:.0} generic={g:.0} ns/call speedup={:.2}x \
({tiles} 4x4 tiles, wasm {:.1} ns/tile)",
g / w,
w / tiles as f64
);
}
pub fn run() {
// Square GEMMs across sizes (compute-bound, the SIMD path's home turf).
bench("square m=64 k=64 n=64", 64, 64, 64, 5_000, 8);
bench("square m=128 k=128 n=128", 128, 128, 128, 1_000, 8);
bench("square m=256 k=256 n=256", 256, 256, 256, 200, 8);
// Transformer-ish: large K, moderate M/N (MiniLM/FFN projections).
bench("m=128 k=384 n=384", 128, 384, 384, 500, 8);
bench("m=64 k=1536 n=64", 64, 1536, 64, 1_000, 8);
// CNN→GEMM (InceptionV1-style im2col), small N.
bench("m=256 k=256 n=16", 256, 256, 16, 2_000, 8);
}
}
// Prototype: int8 4x4 tile via `i32x4_relaxed_dot_i8x16_i7x16_add` (SDOT-analog,
// 4 i8 MACs/lane, no widening) vs the deterministic widening path. Only compiles
// under +relaxed-simd. Isolates a single cache-resident 4x4 tile so the ratio is
// a pure instruction-density read. Includes a bit-exactness check on wasmtime.
#[cfg(all(target_arch = "wasm32", target_feature = "relaxed-simd"))]
mod bench_relaxed_dot {
use std::arch::wasm32::*;
use std::hint::black_box;
use std::time::Instant;
// Logical A is [4][k] row-major (a[m*k + ik]); logical B is [k][4] row-major
// (b[ik*4 + n]). Reference 4x4 = sum_ik A[m][ik] * B[ik][n].
fn reference_tile(a: &[i8], b: &[i8], k: usize) -> [i32; 16] {
let mut c = [0i32; 16];
for ik in 0..k {
for m in 0..4 {
for n in 0..4 {
c[m * 4 + n] += a[m * k + ik] as i32 * b[ik * 4 + n] as i32;
}
}
}
c
}
// K-major A: out[ik*4 + m] = A[m][ik] (m inner) — what the widening kernel reads.
fn pack_a_kmajor(a: &[i8], k: usize) -> Vec<i8> {
let mut o = vec![0i8; k * 4];
for ik in 0..k {
for m in 0..4 {
o[ik * 4 + m] = a[m * k + ik];
}
}
o
}
// K-major B is exactly the logical [ik*4 + n] layout already.
// M-major A, K contiguous, K padded to mult of 4: out[m*kp + ik] = A[m][ik].
fn pack_a_mmajor(a: &[i8], k: usize) -> (Vec<i8>, usize) {
let kp = k.div_ceil(4) * 4;
let mut o = vec![0i8; 4 * kp];
for m in 0..4 {
for ik in 0..k {
o[m * kp + ik] = a[m * k + ik];
}
}
(o, kp)
}
// K=4-inner B: out[kb*16 + n*4 + kr] = B[4kb+kr][n] — the relaxed-dot layout.
fn pack_b_k4(b: &[i8], k: usize) -> Vec<i8> {
let kp = k.div_ceil(4) * 4;
let mut o = vec![0i8; kp * 4];
for kb in 0..kp / 4 {
for kr in 0..4 {
let kk = 4 * kb + kr;
if kk >= k {
continue;
}
for n in 0..4 {
o[kb * 16 + n * 4 + kr] = b[kk * 4 + n];
}
}
}
o
}
// Current deterministic approach: widen B to i32x4 per k, splat A, mul+add.
unsafe fn widening_tile(a_km: *const i8, b_km: *const i8, k: usize) -> [i32; 16] {
unsafe {
let mut acc = [i32x4_splat(0); 4];
for ik in 0..k {
let bw = v128_load32_zero(b_km.add(4 * ik) as *const u32);
let bw = i16x8_extend_low_i8x16(bw);
let bw = i32x4_extend_low_i16x8(bw);
let ar = a_km.add(4 * ik);
acc[0] = i32x4_add(acc[0], i32x4_mul(i32x4_splat(*ar.add(0) as i32), bw));
acc[1] = i32x4_add(acc[1], i32x4_mul(i32x4_splat(*ar.add(1) as i32), bw));
acc[2] = i32x4_add(acc[2], i32x4_mul(i32x4_splat(*ar.add(2) as i32), bw));
acc[3] = i32x4_add(acc[3], i32x4_mul(i32x4_splat(*ar.add(3) as i32), bw));
}
let mut c = [0i32; 16];
for m in 0..4 {
v128_store(c[m * 4..].as_mut_ptr() as *mut v128, acc[m]);
}
c
}
}
// Relaxed-dot: per 4-K block, one v128 B-load shared across 4 rows; each row
// broadcasts its 4 K-bytes and issues one relaxed_dot. 64 MACs in 4 dots.
unsafe fn relaxed_tile(apk: *const i8, bpk: *const i8, kp: usize) -> [i32; 16] {
unsafe {
let mut acc = [i32x4_splat(0); 4];
for kb in 0..kp / 4 {
let b_all = v128_load(bpk.add(kb * 16) as *const v128);
for m in 0..4 {
let a4 = (apk.add(m * kp + kb * 4) as *const i32).read_unaligned();
let a_m = i32x4_splat(a4);
acc[m] = i32x4_relaxed_dot_i8x16_i7x16_add(a_m, b_all, acc[m]);
}
}
let mut c = [0i32; 16];
for m in 0..4 {
v128_store(c[m * 4..].as_mut_ptr() as *mut v128, acc[m]);
}
c
}
}
fn gen_data(k: usize, seed: i32, bits7: bool) -> Vec<i8> {
(0..k * 4)
.map(|i| {
let v = ((i as i32)
.wrapping_mul(97)
.wrapping_add(seed)
.wrapping_mul(31))
& 0xff;
let v = (v - 128) as i8; // full i8 range
if bits7 {
(v as i32).clamp(-63, 63) as i8
} else {
v
}
})
.collect()
}
fn check(label: &str, k: usize, b_bits7: bool) {
let a = gen_data(k, 1, false);
let b = gen_data(k, 7, b_bits7);
let reference = reference_tile(&a, &b, k);
let a_km = pack_a_kmajor(&a, k);
let w = unsafe { widening_tile(a_km.as_ptr(), b.as_ptr(), k) };
assert_eq!(w, reference, "widening_tile mismatch ({label})");
let (a_mm, kp) = pack_a_mmajor(&a, k);
let b_k4 = pack_b_k4(&b, k);
let r = unsafe { relaxed_tile(a_mm.as_ptr(), b_k4.as_ptr(), kp) };
let exact = r == reference;
eprintln!(
" correctness {label} (k={k}, B={}): widening=exact relaxed={}",
if b_bits7 { "7-bit" } else { "full-i8" },
if exact {
"EXACT"
} else {
"DIFFERS (non-deterministic intermediate)"
}
);
if b_bits7 {
assert!(exact, "relaxed_dot must be exact when B is 7-bit ({label})");
}
}
fn time_relaxed(apk: &[i8], bpk: &[i8], kp: usize, iters: usize) -> f64 {
let mut sink = 0i32;
for _ in 0..50 {
sink ^= unsafe { relaxed_tile(apk.as_ptr(), bpk.as_ptr(), kp) }[0];
}
let t0 = Instant::now();
for _ in 0..iters {
let c = unsafe {
relaxed_tile(
black_box(apk).as_ptr(),
black_box(bpk).as_ptr(),
black_box(kp),
)
};
sink ^= c[5];
}
black_box(sink);
t0.elapsed().as_secs_f64() / iters as f64 * 1e9
}
fn time_widening(a_km: &[i8], b_km: &[i8], k: usize, iters: usize) -> f64 {
let mut sink = 0i32;
for _ in 0..50 {
sink ^= unsafe { widening_tile(a_km.as_ptr(), b_km.as_ptr(), k) }[0];
}
let t0 = Instant::now();
for _ in 0..iters {
let c = unsafe {
widening_tile(
black_box(a_km).as_ptr(),
black_box(b_km).as_ptr(),
black_box(k),
)
};
sink ^= c[5];
}
black_box(sink);
t0.elapsed().as_secs_f64() / iters as f64 * 1e9
}
fn min_of_n(f: &mut dyn FnMut() -> f64, reps: usize) -> f64 {
let mut s: Vec<f64> = (0..reps).map(|_| f()).collect();
s.sort_by(|a, b| a.partial_cmp(b).unwrap());
s[0]
}
fn bench(k: usize, iters: usize, reps: usize) {
let a = gen_data(k, 1, false);
let b = gen_data(k, 7, false);
let a_km = pack_a_kmajor(&a, k);
let (a_mm, kp) = pack_a_mmajor(&a, k);
let b_k4 = pack_b_k4(&b, k);
let w = min_of_n(&mut || time_widening(&a_km, &b, k, iters), reps);
let r = min_of_n(&mut || time_relaxed(&a_mm, &b_k4, kp, iters), reps);
eprintln!(
" 4x4 tile k={k} ({iters} iters × {reps} reps): \
widening={w:.1} relaxed={r:.1} ns/call speedup={:.2}x",
w / r
);
}
pub fn run() {
// Bit-exactness on wasmtime: full-i8 (engine-dependent intermediate) and
// 7-bit B (guaranteed no i16 overflow → deterministic on any engine).
check("k=64", 64, false);
check("k=64", 64, true);
check("k=260-padded", 260, false);
check("k=260-padded", 260, true);
eprintln!();
// Throughput: single cache-resident 4x4 tile across K depths.
bench(64, 200_000, 8);
bench(256, 50_000, 8);
bench(1024, 10_000, 8);
bench(1536, 8_000, 8);
}
}