#![cfg(target_arch = "x86_64")]
use core::arch::x86_64::*;
use rlx_gguf::q2_dequant::QK2_0;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Kind {
None,
Avx,
Avx512,
}
#[inline]
fn detect() -> Kind {
if is_x86_feature_detected!("avx512vnni") && is_x86_feature_detected!("avx512vl") {
Kind::Avx512
} else if is_x86_feature_detected!("avxvnni") && is_x86_feature_detected!("avx2") {
Kind::Avx
} else {
Kind::None
}
}
#[inline]
pub fn has_vnni() -> bool {
detect() != Kind::None
}
#[inline]
pub fn q2_0_dot_q8_g128(w: &[u8], a: &[u8]) -> f32 {
match detect() {
Kind::Avx512 => unsafe { dot_avx512(w, a) },
Kind::Avx => unsafe { dot_avx(w, a) },
Kind::None => rlx_gguf::q2_dequant::q2_0_dot_q8_g128(w, a),
}
}
#[inline]
#[target_feature(enable = "avx2")]
unsafe fn unpack32(p: __m128i) -> __m256i {
let bcast = _mm256_broadcastsi128_si256(p);
let shuf = _mm256_setr_epi8(
0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, );
let rep = _mm256_shuffle_epi8(bcast, shuf);
let mul = _mm256_setr_epi16(64, 16, 4, 1, 64, 16, 4, 1, 64, 16, 4, 1, 64, 16, 4, 1);
let three = _mm256_set1_epi16(3);
let lo = _mm256_cvtepu8_epi16(_mm256_castsi256_si128(rep)); let hi = _mm256_cvtepu8_epi16(_mm256_extracti128_si256::<1>(rep)); let lo = _mm256_and_si256(_mm256_srli_epi16::<6>(_mm256_mullo_epi16(lo, mul)), three);
let hi = _mm256_and_si256(_mm256_srli_epi16::<6>(_mm256_mullo_epi16(hi, mul)), three);
let packed = _mm256_packus_epi16(lo, hi);
_mm256_permute4x64_epi64::<0b11_01_10_00>(packed)
}
#[inline]
#[target_feature(enable = "avx2")]
unsafe fn hsum_i32(v: __m256i) -> i32 {
let s = _mm_add_epi32(_mm256_castsi256_si128(v), _mm256_extracti128_si256::<1>(v));
let s = _mm_add_epi32(s, _mm_shuffle_epi32::<0b01_00_11_10>(s));
let s = _mm_add_epi32(s, _mm_shuffle_epi32::<0b00_00_00_01>(s));
_mm_cvtsi128_si32(s)
}
#[inline]
#[target_feature(enable = "avx2,avxvnni")]
unsafe fn dot_avx(w: &[u8], a: &[u8]) -> f32 {
let d = half::f16::from_le_bytes([w[0], w[1]]).to_f32();
let dx = f32::from_le_bytes([a[0], a[1], a[2], a[3]]);
let xsum = i32::from_le_bytes([a[4 + QK2_0], a[5 + QK2_0], a[6 + QK2_0], a[7 + QK2_0]]);
unsafe {
let qs = w.as_ptr().add(2);
let acts = a.as_ptr().add(4);
let mut acc = _mm256_setzero_si256();
for g in 0..(QK2_0 / 32) {
let codes = unpack32(_mm_loadl_epi64(qs.add(g * 8) as *const __m128i));
let act = _mm256_loadu_si256(acts.add(g * 32) as *const __m256i);
acc = _mm256_dpbusd_avx_epi32(acc, codes, act); }
d * dx * (hsum_i32(acc) - xsum) as f32
}
}
#[inline]
#[target_feature(enable = "avx2,avx512vnni,avx512vl")]
unsafe fn dot_avx512(w: &[u8], a: &[u8]) -> f32 {
let d = half::f16::from_le_bytes([w[0], w[1]]).to_f32();
let dx = f32::from_le_bytes([a[0], a[1], a[2], a[3]]);
let xsum = i32::from_le_bytes([a[4 + QK2_0], a[5 + QK2_0], a[6 + QK2_0], a[7 + QK2_0]]);
unsafe {
let qs = w.as_ptr().add(2);
let acts = a.as_ptr().add(4);
let mut acc = _mm256_setzero_si256();
for g in 0..(QK2_0 / 32) {
let codes = unpack32(_mm_loadl_epi64(qs.add(g * 8) as *const __m128i));
let act = _mm256_loadu_si256(acts.add(g * 32) as *const __m256i);
acc = _mm256_dpbusd_epi32(acc, codes, act); }
d * dx * (hsum_i32(acc) - xsum) as f32
}
}
#[cfg(test)]
mod tests {
use super::*;
use rlx_gguf::q2_dequant::{Q8_0_G128_BYTES, quantize_q2_0, quantize_q8_0_g128_row};
#[test]
fn vnni_matches_scalar() {
if !has_vnni() {
eprintln!("skip: no VNNI on this CPU");
return;
}
let mut wf = vec![0f32; 2 * QK2_0];
let mut xf = vec![0f32; 2 * QK2_0];
for j in 0..wf.len() {
wf[j] = (((j * 5) % 4) as i32 - 1) as f32 * 0.25; xf[j] = ((j as f32) * 0.021).cos() * 2.5;
}
let w = quantize_q2_0(&wf).unwrap();
let mut a = vec![0u8; 2 * Q8_0_G128_BYTES];
quantize_q8_0_g128_row(&xf, &mut a);
for b in 0..2 {
let wb = &w[b * 34..(b + 1) * 34];
let ab = &a[b * Q8_0_G128_BYTES..(b + 1) * Q8_0_G128_BYTES];
let simd = q2_0_dot_q8_g128(wb, ab);
let scal = rlx_gguf::q2_dequant::q2_0_dot_q8_g128(wb, ab);
assert!(
(simd - scal).abs() <= 1e-4 * (1.0 + scal.abs()),
"{simd} vs {scal}"
);
}
}
#[test]
#[ignore = "perf microbench; run with --ignored --nocapture on VNNI hw"]
fn bench_q2_0_dot() {
use std::time::Instant;
if !has_vnni() {
eprintln!("skip: no VNNI on this CPU");
return;
}
let (k, n, iters) = (4096usize, 4096usize, 50usize);
let bpr = k / QK2_0;
let wf: Vec<f32> = (0..k * n)
.map(|i| (((i * 7) % 4) as i32 - 1) as f32 * 0.1)
.collect();
let w = quantize_q2_0(&wf).unwrap();
let xf: Vec<f32> = (0..k).map(|i| (i as f32 * 0.01).sin()).collect();
let mut aq = vec![0u8; bpr * Q8_0_G128_BYTES];
quantize_q8_0_g128_row(&xf, &mut aq);
let row_bytes = bpr * 34;
let run = |dot: &dyn Fn(&[u8], &[u8]) -> f32| {
let mut sink = 0f32;
let t = Instant::now();
for _ in 0..iters {
for j in 0..n {
let row = &w[j * row_bytes..(j + 1) * row_bytes];
for b in 0..bpr {
sink += dot(
&row[b * 34..b * 34 + 34],
&aq[b * Q8_0_G128_BYTES..(b + 1) * Q8_0_G128_BYTES],
);
}
}
}
(t.elapsed().as_secs_f64(), sink)
};
let (vnni, s0) = run(&q2_0_dot_q8_g128);
let (scal, s1) = run(&rlx_gguf::q2_dequant::q2_0_dot_q8_g128);
eprintln!(
"q2_0 GEMV {k}x{n} x{iters} (1 thread): VNNI {vnni:.3}s scalar {scal:.3}s \
speedup {:.2}x (sink {s0:.1}/{s1:.1})",
scal / vnni
);
}
}