use diskann_wide::{SIMDMask, SIMDMulAdd, SIMDPartialOrd, SIMDSelect, SIMDSumTree, SIMDVector};
use super::common::square_norm;
use crate::multi_vector::{BlockTransposed, BlockTransposedRef};
use diskann_utils::{
strided::Strided,
views::rowmajor::{self, Matrix, MatrixMut},
};
diskann_wide::alias!(f32s = f32x8);
diskann_wide::alias!(u32s = u32x8);
pub fn distances_in_place(
dataset: BlockTransposedRef<'_, f32, 16>,
data_norms: &[f32],
centers: rowmajor::Ref<'_, f32>,
center_norms: &[f32],
nearest: &mut [u32],
) -> f32 {
assert_eq!(
dataset.nrows(),
data_norms.len(),
"dataset and data norms should have the same length"
);
assert_eq!(
centers.ncols(),
dataset.ncols(),
"dataset and centers should have the same dimensions"
);
assert_eq!(
centers.nrows(),
center_norms.len(),
"centers and center norms should have the same length"
);
assert_eq!(
nearest.len(),
dataset.nrows(),
"dataset and nearest-buffer should have the same length"
);
const N: usize = 16;
const N2: usize = N / 2;
diskann_wide::alias!(m32s = mask_f32x8);
let mut residual = f32s::default(diskann_wide::ARCH);
let process_block_unroll_2 = |block: usize, center_row_start: usize| {
debug_assert!(block < dataset.num_blocks());
debug_assert!(center_row_start + 1 < centers.nrows());
let mut s00 = f32s::default(diskann_wide::ARCH);
let mut s01 = f32s::default(diskann_wide::ARCH);
let mut s10 = f32s::default(diskann_wide::ARCH);
let mut s11 = f32s::default(diskann_wide::ARCH);
let block_ptr = unsafe { dataset.block_ptr_unchecked(block) };
for dim in 0..dataset.ncols() {
let d0 = unsafe { f32s::load_simd(diskann_wide::ARCH, block_ptr.add(N * dim)) };
let d1 = unsafe { f32s::load_simd(diskann_wide::ARCH, block_ptr.add(N * dim + N2)) };
let c0 = f32s::splat(diskann_wide::ARCH, unsafe {
*centers.element_unchecked(center_row_start, dim)
});
let c1 = f32s::splat(diskann_wide::ARCH, unsafe {
*centers.element_unchecked(center_row_start + 1, dim)
});
s00 = c0.mul_add_simd(d0, s00);
s01 = c0.mul_add_simd(d1, s01);
s10 = c1.mul_add_simd(d0, s10);
s11 = c1.mul_add_simd(d1, s11);
}
(s00, s01, s10, s11)
};
let process_block_no_unroll = |block: usize, center_row_start: usize| {
debug_assert!(block < dataset.num_blocks());
debug_assert!(center_row_start + 1 == centers.nrows());
let mut s00 = f32s::default(diskann_wide::ARCH);
let mut s01 = f32s::default(diskann_wide::ARCH);
let block_ptr = unsafe { dataset.block_ptr_unchecked(block) };
for dim in 0..dataset.ncols() {
let d0 = unsafe { f32s::load_simd(diskann_wide::ARCH, block_ptr.add(N * dim)) };
let d1 = unsafe { f32s::load_simd(diskann_wide::ARCH, block_ptr.add(N * dim + N2)) };
let c0 = f32s::splat(diskann_wide::ARCH, unsafe {
*centers.element_unchecked(center_row_start, dim)
});
s00 = c0.mul_add_simd(d0, s00);
s01 = c0.mul_add_simd(d1, s01);
}
(s00, s01)
};
let last_pair = if centers.nrows().is_multiple_of(2) {
centers.nrows()
} else {
centers.nrows() - 1
};
for i in 0..dataset.full_blocks() {
let mut t0 = (
f32s::splat(diskann_wide::ARCH, f32::INFINITY),
u32s::splat(diskann_wide::ARCH, u32::MAX),
);
let mut t1 = (
f32s::splat(diskann_wide::ARCH, f32::INFINITY),
u32s::splat(diskann_wide::ARCH, u32::MAX),
);
let data_norm_ptr = unsafe { data_norms.as_ptr().add(N * i) };
let d0 = unsafe { f32s::load_simd(diskann_wide::ARCH, data_norm_ptr) };
let d1 = unsafe { f32s::load_simd(diskann_wide::ARCH, data_norm_ptr.add(N2)) };
for row_start in (0..last_pair).step_by(2) {
let (s00, s01, s10, s11) = process_block_unroll_2(i, row_start);
let n0 = f32s::splat(diskann_wide::ARCH, *unsafe {
center_norms.get_unchecked(row_start)
});
let n1 = f32s::splat(diskann_wide::ARCH, *unsafe {
center_norms.get_unchecked(row_start + 1)
});
let s00 = n0 - s00 - s00 + d0;
let s01 = n0 - s01 - s01 + d1;
let s10 = n1 - s10 - s10 + d0;
let s11 = n1 - s11 - s11 + d1;
let r0 = u32s::splat(diskann_wide::ARCH, row_start as u32);
let r1 = u32s::splat(diskann_wide::ARCH, (row_start + 1) as u32);
t0 = update(update(t0, (s00, r0)), (s10, r1));
t1 = update(update(t1, (s01, r0)), (s11, r1));
}
if !centers.nrows().is_multiple_of(2) {
let (s00, s01) = process_block_no_unroll(i, last_pair);
let n0 = f32s::splat(diskann_wide::ARCH, unsafe {
*center_norms.get_unchecked(last_pair)
});
let s00 = n0 - s00 - s00 + d0;
let s01 = n0 - s01 - s01 + d1;
let r = u32s::splat(diskann_wide::ARCH, last_pair as u32);
t0 = update(t0, (s00, r));
t1 = update(t1, (s01, r));
}
unsafe { t0.1.store_simd(nearest.as_mut_ptr().add(N * i)) }
unsafe { t1.1.store_simd(nearest.as_mut_ptr().add(N * i + N2)) }
residual = residual + t0.0 + t1.0;
}
let remainder = dataset.remainder();
if remainder != 0 {
let i = dataset.full_blocks();
let lo = remainder.min(N2);
let hi = remainder - lo;
let mut t0 = (
f32s::splat(diskann_wide::ARCH, f32::INFINITY),
u32s::splat(diskann_wide::ARCH, u32::MAX),
);
let mut t1 = (
f32s::splat(diskann_wide::ARCH, f32::INFINITY),
u32s::splat(diskann_wide::ARCH, u32::MAX),
);
let data_norm_ptr = unsafe { data_norms.as_ptr().add(N * i) };
let d0 = unsafe { f32s::load_simd_first(diskann_wide::ARCH, data_norm_ptr, lo) };
let d1 = if hi == 0 {
f32s::default(diskann_wide::ARCH)
} else {
unsafe { f32s::load_simd_first(diskann_wide::ARCH, data_norm_ptr.add(N2), hi) }
};
for row_start in (0..last_pair).step_by(2) {
let (s00, s01, s10, s11) = process_block_unroll_2(i, row_start);
let n0 = f32s::splat(diskann_wide::ARCH, *unsafe {
center_norms.get_unchecked(row_start)
});
let n1 = f32s::splat(diskann_wide::ARCH, *unsafe {
center_norms.get_unchecked(row_start + 1)
});
let s00 = n0 - s00 - s00 + d0;
let s01 = n0 - s01 - s01 + d1;
let s10 = n1 - s10 - s10 + d0;
let s11 = n1 - s11 - s11 + d1;
let r0 = u32s::splat(diskann_wide::ARCH, row_start as u32);
let r1 = u32s::splat(diskann_wide::ARCH, (row_start + 1) as u32);
t0 = update(update(t0, (s00, r0)), (s10, r1));
t1 = update(update(t1, (s01, r0)), (s11, r1));
}
if !centers.nrows().is_multiple_of(2) {
let (s00, s01) = process_block_no_unroll(i, last_pair);
let n0 = f32s::splat(diskann_wide::ARCH, unsafe {
*center_norms.get_unchecked(last_pair)
});
let s00 = n0 - s00 - s00 + d0;
let s01 = n0 - s01 - s01 + d1;
let r = u32s::splat(diskann_wide::ARCH, last_pair as u32);
t0 = update(t0, (s00, r));
t1 = update(t1, (s01, r));
}
unsafe { t0.1.store_simd_first(nearest.as_mut_ptr().add(N * i), lo) };
if hi != 0 {
unsafe {
t1.1.store_simd_first(nearest.as_mut_ptr().add(N * i + N2), hi)
};
}
residual = m32s::keep_first(diskann_wide::ARCH, lo).select(residual + t0.0, residual);
residual = m32s::keep_first(diskann_wide::ARCH, hi).select(residual + t1.0, residual);
}
residual.sum_tree()
}
#[inline(always)]
fn update((d0, i0): (f32s, u32s), (d1, i1): (f32s, u32s)) -> (f32s, u32s) {
let mask = d1.lt_simd(d0);
(
mask.select(d1, d0),
<u32s as SIMDVector>::Mask::from(mask).select(i1, i0),
)
}
fn update_centroids(mut centers: rowmajor::Mut<'_, f32>, data: Strided<'_, f32>, map: &[u32]) {
let mut sums = rowmajor::Owned::<f64>::from_element(centers.nrows(), centers.ncols(), 0.0);
let mut counts: Vec<u32> = vec![0; centers.nrows()];
data.rows().zip(map.iter()).for_each(|(row, ¢er)| {
counts[center as usize] += 1;
let sum = sums.row_mut(center as usize);
std::iter::zip(sum.iter_mut(), row.iter()).for_each(|(s, r)| {
*s += <f32 as Into<f64>>::into(*r);
});
});
std::iter::zip(counts.iter(), sums.rows())
.zip(centers.rows_mut())
.for_each(|((count, sum), center)| {
let count = (*count).max(1);
std::iter::zip(sum.iter(), center.iter_mut()).for_each(|(s, c)| {
*c = (*s / (count as f64)) as f32;
});
});
}
pub(crate) fn lloyds_inner(
data: Strided<'_, f32>,
square_norms: &[f32],
transpose: BlockTransposedRef<'_, f32, 16>,
mut centers: rowmajor::Mut<'_, f32>,
max_reps: usize,
) -> (Vec<u32>, f32) {
let num_data = data.nrows();
assert_eq!(
num_data,
square_norms.len(),
"data and norms should have the same length"
);
assert_eq!(
num_data,
transpose.nrows(),
"data and transpose should have the same length"
);
let dim = data.ncols();
assert_eq!(
dim,
transpose.ncols(),
"data and transpose should have the same dimensions"
);
assert_eq!(
dim,
centers.ncols(),
"data and centers should have the same dimensions"
);
let mut center_square_norms: Vec<f32> = centers.rows().map(square_norm).collect();
let mut assignments: Vec<u32> = vec![0; num_data];
let mut residual = 0.0;
for i in 0..max_reps {
residual = distances_in_place(
transpose,
square_norms,
centers.as_view(),
¢er_square_norms,
&mut assignments,
);
update_centroids(centers.as_view_mut(), data, &assignments);
if i != max_reps - 1 {
std::iter::zip(center_square_norms.iter_mut(), centers.rows()).for_each(
|(c, center)| {
*c = square_norm(center);
},
);
}
}
(assignments, residual)
}
pub fn lloyds(
data: rowmajor::Ref<'_, f32>,
centers: rowmajor::Mut<'_, f32>,
max_reps: usize,
) -> (Vec<u32>, f32) {
assert_eq!(
data.ncols(),
centers.ncols(),
"data and centers must have the same dimension",
);
let transpose = BlockTransposed::<f32, 16>::from_matrix_view(data);
let square_norms: Vec<f32> = data.rows().map(square_norm).collect();
lloyds_inner(
data.into(),
&square_norms,
transpose.as_view(),
centers,
max_reps,
)
}
#[cfg(test)]
mod tests {
#[cfg(not(miri))]
use diskann_utils::lazy_format;
use diskann_vector::{PureDistanceFunction, distance::SquaredL2};
use rand::{Rng, SeedableRng, rngs::StdRng, seq::SliceRandom};
#[cfg(not(miri))]
use rand::{
distr::{Distribution, Uniform},
seq::IndexedRandom,
};
use super::*;
#[cfg(not(miri))]
fn test_distances_in_place_impl<R: Rng>(
ndata: usize,
ncenters: usize,
dim: usize,
trials: usize,
rng: &mut R,
) {
let context = lazy_format!("ncenters = {}, ndata = {}, dim = {}", ncenters, ndata, dim,);
let mut centers = rowmajor::Owned::from_element(ncenters, dim, 0.0);
let mut data = rowmajor::Owned::from_element(ndata, dim, 0.0);
let offsets = [-0.125, -0.0625, -0.03125, 0.03125, 0.0625, 0.125];
for (i, row) in centers.rows_mut().enumerate() {
for c in row {
*c = (i as f32) + *offsets.choose(rng).unwrap();
}
}
let center_norms: Vec<f32> = centers.rows().map(square_norm).collect();
let assignment_distribution = Uniform::<usize>::new(0, centers.nrows()).unwrap();
let mut nearest: Vec<u32> = vec![0; ndata];
for trial in 0..trials {
let assignments: Vec<_> = (0..ndata)
.map(|_| assignment_distribution.sample(rng))
.collect();
for (assignment, row) in std::iter::zip(assignments.iter(), data.rows_mut()) {
for c in row.iter_mut() {
*c = (*assignment as f32) + offsets.choose(rng).unwrap()
}
}
let data_norms: Vec<f32> = data.rows().map(square_norm).collect();
let residual = distances_in_place(
BlockTransposed::<f32, 16>::from_matrix_view(data.as_view()).as_view(),
&data_norms,
centers.as_view(),
¢er_norms,
&mut nearest,
);
for (i, (got, expected)) in
std::iter::zip(nearest.iter(), assignments.iter()).enumerate()
{
assert_eq!(
*got as usize,
*expected,
"failed for data index {} on trial {} -- {}\n\
row = {:?}\n\
expected = {:?}\n\
got = {:?}",
i,
trial,
context,
data.row(i),
centers.row(*expected),
centers.row(*got as usize),
);
}
let mut sum: f32 = 0.0;
for (a, row) in std::iter::zip(assignments.iter(), data.rows()) {
let distance: f32 = SquaredL2::evaluate(row, centers.row(*a));
sum += distance;
}
assert_eq!(sum, residual, "failed on trial {} -- {}", trial, context);
}
}
#[cfg(not(miri))]
const TRIALS: usize = 100;
#[test]
#[cfg(not(miri))]
fn test_distances_in_place() {
let mut rng = StdRng::seed_from_u64(0xece88a9c6cd86a8a);
for ndata in 1..=31 {
for ncenters in 1..=5 {
for dim in 1..=4 {
test_distances_in_place_impl(ndata, ncenters, dim, TRIALS, &mut rng);
}
}
}
}
fn test_miri_distances_in_place_impl(ndata: usize, ncenters: usize, dim: usize) {
let centers = rowmajor::Owned::from_element(ncenters, dim, 0.0);
let data = rowmajor::Owned::from_element(ndata, dim, 0.0);
let data_norms = vec![0.0; ndata];
let center_norms = vec![0.0; ncenters];
let mut nearest = vec![0; ndata];
let _ = distances_in_place(
BlockTransposed::<f32, 16>::from_matrix_view(data.as_view()).as_view(),
&data_norms,
centers.as_view(),
¢er_norms,
&mut nearest,
);
}
#[test]
fn test_miri_distances_in_place() {
for ndata in 1..=35 {
for ncenters in 1..=5 {
for dim in 1..=4 {
test_miri_distances_in_place_impl(ndata, ncenters, dim);
}
}
}
}
#[derive(Debug)]
struct EndToEndSetup {
ncenters: usize,
ndim: usize,
data_per_center: usize,
step_between_clusters: usize,
ntrials: usize,
}
fn end_to_end_test_impl<R: Rng>(setup: &EndToEndSetup, rng: &mut R) {
let mut values: Vec<usize> = (0..setup.ncenters)
.flat_map(|i| {
(0..setup.data_per_center).map(move |j| setup.step_between_clusters * i + j)
})
.collect();
let mut center_order: Vec<usize> = (0..setup.ncenters).collect();
let mut data =
rowmajor::Owned::from_element(setup.ncenters * setup.data_per_center, setup.ndim, 0.0);
let mut centers = rowmajor::Owned::from_element(setup.ncenters, setup.ndim, 0.0);
for trial in 0..setup.ntrials {
values.shuffle(rng);
center_order.shuffle(rng);
assert_eq!(center_order.len(), centers.nrows());
for (c, row) in std::iter::zip(center_order.iter(), centers.rows_mut()) {
row.fill((setup.step_between_clusters * c) as f32 - 1.0);
}
assert_eq!(values.len(), data.nrows());
for (d, row) in std::iter::zip(values.iter(), data.rows_mut()) {
row.fill(*d as f32);
}
let lloyds_iter = 2;
let (assignments, loss) = lloyds(data.as_view(), centers.as_view_mut(), lloyds_iter);
assert_eq!(assignments.len(), values.len());
for (i, (&got, v)) in std::iter::zip(assignments.iter(), values.iter()).enumerate() {
let expected: usize = v / setup.step_between_clusters;
assert_eq!(
center_order[got as usize], expected,
"failed at position {} in trial {} - prevalue: {} -- {:?}",
i, trial, v, setup
);
}
let triangle_sum = setup.data_per_center * (setup.data_per_center - 1) / 2;
center_order.iter().enumerate().for_each(|(i, o)| {
let expected = (setup.step_between_clusters * setup.data_per_center * o
+ triangle_sum) as f32
/ setup.data_per_center as f32;
assert!(
centers.row(i).iter().all(|v| *v == expected),
"at index {}, expected {}, got {:?} -- {:?}",
i,
expected,
centers.row(i),
setup,
);
});
let expected_loss: f32 = std::iter::zip(assignments.iter(), data.rows())
.map(|(a, row)| -> f32 {
let c = centers.row(*a as usize);
SquaredL2::evaluate(row, c)
})
.sum::<f32>();
assert_eq!(loss, expected_loss);
}
}
#[test]
fn end_to_end_test() {
let mut rng = StdRng::seed_from_u64(0xff22c38d0f0531bf);
let setup = if cfg!(miri) {
EndToEndSetup {
ncenters: 3,
ndim: 4,
data_per_center: 2,
step_between_clusters: 20,
ntrials: 2,
}
} else {
EndToEndSetup {
ncenters: 11,
ndim: 4,
data_per_center: 8,
step_between_clusters: 20,
ntrials: 10,
}
};
end_to_end_test_impl(&setup, &mut rng);
}
#[test]
#[should_panic(expected = "dataset and data norms should have the same length")]
fn distances_in_place_panics_data_norms() {
let data = rowmajor::Owned::from_element(5, 8, 0.0);
let data_norms = vec![0.0; data.nrows() + 1]; let centers = rowmajor::Owned::from_element(2, 8, 0.0);
let center_norms = vec![0.0; centers.nrows()];
let mut nearest = vec![0; data.nrows()];
distances_in_place(
BlockTransposed::<f32, 16>::from_matrix_view(data.as_view()).as_view(),
&data_norms,
centers.as_view(),
¢er_norms,
&mut nearest,
);
}
#[test]
#[should_panic(expected = "dataset and centers should have the same dimension")]
fn distances_in_place_panics_different_dim() {
let data = rowmajor::Owned::from_element(5, 8, 0.0);
let data_norms = vec![0.0; data.nrows()];
let centers = rowmajor::Owned::from_element(2, 9, 0.0); let center_norms = vec![0.0; centers.nrows()];
let mut nearest = vec![0; data.nrows()];
distances_in_place(
BlockTransposed::<f32, 16>::from_matrix_view(data.as_view()).as_view(),
&data_norms,
centers.as_view(),
¢er_norms,
&mut nearest,
);
}
#[test]
#[should_panic(expected = "centers and center norms should have the same length")]
fn distances_in_place_panics_center_norms() {
let data = rowmajor::Owned::from_element(5, 8, 0.0);
let data_norms = vec![0.0; data.nrows()];
let centers = rowmajor::Owned::from_element(2, 8, 0.0);
let center_norms = vec![0.0; centers.nrows() + 1]; let mut nearest = vec![0; data.nrows()];
distances_in_place(
BlockTransposed::<f32, 16>::from_matrix_view(data.as_view()).as_view(),
&data_norms,
centers.as_view(),
¢er_norms,
&mut nearest,
);
}
#[test]
#[should_panic(expected = "dataset and nearest-buffer should have the same length")]
fn distances_in_place_panics_nearest() {
let data = rowmajor::Owned::from_element(5, 8, 0.0);
let data_norms = vec![0.0; data.nrows()];
let centers = rowmajor::Owned::from_element(2, 8, 0.0);
let center_norms = vec![0.0; centers.nrows()];
let mut nearest = vec![0; data.nrows() + 1]; distances_in_place(
BlockTransposed::<f32, 16>::from_matrix_view(data.as_view()).as_view(),
&data_norms,
centers.as_view(),
¢er_norms,
&mut nearest,
);
}
#[test]
#[should_panic(expected = "data and norms should have the same length")]
fn lloyds_inner_panics_norms_length() {
let data = rowmajor::Owned::from_element(5, 8, 0.0);
let square_norms = vec![0.0; data.nrows() + 1]; let mut centers = rowmajor::Owned::from_element(2, 8, 0.0);
lloyds_inner(
data.as_view().into(),
&square_norms,
BlockTransposed::<f32, 16>::from_matrix_view(data.as_view()).as_view(),
centers.as_view_mut(),
1,
);
}
#[test]
#[should_panic(expected = "data and transpose should have the same length")]
fn lloyds_inner_panics_transpose_length() {
let data = rowmajor::Owned::from_element(5, 8, 0.0);
let data_incorrect = rowmajor::Owned::from_element(5 + 1, 8, 0.0); let square_norms = vec![0.0; data.nrows()];
let mut centers = rowmajor::Owned::from_element(2, 8, 0.0);
lloyds_inner(
data.as_view().into(),
&square_norms,
BlockTransposed::<f32, 16>::from_matrix_view(data_incorrect.as_view()).as_view(),
centers.as_view_mut(),
1,
);
}
#[test]
#[should_panic(expected = "data and transpose should have the same dimensions")]
fn lloyds_inner_panics_transpose_dim() {
let data = rowmajor::Owned::from_element(5, 8, 0.0);
let data_incorrect = rowmajor::Owned::from_element(5, 8 + 1, 0.0); let square_norms = vec![0.0; data.nrows()];
let mut centers = rowmajor::Owned::from_element(2, 8, 0.0);
lloyds_inner(
data.as_view().into(),
&square_norms,
BlockTransposed::<f32, 16>::from_matrix_view(data_incorrect.as_view()).as_view(), centers.as_view_mut(),
1,
);
}
#[test]
#[should_panic(expected = "data and centers should have the same dimensions")]
fn lloyds_inner_panics_centers_dim() {
let data = rowmajor::Owned::from_element(5, 8, 0.0);
let square_norms = vec![0.0; data.nrows()];
let mut centers = rowmajor::Owned::from_element(2, 8 + 1, 0.0); lloyds_inner(
data.as_view().into(),
&square_norms,
BlockTransposed::<f32, 16>::from_matrix_view(data.as_view()).as_view(),
centers.as_view_mut(),
1,
);
}
#[test]
#[should_panic(expected = "data and centers must have the same dimension")]
fn lloyds_panics_dim_mismatch() {
let data = rowmajor::Owned::from_element(5, 8, 0.0);
let mut centers = rowmajor::Owned::from_element(5, 8 + 1, 0.0); lloyds(data.as_view(), centers.as_view_mut(), 1);
}
}