1use diskann_wide::arch::Scalar;
7#[cfg(target_arch = "aarch64")]
8use diskann_wide::arch::aarch64::Neon;
9#[cfg(target_arch = "x86_64")]
10use diskann_wide::arch::x86_64::{V3, V4};
11
12use super::{
13 kernel::{MinMaxMaxSim, MinMaxMaxSimKernel},
14 prepared,
15 reference::ReferenceKernel,
16};
17use crate::{
18 bits::{Representation, Unsigned},
19 matrix_kernels::maxsim::packed_minmax_x_unpacked_minmax::max_k,
20 minmax::MinMaxMeta,
21 multi_vector::{BuildMaxSimError, MatRef, MaxSimIsa, NotSupported, QueryMatRef},
22};
23
24pub fn build_minmax_max_sim<const A_BITS: usize, const B_BITS: usize>(
62 isa: MaxSimIsa,
63 query: QueryMatRef<'_, MinMaxMeta<A_BITS>>,
64) -> Result<MinMaxMaxSim<B_BITS>, BuildMaxSimError>
65where
66 Unsigned: MinMaxKernelFactory<A_BITS, B_BITS>,
67{
68 <Unsigned as MinMaxKernelFactory<A_BITS, B_BITS>>::build(isa, query)
69}
70
71#[doc(hidden)]
73pub trait MinMaxKernelFactory<const A_BITS: usize, const B_BITS: usize>:
74 Representation<A_BITS> + Representation<B_BITS>
75{
76 fn build(
77 isa: MaxSimIsa,
78 query: QueryMatRef<'_, MinMaxMeta<A_BITS>>,
79 ) -> Result<MinMaxMaxSim<B_BITS>, BuildMaxSimError>
80 where
81 Unsigned: Representation<A_BITS> + Representation<B_BITS>;
82}
83
84macro_rules! factory {
86 ($a:literal, $b:literal, $scalar:ident, $v3:ident, $v4:ident) => {
87 impl MinMaxKernelFactory<$a, $b> for Unsigned {
88 fn build(
89 isa: MaxSimIsa,
90 query: QueryMatRef<'_, MinMaxMeta<$a>>,
91 ) -> Result<MinMaxMaxSim<$b>, BuildMaxSimError> {
92 let query = query.0;
93 validate_query::<$a, $b>(query)?;
94 let inner: Box<dyn MinMaxMaxSimKernel<$b>> = match RuntimeArch::resolve(isa)? {
95 RuntimeArch::Scalar(a) => Box::new(prepared::$scalar::<$b>::new(a, query)?),
96 #[cfg(target_arch = "x86_64")]
97 RuntimeArch::V3(a) => Box::new(prepared::$v3::<$b>::new(a, query)?),
98 #[cfg(target_arch = "x86_64")]
99 RuntimeArch::V4(a) => Box::new(prepared::$v4::<$b>::new(a, query)?),
100 #[cfg(target_arch = "aarch64")]
101 RuntimeArch::Neon(a) => Box::new(prepared::NeonBytes::<$b>::new(a, query)?),
102 RuntimeArch::Reference => Box::new(ReferenceKernel::<$a, $b>::new(query)?),
103 };
104 Ok(MinMaxMaxSim { inner })
105 }
106 }
107 };
108}
109factory!(1, 1, ScalarBytes, V3Bytes, V4Bytes);
110factory!(2, 2, ScalarBytes, V3Bytes, V4Bytes);
111factory!(3, 3, ScalarBytes, V3Bytes, V4Bytes);
112factory!(4, 4, ScalarBytes, V3Bytes, V4Bytes);
113factory!(5, 5, ScalarBytes, V3Bytes, V4Bytes);
114factory!(6, 6, ScalarBytes, V3Bytes, V4Bytes);
115factory!(7, 7, ScalarBytes, V3Words, V4Bytes);
116factory!(8, 8, ScalarWords, V3Words, V4Words);
117factory!(8, 1, ScalarBytes, V3Bytes, V4Bytes);
118factory!(8, 2, ScalarBytes, V3Bytes, V4Bytes);
119factory!(8, 4, ScalarBytes, V3Bytes, V4Bytes);
120
121fn validate_query<const A_BITS: usize, const B_BITS: usize>(
122 query: MatRef<'_, MinMaxMeta<A_BITS>>,
123) -> Result<(), BuildMaxSimError>
124where
125 Unsigned: Representation<A_BITS> + Representation<B_BITS>,
126{
127 let dim = query.repr().intrinsic_dim();
128 let limit = max_k(A_BITS, B_BITS);
129 if dim > limit {
130 return Err(BuildMaxSimError::DimTooLarge(dim, limit));
131 }
132 query.as_canonical_view()?;
133 Ok(())
134}
135
136#[derive(Debug, Clone, Copy)]
137enum RuntimeArch {
138 Scalar(Scalar),
139 #[cfg(target_arch = "x86_64")]
140 V3(V3),
141 #[cfg(target_arch = "x86_64")]
142 V4(V4),
143 #[cfg(target_arch = "aarch64")]
144 Neon(Neon),
145 Reference,
146}
147
148impl RuntimeArch {
149 fn resolve(isa: MaxSimIsa) -> Result<Self, NotSupported> {
150 let unavailable = || NotSupported {
151 isa,
152 reason: "requested ISA unavailable on this host",
153 };
154 Ok(match isa {
155 MaxSimIsa::Auto => {
156 #[cfg(target_arch = "x86_64")]
157 {
158 if let Some(arch) = V4::new_checked() {
159 return Ok(Self::V4(arch));
160 }
161 if let Some(arch) = V3::new_checked() {
162 return Ok(Self::V3(arch));
163 }
164 }
165 #[cfg(target_arch = "aarch64")]
166 if let Some(arch) = Neon::new_checked() {
167 return Ok(Self::Neon(arch));
168 }
169 Self::Scalar(Scalar::new())
170 }
171 MaxSimIsa::Scalar => Self::Scalar(Scalar::new()),
172 MaxSimIsa::Reference => Self::Reference,
173 #[cfg(target_arch = "x86_64")]
174 MaxSimIsa::X86_64_V3 => Self::V3(V3::new_checked().ok_or_else(unavailable)?),
175 #[cfg(target_arch = "x86_64")]
176 MaxSimIsa::X86_64_V4 => Self::V4(V4::new_checked().ok_or_else(unavailable)?),
177 #[cfg(not(target_arch = "x86_64"))]
178 MaxSimIsa::X86_64_V3 | MaxSimIsa::X86_64_V4 => return Err(unavailable()),
179 #[cfg(target_arch = "aarch64")]
180 MaxSimIsa::Neon => Self::Neon(Neon::new_checked().ok_or_else(unavailable)?),
181 #[cfg(not(target_arch = "aarch64"))]
182 MaxSimIsa::Neon => return Err(unavailable()),
183 })
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 use crate::{
191 minmax::MinMaxCompensation,
192 multi_vector::{Defaulted, Mat, MaxSimError},
193 };
194
195 fn matrix<const BITS: usize>(rows: usize, dim: usize) -> Mat<MinMaxMeta<BITS>>
196 where
197 Unsigned: Representation<BITS>,
198 {
199 let mut mat = Mat::new(MinMaxMeta::new(rows, dim), Defaulted).unwrap();
200 for (i, mut row) in mat.rows_mut().enumerate() {
201 let mut sum = 0;
202 for d in 0..dim {
203 let code = ((i * 17 + d * 29 + 7) % (1 << BITS)) as i64;
204 row.vector_mut().set(d, code).unwrap();
205 sum += code;
206 }
207 let a = (i + 1) as f32 / 8.0;
208 row.set_meta(MinMaxCompensation {
209 dim: dim as u32,
210 a,
211 b: i as f32 / 4.0 - 1.0,
212 n: a * sum as f32,
213 norm_squared: 0.0,
214 });
215 }
216 mat
217 }
218
219 fn constant_matrix<const BITS: usize>(
220 rows: usize,
221 dim: usize,
222 code: i64,
223 ) -> Mat<MinMaxMeta<BITS>>
224 where
225 Unsigned: Representation<BITS>,
226 {
227 let mut mat = Mat::new(MinMaxMeta::new(rows, dim), Defaulted).unwrap();
228 for mut row in mat.rows_mut() {
229 for d in 0..dim {
230 row.vector_mut().set(d, code).unwrap();
231 }
232 row.set_meta(MinMaxCompensation {
233 dim: dim as u32,
234 a: 1.0,
235 n: dim as f32 * code as f32,
236 ..Default::default()
237 });
238 }
239 mat
240 }
241
242 fn available_isas() -> impl Iterator<Item = MaxSimIsa> {
243 [
244 MaxSimIsa::Scalar,
245 MaxSimIsa::Auto,
246 MaxSimIsa::X86_64_V3,
247 MaxSimIsa::X86_64_V4,
248 MaxSimIsa::Neon,
249 MaxSimIsa::Reference,
250 ]
251 .into_iter()
252 .filter(|isa| isa.is_available())
253 }
254
255 fn assert_matches_reference<const A_BITS: usize, const B_BITS: usize>()
256 where
257 Unsigned: MinMaxKernelFactory<A_BITS, B_BITS>,
258 {
259 let (rows, docs, dim) = (17, 9, 65);
261 let query = matrix::<A_BITS>(rows, dim);
262 let document = matrix::<B_BITS>(docs, dim);
263 let reference =
264 build_minmax_max_sim::<A_BITS, B_BITS>(MaxSimIsa::Reference, query.as_view().into())
265 .unwrap();
266 let mut expected = vec![0.0; rows];
267 reference
268 .compute_max_sim(document.as_view(), &mut expected)
269 .unwrap();
270 for isa in available_isas().filter(|&isa| isa != MaxSimIsa::Reference) {
271 let kernel =
272 build_minmax_max_sim::<A_BITS, B_BITS>(isa, query.as_view().into()).unwrap();
273 assert_eq!(kernel.nrows(), rows);
274 let mut guarded = vec![123.0; rows + 2];
275 kernel
276 .compute_max_sim(document.as_view(), &mut guarded[1..rows + 1])
277 .unwrap();
278 for (actual, expected) in guarded[1..rows + 1].iter().zip(&expected) {
279 assert!(
280 actual == expected
281 || (actual.is_finite()
282 && expected.is_finite()
283 && (actual - expected).abs() <= 1e-5 * expected.abs().max(1.0)),
284 "{A_BITS}x{B_BITS}, {isa}: {actual} != {expected}"
285 );
286 }
287 assert_eq!((guarded[0], guarded[rows + 1]), (123.0, 123.0));
288 }
289 }
290
291 #[test]
292 fn matches_reference_across_supported_bit_widths() {
293 assert_matches_reference::<1, 1>();
294 assert_matches_reference::<2, 2>();
295 assert_matches_reference::<3, 3>();
296 assert_matches_reference::<4, 4>();
297 assert_matches_reference::<5, 5>();
298 assert_matches_reference::<6, 6>();
299 assert_matches_reference::<7, 7>();
300 assert_matches_reference::<8, 8>();
301 assert_matches_reference::<8, 1>();
302 assert_matches_reference::<8, 2>();
303 assert_matches_reference::<8, 4>();
304 }
305
306 #[test]
307 fn handles_empty_inputs() {
308 fn check<const B_BITS: usize>()
309 where
310 Unsigned: MinMaxKernelFactory<8, B_BITS>,
311 {
312 for (rows, docs, dim, expected) in [
313 (0, 5, 7, 0.0),
314 (3, 0, 7, f32::MAX),
315 (3, 5, 0, 0.0),
316 (3, 0, 0, f32::MAX),
317 ] {
318 let query = matrix::<8>(rows, dim);
319 let document = matrix::<B_BITS>(docs, dim);
320 for isa in available_isas() {
321 let kernel =
322 build_minmax_max_sim::<8, B_BITS>(isa, query.as_view().into()).unwrap();
323 let mut scores = vec![f32::NAN; rows];
324 kernel
325 .compute_max_sim(document.as_view(), &mut scores)
326 .unwrap();
327 assert_eq!(
328 scores,
329 vec![expected; rows],
330 "{isa}, B_BITS={B_BITS}, {rows}x{docs}x{dim}"
331 );
332 }
333 }
334 }
335 check::<4>();
336 check::<8>();
337 }
338
339 #[test]
340 fn preserves_scores_on_error() {
341 let query = matrix::<8>(2, 5);
342 for isa in available_isas() {
343 let kernel = build_minmax_max_sim::<8, 8>(isa, query.as_view().into()).unwrap();
344 let doc = matrix::<8>(1, 6);
345 let mut scores = [123.0; 2];
346 assert!(matches!(
347 kernel.compute_max_sim(doc.as_view(), &mut scores),
348 Err(MaxSimError::UnequalDim(6, 5))
349 ));
350 assert_eq!(scores, [123.0; 2]);
351 let doc = matrix::<8>(1, 5);
352 let mut scores = [123.0];
353 assert!(matches!(
354 kernel.compute_max_sim(doc.as_view(), &mut scores),
355 Err(MaxSimError::InvalidBufferLength(1, 2))
356 ));
357 assert_eq!(scores, [123.0]);
358 }
359 }
360
361 #[test]
362 fn rejects_unavailable_isas() {
363 let query = matrix::<8>(2, 5);
364 for isa in [MaxSimIsa::X86_64_V3, MaxSimIsa::X86_64_V4, MaxSimIsa::Neon] {
365 if !isa.is_available() {
366 assert!(matches!(
367 build_minmax_max_sim::<8, 8>(isa, query.as_view().into()),
368 Err(BuildMaxSimError::NotSupported(error)) if error.isa == isa
369 ));
370 }
371 }
372 }
373
374 #[test]
375 fn rejects_excessive_dimensions() {
376 let limit = max_k(8, 8);
377 let dim = limit + 1;
378 let query = MatRef::new(MinMaxMeta::<8>::new(0, dim), &[] as &[u8]).unwrap();
379 for isa in [MaxSimIsa::Auto, MaxSimIsa::Reference] {
380 let result = build_minmax_max_sim::<8, 8>(isa, query.into());
381 assert!(
382 matches!(result, Err(BuildMaxSimError::DimTooLarge(found, max))
383 if found == dim && max == limit),
384 "{isa}"
385 );
386 }
387 }
388
389 #[test]
390 fn owns_query_data() {
391 let document = constant_matrix::<4>(2, 3, 2);
392 for isa in available_isas() {
393 let kernel = {
394 let query = constant_matrix::<8>(2, 3, 1);
395 build_minmax_max_sim::<8, 4>(isa, query.as_view().into()).unwrap()
396 };
397 let mut scores = [f32::NAN; 2];
398 kernel
399 .compute_max_sim(document.as_view(), &mut scores)
400 .unwrap();
401 assert_eq!(scores, [-6.0; 2], "{isa}");
402 }
403 }
404
405 #[test]
406 fn selects_backend_and_layout() {
407 fn assert_layout<const A_BITS: usize, const B_BITS: usize, K: 'static>(isa: MaxSimIsa)
408 where
409 Unsigned: MinMaxKernelFactory<A_BITS, B_BITS>,
410 {
411 let query = matrix::<A_BITS>(1, 3);
412 assert_eq!(
413 build_minmax_max_sim::<A_BITS, B_BITS>(isa, query.as_view().into())
414 .unwrap()
415 .inner
416 .inner_type_id(),
417 std::any::TypeId::of::<K>(),
418 "{isa}, A_BITS={A_BITS}, B_BITS={B_BITS}",
419 );
420 }
421
422 assert_layout::<7, 7, prepared::ScalarBytes<7>>(MaxSimIsa::Scalar);
423 assert_layout::<8, 8, prepared::ScalarWords<8>>(MaxSimIsa::Scalar);
424 #[cfg(target_arch = "x86_64")]
425 {
426 if V3::new_checked().is_some() {
427 assert_layout::<6, 6, prepared::V3Bytes<6>>(MaxSimIsa::X86_64_V3);
428 assert_layout::<7, 7, prepared::V3Words<7>>(MaxSimIsa::X86_64_V3);
429 }
430 if V4::new_checked().is_some() {
431 assert_layout::<7, 7, prepared::V4Bytes<7>>(MaxSimIsa::X86_64_V4);
432 assert_layout::<8, 8, prepared::V4Words<8>>(MaxSimIsa::X86_64_V4);
433 }
434 }
435 #[cfg(target_arch = "aarch64")]
436 if Neon::new_checked().is_some() {
437 assert_layout::<8, 8, prepared::NeonBytes<8>>(MaxSimIsa::Neon);
438 }
439 }
440
441 #[test]
442 fn prepared_type_is_independent_of_query_bit_width() {
443 fn check<const B_BITS: usize>()
444 where
445 Unsigned: MinMaxKernelFactory<B_BITS, B_BITS> + MinMaxKernelFactory<8, B_BITS>,
446 {
447 let narrow = matrix::<B_BITS>(1, 3);
448 let wide = matrix::<8>(1, 3);
449 for isa in available_isas().filter(|&isa| isa != MaxSimIsa::Reference) {
450 let narrow =
451 build_minmax_max_sim::<B_BITS, B_BITS>(isa, narrow.as_view().into()).unwrap();
452 let wide = build_minmax_max_sim::<8, B_BITS>(isa, wide.as_view().into()).unwrap();
453 assert_eq!(
454 narrow.inner.inner_type_id(),
455 wide.inner.inner_type_id(),
456 "{isa}, B_BITS={B_BITS}",
457 );
458 }
459 }
460 check::<1>();
461 check::<2>();
462 check::<4>();
463 }
464
465 #[test]
466 fn handles_nonfinite_scores() {
467 fn check<const A_BITS: usize, const B_BITS: usize>()
468 where
469 Unsigned: MinMaxKernelFactory<A_BITS, B_BITS>,
470 {
471 let max_code = (1i64 << B_BITS) - 1;
472 let max_query = (1i64 << A_BITS) - 1;
473 let query = constant_matrix::<A_BITS>(1, 5, max_query);
474 let mut document = constant_matrix::<B_BITS>(2, 5, max_code);
475 let finite_score = -(5 * max_query * max_code) as f32;
476 for isa in available_isas() {
477 let kernel =
478 build_minmax_max_sim::<A_BITS, B_BITS>(isa, query.as_view().into()).unwrap();
479 let mut scores = [0.0];
480 for (scale, expected) in [
481 (1.0, finite_score),
482 (f32::NEG_INFINITY, f32::MAX),
483 (f32::INFINITY, f32::NEG_INFINITY),
484 (f32::NAN, f32::MAX),
485 (1.0, finite_score),
486 ] {
487 for mut row in document.rows_mut() {
488 let mut meta = row.meta();
489 meta.a = scale;
490 row.set_meta(meta);
491 }
492 kernel
493 .compute_max_sim(document.as_view(), &mut scores)
494 .unwrap();
495 assert_eq!(scores, [expected], "{isa}, B_BITS={B_BITS}");
496 }
497 let mut row = document.get_row_mut(0).unwrap();
498 let mut meta = row.meta();
499 meta.a = f32::NAN;
500 row.set_meta(meta);
501 kernel
502 .compute_max_sim(document.as_view(), &mut scores)
503 .unwrap();
504 assert_eq!(
505 scores,
506 [finite_score],
507 "{isa}, B_BITS={B_BITS}, mixed NaN/finite"
508 );
509 }
510 }
511 check::<6, 6>();
512 check::<7, 7>();
513 check::<8, 8>();
514 check::<8, 4>();
515 }
516
517 #[cfg(not(debug_assertions))]
518 #[test]
519 fn rejects_query_size_overflow() {
520 let rows = 1usize << (usize::BITS - 2);
523 let query = MatRef::new(MinMaxMeta::<8>::new(rows, 0), &[] as &[u8]).unwrap();
524 for isa in [MaxSimIsa::Auto, MaxSimIsa::Reference] {
525 assert!(matches!(
526 build_minmax_max_sim::<8, 4>(isa, query.into()),
527 Err(BuildMaxSimError::Overflow(_))
528 ));
529 }
530 }
531}