1use std::{io::Write, num::NonZeroUsize};
9
10use diskann_utils::views::rowmajor::{self, Matrix};
11use diskann_vector::distance::simd;
12use diskann_wide::Architecture;
13use half::f16;
14use rand::{
15 distr::{Distribution, StandardUniform},
16 rngs::StdRng,
17 SeedableRng,
18};
19use serde::{Deserialize, Serialize};
20use thiserror::Error;
21
22use diskann_benchmark_runner::{
23 benchmark::{MatchContext, PassFail, Regression, Score},
24 utils::{
25 datatype::{AsDataType, DataType},
26 num::{relative_change, NonNegativeFinite},
27 percentiles, MicroSeconds,
28 },
29 Benchmark, Checker, Input, Registry,
30};
31
32pub fn register(registry: &mut Registry) -> anyhow::Result<()> {
37 Ok(register_benchmarks_impl(registry)?)
38}
39
40#[derive(Debug, Clone, Copy)]
45struct DisplayWrapper<'a, T: ?Sized>(&'a T);
46
47impl<T: ?Sized> std::ops::Deref for DisplayWrapper<'_, T> {
48 type Target = T;
49 fn deref(&self) -> &T {
50 self.0
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum SimilarityMeasure {
61 SquaredL2,
62 InnerProduct,
63 Cosine,
64}
65
66impl std::fmt::Display for SimilarityMeasure {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 let st = match self {
69 Self::SquaredL2 => "squared_l2",
70 Self::InnerProduct => "inner_product",
71 Self::Cosine => "cosine",
72 };
73 write!(f, "{}", st)
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
78#[serde(rename_all = "kebab-case")]
79enum Arch {
80 #[serde(rename = "x86-64-v4")]
81 #[expect(non_camel_case_types)]
82 X86_64_V4,
83 #[serde(rename = "x86-64-v3")]
84 #[expect(non_camel_case_types)]
85 X86_64_V3,
86 Neon,
87 Scalar,
88 Reference,
89}
90
91impl std::fmt::Display for Arch {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 let st = match self {
94 Self::X86_64_V4 => "x86-64-v4",
95 Self::X86_64_V3 => "x86-64-v3",
96 Self::Neon => "neon",
97 Self::Scalar => "scalar",
98 Self::Reference => "reference",
99 };
100 write!(f, "{}", st)
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
105struct Run {
106 distance: SimilarityMeasure,
107 dim: NonZeroUsize,
108 num_points: NonZeroUsize,
109 loops_per_measurement: NonZeroUsize,
110 num_measurements: NonZeroUsize,
111}
112
113#[derive(Debug, Serialize, Deserialize)]
114pub struct SimdOp {
115 query_type: DataType,
116 data_type: DataType,
117 arch: Arch,
118 runs: Vec<Run>,
119}
120
121macro_rules! write_field {
122 ($f:ident, $field:tt, $($expr:tt)*) => {
123 writeln!($f, "{:>18}: {}", $field, $($expr)*)
124 }
125}
126
127impl SimdOp {
128 fn summarize_fields(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 write_field!(f, "query type", self.query_type)?;
130 write_field!(f, "data type", self.data_type)?;
131 write_field!(f, "arch", self.arch)?;
132 write_field!(f, "number of runs", self.runs.len())?;
133 Ok(())
134 }
135}
136
137impl std::fmt::Display for SimdOp {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 writeln!(f, "SIMD Operation\n")?;
140 write_field!(f, "tag", Self::tag())?;
141 self.summarize_fields(f)
142 }
143}
144
145impl Input for SimdOp {
146 type Raw = Self;
147
148 fn tag() -> &'static str {
149 "simd-op"
150 }
151
152 fn from_raw(raw: Self::Raw, _checker: &mut Checker) -> anyhow::Result<Self> {
153 Ok(raw)
154 }
155
156 fn serialize(&self) -> anyhow::Result<serde_json::Value> {
157 Ok(serde_json::to_value(self)?)
158 }
159
160 fn example() -> Self::Raw {
161 const DIM: [NonZeroUsize; 2] = [
162 NonZeroUsize::new(128).unwrap(),
163 NonZeroUsize::new(150).unwrap(),
164 ];
165
166 const NUM_POINTS: [NonZeroUsize; 2] = [
167 NonZeroUsize::new(1000).unwrap(),
168 NonZeroUsize::new(800).unwrap(),
169 ];
170
171 const LOOPS_PER_MEASUREMENT: NonZeroUsize = NonZeroUsize::new(100).unwrap();
172 const NUM_MEASUREMENTS: NonZeroUsize = NonZeroUsize::new(100).unwrap();
173
174 let runs = vec![
175 Run {
176 distance: SimilarityMeasure::SquaredL2,
177 dim: DIM[0],
178 num_points: NUM_POINTS[0],
179 loops_per_measurement: LOOPS_PER_MEASUREMENT,
180 num_measurements: NUM_MEASUREMENTS,
181 },
182 Run {
183 distance: SimilarityMeasure::InnerProduct,
184 dim: DIM[1],
185 num_points: NUM_POINTS[1],
186 loops_per_measurement: LOOPS_PER_MEASUREMENT,
187 num_measurements: NUM_MEASUREMENTS,
188 },
189 ];
190
191 Self {
192 query_type: DataType::Float32,
193 data_type: DataType::Float32,
194 arch: Arch::X86_64_V3,
195 runs,
196 }
197 }
198}
199
200#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
209struct SimdTolerance {
210 min_time_regression: NonNegativeFinite,
211}
212
213impl Input for SimdTolerance {
214 type Raw = Self;
215
216 fn tag() -> &'static str {
217 "simd-tolerance"
218 }
219
220 fn from_raw(raw: Self::Raw, _checker: &mut Checker) -> anyhow::Result<Self> {
221 Ok(raw)
222 }
223
224 fn serialize(&self) -> anyhow::Result<serde_json::Value> {
225 Ok(serde_json::to_value(self)?)
226 }
227
228 fn example() -> Self {
229 const EXAMPLE: NonNegativeFinite = match NonNegativeFinite::new(0.10) {
230 Ok(v) => v,
231 Err(_) => panic!("use a non-negative finite please"),
232 };
233
234 SimdTolerance {
235 min_time_regression: EXAMPLE,
236 }
237 }
238}
239
240#[derive(Debug, Serialize)]
242struct Comparison {
243 run: Run,
244 tolerance: SimdTolerance,
245 before_min: f64,
246 after_min: f64,
247}
248
249#[derive(Debug, Serialize)]
251struct CheckResult {
252 checks: Vec<Comparison>,
253}
254
255impl std::fmt::Display for CheckResult {
256 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257 let header = [
258 "Distance",
259 "Dim",
260 "Min Before (ns)",
261 "Min After (ns)",
262 "Change (%)",
263 "Remark",
264 ];
265
266 let mut table = diskann_benchmark_runner::utils::fmt::Table::new(header, self.checks.len());
267
268 for (i, c) in self.checks.iter().enumerate() {
269 let mut row = table.row(i);
270 let change = relative_change(c.before_min, c.after_min);
271
272 row.insert(c.run.distance, 0);
273 row.insert(c.run.dim, 1);
274 row.insert(format!("{:.3}", c.before_min), 2);
275 row.insert(format!("{:.3}", c.after_min), 3);
276 match change {
277 Ok(change) => {
278 row.insert(format!("{:.3} %", change * 100.0), 4);
279 if change > c.tolerance.min_time_regression.get() {
280 row.insert("FAIL", 5);
281 }
282 }
283 Err(err) => {
284 row.insert("invalid", 4);
285 row.insert(err, 5);
286 }
287 }
288 }
289
290 table.fmt(f)
291 }
292}
293
294fn register_benchmarks_impl(
299 registry: &mut diskann_benchmark_runner::Registry,
300) -> Result<(), diskann_benchmark_runner::RegistryError> {
301 #[cfg(target_arch = "x86_64")]
303 {
304 registry.register_regression(
305 "simd-op-f32xf32-x86_64_V4",
306 Kernel::<diskann_wide::arch::x86_64::V4, f32, f32>::new(),
307 )?;
308 registry.register_regression(
309 "simd-op-f16xf16-x86_64_V4",
310 Kernel::<diskann_wide::arch::x86_64::V4, f16, f16>::new(),
311 )?;
312 registry.register_regression(
313 "simd-op-u8xu8-x86_64_V4",
314 Kernel::<diskann_wide::arch::x86_64::V4, u8, u8>::new(),
315 )?;
316 registry.register_regression(
317 "simd-op-i8xi8-x86_64_V4",
318 Kernel::<diskann_wide::arch::x86_64::V4, i8, i8>::new(),
319 )?;
320 }
321
322 #[cfg(target_arch = "x86_64")]
324 {
325 registry.register_regression(
326 "simd-op-f32xf32-x86_64_V3",
327 Kernel::<diskann_wide::arch::x86_64::V3, f32, f32>::new(),
328 )?;
329 registry.register_regression(
330 "simd-op-f16xf16-x86_64_V3",
331 Kernel::<diskann_wide::arch::x86_64::V3, f16, f16>::new(),
332 )?;
333 registry.register_regression(
334 "simd-op-u8xu8-x86_64_V3",
335 Kernel::<diskann_wide::arch::x86_64::V3, u8, u8>::new(),
336 )?;
337 registry.register_regression(
338 "simd-op-i8xi8-x86_64_V3",
339 Kernel::<diskann_wide::arch::x86_64::V3, i8, i8>::new(),
340 )?;
341 }
342
343 #[cfg(target_arch = "aarch64")]
345 {
346 registry.register_regression(
347 "simd-op-f32xf32-aarch64_neon",
348 Kernel::<diskann_wide::arch::aarch64::Neon, f32, f32>::new(),
349 )?;
350 registry.register_regression(
351 "simd-op-f16xf16-aarch64_neon",
352 Kernel::<diskann_wide::arch::aarch64::Neon, f16, f16>::new(),
353 )?;
354 registry.register_regression(
355 "simd-op-u8xu8-aarch64_neon",
356 Kernel::<diskann_wide::arch::aarch64::Neon, u8, u8>::new(),
357 )?;
358 registry.register_regression(
359 "simd-op-i8xi8-aarch64_neon",
360 Kernel::<diskann_wide::arch::aarch64::Neon, i8, i8>::new(),
361 )?;
362 }
363
364 registry.register_regression(
366 "simd-op-f32xf32-scalar",
367 Kernel::<diskann_wide::arch::Scalar, f32, f32>::new(),
368 )?;
369 registry.register_regression(
370 "simd-op-f16xf16-scalar",
371 Kernel::<diskann_wide::arch::Scalar, f16, f16>::new(),
372 )?;
373 registry.register_regression(
374 "simd-op-u8xu8-scalar",
375 Kernel::<diskann_wide::arch::Scalar, u8, u8>::new(),
376 )?;
377 registry.register_regression(
378 "simd-op-i8xi8-scalar",
379 Kernel::<diskann_wide::arch::Scalar, i8, i8>::new(),
380 )?;
381
382 registry.register_regression(
384 "simd-op-f32xf32-reference",
385 Kernel::<Reference, f32, f32>::new(),
386 )?;
387 registry.register_regression(
388 "simd-op-f16xf16-reference",
389 Kernel::<Reference, f16, f16>::new(),
390 )?;
391 registry.register_regression(
392 "simd-op-u8xu8-reference",
393 Kernel::<Reference, u8, u8>::new(),
394 )?;
395 registry.register_regression(
396 "simd-op-i8xi8-reference",
397 Kernel::<Reference, i8, i8>::new(),
398 )?;
399 Ok(())
400}
401
402struct Reference;
408
409struct Kernel<A, Q, D> {
410 _type: std::marker::PhantomData<(A, Q, D)>,
411}
412
413impl<A, Q, D> Kernel<A, Q, D> {
414 fn new() -> Self {
415 Self {
416 _type: std::marker::PhantomData,
417 }
418 }
419}
420
421#[derive(Debug, Error)]
422#[error("architecture {0} is not supported by this CPU")]
423pub(crate) struct ArchNotSupported(Arch);
424
425trait AsArch: Sized + 'static {
427 const ARCH: Arch;
428 const DISPLAY_NAME: &'static str;
429
430 fn is_available() -> bool {
431 true
432 }
433
434 fn try_new() -> Result<Self, ArchNotSupported>;
435
436 fn describe(arch: Arch) -> ArchDescribe {
437 if arch != Self::ARCH {
438 ArchDescribe::Mismatch {
439 expected: Self::ARCH,
440 got: arch,
441 }
442 } else if !Self::is_available() {
443 ArchDescribe::Unsupported(Self::ARCH)
444 } else {
445 ArchDescribe::Match(arch)
446 }
447 }
448}
449
450#[derive(Debug, Clone, Copy)]
451enum ArchDescribe {
452 Match(Arch),
453 Unsupported(Arch),
454 Mismatch { expected: Arch, got: Arch },
455}
456
457impl ArchDescribe {
458 fn is_match(&self) -> bool {
459 matches!(self, ArchDescribe::Match(_))
460 }
461}
462
463impl std::fmt::Display for ArchDescribe {
464 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
465 match self {
466 Self::Match(arch) => write!(f, "matched {}", arch),
467 Self::Unsupported(arch) => {
468 write!(f, "matched {} but unsupported by this CPU", arch)
469 }
470 Self::Mismatch { expected, got } => write!(f, "expected {}, got {}", expected, got),
471 }
472 }
473}
474
475impl AsArch for Reference {
476 const ARCH: Arch = Arch::Reference;
477 const DISPLAY_NAME: &'static str = "loop based";
478
479 fn try_new() -> Result<Self, ArchNotSupported> {
480 Ok(Reference)
481 }
482}
483
484impl AsArch for diskann_wide::arch::Scalar {
485 const ARCH: Arch = Arch::Scalar;
486 const DISPLAY_NAME: &'static str = "scalar (compilation target CPU)";
487
488 fn try_new() -> Result<Self, ArchNotSupported> {
489 Ok(diskann_wide::arch::Scalar)
490 }
491}
492
493macro_rules! match_arch {
494 ($target_arch:literal, $arch:path, $enum:ident) => {
495 #[cfg(target_arch = $target_arch)]
496 impl AsArch for $arch {
497 const ARCH: Arch = Arch::$enum;
498 const DISPLAY_NAME: &'static str = stringify!($enum);
499
500 fn is_available() -> bool {
501 <$arch>::new_checked().is_some()
502 }
503
504 fn try_new() -> Result<Self, ArchNotSupported> {
505 <$arch>::new_checked().ok_or(ArchNotSupported(Arch::$enum))
506 }
507 }
508 };
509}
510
511match_arch!("x86_64", diskann_wide::arch::x86_64::V4, X86_64_V4);
512match_arch!("x86_64", diskann_wide::arch::x86_64::V3, X86_64_V3);
513match_arch!("aarch64", diskann_wide::arch::aarch64::Neon, Neon);
514
515impl<A, Q, D> Benchmark for Kernel<A, Q, D>
516where
517 Q: AsDataType,
518 D: AsDataType,
519 A: AsArch,
520 Kernel<A, Q, D>: RunBenchmark<A>,
521{
522 type Input = SimdOp;
523 type Output = Vec<RunResult>;
524
525 fn try_match(&self, from: &SimdOp, context: &MatchContext) -> Score {
527 let mut score = context.success(0);
528
529 let desc = Q::describe(from.query_type);
530 if !desc.is_match() {
531 score.fail(10, &format_args!("Mismatched query type: {}", desc));
532 }
533
534 let desc = D::describe(from.data_type);
535 if !desc.is_match() {
536 score.fail(10, &format_args!("Mismatched data type: {}", desc));
537 }
538
539 let desc = A::describe(from.arch);
540 if !desc.is_match() {
541 let penalty = if from.arch == A::ARCH { 2 } else { 3 };
542 score.fail(penalty, &format_args!("Mismatched architecture: {}", desc));
543 }
544
545 score
546 }
547
548 fn run(
549 &self,
550 input: &SimdOp,
551 _: diskann_benchmark_runner::Checkpoint<'_>,
552 mut output: &mut dyn diskann_benchmark_runner::Output,
553 ) -> anyhow::Result<Self::Output> {
554 if input.arch != A::ARCH {
555 anyhow::bail!(
556 "architecture mismatch: input requested {:?}, but kernel implementation requires {:?}",
557 input.arch,
558 A::ARCH
559 );
560 }
561
562 let arch = A::try_new()?;
563 writeln!(output, "{}", input)?;
564 let results = self.run_benchmark(input, arch)?;
565 writeln!(output, "\n\n{}", DisplayWrapper(&*results))?;
566 Ok(results)
567 }
568
569 fn description(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
570 writeln!(f, "- Query Type: {}", Q::DATA_TYPE)?;
571 writeln!(f, "- Data Type: {}", D::DATA_TYPE)?;
572 writeln!(f, "- Implementation: {}", A::DISPLAY_NAME)?;
573 Ok(())
574 }
575}
576
577impl<A, Q, D> Regression for Kernel<A, Q, D>
578where
579 Q: AsDataType,
580 D: AsDataType,
581 A: AsArch,
582 Kernel<A, Q, D>: RunBenchmark<A>,
583{
584 type Tolerances = SimdTolerance;
585 type Pass = CheckResult;
586 type Fail = CheckResult;
587
588 fn check(
589 &self,
590 tolerance: &SimdTolerance,
591 _input: &SimdOp,
592 before: &Vec<RunResult>,
593 after: &Vec<RunResult>,
594 ) -> anyhow::Result<PassFail<CheckResult, CheckResult>> {
595 anyhow::ensure!(
596 before.len() == after.len(),
597 "before has {} runs but after has {}",
598 before.len(),
599 after.len(),
600 );
601
602 let mut passed = true;
603 let checks: Vec<Comparison> = std::iter::zip(before.iter(), after.iter())
604 .enumerate()
605 .map(|(i, (b, a))| {
606 anyhow::ensure!(b.run == a.run, "run {i} mismatched");
607
608 let computations_per_latency = b.computations_per_latency() as f64;
609
610 let before_min = b.percentiles.minimum.as_f64() * 1000.0 / computations_per_latency;
611 let after_min = a.percentiles.minimum.as_f64() * 1000.0 / computations_per_latency;
612
613 let comparison = Comparison {
614 run: b.run.clone(),
615 tolerance: *tolerance,
616 before_min,
617 after_min,
618 };
619
620 match relative_change(before_min, after_min) {
622 Ok(change) => {
623 if change > tolerance.min_time_regression.get() {
624 passed = false;
625 }
626 }
627 Err(_) => passed = false,
628 };
629
630 Ok(comparison)
631 })
632 .collect::<anyhow::Result<Vec<Comparison>>>()?;
633
634 let check = CheckResult { checks };
635
636 if passed {
637 Ok(PassFail::Pass(check))
638 } else {
639 Ok(PassFail::Fail(check))
640 }
641 }
642}
643
644trait RunBenchmark<A> {
649 fn run_benchmark(&self, input: &SimdOp, arch: A) -> Result<Vec<RunResult>, anyhow::Error>;
650}
651
652#[derive(Debug, Serialize, Deserialize)]
653struct RunResult {
654 run: Run,
656 latencies: Vec<MicroSeconds>,
658 percentiles: percentiles::Percentiles<MicroSeconds>,
660}
661
662impl RunResult {
663 fn computations_per_latency(&self) -> usize {
664 self.run.num_points.get() * self.run.loops_per_measurement.get()
665 }
666}
667
668impl std::fmt::Display for DisplayWrapper<'_, [RunResult]> {
669 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
670 if self.is_empty() {
671 return Ok(());
672 }
673
674 let header = [
675 "Distance",
676 "Dim",
677 "Min Time (ns)",
678 "Mean Time (ns)",
679 "Points",
680 "Loops",
681 "Measurements",
682 ];
683
684 let mut table = diskann_benchmark_runner::utils::fmt::Table::new(header, self.len());
685
686 self.iter().enumerate().for_each(|(row, r)| {
687 let mut row = table.row(row);
688
689 let min_latency = r
690 .latencies
691 .iter()
692 .min()
693 .copied()
694 .unwrap_or(MicroSeconds::new(u64::MAX));
695 let mean_latency = r.percentiles.mean;
696
697 let computations_per_latency = r.computations_per_latency() as f64;
698
699 let min_time = min_latency.as_f64() / computations_per_latency * 1000.0;
701 let mean_time = mean_latency / computations_per_latency * 1000.0;
702
703 row.insert(r.run.distance, 0);
704 row.insert(r.run.dim, 1);
705 row.insert(format!("{:.3}", min_time), 2);
706 row.insert(format!("{:.3}", mean_time), 3);
707 row.insert(r.run.num_points, 4);
708 row.insert(r.run.loops_per_measurement, 5);
709 row.insert(r.run.num_measurements, 6);
710 });
711
712 table.fmt(f)
713 }
714}
715
716fn run_loops<Q, D, F>(query: &[Q], data: rowmajor::Ref<D>, run: &Run, f: F) -> RunResult
717where
718 F: Fn(&[Q], &[D]) -> f32,
719{
720 let mut latencies = Vec::with_capacity(run.num_measurements.get());
721 let mut dst = vec![0.0; data.nrows()];
722
723 for _ in 0..run.num_measurements.get() {
724 let start = std::time::Instant::now();
725 for _ in 0..run.loops_per_measurement.get() {
726 std::iter::zip(dst.iter_mut(), data.rows()).for_each(|(d, r)| {
727 *d = f(query, r);
728 });
729 std::hint::black_box(&mut dst);
730 }
731 latencies.push(start.elapsed().into());
732 }
733
734 let percentiles = percentiles::compute_percentiles(&mut latencies).unwrap();
735 RunResult {
736 run: run.clone(),
737 latencies,
738 percentiles,
739 }
740}
741
742struct Data<Q, D> {
743 query: Box<[Q]>,
744 data: rowmajor::Owned<D>,
745}
746
747impl<Q, D> Data<Q, D> {
748 fn new(run: &Run) -> Self
749 where
750 StandardUniform: Distribution<Q>,
751 StandardUniform: Distribution<D>,
752 {
753 let mut rng = StdRng::seed_from_u64(0x12345);
754 let query: Box<[Q]> = (0..run.dim.get())
755 .map(|_| StandardUniform.sample(&mut rng))
756 .collect();
757 let data = rowmajor::Owned::<D>::from_fn(run.num_points.get(), run.dim.get(), |_| {
758 StandardUniform.sample(&mut rng)
759 });
760
761 Self { query, data }
762 }
763
764 fn run<F>(&self, run: &Run, f: F) -> RunResult
765 where
766 F: Fn(&[Q], &[D]) -> f32,
767 {
768 run_loops(&self.query, self.data.as_view(), run, f)
769 }
770}
771
772macro_rules! stamp {
777 (reference, $Q:ty, $D:ty, $f_l2:ident, $f_ip:ident, $f_cosine:ident) => {
778 impl RunBenchmark<Reference> for Kernel<Reference, $Q, $D> {
779 fn run_benchmark(
780 &self,
781 input: &SimdOp,
782 _arch: Reference,
783 ) -> Result<Vec<RunResult>, anyhow::Error> {
784 let mut results = Vec::new();
785 for run in input.runs.iter() {
786 let data = Data::<$Q, $D>::new(run);
787 let result = match run.distance {
788 SimilarityMeasure::SquaredL2 => data.run(run, reference::$f_l2),
789 SimilarityMeasure::InnerProduct => data.run(run, reference::$f_ip),
790 SimilarityMeasure::Cosine => data.run(run, reference::$f_cosine),
791 };
792 results.push(result);
793 }
794 Ok(results)
795 }
796 }
797 };
798 ($arch:path, $Q:ty, $D:ty) => {
799 impl RunBenchmark<$arch> for Kernel<$arch, $Q, $D> {
800 fn run_benchmark(
801 &self,
802 input: &SimdOp,
803 arch: $arch,
804 ) -> Result<Vec<RunResult>, anyhow::Error> {
805 let mut results = Vec::new();
806
807 let l2 = &simd::L2 {};
808 let ip = &simd::IP {};
809 let cosine = &simd::CosineStateless {};
810
811 for run in input.runs.iter() {
812 let data = Data::<$Q, $D>::new(run);
813 let result = match run.distance {
820 SimilarityMeasure::SquaredL2 => data.run(run, |q, d| {
821 arch.run2(|q, d| simd::simd_op(l2, arch, q, d), q, d)
822 }),
823 SimilarityMeasure::InnerProduct => data.run(run, |q, d| {
824 arch.run2(|q, d| simd::simd_op(ip, arch, q, d), q, d)
825 }),
826 SimilarityMeasure::Cosine => data.run(run, |q, d| {
827 arch.run2(|q, d| simd::simd_op(cosine, arch, q, d), q, d)
828 }),
829 };
830 results.push(result)
831 }
832 Ok(results)
833 }
834 }
835 };
836 ($target_arch:literal, $arch:path, $Q:ty, $D:ty) => {
837 #[cfg(target_arch = $target_arch)]
838 stamp!($arch, $Q, $D);
839 };
840}
841
842stamp!("x86_64", diskann_wide::arch::x86_64::V4, f32, f32);
843stamp!("x86_64", diskann_wide::arch::x86_64::V4, f16, f16);
844stamp!("x86_64", diskann_wide::arch::x86_64::V4, u8, u8);
845stamp!("x86_64", diskann_wide::arch::x86_64::V4, i8, i8);
846
847stamp!("x86_64", diskann_wide::arch::x86_64::V3, f32, f32);
848stamp!("x86_64", diskann_wide::arch::x86_64::V3, f16, f16);
849stamp!("x86_64", diskann_wide::arch::x86_64::V3, u8, u8);
850stamp!("x86_64", diskann_wide::arch::x86_64::V3, i8, i8);
851
852stamp!("aarch64", diskann_wide::arch::aarch64::Neon, f32, f32);
853stamp!("aarch64", diskann_wide::arch::aarch64::Neon, f16, f16);
854stamp!("aarch64", diskann_wide::arch::aarch64::Neon, u8, u8);
855stamp!("aarch64", diskann_wide::arch::aarch64::Neon, i8, i8);
856
857stamp!(diskann_wide::arch::Scalar, f32, f32);
858stamp!(diskann_wide::arch::Scalar, f16, f16);
859stamp!(diskann_wide::arch::Scalar, u8, u8);
860stamp!(diskann_wide::arch::Scalar, i8, i8);
861
862stamp!(
863 reference,
864 f32,
865 f32,
866 squared_l2_f32,
867 inner_product_f32,
868 cosine_f32
869);
870stamp!(
871 reference,
872 f16,
873 f16,
874 squared_l2_f16,
875 inner_product_f16,
876 cosine_f16
877);
878stamp!(
879 reference,
880 u8,
881 u8,
882 squared_l2_u8,
883 inner_product_u8,
884 cosine_u8
885);
886stamp!(
887 reference,
888 i8,
889 i8,
890 squared_l2_i8,
891 inner_product_i8,
892 cosine_i8
893);
894
895mod reference {
902 use diskann_wide::ARCH;
903 use half::f16;
904
905 trait MaybeFMA {
906 fn maybe_fma(self, a: f32, b: f32, c: f32) -> f32;
909 }
910
911 impl MaybeFMA for diskann_wide::arch::Scalar {
912 fn maybe_fma(self, a: f32, b: f32, c: f32) -> f32 {
913 a * b + c
914 }
915 }
916
917 #[cfg(target_arch = "x86_64")]
918 impl MaybeFMA for diskann_wide::arch::x86_64::V3 {
919 fn maybe_fma(self, a: f32, b: f32, c: f32) -> f32 {
920 a.mul_add(b, c)
921 }
922 }
923
924 #[cfg(target_arch = "x86_64")]
925 impl MaybeFMA for diskann_wide::arch::x86_64::V4 {
926 fn maybe_fma(self, a: f32, b: f32, c: f32) -> f32 {
927 a.mul_add(b, c)
928 }
929 }
930
931 #[cfg(target_arch = "aarch64")]
932 impl MaybeFMA for diskann_wide::arch::aarch64::Neon {
933 fn maybe_fma(self, a: f32, b: f32, c: f32) -> f32 {
934 a.mul_add(b, c)
935 }
936 }
937
938 pub(super) fn squared_l2_i8(x: &[i8], y: &[i8]) -> f32 {
943 assert_eq!(x.len(), y.len());
944 std::iter::zip(x.iter(), y.iter())
945 .map(|(&a, &b)| {
946 let a: i32 = a.into();
947 let b: i32 = b.into();
948 let diff = a - b;
949 diff * diff
950 })
951 .sum::<i32>() as f32
952 }
953
954 pub(super) fn squared_l2_u8(x: &[u8], y: &[u8]) -> f32 {
955 assert_eq!(x.len(), y.len());
956 std::iter::zip(x.iter(), y.iter())
957 .map(|(&a, &b)| {
958 let a: i32 = a.into();
959 let b: i32 = b.into();
960 let diff = a - b;
961 diff * diff
962 })
963 .sum::<i32>() as f32
964 }
965
966 pub(super) fn squared_l2_f16(x: &[f16], y: &[f16]) -> f32 {
967 assert_eq!(x.len(), y.len());
968 std::iter::zip(x.iter(), y.iter()).fold(0.0f32, |acc, (&a, &b)| {
969 let a: f32 = a.into();
970 let b: f32 = b.into();
971 let diff = a - b;
972 ARCH.maybe_fma(diff, diff, acc)
973 })
974 }
975
976 pub(super) fn squared_l2_f32(x: &[f32], y: &[f32]) -> f32 {
977 assert_eq!(x.len(), y.len());
978 std::iter::zip(x.iter(), y.iter()).fold(0.0f32, |acc, (&a, &b)| {
979 let diff = a - b;
980 ARCH.maybe_fma(diff, diff, acc)
981 })
982 }
983
984 pub(super) fn inner_product_i8(x: &[i8], y: &[i8]) -> f32 {
989 assert_eq!(x.len(), y.len());
990 std::iter::zip(x.iter(), y.iter())
991 .map(|(&a, &b)| {
992 let a: i32 = a.into();
993 let b: i32 = b.into();
994 a * b
995 })
996 .sum::<i32>() as f32
997 }
998
999 pub(super) fn inner_product_u8(x: &[u8], y: &[u8]) -> f32 {
1000 assert_eq!(x.len(), y.len());
1001 std::iter::zip(x.iter(), y.iter())
1002 .map(|(&a, &b)| {
1003 let a: i32 = a.into();
1004 let b: i32 = b.into();
1005 a * b
1006 })
1007 .sum::<i32>() as f32
1008 }
1009
1010 pub(super) fn inner_product_f16(x: &[f16], y: &[f16]) -> f32 {
1011 assert_eq!(x.len(), y.len());
1012 std::iter::zip(x.iter(), y.iter()).fold(0.0f32, |acc, (&a, &b)| {
1013 let a: f32 = a.into();
1014 let b: f32 = b.into();
1015 ARCH.maybe_fma(a, b, acc)
1016 })
1017 }
1018
1019 pub(super) fn inner_product_f32(x: &[f32], y: &[f32]) -> f32 {
1020 assert_eq!(x.len(), y.len());
1021 std::iter::zip(x.iter(), y.iter()).fold(0.0f32, |acc, (&a, &b)| ARCH.maybe_fma(a, b, acc))
1022 }
1023
1024 #[derive(Default)]
1029 struct XY<T> {
1030 xnorm: T,
1031 ynorm: T,
1032 xy: T,
1033 }
1034
1035 pub(super) fn cosine_i8(x: &[i8], y: &[i8]) -> f32 {
1036 assert_eq!(x.len(), y.len());
1037 let r: XY<i32> =
1038 std::iter::zip(x.iter(), y.iter()).fold(XY::<i32>::default(), |acc, (&vx, &vy)| {
1039 let vx: i32 = vx.into();
1040 let vy: i32 = vy.into();
1041 XY {
1042 xnorm: acc.xnorm + vx * vx,
1043 ynorm: acc.ynorm + vy * vy,
1044 xy: acc.xy + vx * vy,
1045 }
1046 });
1047
1048 if r.xnorm == 0 || r.ynorm == 0 {
1049 return 0.0;
1050 }
1051
1052 (r.xy as f32 / ((r.xnorm as f32).sqrt() * (r.ynorm as f32).sqrt())).clamp(-1.0, 1.0)
1053 }
1054
1055 pub(super) fn cosine_u8(x: &[u8], y: &[u8]) -> f32 {
1056 assert_eq!(x.len(), y.len());
1057 let r: XY<i32> =
1058 std::iter::zip(x.iter(), y.iter()).fold(XY::<i32>::default(), |acc, (&vx, &vy)| {
1059 let vx: i32 = vx.into();
1060 let vy: i32 = vy.into();
1061 XY {
1062 xnorm: acc.xnorm + vx * vx,
1063 ynorm: acc.ynorm + vy * vy,
1064 xy: acc.xy + vx * vy,
1065 }
1066 });
1067
1068 if r.xnorm == 0 || r.ynorm == 0 {
1069 return 0.0;
1070 }
1071
1072 (r.xy as f32 / ((r.xnorm as f32).sqrt() * (r.ynorm as f32).sqrt())).clamp(-1.0, 1.0)
1073 }
1074
1075 pub(super) fn cosine_f16(x: &[f16], y: &[f16]) -> f32 {
1076 assert_eq!(x.len(), y.len());
1077 let r: XY<f32> =
1078 std::iter::zip(x.iter(), y.iter()).fold(XY::<f32>::default(), |acc, (&vx, &vy)| {
1079 let vx: f32 = vx.into();
1080 let vy: f32 = vy.into();
1081 XY {
1082 xnorm: ARCH.maybe_fma(vx, vx, acc.xnorm),
1083 ynorm: ARCH.maybe_fma(vy, vy, acc.ynorm),
1084 xy: ARCH.maybe_fma(vx, vy, acc.xy),
1085 }
1086 });
1087
1088 if r.xnorm < f32::EPSILON || r.ynorm < f32::EPSILON {
1089 return 0.0;
1090 }
1091
1092 (r.xy / (r.xnorm.sqrt() * r.ynorm.sqrt())).clamp(-1.0, 1.0)
1093 }
1094
1095 pub(super) fn cosine_f32(x: &[f32], y: &[f32]) -> f32 {
1096 assert_eq!(x.len(), y.len());
1097 let r: XY<f32> =
1098 std::iter::zip(x.iter(), y.iter()).fold(XY::<f32>::default(), |acc, (&vx, &vy)| XY {
1099 xnorm: vx.mul_add(vx, acc.xnorm),
1100 ynorm: vy.mul_add(vy, acc.ynorm),
1101 xy: vx.mul_add(vy, acc.xy),
1102 });
1103
1104 if r.xnorm < f32::EPSILON || r.ynorm < f32::EPSILON {
1105 return 0.0;
1106 }
1107
1108 (r.xy / (r.xnorm.sqrt() * r.ynorm.sqrt())).clamp(-1.0, 1.0)
1109 }
1110}
1111
1112#[cfg(test)]
1117mod tests {
1118 use super::*;
1119
1120 use diskann_benchmark_runner::{
1121 benchmark::{PassFail, Regression},
1122 utils::percentiles::compute_percentiles,
1123 };
1124
1125 fn tiny_run(distance: SimilarityMeasure) -> Run {
1126 Run {
1127 distance,
1128 dim: NonZeroUsize::new(8).unwrap(),
1129 num_points: NonZeroUsize::new(1).unwrap(),
1130 loops_per_measurement: NonZeroUsize::new(1).unwrap(),
1131 num_measurements: NonZeroUsize::new(1).unwrap(),
1132 }
1133 }
1134
1135 fn tiny_op() -> SimdOp {
1136 SimdOp {
1137 query_type: DataType::Float32,
1138 data_type: DataType::Float32,
1139 arch: Arch::Scalar,
1140 runs: vec![tiny_run(SimilarityMeasure::SquaredL2)],
1141 }
1142 }
1143
1144 fn tiny_result(distance: SimilarityMeasure, minimum: u64) -> RunResult {
1145 let run = tiny_run(distance);
1146 let minimum = MicroSeconds::new(minimum);
1147 let mut latencies = vec![minimum];
1148 let percentiles = compute_percentiles(&mut latencies).unwrap();
1149 RunResult {
1150 run,
1151 latencies,
1152 percentiles,
1153 }
1154 }
1155
1156 fn tolerance(limit: f64) -> SimdTolerance {
1157 SimdTolerance {
1158 min_time_regression: NonNegativeFinite::new(limit).unwrap(),
1159 }
1160 }
1161
1162 #[test]
1163 fn check_rejects_mismatched_runs() {
1164 let kernel = Kernel::<diskann_wide::arch::Scalar, f32, f32>::new();
1165
1166 let err = kernel
1167 .check(
1168 &tolerance(0.0),
1169 &tiny_op(),
1170 &vec![tiny_result(SimilarityMeasure::SquaredL2, 100)],
1171 &vec![tiny_result(SimilarityMeasure::Cosine, 100)],
1172 )
1173 .unwrap_err();
1174
1175 assert_eq!(err.to_string(), "run 0 mismatched");
1176 }
1177
1178 #[test]
1179 fn check_allows_negative_relative_change() {
1180 let kernel = Kernel::<diskann_wide::arch::Scalar, f32, f32>::new();
1181
1182 let result = kernel
1183 .check(
1184 &tolerance(0.0),
1185 &tiny_op(),
1186 &vec![tiny_result(SimilarityMeasure::SquaredL2, 100)],
1187 &vec![tiny_result(SimilarityMeasure::SquaredL2, 95)],
1188 )
1189 .unwrap();
1190
1191 assert!(matches!(result, PassFail::Pass(_)));
1192 }
1193
1194 #[test]
1195 fn check_passes_on_tolerance_boundary() {
1196 let kernel = Kernel::<diskann_wide::arch::Scalar, f32, f32>::new();
1197
1198 let result = kernel
1199 .check(
1200 &tolerance(0.05),
1201 &tiny_op(),
1202 &vec![tiny_result(SimilarityMeasure::SquaredL2, 100)],
1203 &vec![tiny_result(SimilarityMeasure::SquaredL2, 105)],
1204 )
1205 .unwrap();
1206
1207 assert!(matches!(result, PassFail::Pass(_)));
1208 }
1209
1210 #[test]
1211 fn check_fails_above_tolerance_boundary() {
1212 let kernel = Kernel::<diskann_wide::arch::Scalar, f32, f32>::new();
1213
1214 let result = kernel
1215 .check(
1216 &tolerance(0.05),
1217 &tiny_op(),
1218 &vec![tiny_result(SimilarityMeasure::SquaredL2, 100)],
1219 &vec![tiny_result(SimilarityMeasure::SquaredL2, 106)],
1220 )
1221 .unwrap();
1222
1223 assert!(matches!(result, PassFail::Fail(_)));
1224 }
1225
1226 #[test]
1227 fn check_result_display_includes_failure_details() {
1228 let check = CheckResult {
1229 checks: vec![Comparison {
1230 run: tiny_run(SimilarityMeasure::SquaredL2),
1231 tolerance: tolerance(0.05),
1232 before_min: 100.0,
1233 after_min: 106.0,
1234 }],
1235 };
1236
1237 let rendered = check.to_string();
1238 assert!(rendered.contains("Distance"), "rendered = {rendered}");
1239 assert!(rendered.contains("squared_l2"), "rendered = {rendered}");
1240 assert!(rendered.contains("100.000"), "rendered = {rendered}");
1241 assert!(rendered.contains("106.000"), "rendered = {rendered}");
1242 assert!(rendered.contains("6.000 %"), "rendered = {rendered}");
1243 assert!(rendered.contains("FAIL"), "rendered = {rendered}");
1244 }
1245
1246 #[test]
1252 fn zero_values_rejected() {
1253 let kernel = Kernel::<diskann_wide::arch::Scalar, f32, f32>::new();
1254
1255 let result = kernel
1256 .check(
1257 &tolerance(0.05),
1258 &tiny_op(),
1259 &vec![tiny_result(SimilarityMeasure::SquaredL2, 0)],
1260 &vec![tiny_result(SimilarityMeasure::SquaredL2, 0)],
1261 )
1262 .unwrap();
1263
1264 assert!(matches!(result, PassFail::Fail(_)));
1265 }
1266}