use super::detect;
const TILE: usize = 16;
const BLK: usize = 2 * TILE;
unsafe extern "C" {
fn rlx_sme_fmopa_panel(
a_packed: *const f32,
b_packed: *const f32,
c_out: *mut f32,
k: usize,
n_tiles: usize,
);
}
core::arch::global_asm!(
r#"
.arch armv9-a+sme2
.p2align 2
.globl _rlx_sme_fmopa_panel
_rlx_sme_fmopa_panel:
// x0=a_packed x1=b_packed x2=c_out x3=k x4=n_tiles
smstart
ptrue p0.b
lsl x13, x4, #7 // c row stride bytes = n_tiles*32*4
lsl x15, x3, #7 // b per-tile stride bytes = k*32*4
mov x5, #0 // tn = 0
mov x6, x1 // b tile base
mov x14, #0 // c column byte offset = tn*128
1: cmp x5, x4 // for tn in 0..n_tiles
b.ge 4f
zero {{za}}
mov x7, x0 // a working ptr (reset per tile)
mov x9, x6 // b working ptr
cbz x3, 3f // k == 0 -> skip accumulate
mov x8, x3 // k counter
2: ld1w {{z0.s}}, p0/z, [x7]
ld1w {{z1.s}}, p0/z, [x7, #1, mul vl]
ld1w {{z2.s}}, p0/z, [x9]
ld1w {{z3.s}}, p0/z, [x9, #1, mul vl]
fmopa za0.s, p0/m, p0/m, z0.s, z2.s
fmopa za1.s, p0/m, p0/m, z0.s, z3.s
fmopa za2.s, p0/m, p0/m, z1.s, z2.s
fmopa za3.s, p0/m, p0/m, z1.s, z3.s
add x7, x7, #128
add x9, x9, #128
subs x8, x8, #1
b.ne 2b
3: mov w12, #0 // store 32×32 tile at column offset x14
mov x11, #0 // row byte offset = i*rowstride
5: mova z4.s, p0/m, za0h.s[w12, 0]
mova z5.s, p0/m, za1h.s[w12, 0]
mova z6.s, p0/m, za2h.s[w12, 0]
mova z7.s, p0/m, za3h.s[w12, 0]
add x16, x2, x11 // c_out + i*rowstride
add x16, x16, x14 // + column offset
add x17, x16, x13, lsl #4 // + 16 rows (bottom half)
st1w {{z4.s}}, p0, [x16]
st1w {{z5.s}}, p0, [x16, #1, mul vl]
st1w {{z6.s}}, p0, [x17]
st1w {{z7.s}}, p0, [x17, #1, mul vl]
add x11, x11, x13
add w12, w12, #1
cmp w12, #16
b.lt 5b
add x6, x6, x15 // next b tile
add x14, x14, #128 // next c column (+32 f32)
add x5, x5, #1
b 1b
4: smstop
ret
"#
);
pub fn is_available() -> bool {
detect::has_sme2() && detect::sme_f32f32() && detect::svl_bytes() == TILE * 4
}
pub fn dispatch_enabled() -> bool {
is_available()
&& matches!(
rlx_ir::env::var("RLX_CPU_SME").as_deref(),
Some("1") | Some("on") | Some("true")
)
}
pub fn w8a8_dispatch_enabled() -> bool {
is_available_i8()
&& matches!(
rlx_ir::env::var("RLX_CPU_SME_W8A8").as_deref(),
Some("1") | Some("on") | Some("true")
)
}
pub fn worth_sme(m: usize, k: usize, n: usize) -> bool {
m >= 16 && k >= 64 && n >= 32 && m.saturating_mul(n) >= 2048
}
#[inline]
fn pack_a_panel(a: &[f32], m0: usize, mt: usize, k: usize, dst: &mut [f32]) {
for p in 0..k {
let d = &mut dst[p * BLK..p * BLK + BLK];
for (i, slot) in d.iter_mut().enumerate().take(BLK) {
*slot = if i < mt { a[(m0 + i) * k + p] } else { 0.0 };
}
}
}
pub fn sme_sgemm(a: &[f32], b: &[f32], c: &mut [f32], m: usize, k: usize, n: usize) {
debug_assert_eq!(a.len(), m * k);
debug_assert_eq!(b.len(), k * n);
debug_assert_eq!(c.len(), m * n);
if m == 0 || n == 0 {
return;
}
let m_tiles = m.div_ceil(BLK);
let n_tiles = n.div_ceil(BLK);
let mut b_pack_all = vec![0f32; n_tiles * k * BLK];
for tn in 0..n_tiles {
let n0 = tn * BLK;
let nt = BLK.min(n - n0);
let base = tn * k * BLK;
for p in 0..k {
let src = &b[p * n + n0..p * n + n0 + nt];
b_pack_all[base + p * BLK..base + p * BLK + nt].copy_from_slice(src);
}
}
let c_ptr = c.as_mut_ptr() as usize;
let scratch_cols = n_tiles * BLK;
let run_panel = |tm: usize, a_pack: &mut [f32], c_scratch: &mut [f32]| {
let m0 = tm * BLK;
let mt = BLK.min(m - m0);
pack_a_panel(a, m0, mt, k, a_pack);
unsafe {
rlx_sme_fmopa_panel(
a_pack.as_ptr(),
b_pack_all.as_ptr(),
c_scratch.as_mut_ptr(),
k,
n_tiles,
);
}
let c = unsafe { std::slice::from_raw_parts_mut(c_ptr as *mut f32, m * n) };
for i in 0..mt {
c[(m0 + i) * n..(m0 + i) * n + n]
.copy_from_slice(&c_scratch[i * scratch_cols..i * scratch_cols + n]);
}
};
let threads = crate::pool::num_threads();
if threads > 1 && m_tiles > 1 {
crate::pool::par_for(m_tiles, 1, &|off, cnt| {
let mut a_pack = vec![0f32; k * BLK];
let mut c_scratch = vec![0f32; BLK * scratch_cols];
for tm in off..off + cnt {
run_panel(tm, &mut a_pack, &mut c_scratch);
}
});
} else {
let mut a_pack = vec![0f32; k * BLK];
let mut c_scratch = vec![0f32; BLK * scratch_cols];
for tm in 0..m_tiles {
run_panel(tm, &mut a_pack, &mut c_scratch);
}
}
}
const KG: usize = 4;
unsafe extern "C" {
fn rlx_sme_smopa_32x32_i8(
a_packed: *const i8,
b_packed: *const i8,
c_out: *mut i32,
k_groups: usize,
);
}
core::arch::global_asm!(
r#"
.arch armv9-a+sme2
.p2align 2
.globl _rlx_sme_smopa_32x32_i8
_rlx_sme_smopa_32x32_i8:
// x0=a_packed(i8) x1=b_packed(i8) x2=c_out(i32) x3=k_groups
smstart
zero {{za}}
ptrue p0.b
cbz x3, 2f
mov x4, x3
1: ld1b {{z0.b}}, p0/z, [x0] // a0 = rows 0:16 (16×4 bytes)
ld1b {{z1.b}}, p0/z, [x0, #1, mul vl] // a1 = rows 16:32
ld1b {{z2.b}}, p0/z, [x1] // b0 = cols 0:16
ld1b {{z3.b}}, p0/z, [x1, #1, mul vl] // b1 = cols 16:32
smopa za0.s, p0/m, p0/m, z0.b, z2.b
smopa za1.s, p0/m, p0/m, z0.b, z3.b
smopa za2.s, p0/m, p0/m, z1.b, z2.b
smopa za3.s, p0/m, p0/m, z1.b, z3.b
add x0, x0, #128
add x1, x1, #128
subs x4, x4, #1
b.ne 1b
2: mov w12, #0
mov x8, #0
3: mova z4.s, p0/m, za0h.s[w12, 0]
mova z5.s, p0/m, za1h.s[w12, 0]
mova z6.s, p0/m, za2h.s[w12, 0]
mova z7.s, p0/m, za3h.s[w12, 0]
add x6, x2, x8
add x7, x6, #2048
st1w {{z4.s}}, p0, [x6]
st1w {{z5.s}}, p0, [x6, #1, mul vl]
st1w {{z6.s}}, p0, [x7]
st1w {{z7.s}}, p0, [x7, #1, mul vl]
add x8, x8, #128
add w12, w12, #1
cmp w12, #16
b.lt 3b
smstop
ret
"#
);
pub fn is_available_i8() -> bool {
detect::has_sme2() && detect::sme_i8i32() && detect::svl_bytes() == TILE * 4
}
#[inline]
fn pack_a_panel_i8(a: &[i8], m0: usize, mt: usize, k: usize, k_groups: usize, dst: &mut [i8]) {
for g in 0..k_groups {
for blk in 0..2 {
for i in 0..TILE {
let local = blk * TILE + i;
for d in 0..KG {
let kk = g * KG + d;
dst[g * 128 + blk * 64 + i * 4 + d] = if local < mt && kk < k {
a[(m0 + local) * k + kk]
} else {
0
};
}
}
}
}
}
#[inline]
fn pack_b_panel_i8(
b: &[i8],
n0: usize,
nt: usize,
k: usize,
n: usize,
k_groups: usize,
dst: &mut [i8],
) {
for g in 0..k_groups {
for blk in 0..2 {
for j in 0..TILE {
let col = blk * TILE + j;
for d in 0..KG {
let kk = g * KG + d;
dst[g * 128 + blk * 64 + j * 4 + d] = if col < nt && kk < k {
b[kk * n + n0 + col]
} else {
0
};
}
}
}
}
}
fn qmatmul_i8_tiled(
xq: &[i8],
wq: &[i8],
out: &mut [f32],
m: usize,
k: usize,
n: usize,
dequant: &(dyn Fn(usize, i32) -> f32 + Sync),
) {
debug_assert_eq!(xq.len(), m * k);
debug_assert_eq!(wq.len(), k * n);
debug_assert_eq!(out.len(), m * n);
if m == 0 || n == 0 {
return;
}
let k_groups = k.div_ceil(KG);
let m_tiles = m.div_ceil(BLK);
let n_tiles = n.div_ceil(BLK);
let mut b_pack_all = vec![0i8; n_tiles * k_groups * 128];
for tn in 0..n_tiles {
let n0 = tn * BLK;
let nt = BLK.min(n - n0);
let base = tn * k_groups * 128;
pack_b_panel_i8(
wq,
n0,
nt,
k,
n,
k_groups,
&mut b_pack_all[base..base + k_groups * 128],
);
}
let out_ptr = out.as_mut_ptr() as usize;
let run_panel = |tm: usize, a_pack: &mut [i8], c_tile: &mut [i32]| {
let m0 = tm * BLK;
let mt = BLK.min(m - m0);
pack_a_panel_i8(xq, m0, mt, k, k_groups, a_pack);
for tn in 0..n_tiles {
let n0 = tn * BLK;
let nt = BLK.min(n - n0);
let b_pack = &b_pack_all[tn * k_groups * 128..tn * k_groups * 128 + k_groups * 128];
unsafe {
rlx_sme_smopa_32x32_i8(
a_pack.as_ptr(),
b_pack.as_ptr(),
c_tile.as_mut_ptr(),
k_groups,
);
}
let out = unsafe { std::slice::from_raw_parts_mut(out_ptr as *mut f32, m * n) };
for i in 0..mt {
let row = &mut out[(m0 + i) * n + n0..(m0 + i) * n + n0 + nt];
for (jj, slot) in row.iter_mut().enumerate() {
*slot = dequant(n0 + jj, c_tile[i * BLK + jj]);
}
}
}
};
let threads = crate::pool::num_threads();
if threads > 1 && m_tiles > 1 {
crate::pool::par_for(m_tiles, 1, &|off, cnt| {
let mut a_pack = vec![0i8; k_groups * 128];
let mut c_tile = vec![0i32; BLK * BLK];
for tm in off..off + cnt {
run_panel(tm, &mut a_pack, &mut c_tile);
}
});
} else {
let mut a_pack = vec![0i8; k_groups * 128];
let mut c_tile = vec![0i32; BLK * BLK];
for tm in 0..m_tiles {
run_panel(tm, &mut a_pack, &mut c_tile);
}
}
}
pub fn sme_qmatmul_i8(
xq: &[i8],
x_scale: f32,
wq: &[i8],
w_scale: f32,
out: &mut [f32],
m: usize,
k: usize,
n: usize,
) {
let scale = x_scale * w_scale;
qmatmul_i8_tiled(xq, wq, out, m, k, n, &move |_j, v| scale * v as f32);
}
pub fn sme_qmatmul_i8_percol(
xq: &[i8],
x_scale: f32,
wq: &[i8],
w_col_scale: &[f32],
out: &mut [f32],
m: usize,
k: usize,
n: usize,
) {
debug_assert_eq!(w_col_scale.len(), n);
qmatmul_i8_tiled(xq, wq, out, m, k, n, &move |j, v| {
x_scale * w_col_scale[j] * v as f32
});
}
pub fn qgemv_i8(
xq: &[i8],
x_scale: f32,
wq: &[i8],
w_col_scale: &[f32],
out: &mut [f32],
k: usize,
n: usize,
) {
debug_assert_eq!(xq.len(), k);
debug_assert_eq!(wq.len(), k * n);
debug_assert_eq!(w_col_scale.len(), n);
debug_assert_eq!(out.len(), n);
if n == 0 {
return;
}
let out_ptr = out.as_mut_ptr() as usize;
let compute_block = |j0: usize, bn: usize| {
let out = unsafe { std::slice::from_raw_parts_mut(out_ptr as *mut f32, n) };
let mut col = j0;
let end = j0 + bn;
#[cfg(target_arch = "aarch64")]
unsafe {
use std::arch::aarch64::*;
while col + 64 <= end {
let mut a = [vdupq_n_s32(0); 16];
let wbase = wq.as_ptr().add(col);
for p in 0..k {
let xkv = vdupq_n_s32(*xq.get_unchecked(p) as i32);
let row = wbase.add(p * n);
for c in 0..4 {
let w = vld1q_s8(row.add(c * 16));
let wlo = vmovl_s8(vget_low_s8(w));
let whi = vmovl_s8(vget_high_s8(w));
let b = c * 4;
a[b] = vmlaq_s32(a[b], vmovl_s16(vget_low_s16(wlo)), xkv);
a[b + 1] = vmlaq_s32(a[b + 1], vmovl_high_s16(wlo), xkv);
a[b + 2] = vmlaq_s32(a[b + 2], vmovl_s16(vget_low_s16(whi)), xkv);
a[b + 3] = vmlaq_s32(a[b + 3], vmovl_high_s16(whi), xkv);
}
}
let mut acc = [0i32; 64];
for (i, v) in a.iter().enumerate() {
vst1q_s32(acc.as_mut_ptr().add(i * 4), *v);
}
for (d, &av) in acc.iter().enumerate() {
*out.get_unchecked_mut(col + d) =
x_scale * *w_col_scale.get_unchecked(col + d) * av as f32;
}
col += 64;
}
}
while col < end {
let mut a = 0i32;
for p in 0..k {
a += xq[p] as i32 * wq[p * n + col] as i32;
}
out[col] = x_scale * w_col_scale[col] * a as f32;
col += 1;
}
};
let threads = crate::pool::num_threads();
let target = n.div_ceil(threads.max(1)).clamp(64, 1024);
let n_blocks = n.div_ceil(target);
if threads > 1 && n_blocks > 1 {
crate::pool::par_for(n_blocks, 1, &|off, cnt| {
for b in off..off + cnt {
let j0 = b * target;
compute_block(j0, target.min(n - j0));
}
});
} else {
let mut j0 = 0;
while j0 < n {
let bn = target.min(n - j0);
compute_block(j0, bn);
j0 += bn;
}
}
}
unsafe extern "C" {
fn rlx_sdot_i8(x: *const i8, w: *const i8, k: usize) -> i32;
}
core::arch::global_asm!(
r#"
.arch armv8.2-a+dotprod
.p2align 2
.globl _rlx_sdot_i8
_rlx_sdot_i8:
// x0=x x1=w x2=k (MUST be a positive multiple of 16) -> w0.
// The K remainder is handled in Rust, so this stays a clean vector loop.
movi v0.4s, #0
movi v16.4s, #0
cmp x2, #32
b.lt 2f
1: ld1 {{v1.16b}}, [x0], #16
ld1 {{v2.16b}}, [x1], #16
ld1 {{v3.16b}}, [x0], #16
ld1 {{v4.16b}}, [x1], #16
sdot v0.4s, v1.16b, v2.16b
sdot v16.4s, v3.16b, v4.16b
sub x2, x2, #32
cmp x2, #32
b.ge 1b
2: cbz x2, 3f
ld1 {{v1.16b}}, [x0], #16
ld1 {{v2.16b}}, [x1], #16
sdot v0.4s, v1.16b, v2.16b
3: add v0.4s, v0.4s, v16.4s
addv s0, v0.4s
fmov w0, s0
ret
"#
);
pub fn qgemv_i8_nk(
xq: &[i8],
x_scale: f32,
wq: &[i8],
w_row_scale: &[f32],
out: &mut [f32],
k: usize,
n: usize,
) {
debug_assert_eq!(xq.len(), k);
debug_assert_eq!(wq.len(), n * k);
debug_assert_eq!(w_row_scale.len(), n);
debug_assert_eq!(out.len(), n);
if n == 0 {
return;
}
let out_ptr = out.as_mut_ptr() as usize;
#[inline(always)]
fn dot_i8(x: &[i8], w: &[i8], k: usize) -> i32 {
let mut s;
let k16 = k & !15;
#[cfg(target_arch = "aarch64")]
{
s = if k16 > 0 {
unsafe { rlx_sdot_i8(x.as_ptr(), w.as_ptr(), k16) }
} else {
0
};
}
#[cfg(not(target_arch = "aarch64"))]
{
s = 0i32;
for p in 0..k16 {
s += x[p] as i32 * w[p] as i32;
}
}
for p in k16..k {
s += x[p] as i32 * w[p] as i32;
}
s
}
let compute = |j0: usize, jn: usize| {
let out = unsafe { std::slice::from_raw_parts_mut(out_ptr as *mut f32, n) };
for j in j0..j0 + jn {
let s = dot_i8(xq, &wq[j * k..j * k + k], k);
out[j] = x_scale * w_row_scale[j] * s as f32;
}
};
let threads = crate::pool::num_threads();
if threads > 1 && n >= 2 * threads {
let chunk = n.div_ceil(threads);
crate::pool::par_for(threads, 1, &|off, cnt| {
for t in off..off + cnt {
let j0 = t * chunk;
if j0 < n {
compute(j0, chunk.min(n - j0));
}
}
});
} else {
compute(0, n);
}
}
pub fn qgemv_q8_0(w_bytes: &[u8], x: &[f32], out: &mut [f32], k: usize, n: usize) {
const QK: usize = 32; const BB: usize = 2 + QK; debug_assert_eq!(k % QK, 0, "Q8_0 GEMV requires k % 32 == 0");
debug_assert_eq!(out.len(), n);
let n_blocks = k / QK;
debug_assert!(w_bytes.len() >= n * n_blocks * BB);
if n == 0 {
return;
}
let mut xq = vec![0i8; n_blocks * QK];
let mut xs = vec![0f32; n_blocks];
for b in 0..n_blocks {
let (codes, scale) = quantize_i8_symmetric(&x[b * QK..b * QK + QK]);
xq[b * QK..b * QK + QK].copy_from_slice(&codes);
xs[b] = scale;
}
let out_ptr = out.as_mut_ptr() as usize;
let wptr = w_bytes.as_ptr() as usize;
let compute = |j0: usize, jn: usize| {
let out = unsafe { std::slice::from_raw_parts_mut(out_ptr as *mut f32, n) };
for j in j0..j0 + jn {
let mut acc = 0f32;
for b in 0..n_blocks {
let blk = (j * n_blocks + b) * BB;
let ws = unsafe {
let lo = *(wptr as *const u8).add(blk) as u16;
let hi = *(wptr as *const u8).add(blk + 1) as u16;
half::f16::from_bits(lo | (hi << 8)).to_f32()
};
let wq = unsafe { (wptr as *const i8).add(blk + 2) };
let dot = {
#[cfg(target_arch = "aarch64")]
{
unsafe { rlx_sdot_i8(xq.as_ptr().add(b * QK), wq, QK) }
}
#[cfg(not(target_arch = "aarch64"))]
{
let mut s = 0i32;
for i in 0..QK {
s += xq[b * QK + i] as i32 * unsafe { *wq.add(i) } as i32;
}
s
}
};
acc += dot as f32 * xs[b] * ws;
}
out[j] = acc;
}
};
let threads = crate::pool::num_threads();
if threads > 1 && n >= 2 * threads {
let chunk = n.div_ceil(threads);
crate::pool::par_for(threads, 1, &|off, cnt| {
for t in off..off + cnt {
let j0 = t * chunk;
if j0 < n {
compute(j0, chunk.min(n - j0));
}
}
});
} else {
compute(0, n);
}
}
const KG_BF: usize = 2;
unsafe extern "C" {
fn rlx_sme_bfmopa_32x32(
a_packed: *const u16,
b_packed: *const u16,
c_out: *mut f32,
k_groups: usize,
);
}
core::arch::global_asm!(
r#"
.arch armv9-a+sme2
.p2align 2
.globl _rlx_sme_bfmopa_32x32
_rlx_sme_bfmopa_32x32:
// x0=a_packed(bf16) x1=b_packed(bf16) x2=c_out(f32) x3=k_groups
smstart
zero {{za}}
ptrue p0.b
cbz x3, 2f
mov x4, x3
1: ld1h {{z0.h}}, p0/z, [x0] // a0 = rows 0:16 (16×2 bf16)
ld1h {{z1.h}}, p0/z, [x0, #1, mul vl] // a1 = rows 16:32
ld1h {{z2.h}}, p0/z, [x1] // b0 = cols 0:16
ld1h {{z3.h}}, p0/z, [x1, #1, mul vl] // b1 = cols 16:32
bfmopa za0.s, p0/m, p0/m, z0.h, z2.h
bfmopa za1.s, p0/m, p0/m, z0.h, z3.h
bfmopa za2.s, p0/m, p0/m, z1.h, z2.h
bfmopa za3.s, p0/m, p0/m, z1.h, z3.h
add x0, x0, #128 // += 32 bf16 ×2 blocks = 128 bytes
add x1, x1, #128
subs x4, x4, #1
b.ne 1b
2: mov w12, #0
mov x8, #0
3: mova z4.s, p0/m, za0h.s[w12, 0]
mova z5.s, p0/m, za1h.s[w12, 0]
mova z6.s, p0/m, za2h.s[w12, 0]
mova z7.s, p0/m, za3h.s[w12, 0]
add x6, x2, x8
add x7, x6, #2048
st1w {{z4.s}}, p0, [x6]
st1w {{z5.s}}, p0, [x6, #1, mul vl]
st1w {{z6.s}}, p0, [x7]
st1w {{z7.s}}, p0, [x7, #1, mul vl]
add x8, x8, #128
add w12, w12, #1
cmp w12, #16
b.lt 3b
smstop
ret
"#
);
pub fn is_available_bf16() -> bool {
detect::has_sme2() && detect::sme_b16f32() && detect::svl_bytes() == TILE * 4
}
pub fn bf16_dispatch_enabled() -> bool {
is_available_bf16()
&& matches!(
rlx_ir::env::var("RLX_CPU_SME_BF16").as_deref(),
Some("1") | Some("on") | Some("true")
)
}
#[inline]
fn pack_a_panel_bf16(a: &[f32], m0: usize, mt: usize, k: usize, k_groups: usize, dst: &mut [u16]) {
for g in 0..k_groups {
for blk in 0..2 {
for i in 0..TILE {
let local = blk * TILE + i;
for d in 0..KG_BF {
let kk = g * KG_BF + d;
dst[g * 64 + blk * 32 + i * 2 + d] = if local < mt && kk < k {
half::bf16::from_f32(a[(m0 + local) * k + kk]).to_bits()
} else {
0
};
}
}
}
}
}
#[inline]
fn pack_b_panel_bf16(
b: &[f32],
n0: usize,
nt: usize,
k: usize,
n: usize,
k_groups: usize,
dst: &mut [u16],
) {
for g in 0..k_groups {
for blk in 0..2 {
for j in 0..TILE {
let col = blk * TILE + j;
for d in 0..KG_BF {
let kk = g * KG_BF + d;
dst[g * 64 + blk * 32 + j * 2 + d] = if col < nt && kk < k {
half::bf16::from_f32(b[kk * n + n0 + col]).to_bits()
} else {
0
};
}
}
}
}
}
pub fn sme_sgemm_bf16(a: &[f32], b: &[f32], c: &mut [f32], m: usize, k: usize, n: usize) {
debug_assert_eq!(a.len(), m * k);
debug_assert_eq!(b.len(), k * n);
debug_assert_eq!(c.len(), m * n);
if m == 0 || n == 0 {
return;
}
let k_groups = k.div_ceil(KG_BF);
let m_tiles = m.div_ceil(BLK);
let n_tiles = n.div_ceil(BLK);
let mut b_pack_all = vec![0u16; n_tiles * k_groups * 64];
for tn in 0..n_tiles {
let n0 = tn * BLK;
let nt = BLK.min(n - n0);
let base = tn * k_groups * 64;
pack_b_panel_bf16(
b,
n0,
nt,
k,
n,
k_groups,
&mut b_pack_all[base..base + k_groups * 64],
);
}
let c_ptr = c.as_mut_ptr() as usize;
let run_panel = |tm: usize, a_pack: &mut [u16], c_tile: &mut [f32]| {
let m0 = tm * BLK;
let mt = BLK.min(m - m0);
pack_a_panel_bf16(a, m0, mt, k, k_groups, a_pack);
for tn in 0..n_tiles {
let n0 = tn * BLK;
let nt = BLK.min(n - n0);
let b_pack = &b_pack_all[tn * k_groups * 64..tn * k_groups * 64 + k_groups * 64];
unsafe {
rlx_sme_bfmopa_32x32(
a_pack.as_ptr(),
b_pack.as_ptr(),
c_tile.as_mut_ptr(),
k_groups,
);
}
let c = unsafe { std::slice::from_raw_parts_mut(c_ptr as *mut f32, m * n) };
for i in 0..mt {
c[(m0 + i) * n + n0..(m0 + i) * n + n0 + nt]
.copy_from_slice(&c_tile[i * BLK..i * BLK + nt]);
}
}
};
let threads = crate::pool::num_threads();
if threads > 1 && m_tiles > 1 {
crate::pool::par_for(m_tiles, 1, &|off, cnt| {
let mut a_pack = vec![0u16; k_groups * 64];
let mut c_tile = vec![0f32; BLK * BLK];
for tm in off..off + cnt {
run_panel(tm, &mut a_pack, &mut c_tile);
}
});
} else {
let mut a_pack = vec![0u16; k_groups * 64];
let mut c_tile = vec![0f32; BLK * BLK];
for tm in 0..m_tiles {
run_panel(tm, &mut a_pack, &mut c_tile);
}
}
}
pub fn quantize_i8_symmetric(v: &[f32]) -> (Vec<i8>, f32) {
let max_abs = v.iter().fold(0f32, |acc, &x| acc.max(x.abs()));
let scale = if max_abs > 0.0 { max_abs / 127.0 } else { 1.0 };
let inv = 1.0 / scale;
(
v.iter()
.map(|&x| (x * inv).round().clamp(-127.0, 127.0) as i8)
.collect(),
scale,
)
}
#[cfg(test)]
mod tests {
use super::*;
fn naive(a: &[f32], b: &[f32], m: usize, k: usize, n: usize) -> Vec<f32> {
let mut c = vec![0f32; m * n];
for i in 0..m {
for j in 0..n {
let mut acc = 0f32;
for p in 0..k {
acc += a[i * k + p] * b[p * n + j];
}
c[i * n + j] = acc;
}
}
c
}
fn fill(len: usize, seed: u64) -> Vec<f32> {
let mut s = seed | 1;
(0..len)
.map(|_| {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
((s >> 40) as f32 / (1u64 << 24) as f32) - 0.5
})
.collect()
}
#[test]
fn sme_matches_naive_across_shapes() {
if !is_available() {
eprintln!("SME2 not available on this host; skipping");
return;
}
let shapes = [
(1, 1, 1),
(16, 16, 16),
(32, 32, 32),
(17, 33, 15),
(33, 65, 31),
(64, 128, 96),
(100, 100, 100),
(5, 5, 100),
];
for (m, k, n) in shapes {
let a = fill(m * k, 0x1234 + (m * k) as u64);
let b = fill(k * n, 0x9abc + (k * n) as u64);
let mut c = vec![0f32; m * n];
sme_sgemm(&a, &b, &mut c, m, k, n);
let want = naive(&a, &b, m, k, n);
let max_abs = c
.iter()
.zip(&want)
.fold(0f32, |acc, (x, y)| acc.max((x - y).abs()));
let tol = 1e-3 * (k as f32).sqrt().max(1.0);
assert!(
max_abs <= tol,
"shape {m}x{k}x{n}: max diff {max_abs} > {tol}"
);
}
}
fn fill_i8(len: usize, seed: u64) -> Vec<i8> {
let mut s = seed | 1;
(0..len)
.map(|_| {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
((s >> 32) as i32 % 255 - 127) as i8
})
.collect()
}
fn naive_i32(a: &[i8], b: &[i8], m: usize, k: usize, n: usize) -> Vec<i32> {
let mut c = vec![0i32; m * n];
for i in 0..m {
for j in 0..n {
let mut acc = 0i32;
for p in 0..k {
acc += a[i * k + p] as i32 * b[p * n + j] as i32;
}
c[i * n + j] = acc;
}
}
c
}
#[test]
fn sme_i8_exact_integer() {
if !is_available_i8() {
eprintln!("SME2 int8 not available; skipping");
return;
}
for (m, k, n) in [
(1, 1, 1),
(16, 4, 16),
(17, 7, 15),
(32, 32, 32),
(33, 70, 31),
(48, 100, 96),
] {
let a = fill_i8(m * k, 0x51 + (m * k) as u64);
let b = fill_i8(k * n, 0x73 + (k * n) as u64);
let mut out = vec![0f32; m * n];
sme_qmatmul_i8(&a, 1.0, &b, 1.0, &mut out, m, k, n);
let want = naive_i32(&a, &b, m, k, n);
for (idx, (g, w)) in out.iter().zip(&want).enumerate() {
assert_eq!(*g, *w as f32, "shape {m}x{k}x{n} idx {idx}: {g} != {w}");
}
}
}
#[test]
fn sme_i8_w8a8_tracks_f32() {
if !is_available_i8() {
return;
}
for (m, k, n) in [(8, 64, 8), (32, 256, 48), (64, 512, 64)] {
let x = fill(m * k, 3 + (m * k) as u64);
let w = fill(k * n, 5 + (k * n) as u64);
let (xq, sx) = quantize_i8_symmetric(&x);
let (wq, sw) = quantize_i8_symmetric(&w);
let mut got = vec![0f32; m * n];
sme_qmatmul_i8(&xq, sx, &wq, sw, &mut got, m, k, n);
let want = naive(&x, &w, m, k, n);
let (mut dot, mut ng, mut nw, mut num, mut den) = (0f64, 0f64, 0f64, 0f64, 0f64);
for (g, t) in got.iter().zip(&want) {
dot += (*g as f64) * (*t as f64);
ng += (*g as f64).powi(2);
nw += (*t as f64).powi(2);
num += ((*g - *t) as f64).powi(2);
den += (*t as f64).powi(2);
}
let cos = dot / (ng.sqrt() * nw.sqrt());
let rel = (num / den).sqrt();
eprintln!("SME W8A8 {m}x{k}x{n}: cosine={cos:.5} rel={rel:.4}");
assert!(cos > 0.99, "cosine {cos} too low");
assert!(rel < 0.06, "rel err {rel} too high");
}
}
#[test]
#[ignore = "benchmark; run explicitly with --ignored --nocapture"]
fn sme_lowprec_throughput_report() {
use std::time::Instant;
if !is_available_i8() {
return;
}
let gflops = |m: usize, k: usize, n: usize, iters: usize, f: &mut dyn FnMut()| -> f64 {
for _ in 0..2 {
f();
}
let t0 = Instant::now();
for _ in 0..iters {
f();
}
2.0 * m as f64 * n as f64 * k as f64 * iters as f64 / t0.elapsed().as_secs_f64() / 1e9
};
eprintln!(
"{:>16} {:>12} {:>12} {:>11} {:>10}",
"shape", "scalar-i8", "SME-i8", "SME-bf16", "Accel-f32"
);
for (m, k, n) in [
(256usize, 256usize, 256usize),
(512, 512, 512),
(1024, 1024, 1024),
] {
let xf = fill(m * k, 1 + (m * k) as u64);
let wf = fill(k * n, 2 + (k * n) as u64);
let (xq, sx) = quantize_i8_symmetric(&xf);
let (wq, sw) = quantize_i8_symmetric(&wf);
let mut out = vec![0f32; m * n];
let iters =
((6e8 / (2.0 * m as f64 * k as f64 * n as f64)).ceil() as usize).clamp(3, 100);
let scales_col = vec![sw; n];
let scalar = gflops(m, k, n, iters, &mut || {
crate::thunk::dequant_matmul_int8(
&xf,
&wq,
&scales_col,
&[],
&mut out,
m,
k,
n,
k,
false,
)
});
let sme_i8 = gflops(m, k, n, iters, &mut || {
sme_qmatmul_i8(&xq, sx, &wq, sw, &mut out, m, k, n);
});
let sme_bf = if is_available_bf16() {
gflops(m, k, n, iters, &mut || {
sme_sgemm_bf16(&xf, &wf, &mut out, m, k, n)
})
} else {
0.0
};
let accel = gflops(m, k, n, iters, &mut || {
crate::blas::sgemm(&xf, &wf, &mut out, m, k, n)
});
eprintln!(
"{:>16} {:>12.1} {:>12.1} {:>11.1} {:>10.1}",
format!("{m}x{k}x{n}"),
scalar,
sme_i8,
sme_bf,
accel
);
}
}
#[test]
#[ignore = "diagnostic; run with --ignored --nocapture"]
fn sme_kblock_probe() {
use std::time::Instant;
if !is_available() {
return;
}
let gf = |m: usize, k: usize, n: usize, f: &mut dyn FnMut()| -> f64 {
for _ in 0..2 {
f();
}
let it = 5;
let t0 = Instant::now();
for _ in 0..it {
f();
}
2.0 * m as f64 * n as f64 * k as f64 * it as f64 / t0.elapsed().as_secs_f64() / 1e9
};
let (m, n) = (1024usize, 1024usize);
eprintln!("{:>10} {:>10} {:>10}", "K", "f32 GF/s", "int8 GF/s");
for k in [256usize, 1024, 4096, 8192] {
let a = fill(m * k, 1 + k as u64);
let b = fill(k * n, 2 + k as u64);
let (aq, sa) = quantize_i8_symmetric(&a);
let (bq, sb) = quantize_i8_symmetric(&b);
let mut c = vec![0f32; m * n];
let gf_f32 = if is_available() {
gf(m, k, n, &mut || sme_sgemm(&a, &b, &mut c, m, k, n))
} else {
0.0
};
let gf_i8 = if is_available_i8() {
gf(m, k, n, &mut || {
sme_qmatmul_i8(&aq, sa, &bq, sb, &mut c, m, k, n)
})
} else {
0.0
};
eprintln!("{k:>10} {gf_f32:>10.1} {gf_i8:>10.1}");
}
}
#[test]
fn qgemv_i8_exact() {
for (k, n) in [(1, 1), (7, 15), (64, 32), (100, 257), (512, 300)] {
let x = fill_i8(k, 0x11 + (k * n) as u64);
let w = fill_i8(k * n, 0x22 + (k * n) as u64);
let scales = vec![1.0f32; n];
let mut out = vec![0f32; n];
qgemv_i8(&x, 1.0, &w, &scales, &mut out, k, n);
for j in 0..n {
let mut acc = 0i32;
for p in 0..k {
acc += x[p] as i32 * w[p * n + j] as i32;
}
assert_eq!(out[j], acc as f32, "k{k} n{n} j{j}");
}
}
}
#[test]
fn qgemv_q8_0_tracks_f32() {
use rlx_gguf::dequant_q8_0;
use rlx_gguf::quantize::quantize_q8_0;
let (k, n) = (256usize, 64usize); let x = fill(k, 7);
let mut wbytes: Vec<u8> = Vec::new();
let mut wref = vec![0f32; n * k];
for j in 0..n {
let wrow = fill(k, 100 + j as u64);
let q = quantize_q8_0(&wrow).unwrap();
let dq = dequant_q8_0(&q, k).unwrap();
wref[j * k..j * k + k].copy_from_slice(&dq);
wbytes.extend_from_slice(&q);
}
let mut out = vec![0f32; n];
qgemv_q8_0(&wbytes, &x, &mut out, k, n);
let (mut dot, mut ng, mut nw) = (0f64, 0f64, 0f64);
for j in 0..n {
let mut r = 0f32;
for p in 0..k {
r += x[p] * wref[j * k + p];
}
dot += out[j] as f64 * r as f64;
ng += (out[j] as f64).powi(2);
nw += (r as f64).powi(2);
}
let cos = dot / (ng.sqrt() * nw.sqrt());
eprintln!("Q8_0 SDOT GEMV k={k} n={n}: cosine={cos:.5}");
assert!(cos > 0.999, "Q8_0 SDOT GEMV cosine {cos} too low");
}
#[test]
fn sdot_i8_debug() {
for k in [16usize, 32, 48, 64, 96, 256, 1024] {
let x = fill_i8(k, k as u64);
let w = fill_i8(k, (k + 1) as u64);
let got = unsafe { super::rlx_sdot_i8(x.as_ptr(), w.as_ptr(), k) };
let mut exp = 0i32;
for p in 0..k {
exp += x[p] as i32 * w[p] as i32;
}
assert_eq!(got, exp, "k={k}");
}
}
#[test]
fn qgemv_i8_nk_exact() {
for (k, n) in [(1, 1), (7, 15), (63, 40), (100, 257), (512, 300), (37, 9)] {
let x = fill_i8(k, 0x31 + (k * n) as u64);
let w = fill_i8(n * k, 0x42 + (k * n) as u64); let scales = vec![1.0f32; n];
let mut out = vec![0f32; n];
qgemv_i8_nk(&x, 1.0, &w, &scales, &mut out, k, n);
for j in 0..n {
let mut acc = 0i32;
for p in 0..k {
acc += x[p] as i32 * w[j * k + p] as i32;
}
assert_eq!(out[j], acc as f32, "k{k} n{n} j{j}");
}
}
}
#[test]
#[ignore = "benchmark; run explicitly with --ignored --nocapture"]
fn sme_gemv_report() {
use std::time::Instant;
if !is_available_i8() {
return;
}
let bench = |bytes_moved: f64, iters: usize, f: &mut dyn FnMut()| -> (f64, f64) {
for _ in 0..3 {
f();
}
let t0 = Instant::now();
for _ in 0..iters {
f();
}
let s = t0.elapsed().as_secs_f64() / iters as f64;
(s * 1e6, bytes_moved / s / 1e9) };
eprintln!(
"{:>14} {:>16} {:>20} {:>13}",
"shape (1×k×n)", "[k,n] qgemv µs", "[n,k] SDOT µs (GB/s)", "Accel-f32 µs"
);
for (k, n) in [(4096usize, 4096usize), (4096, 11008), (2048, 2048)] {
let m = 1;
let xf = fill(m * k, 1 + (k * n) as u64);
let wf = fill(k * n, 2 + (k * n) as u64);
let (xq, sx) = quantize_i8_symmetric(&xf);
let (wq_kn, sw) = quantize_i8_symmetric(&wf); let mut wq_nk = vec![0i8; n * k]; for p in 0..k {
for j in 0..n {
wq_nk[j * k + p] = wq_kn[p * n + j];
}
}
let scales_col = vec![sw; n];
let mut out = vec![0f32; m * n];
let iters = 50;
let wbytes = (k * n) as f64; let (kn_us, _) = bench(wbytes, iters, &mut || {
qgemv_i8(&xq, sx, &wq_kn, &scales_col, &mut out, k, n)
});
let (nk_us, nk_gbs) = bench(wbytes, iters, &mut || {
qgemv_i8_nk(&xq, sx, &wq_nk, &scales_col, &mut out, k, n)
});
let (acc_us, _) = bench((k * n * 4) as f64, iters, &mut || {
crate::blas::sgemm(&xf, &wf, &mut out, m, k, n)
});
eprintln!(
"{:>14} {:>16.1} {:>12.1} ({:>4.0}GB/s) {:>13.1}",
format!("1x{k}x{n}"),
kn_us,
nk_us,
nk_gbs,
acc_us
);
}
}
#[test]
fn sme_bf16_tracks_f32() {
if !is_available_bf16() {
eprintln!("SME2 bf16 not available; skipping");
return;
}
for (m, k, n) in [(1, 1, 1), (17, 33, 15), (32, 256, 48), (64, 512, 64)] {
let a = fill(m * k, 3 + (m * k) as u64);
let b = fill(k * n, 5 + (k * n) as u64);
let mut got = vec![0f32; m * n];
sme_sgemm_bf16(&a, &b, &mut got, m, k, n);
let want = naive(&a, &b, m, k, n);
let (mut dot, mut ng, mut nw, mut num, mut den) = (0f64, 0f64, 0f64, 0f64, 0f64);
for (g, t) in got.iter().zip(&want) {
dot += (*g as f64) * (*t as f64);
ng += (*g as f64).powi(2);
nw += (*t as f64).powi(2);
num += ((*g - *t) as f64).powi(2);
den += (*t as f64).powi(2);
}
if den > 0.0 {
let cos = dot / (ng.sqrt() * nw.sqrt());
let rel = (num / den).sqrt();
eprintln!("SME bf16 {m}x{k}x{n}: cosine={cos:.5} rel={rel:.4}");
assert!(cos > 0.999, "cosine {cos} too low");
assert!(rel < 0.05, "rel err {rel} too high");
}
}
}
}