blob: 68655901a089f92f8f0c6dccbd91e51a7af50603 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! FastScan: Faiss-style block layout (bbs=32) + vpshufb 32-way parallel lookup
//! for 4-bit PQ codes.
//!
//! Block layout: 32 vectors per block, codes interleaved by sub-quantizer pair.
//! Each block stores M/2 groups of 32 bytes (one byte per vector per sub-quant pair).
//!
//! Layout: [block0_pair0(32B), block0_pair1(32B), ..., block0_pairM/2(32B),
//! block1_pair0(32B), ...]
/// Block size: 32 vectors per block (matches AVX2 register width).
pub const BBS: usize = 32;
/// Pack 4-bit codes from row-major [n][cs] into block layout.
/// Output layout: [num_blocks][cs][BBS] where cs = M/2.
/// Pads the last block with zeros if n is not a multiple of BBS.
pub fn pack_codes_block_layout(codes: &[u8], n: usize, cs: usize) -> Vec<u8> {
let num_blocks = n.div_ceil(BBS);
let block_size = cs * BBS; // bytes per block
let mut packed = vec![0u8; num_blocks * block_size];
for block in 0..num_blocks {
let block_start = block * BBS;
for pair in 0..cs {
for vec_in_block in 0..BBS {
let global_vec = block_start + vec_in_block;
if global_vec < n {
packed[block * block_size + pair * BBS + vec_in_block] =
codes[global_vec * cs + pair];
}
// else: remains 0 (padding)
}
}
}
packed
}
/// Unpack block layout back to row-major [n][cs] (for compatibility).
pub fn unpack_codes_block_layout(packed: &[u8], n: usize, cs: usize) -> Vec<u8> {
let num_blocks = n.div_ceil(BBS);
let block_size = cs * BBS;
let mut codes = vec![0u8; n * cs];
for block in 0..num_blocks {
let block_start = block * BBS;
for pair in 0..cs {
for vec_in_block in 0..BBS {
let global_vec = block_start + vec_in_block;
if global_vec < n {
codes[global_vec * cs + pair] =
packed[block * block_size + pair * BBS + vec_in_block];
}
}
}
}
codes
}
/// Quantize a f32 distance table [M * 16] to u8 [M * 16].
/// Returns (qmin, qmax_used, quantized_table).
pub fn quantize_distance_table(table: &[f32], qmax_hint: f32) -> (f32, f32, Vec<u8>) {
let qmin = table.iter().cloned().fold(f32::INFINITY, f32::min);
let qmax = qmax_hint.max(table.iter().cloned().fold(f32::MIN, f32::max));
let range = (qmax - qmin).max(1e-10);
let factor = 255.0 / range;
let qtable: Vec<u8> = table
.iter()
.map(|&d| ((d - qmin) * factor).clamp(0.0, 255.0) as u8)
.collect();
(qmin, qmax, qtable)
}
/// FastScan: scan block-layout 4-bit codes using SIMD.
/// codes: block layout [num_blocks][cs][BBS]
/// sim_table: [M * 16] f32 distance table
/// Returns f32 distances for `n` vectors.
pub fn fastscan_4bit(sim_table: &[f32], codes: &[u8], n: usize, m: usize, dists: &mut [f32]) {
let cs = m / 2;
// Step 1: f32 exact for first min(200, n) vectors as qmax calibration
const FLAT_NUM: usize = 200;
let flat_end = n.min(FLAT_NUM);
let block_size = cs * BBS;
for i in 0..flat_end {
let block = i / BBS;
let vec_in_block = i % BBS;
let mut d = 0.0f32;
for pair in 0..cs {
let byte = codes[block * block_size + pair * BBS + vec_in_block];
let lo = (byte & 0x0F) as usize;
let hi = ((byte >> 4) & 0x0F) as usize;
d += sim_table[(pair * 2) * 16 + lo];
d += sim_table[(pair * 2 + 1) * 16 + hi];
}
dists[i] = d;
}
if n <= FLAT_NUM {
return;
}
// Step 2: Quantize distance table using the table's own range
let table_max = sim_table.iter().cloned().fold(f32::MIN, f32::max);
let (qmin, _, qtable) = quantize_distance_table(sim_table, table_max);
let range = (table_max - qmin).max(1e-10);
// Step 3: Scan blocks using SIMD
let num_blocks = n.div_ceil(BBS);
let start_block = flat_end.div_ceil(BBS);
#[cfg(target_arch = "x86_64")]
{
if is_x86_feature_detected!("avx2") {
unsafe {
fastscan_blocks_avx2(
&qtable,
codes,
cs,
start_block,
num_blocks,
block_size,
qmin,
range,
m,
dists,
);
}
let partial_start = start_block * BBS;
for i in flat_end..partial_start.min(n) {
let block = i / BBS;
let vec_in_block = i % BBS;
let mut d = 0.0f32;
for pair in 0..cs {
let byte = codes[block * block_size + pair * BBS + vec_in_block];
let lo = (byte & 0x0F) as usize;
let hi = ((byte >> 4) & 0x0F) as usize;
d += sim_table[(pair * 2) * 16 + lo];
d += sim_table[(pair * 2 + 1) * 16 + hi];
}
dists[i] = d;
}
return;
}
}
#[cfg(target_arch = "aarch64")]
{
unsafe {
fastscan_blocks_neon(
&qtable,
codes,
cs,
start_block,
num_blocks,
block_size,
qmin,
range,
m,
dists,
);
}
let partial_start = start_block * BBS;
for i in flat_end..partial_start.min(n) {
let block = i / BBS;
let vec_in_block = i % BBS;
let mut d = 0.0f32;
for pair in 0..cs {
let byte = codes[block * block_size + pair * BBS + vec_in_block];
let lo = (byte & 0x0F) as usize;
let hi = ((byte >> 4) & 0x0F) as usize;
d += sim_table[(pair * 2) * 16 + lo];
d += sim_table[(pair * 2 + 1) * 16 + hi];
}
dists[i] = d;
}
return;
}
// Fallback: scalar scan on blocks (used when no SIMD available)
#[allow(unreachable_code)]
for block in start_block..num_blocks {
let base_vec = block * BBS;
let vecs_in_block = BBS.min(n - base_vec);
let mut q_dists = [0u16; BBS];
for pair in 0..cs {
let qtab_lo = &qtable[(pair * 2) * 16..(pair * 2 + 1) * 16];
let qtab_hi = &qtable[(pair * 2 + 1) * 16..(pair * 2 + 2) * 16];
let block_codes = &codes[block * block_size + pair * BBS..];
for v in 0..vecs_in_block {
let byte = block_codes[v];
let lo = (byte & 0x0F) as usize;
let hi = ((byte >> 4) & 0x0F) as usize;
q_dists[v] += qtab_lo[lo] as u16 + qtab_hi[hi] as u16;
}
}
// Dequantize
let inv_factor = range / 255.0;
let base_dist = qmin * m as f32;
for v in 0..vecs_in_block {
dists[base_vec + v] = q_dists[v] as f32 * inv_factor + base_dist;
}
}
// Fill gap between flat_end and start_block*BBS with exact computation
let partial_start = start_block * BBS;
for i in flat_end..partial_start.min(n) {
let block = i / BBS;
let vec_in_block = i % BBS;
let mut d = 0.0f32;
for pair in 0..cs {
let byte = codes[block * block_size + pair * BBS + vec_in_block];
let lo = (byte & 0x0F) as usize;
let hi = ((byte >> 4) & 0x0F) as usize;
d += sim_table[(pair * 2) * 16 + lo];
d += sim_table[(pair * 2 + 1) * 16 + hi];
}
dists[i] = d;
}
}
/// AVX2 block scan using vpshufb for 32-way parallel 4-bit lookup.
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn fastscan_blocks_avx2(
qtable: &[u8],
codes: &[u8],
cs: usize,
start_block: usize,
num_blocks: usize,
block_size: usize,
qmin: f32,
range: f32,
m: usize,
dists: &mut [f32],
) {
use std::arch::x86_64::*;
let inv_factor = range / 255.0;
let base_dist = qmin * m as f32;
for block in start_block..num_blocks {
let base_vec = block * BBS;
// u16 accumulators: 2 × __m256i = 32 × u16 values
let mut accu_lo = _mm256_setzero_si256(); // vecs 0-15
let mut accu_hi = _mm256_setzero_si256(); // vecs 16-31
for pair in 0..cs {
// Load 32-byte quantized LUT for this pair (lo + hi sub-quantizers)
// LUT layout: [16 bytes for sub_lo, 16 bytes for sub_hi]
let lut_lo_ptr = qtable.as_ptr().add((pair * 2) * 16);
let lut_hi_ptr = qtable.as_ptr().add((pair * 2 + 1) * 16);
// Broadcast 16-byte LUTs into 256-bit registers (same 16-byte table in both lanes)
let lut_lo = _mm256_broadcastsi128_si256(_mm_loadu_si128(lut_lo_ptr as *const __m128i));
let lut_hi = _mm256_broadcastsi128_si256(_mm_loadu_si128(lut_hi_ptr as *const __m128i));
// Load 32 code bytes for this pair in this block
let code_ptr = codes.as_ptr().add(block * block_size + pair * BBS);
let code_vec = _mm256_loadu_si256(code_ptr as *const __m256i);
// Split nibbles
let mask = _mm256_set1_epi8(0x0F);
let lo_nibbles = _mm256_and_si256(code_vec, mask);
let hi_nibbles = _mm256_and_si256(_mm256_srli_epi16(code_vec, 4), mask);
// vpshufb: 32-way parallel lookup
let dist_lo = _mm256_shuffle_epi8(lut_lo, lo_nibbles);
let dist_hi = _mm256_shuffle_epi8(lut_hi, hi_nibbles);
// Widen each to u16 separately then add (avoids u8 saturation overflow)
let zero = _mm256_setzero_si256();
let dlo_lo = _mm256_unpacklo_epi8(dist_lo, zero);
let dlo_hi = _mm256_unpackhi_epi8(dist_lo, zero);
let dhi_lo = _mm256_unpacklo_epi8(dist_hi, zero);
let dhi_hi = _mm256_unpackhi_epi8(dist_hi, zero);
accu_lo = _mm256_add_epi16(accu_lo, _mm256_add_epi16(dlo_lo, dhi_lo));
accu_hi = _mm256_add_epi16(accu_hi, _mm256_add_epi16(dlo_hi, dhi_hi));
}
// Extract u16 values and dequantize to f32.
// _mm256_unpacklo/hi_epi8 operates per 128-bit lane, so:
// accu_lo = [v0..v7 | v16..v23], accu_hi = [v8..v15 | v24..v31]
// Reassemble correct order with cross-lane permute.
let result_lo = _mm256_permute2x128_si256(accu_lo, accu_hi, 0x20);
let result_hi = _mm256_permute2x128_si256(accu_lo, accu_hi, 0x31);
let mut q_vals = [0u16; BBS];
_mm256_storeu_si256(q_vals.as_mut_ptr() as *mut __m256i, result_lo);
_mm256_storeu_si256(q_vals.as_mut_ptr().add(16) as *mut __m256i, result_hi);
for v in 0..BBS {
let idx = base_vec + v;
if idx < dists.len() {
dists[idx] = q_vals[v] as f32 * inv_factor + base_dist;
}
}
}
}
/// ARM NEON block scan (16-way per instruction, 2 passes per block).
#[cfg(target_arch = "aarch64")]
#[target_feature(enable = "neon")]
unsafe fn fastscan_blocks_neon(
qtable: &[u8],
codes: &[u8],
cs: usize,
start_block: usize,
num_blocks: usize,
block_size: usize,
qmin: f32,
range: f32,
m: usize,
dists: &mut [f32],
) {
use std::arch::aarch64::*;
let inv_factor = range / 255.0;
let base_dist = qmin * m as f32;
for block in start_block..num_blocks {
let base_vec = block * BBS;
// u16 accumulators: 4 × uint16x8_t = 32 × u16
let mut accu = [vdupq_n_u16(0); 4];
for pair in 0..cs {
let lut_lo = vld1q_u8(qtable.as_ptr().add((pair * 2) * 16));
let lut_hi = vld1q_u8(qtable.as_ptr().add((pair * 2 + 1) * 16));
let code_ptr = codes.as_ptr().add(block * block_size + pair * BBS);
// Process 16 vectors at a time (NEON = 128-bit)
for half in 0..2 {
let code_vec = vld1q_u8(code_ptr.add(half * 16));
let mask = vdupq_n_u8(0x0F);
let lo_nib = vandq_u8(code_vec, mask);
let hi_nib = vshrq_n_u8(code_vec, 4);
// tbl: 16-way lookup
let dist_lo = vqtbl1q_u8(lut_lo, lo_nib);
let dist_hi = vqtbl1q_u8(lut_hi, hi_nib);
// Widen each to u16 separately then add (avoids u8 saturation overflow)
accu[half * 2] = vaddq_u16(accu[half * 2], vmovl_u8(vget_low_u8(dist_lo)));
accu[half * 2] = vaddq_u16(accu[half * 2], vmovl_u8(vget_low_u8(dist_hi)));
accu[half * 2 + 1] = vaddq_u16(accu[half * 2 + 1], vmovl_u8(vget_high_u8(dist_lo)));
accu[half * 2 + 1] = vaddq_u16(accu[half * 2 + 1], vmovl_u8(vget_high_u8(dist_hi)));
}
}
// Extract and dequantize
let mut q_vals = [0u16; BBS];
for i in 0..4 {
vst1q_u16(q_vals.as_mut_ptr().add(i * 8), accu[i]);
}
for v in 0..BBS {
let idx = base_vec + v;
if idx < dists.len() {
dists[idx] = q_vals[v] as f32 * inv_factor + base_dist;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pack_unpack_roundtrip() {
let n = 100;
let cs = 4; // m/2 = 4, i.e. m=8
let codes: Vec<u8> = (0..n * cs).map(|i| (i % 256) as u8).collect();
let packed = pack_codes_block_layout(&codes, n, cs);
let unpacked = unpack_codes_block_layout(&packed, n, cs);
assert_eq!(codes, unpacked);
}
#[test]
fn test_fastscan_correctness() {
let m = 8;
let cs = m / 2;
let n = 100;
// Random-ish codes
let codes_row: Vec<u8> = (0..n * cs).map(|i| ((i * 7 + 3) % 256) as u8).collect();
// Random-ish distance table [M * 16]
let sim_table: Vec<f32> = (0..m * 16).map(|i| (i as f32) * 0.1 + 0.5).collect();
// Compute ground truth with scalar
let mut expected = vec![0.0f32; n];
for i in 0..n {
for pair in 0..cs {
let byte = codes_row[i * cs + pair];
let lo = (byte & 0x0F) as usize;
let hi = ((byte >> 4) & 0x0F) as usize;
expected[i] += sim_table[(pair * 2) * 16 + lo];
expected[i] += sim_table[(pair * 2 + 1) * 16 + hi];
}
}
// Pack into block layout and scan
let packed = pack_codes_block_layout(&codes_row, n, cs);
let mut result = vec![0.0f32; n];
fastscan_4bit(&sim_table, &packed, n, m, &mut result);
// Check first 200 (f32 exact) — should match perfectly
for i in 0..n.min(200) {
assert!(
(result[i] - expected[i]).abs() < 1e-5,
"Mismatch at {}: {} vs {}",
i,
result[i],
expected[i]
);
}
}
#[test]
fn test_fastscan_large() {
let m = 16;
let cs = m / 2;
let n = 1000; // > 200, exercises quantized path
let codes_row: Vec<u8> = (0..n * cs).map(|i| ((i * 13 + 7) % 256) as u8).collect();
let sim_table: Vec<f32> = (0..m * 16).map(|i| (i as f32) * 0.05 + 1.0).collect();
// Compute scalar reference
let mut expected = vec![0.0f32; n];
for i in 0..n {
for pair in 0..cs {
let byte = codes_row[i * cs + pair];
let lo = (byte & 0x0F) as usize;
let hi = ((byte >> 4) & 0x0F) as usize;
expected[i] += sim_table[(pair * 2) * 16 + lo];
expected[i] += sim_table[(pair * 2 + 1) * 16 + hi];
}
}
let packed = pack_codes_block_layout(&codes_row, n, cs);
let mut result = vec![0.0f32; n];
fastscan_4bit(&sim_table, &packed, n, m, &mut result);
// First 200 are computed with f32 exact — should match perfectly
for i in 0..200 {
assert!(
(result[i] - expected[i]).abs() < 1e-5,
"Exact mismatch at {}: got {}, expected {}",
i,
result[i],
expected[i]
);
}
// Beyond 200: quantized path — allow quantization tolerance
let max_expected = expected.iter().cloned().fold(f32::MIN, f32::max);
let tolerance = max_expected * 0.02; // 2% relative tolerance for u8 quantization
for i in 200..n {
assert!(
(result[i] - expected[i]).abs() <= tolerance,
"SIMD mismatch at {}: got {}, expected {}, diff {}",
i,
result[i],
expected[i],
(result[i] - expected[i]).abs()
);
}
}
}