use crate::rank::bucket_centre;
use crate::util::TopK;
#[allow(clippy::too_many_arguments)] pub(crate) fn scan_via_lut_scalar(
packed: &[u8],
n: usize,
dim: usize,
bits: u8,
n_buckets: usize,
q_unit: &[f32],
scale: f32,
top: &mut TopK,
) {
let mut lut = Vec::new();
scan_via_lut_scalar_with_lut(
packed, n, dim, bits, n_buckets, q_unit, scale, top, &mut lut,
);
}
pub(crate) fn build_asym_lut_into(
lut: &mut Vec<f32>,
dim: usize,
bits: u8,
n_buckets: usize,
q_unit: &[f32],
) {
assert_eq!(q_unit.len(), dim);
lut.resize(dim * n_buckets, 0.0);
for (&qd, row) in q_unit.iter().zip(lut.chunks_exact_mut(n_buckets)) {
for (b, slot) in row.iter_mut().enumerate() {
*slot = qd * bucket_centre(b as u8, bits);
}
}
}
#[allow(clippy::too_many_arguments)] pub(crate) fn scan_via_lut_scalar_with_lut(
packed: &[u8],
n: usize,
dim: usize,
bits: u8,
n_buckets: usize,
q_unit: &[f32],
scale: f32,
top: &mut TopK,
lut: &mut Vec<f32>,
) {
build_asym_lut_into(lut, dim, bits, n_buckets, q_unit);
match bits {
1 => scan_b1_to_topk(packed, n, dim, lut, scale, top),
2 => scan_b2_to_topk(packed, n, dim, lut, scale, top),
4 => scan_b4_to_topk(packed, n, dim, lut, scale, top),
8 => scan_b8_to_topk(packed, n, dim, lut, scale, top),
_ => unreachable!("bits validated in new()"),
}
}
pub(crate) fn scan_b1_to_topk(
packed: &[u8],
n: usize,
dim: usize,
lut: &[f32],
scale: f32,
top: &mut TopK,
) {
let bytes_per_vec = dim / 8;
for di in 0..n {
let doc = &packed[di * bytes_per_vec..(di + 1) * bytes_per_vec];
let mut acc = 0.0f32;
for (g, &byte) in doc.iter().enumerate() {
let base = (g * 8) * 2;
acc += lut[base + (((byte >> 7) & 1) as usize)];
acc += lut[base + 2 + (((byte >> 6) & 1) as usize)];
acc += lut[base + 4 + (((byte >> 5) & 1) as usize)];
acc += lut[base + 6 + (((byte >> 4) & 1) as usize)];
acc += lut[base + 8 + (((byte >> 3) & 1) as usize)];
acc += lut[base + 10 + (((byte >> 2) & 1) as usize)];
acc += lut[base + 12 + (((byte >> 1) & 1) as usize)];
acc += lut[base + 14 + ((byte & 1) as usize)];
}
top.maybe_insert(acc * scale, di);
}
}
pub(crate) fn scan_b2_to_topk(
packed: &[u8],
n: usize,
dim: usize,
lut: &[f32],
scale: f32,
top: &mut TopK,
) {
let bytes_per_vec = dim / 4;
for di in 0..n {
let doc = &packed[di * bytes_per_vec..(di + 1) * bytes_per_vec];
let mut acc = 0.0f32;
for (g, &byte) in doc.iter().enumerate() {
let base = (g * 4) * 4;
acc += lut[base + (((byte >> 6) & 3) as usize)];
acc += lut[base + 4 + (((byte >> 4) & 3) as usize)];
acc += lut[base + 8 + (((byte >> 2) & 3) as usize)];
acc += lut[base + 12 + ((byte & 3) as usize)];
}
top.maybe_insert(acc * scale, di);
}
}
pub(crate) fn scan_b4_to_topk(
packed: &[u8],
n: usize,
dim: usize,
lut: &[f32],
scale: f32,
top: &mut TopK,
) {
let bytes_per_vec = dim / 2;
for di in 0..n {
let doc = &packed[di * bytes_per_vec..(di + 1) * bytes_per_vec];
let mut acc = 0.0f32;
for (g, &byte) in doc.iter().enumerate() {
let base = (g * 2) * 16;
acc += lut[base + (((byte >> 4) & 0xF) as usize)];
acc += lut[base + 16 + ((byte & 0xF) as usize)];
}
top.maybe_insert(acc * scale, di);
}
}
pub(crate) fn build_b8_asym_lut_into(lut: &mut Vec<f32>, q_unit: &[f32]) {
let dim = q_unit.len();
lut.resize(dim * 256, 0.0);
for (&qd, row) in q_unit.iter().zip(lut.chunks_exact_mut(256)) {
for (code, slot) in row.iter_mut().enumerate() {
*slot = qd * bucket_centre(code as u8, 8);
}
}
}
pub(crate) fn scan_b8_to_topk(
packed: &[u8],
n: usize,
dim: usize,
lut: &[f32],
scale: f32,
top: &mut TopK,
) {
let bytes_per_vec = dim; for di in 0..n {
let doc = &packed[di * bytes_per_vec..(di + 1) * bytes_per_vec];
let mut acc = 0.0f32;
for (d, &code) in doc.iter().enumerate() {
acc += lut[d * 256 + code as usize];
}
top.maybe_insert(acc * scale, di);
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2,fma")]
pub(crate) unsafe fn scan_b2_asym_avx2(
packed: &[u8],
n: usize,
dim: usize,
q: &[f32],
scale: f32,
top: &mut TopK,
) {
use std::arch::x86_64::*;
unsafe {
assert_eq!(dim % 16, 0, "b=2 AVX2 path needs dim % 16 == 0");
let bytes_per_vec = dim / 4;
let shifts_hi = _mm256_setr_epi32(30, 28, 26, 24, 22, 20, 18, 16);
let shifts_lo = _mm256_setr_epi32(14, 12, 10, 8, 6, 4, 2, 0);
let mask3 = _mm256_set1_epi32(3);
let bytes_per_chunk = 4usize;
let chunks_per_vec = bytes_per_vec / bytes_per_chunk;
for di in 0..n {
let doc = packed.as_ptr().add(di * bytes_per_vec);
let mut acc_hi = _mm256_setzero_ps();
let mut acc_lo = _mm256_setzero_ps();
for c in 0..chunks_per_vec {
let chunk_ptr = doc.add(c * bytes_per_chunk);
let b0 = *chunk_ptr as u32;
let b1 = *chunk_ptr.add(1) as u32;
let b2 = *chunk_ptr.add(2) as u32;
let b3 = *chunk_ptr.add(3) as u32;
let chunk = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
let broadcast = _mm256_set1_epi32(chunk as i32);
let codes_hi = _mm256_and_si256(_mm256_srlv_epi32(broadcast, shifts_hi), mask3);
let codes_lo = _mm256_and_si256(_mm256_srlv_epi32(broadcast, shifts_lo), mask3);
let codes_f_hi = _mm256_cvtepi32_ps(codes_hi);
let codes_f_lo = _mm256_cvtepi32_ps(codes_lo);
let d_base = c * 16;
let q_hi = _mm256_loadu_ps(q.as_ptr().add(d_base));
let q_lo = _mm256_loadu_ps(q.as_ptr().add(d_base + 8));
acc_hi = _mm256_fmadd_ps(codes_f_hi, q_hi, acc_hi);
acc_lo = _mm256_fmadd_ps(codes_f_lo, q_lo, acc_lo);
}
let total = _mm256_add_ps(acc_hi, acc_lo);
let raw = horizontal_sum_avx2(total);
top.maybe_insert(raw * scale, di);
}
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2,fma")]
pub(crate) unsafe fn scan_b4_asym_avx2(
packed: &[u8],
n: usize,
dim: usize,
q: &[f32],
scale: f32,
top: &mut TopK,
) {
use std::arch::x86_64::*;
unsafe {
assert_eq!(dim % 8, 0, "b=4 AVX2 path needs dim % 8 == 0");
let bytes_per_vec = dim / 2;
let shifts = _mm256_setr_epi32(28, 24, 20, 16, 12, 8, 4, 0);
let mask_f = _mm256_set1_epi32(0xF);
let bytes_per_chunk = 4usize;
let chunks_per_vec = bytes_per_vec / bytes_per_chunk;
for di in 0..n {
let doc = packed.as_ptr().add(di * bytes_per_vec);
let mut acc = _mm256_setzero_ps();
for c in 0..chunks_per_vec {
let chunk_ptr = doc.add(c * bytes_per_chunk);
let b0 = *chunk_ptr as u32;
let b1 = *chunk_ptr.add(1) as u32;
let b2 = *chunk_ptr.add(2) as u32;
let b3 = *chunk_ptr.add(3) as u32;
let chunk = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
let broadcast = _mm256_set1_epi32(chunk as i32);
let codes = _mm256_and_si256(_mm256_srlv_epi32(broadcast, shifts), mask_f);
let codes_f = _mm256_cvtepi32_ps(codes);
let d_base = c * 8;
let q_vec = _mm256_loadu_ps(q.as_ptr().add(d_base));
acc = _mm256_fmadd_ps(codes_f, q_vec, acc);
}
let raw = horizontal_sum_avx2(acc);
top.maybe_insert(raw * scale, di);
}
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2,fma")]
unsafe fn horizontal_sum_avx2(v: std::arch::x86_64::__m256) -> f32 {
use std::arch::x86_64::*;
let hi128 = _mm256_extractf128_ps(v, 1);
let lo128 = _mm256_castps256_ps128(v);
let sum128 = _mm_add_ps(lo128, hi128);
let shuf = _mm_movehdup_ps(sum128);
let sums = _mm_add_ps(sum128, shuf);
let shuf2 = _mm_movehl_ps(sums, sums);
let sums2 = _mm_add_ss(sums, shuf2);
_mm_cvtss_f32(sums2)
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx512f,avx512dq")]
pub(crate) unsafe fn scan_b2_asym_avx512(
packed: &[u8],
n: usize,
dim: usize,
q: &[f32],
scale: f32,
top: &mut TopK,
) {
use std::arch::x86_64::*;
unsafe {
assert_eq!(
dim % 64,
0,
"b=2 AVX-512 path needs dim % 64 == 0 for 4-way unroll"
);
let bytes_per_vec = dim / 4;
let shifts = _mm512_setr_epi32(30, 28, 26, 24, 22, 20, 18, 16, 14, 12, 10, 8, 6, 4, 2, 0);
let mask3 = _mm512_set1_epi32(3);
let bytes_per_chunk = 4usize;
let chunks_per_vec = bytes_per_vec / bytes_per_chunk;
let outer_iters = chunks_per_vec / 4;
debug_assert_eq!(chunks_per_vec % 4, 0);
for di in 0..n {
let doc = packed.as_ptr().add(di * bytes_per_vec);
let mut acc0 = _mm512_setzero_ps();
let mut acc1 = _mm512_setzero_ps();
let mut acc2 = _mm512_setzero_ps();
let mut acc3 = _mm512_setzero_ps();
for outer in 0..outer_iters {
let c0 = outer * 4;
let c1 = c0 + 1;
let c2 = c0 + 2;
let c3 = c0 + 3;
macro_rules! step {
($c:expr, $acc:expr) => {{
let chunk_ptr = doc.add($c * bytes_per_chunk);
let b0 = *chunk_ptr as u32;
let b1 = *chunk_ptr.add(1) as u32;
let b2 = *chunk_ptr.add(2) as u32;
let b3 = *chunk_ptr.add(3) as u32;
let chunk = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
let broadcast = _mm512_set1_epi32(chunk as i32);
let codes = _mm512_and_si512(_mm512_srlv_epi32(broadcast, shifts), mask3);
let codes_f = _mm512_cvtepi32_ps(codes);
let d_base = $c * 16;
let q_vec = _mm512_loadu_ps(q.as_ptr().add(d_base));
$acc = _mm512_fmadd_ps(codes_f, q_vec, $acc);
}};
}
step!(c0, acc0);
step!(c1, acc1);
step!(c2, acc2);
step!(c3, acc3);
}
let s01 = _mm512_add_ps(acc0, acc1);
let s23 = _mm512_add_ps(acc2, acc3);
let total = _mm512_add_ps(s01, s23);
let raw = _mm512_reduce_add_ps(total);
top.maybe_insert(raw * scale, di);
}
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx512f,avx512dq")]
pub(crate) unsafe fn scan_b4_asym_avx512(
packed: &[u8],
n: usize,
dim: usize,
q: &[f32],
scale: f32,
top: &mut TopK,
) {
use std::arch::x86_64::*;
unsafe {
assert_eq!(
dim % 64,
0,
"b=4 AVX-512 path needs dim % 64 == 0 for 4-way unroll"
);
let bytes_per_vec = dim / 2;
let shifts = _mm512_setr_epi32(28, 24, 20, 16, 12, 8, 4, 0, 28, 24, 20, 16, 12, 8, 4, 0);
let mask_f = _mm512_set1_epi32(0xF);
let bytes_per_chunk = 8usize;
let chunks_per_vec = bytes_per_vec / bytes_per_chunk;
let outer_iters = chunks_per_vec / 4;
debug_assert_eq!(chunks_per_vec % 4, 0);
for di in 0..n {
let doc = packed.as_ptr().add(di * bytes_per_vec);
let mut acc0 = _mm512_setzero_ps();
let mut acc1 = _mm512_setzero_ps();
let mut acc2 = _mm512_setzero_ps();
let mut acc3 = _mm512_setzero_ps();
for outer in 0..outer_iters {
macro_rules! step {
($c:expr, $acc:expr) => {{
let chunk_ptr = doc.add($c * bytes_per_chunk);
let lo0 = *chunk_ptr as u32;
let lo1 = *chunk_ptr.add(1) as u32;
let lo2 = *chunk_ptr.add(2) as u32;
let lo3 = *chunk_ptr.add(3) as u32;
let hi0 = *chunk_ptr.add(4) as u32;
let hi1 = *chunk_ptr.add(5) as u32;
let hi2 = *chunk_ptr.add(6) as u32;
let hi3 = *chunk_ptr.add(7) as u32;
let chunk_lo = (lo0 << 24) | (lo1 << 16) | (lo2 << 8) | lo3;
let chunk_hi = (hi0 << 24) | (hi1 << 16) | (hi2 << 8) | hi3;
let lo_zmm = _mm512_set1_epi32(chunk_lo as i32);
let hi_zmm = _mm512_set1_epi32(chunk_hi as i32);
let combined = _mm512_mask_blend_epi32(0xFF00u16, lo_zmm, hi_zmm);
let codes = _mm512_and_si512(_mm512_srlv_epi32(combined, shifts), mask_f);
let codes_f = _mm512_cvtepi32_ps(codes);
let d_base = $c * 16;
let q_vec = _mm512_loadu_ps(q.as_ptr().add(d_base));
$acc = _mm512_fmadd_ps(codes_f, q_vec, $acc);
}};
}
let c0 = outer * 4;
step!(c0, acc0);
step!(c0 + 1, acc1);
step!(c0 + 2, acc2);
step!(c0 + 3, acc3);
}
let s01 = _mm512_add_ps(acc0, acc1);
let s23 = _mm512_add_ps(acc2, acc3);
let total = _mm512_add_ps(s01, s23);
let raw = _mm512_reduce_add_ps(total);
top.maybe_insert(raw * scale, di);
}
}
}
pub(crate) fn scan_b8_asym(
packed: &[u8],
n: usize,
dim: usize,
q_unit: &[f32],
scale: f32,
top: &mut TopK,
) {
let mut lut = Vec::new();
scan_b8_asym_with_lut(packed, n, dim, q_unit, scale, top, &mut lut);
}
pub(crate) fn scan_b8_asym_with_lut(
packed: &[u8],
n: usize,
dim: usize,
q_unit: &[f32],
scale: f32,
top: &mut TopK,
lut: &mut Vec<f32>,
) {
assert_eq!(q_unit.len(), dim);
build_b8_asym_lut_into(lut, q_unit);
#[cfg(target_arch = "x86_64")]
{
if is_x86_feature_detected!("avx512f")
&& is_x86_feature_detected!("avx512bw")
&& dim.is_multiple_of(16)
{
unsafe {
scan_b8_asym_avx512_gather(packed, n, dim, lut, scale, top);
}
return;
}
}
scan_b8_to_topk(packed, n, dim, lut, scale, top);
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx512f,avx512bw")]
pub(crate) unsafe fn scan_b8_asym_avx512_gather(
packed: &[u8],
n: usize,
dim: usize,
lut: &[f32],
scale: f32,
top: &mut TopK,
) {
use std::arch::x86_64::*;
unsafe {
assert_eq!(dim % 16, 0, "b=8 AVX-512 gather path needs dim % 16 == 0");
assert_eq!(lut.len(), dim * 256, "b=8 LUT must be dim * 256 entries");
let bytes_per_vec = dim; let lut_ptr = lut.as_ptr();
let lane_row_base = _mm512_setr_epi32(
0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584,
3840,
);
let chunks_per_vec = bytes_per_vec / 16;
for di in 0..n {
let doc = packed.as_ptr().add(di * bytes_per_vec);
let mut acc0 = _mm512_setzero_ps();
let mut acc1 = _mm512_setzero_ps();
let mut acc2 = _mm512_setzero_ps();
let mut acc3 = _mm512_setzero_ps();
let unrolled = chunks_per_vec & !3;
let mut c = 0usize;
while c < unrolled {
macro_rules! step {
($cc:expr, $acc:expr) => {{
let chunk_base = _mm512_set1_epi32(($cc * 16 * 256) as i32);
let bytes = _mm_loadu_si128(doc.add($cc * 16) as *const __m128i);
let codes = _mm512_cvtepu8_epi32(bytes);
let idx =
_mm512_add_epi32(_mm512_add_epi32(chunk_base, lane_row_base), codes);
let vals = _mm512_i32gather_ps::<4>(idx, lut_ptr);
$acc = _mm512_add_ps($acc, vals);
}};
}
step!(c, acc0);
step!(c + 1, acc1);
step!(c + 2, acc2);
step!(c + 3, acc3);
c += 4;
}
while c < chunks_per_vec {
let chunk_base = _mm512_set1_epi32((c * 16 * 256) as i32);
let bytes = _mm_loadu_si128(doc.add(c * 16) as *const __m128i);
let codes = _mm512_cvtepu8_epi32(bytes);
let idx = _mm512_add_epi32(_mm512_add_epi32(chunk_base, lane_row_base), codes);
let vals = _mm512_i32gather_ps::<4>(idx, lut_ptr);
acc0 = _mm512_add_ps(acc0, vals);
c += 1;
}
let s01 = _mm512_add_ps(acc0, acc1);
let s23 = _mm512_add_ps(acc2, acc3);
let total = _mm512_add_ps(s01, s23);
let raw = _mm512_reduce_add_ps(total);
top.maybe_insert(raw * scale, di);
}
}
}
#[cfg(all(test, target_arch = "x86_64"))]
mod b8_gather_tests {
use super::{build_b8_asym_lut_into, scan_b8_asym_avx512_gather, scan_b8_to_topk};
use crate::util::TopK;
use rand::{RngExt, SeedableRng};
use rand_chacha::ChaCha8Rng;
fn drain(top: &TopK, k: usize) -> (Vec<f32>, Vec<i64>) {
let mut scores = vec![f32::NEG_INFINITY; k];
let mut idxs = vec![-1i64; k];
top.finalize_into(&mut scores, &mut idxs);
(scores, idxs)
}
fn b8_lut(q_unit: &[f32]) -> Vec<f32> {
let mut lut = Vec::new();
build_b8_asym_lut_into(&mut lut, q_unit);
lut
}
#[test]
fn b8_gather_matches_scalar_reference() {
if !(is_x86_feature_detected!("avx512f") && is_x86_feature_detected!("avx512bw")) {
eprintln!("skipping b8 gather parity: no avx512f+avx512bw on this host");
return;
}
for &dim in &[384usize, 400, 768, 1024, 1536] {
assert_eq!(dim % 16, 0, "test dims must be % 16 for the gather path");
let n = 64;
let k = 10;
let mut rng = ChaCha8Rng::seed_from_u64(0x00B8_0000 + dim as u64);
let packed: Vec<u8> = (0..n * dim).map(|_| rng.random::<u8>()).collect();
let q: Vec<f32> = (0..dim).map(|_| rng.random_range(-1.0..1.0)).collect();
let qn: f32 = q.iter().map(|x| x * x).sum::<f32>().sqrt();
let q_unit: Vec<f32> = q.iter().map(|x| x / qn).collect();
let scale = 1.0f32 / 137.0;
let lut = b8_lut(&q_unit);
let mut top_scalar = TopK::new(k);
scan_b8_to_topk(&packed, n, dim, &lut, scale, &mut top_scalar);
let (s_scalar, i_scalar) = drain(&top_scalar, k);
let mut top_gather = TopK::new(k);
unsafe {
scan_b8_asym_avx512_gather(&packed, n, dim, &lut, scale, &mut top_gather);
}
let (s_gather, i_gather) = drain(&top_gather, k);
for slot in 0..k {
assert!(
(s_scalar[slot] - s_gather[slot]).abs() < 1e-4,
"dim={dim} slot={slot}: scalar {} vs gather {}",
s_scalar[slot],
s_gather[slot],
);
}
assert_eq!(
i_scalar, i_gather,
"dim={dim}: top-{k} id ordering diverged between scalar and gather"
);
}
}
#[test]
fn b8_gather_raw_score_is_exact_gather_sum() {
if !(is_x86_feature_detected!("avx512f") && is_x86_feature_detected!("avx512bw")) {
return;
}
let dim = 256usize;
let n = 8;
let k = n;
let mut rng = ChaCha8Rng::seed_from_u64(0x00B8_FACE);
let packed: Vec<u8> = (0..n * dim).map(|_| rng.random::<u8>()).collect();
let q_unit: Vec<f32> = (0..dim).map(|_| rng.random_range(-1.0..1.0)).collect();
let lut = b8_lut(&q_unit);
let mut top = TopK::new(k);
unsafe {
scan_b8_asym_avx512_gather(&packed, n, dim, &lut, 1.0, &mut top);
}
let (scores, idxs) = drain(&top, k);
let want: Vec<f32> = (0..n)
.map(|di| {
let doc = &packed[di * dim..(di + 1) * dim];
doc.iter()
.enumerate()
.map(|(d, &code)| lut[d * 256 + code as usize])
.sum::<f32>()
})
.collect();
for slot in 0..k {
let di = idxs[slot] as usize;
let rel = (scores[slot] - want[di]).abs() / want[di].abs().max(1.0);
assert!(
rel < 1e-4,
"doc {di}: gather {} vs brute {} (rel {rel})",
scores[slot],
want[di]
);
}
}
#[test]
#[ignore = "perf micro-bench; run explicitly with --ignored --nocapture --release"]
fn b8_kernel_microbench() {
use crate::quant_kernels::{scan_b4_asym_avx512, scan_b8_asym_avx512_gather};
use std::time::Instant;
let have_avx512 = is_x86_feature_detected!("avx512f")
&& is_x86_feature_detected!("avx512dq")
&& is_x86_feature_detected!("avx512bw"); let dim = 1024usize; let n = 50_000usize;
let k = 10usize;
let iters = 20usize;
let mut rng = ChaCha8Rng::seed_from_u64(0x00B8_4BE4);
let q: Vec<f32> = (0..dim).map(|_| rng.random_range(-1.0..1.0)).collect();
let qn: f32 = q.iter().map(|x| x * x).sum::<f32>().sqrt();
let q_unit: Vec<f32> = q.iter().map(|x| x / qn).collect();
let scale = 1.0f32 / 137.0;
let packed8: Vec<u8> = (0..n * dim).map(|_| rng.random::<u8>()).collect();
let packed4: Vec<u8> = (0..n * dim / 2).map(|_| rng.random::<u8>()).collect();
let lut8 = b8_lut(&q_unit);
let bench = |label: &str, mut f: Box<dyn FnMut()>| {
f(); let t0 = Instant::now();
for _ in 0..iters {
f();
}
let per = t0.elapsed().as_secs_f64() / iters as f64;
let ns_per_doc_dim = per * 1e9 / (n as f64 * dim as f64);
let gdocs = n as f64 / per / 1e9;
println!(
" {label:<26} {:>8.3} ms/scan {:>7.3} ns/doc/dim {:>7.3} Gdoc/s",
per * 1e3,
ns_per_doc_dim,
gdocs,
);
};
println!(
"\nb=8 asymmetric kernel micro-bench (dim={dim}, n={n}, k={k}, iters={iters}, avx512={have_avx512})"
);
{
let packed8 = packed8.clone();
let lut8 = lut8.clone();
bench(
"b=8 scalar LUT",
Box::new(move || {
let mut top = TopK::new(k);
scan_b8_to_topk(&packed8, n, dim, &lut8, scale, &mut top);
std::hint::black_box(&top);
}),
);
}
if have_avx512 {
let packed8 = packed8.clone();
let lut8 = lut8.clone();
bench(
"b=8 AVX-512 gather",
Box::new(move || {
let mut top = TopK::new(k);
unsafe {
scan_b8_asym_avx512_gather(&packed8, n, dim, &lut8, scale, &mut top);
}
std::hint::black_box(&top);
}),
);
let packed4 = packed4.clone();
let q_unit4 = q_unit.clone();
bench(
"b=4 AVX-512 asym (context)",
Box::new(move || {
let mut top = TopK::new(k);
unsafe {
scan_b4_asym_avx512(&packed4, n, dim, &q_unit4, scale, &mut top);
}
std::hint::black_box(&top);
}),
);
} else {
println!(" (avx512 unavailable — SIMD rows skipped)");
}
}
}