1use std::{io::Write, num::NonZeroUsize};
9
10use diskann_utils::views::{Matrix, MatrixView};
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 #[allow(non_camel_case_types)]
82 X86_64_V4,
83 #[serde(rename = "x86-64-v3")]
84 #[allow(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: MatrixView<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.row_iter()).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: Matrix<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 = Matrix::<D>::new(
758 diskann_utils::views::Init(|| StandardUniform.sample(&mut rng)),
759 run.num_points.get(),
760 run.dim.get(),
761 );
762
763 Self { query, data }
764 }
765
766 fn run<F>(&self, run: &Run, f: F) -> RunResult
767 where
768 F: Fn(&[Q], &[D]) -> f32,
769 {
770 run_loops(&self.query, self.data.as_view(), run, f)
771 }
772}
773
774macro_rules! stamp {
779 (reference, $Q:ty, $D:ty, $f_l2:ident, $f_ip:ident, $f_cosine:ident) => {
780 impl RunBenchmark<Reference> for Kernel<Reference, $Q, $D> {
781 fn run_benchmark(
782 &self,
783 input: &SimdOp,
784 _arch: Reference,
785 ) -> Result<Vec<RunResult>, anyhow::Error> {
786 let mut results = Vec::new();
787 for run in input.runs.iter() {
788 let data = Data::<$Q, $D>::new(run);
789 let result = match run.distance {
790 SimilarityMeasure::SquaredL2 => data.run(run, reference::$f_l2),
791 SimilarityMeasure::InnerProduct => data.run(run, reference::$f_ip),
792 SimilarityMeasure::Cosine => data.run(run, reference::$f_cosine),
793 };
794 results.push(result);
795 }
796 Ok(results)
797 }
798 }
799 };
800 ($arch:path, $Q:ty, $D:ty) => {
801 impl RunBenchmark<$arch> for Kernel<$arch, $Q, $D> {
802 fn run_benchmark(
803 &self,
804 input: &SimdOp,
805 arch: $arch,
806 ) -> Result<Vec<RunResult>, anyhow::Error> {
807 let mut results = Vec::new();
808
809 let l2 = &simd::L2 {};
810 let ip = &simd::IP {};
811 let cosine = &simd::CosineStateless {};
812
813 for run in input.runs.iter() {
814 let data = Data::<$Q, $D>::new(run);
815 let result = match run.distance {
822 SimilarityMeasure::SquaredL2 => data.run(run, |q, d| {
823 arch.run2(|q, d| simd::simd_op(l2, arch, q, d), q, d)
824 }),
825 SimilarityMeasure::InnerProduct => data.run(run, |q, d| {
826 arch.run2(|q, d| simd::simd_op(ip, arch, q, d), q, d)
827 }),
828 SimilarityMeasure::Cosine => data.run(run, |q, d| {
829 arch.run2(|q, d| simd::simd_op(cosine, arch, q, d), q, d)
830 }),
831 };
832 results.push(result)
833 }
834 Ok(results)
835 }
836 }
837 };
838 ($target_arch:literal, $arch:path, $Q:ty, $D:ty) => {
839 #[cfg(target_arch = $target_arch)]
840 stamp!($arch, $Q, $D);
841 };
842}
843
844stamp!("x86_64", diskann_wide::arch::x86_64::V4, f32, f32);
845stamp!("x86_64", diskann_wide::arch::x86_64::V4, f16, f16);
846stamp!("x86_64", diskann_wide::arch::x86_64::V4, u8, u8);
847stamp!("x86_64", diskann_wide::arch::x86_64::V4, i8, i8);
848
849stamp!("x86_64", diskann_wide::arch::x86_64::V3, f32, f32);
850stamp!("x86_64", diskann_wide::arch::x86_64::V3, f16, f16);
851stamp!("x86_64", diskann_wide::arch::x86_64::V3, u8, u8);
852stamp!("x86_64", diskann_wide::arch::x86_64::V3, i8, i8);
853
854stamp!("aarch64", diskann_wide::arch::aarch64::Neon, f32, f32);
855stamp!("aarch64", diskann_wide::arch::aarch64::Neon, f16, f16);
856stamp!("aarch64", diskann_wide::arch::aarch64::Neon, u8, u8);
857stamp!("aarch64", diskann_wide::arch::aarch64::Neon, i8, i8);
858
859stamp!(diskann_wide::arch::Scalar, f32, f32);
860stamp!(diskann_wide::arch::Scalar, f16, f16);
861stamp!(diskann_wide::arch::Scalar, u8, u8);
862stamp!(diskann_wide::arch::Scalar, i8, i8);
863
864stamp!(
865 reference,
866 f32,
867 f32,
868 squared_l2_f32,
869 inner_product_f32,
870 cosine_f32
871);
872stamp!(
873 reference,
874 f16,
875 f16,
876 squared_l2_f16,
877 inner_product_f16,
878 cosine_f16
879);
880stamp!(
881 reference,
882 u8,
883 u8,
884 squared_l2_u8,
885 inner_product_u8,
886 cosine_u8
887);
888stamp!(
889 reference,
890 i8,
891 i8,
892 squared_l2_i8,
893 inner_product_i8,
894 cosine_i8
895);
896
897mod reference {
904 use diskann_wide::ARCH;
905 use half::f16;
906
907 trait MaybeFMA {
908 fn maybe_fma(self, a: f32, b: f32, c: f32) -> f32;
911 }
912
913 impl MaybeFMA for diskann_wide::arch::Scalar {
914 fn maybe_fma(self, a: f32, b: f32, c: f32) -> f32 {
915 a * b + c
916 }
917 }
918
919 #[cfg(target_arch = "x86_64")]
920 impl MaybeFMA for diskann_wide::arch::x86_64::V3 {
921 fn maybe_fma(self, a: f32, b: f32, c: f32) -> f32 {
922 a.mul_add(b, c)
923 }
924 }
925
926 #[cfg(target_arch = "x86_64")]
927 impl MaybeFMA for diskann_wide::arch::x86_64::V4 {
928 fn maybe_fma(self, a: f32, b: f32, c: f32) -> f32 {
929 a.mul_add(b, c)
930 }
931 }
932
933 #[cfg(target_arch = "aarch64")]
934 impl MaybeFMA for diskann_wide::arch::aarch64::Neon {
935 fn maybe_fma(self, a: f32, b: f32, c: f32) -> f32 {
936 a.mul_add(b, c)
937 }
938 }
939
940 pub(super) fn squared_l2_i8(x: &[i8], y: &[i8]) -> f32 {
945 assert_eq!(x.len(), y.len());
946 std::iter::zip(x.iter(), y.iter())
947 .map(|(&a, &b)| {
948 let a: i32 = a.into();
949 let b: i32 = b.into();
950 let diff = a - b;
951 diff * diff
952 })
953 .sum::<i32>() as f32
954 }
955
956 pub(super) fn squared_l2_u8(x: &[u8], y: &[u8]) -> f32 {
957 assert_eq!(x.len(), y.len());
958 std::iter::zip(x.iter(), y.iter())
959 .map(|(&a, &b)| {
960 let a: i32 = a.into();
961 let b: i32 = b.into();
962 let diff = a - b;
963 diff * diff
964 })
965 .sum::<i32>() as f32
966 }
967
968 pub(super) fn squared_l2_f16(x: &[f16], y: &[f16]) -> f32 {
969 assert_eq!(x.len(), y.len());
970 std::iter::zip(x.iter(), y.iter()).fold(0.0f32, |acc, (&a, &b)| {
971 let a: f32 = a.into();
972 let b: f32 = b.into();
973 let diff = a - b;
974 ARCH.maybe_fma(diff, diff, acc)
975 })
976 }
977
978 pub(super) fn squared_l2_f32(x: &[f32], y: &[f32]) -> f32 {
979 assert_eq!(x.len(), y.len());
980 std::iter::zip(x.iter(), y.iter()).fold(0.0f32, |acc, (&a, &b)| {
981 let diff = a - b;
982 ARCH.maybe_fma(diff, diff, acc)
983 })
984 }
985
986 pub(super) fn inner_product_i8(x: &[i8], y: &[i8]) -> f32 {
991 assert_eq!(x.len(), y.len());
992 std::iter::zip(x.iter(), y.iter())
993 .map(|(&a, &b)| {
994 let a: i32 = a.into();
995 let b: i32 = b.into();
996 a * b
997 })
998 .sum::<i32>() as f32
999 }
1000
1001 pub(super) fn inner_product_u8(x: &[u8], y: &[u8]) -> f32 {
1002 assert_eq!(x.len(), y.len());
1003 std::iter::zip(x.iter(), y.iter())
1004 .map(|(&a, &b)| {
1005 let a: i32 = a.into();
1006 let b: i32 = b.into();
1007 a * b
1008 })
1009 .sum::<i32>() as f32
1010 }
1011
1012 pub(super) fn inner_product_f16(x: &[f16], y: &[f16]) -> f32 {
1013 assert_eq!(x.len(), y.len());
1014 std::iter::zip(x.iter(), y.iter()).fold(0.0f32, |acc, (&a, &b)| {
1015 let a: f32 = a.into();
1016 let b: f32 = b.into();
1017 ARCH.maybe_fma(a, b, acc)
1018 })
1019 }
1020
1021 pub(super) fn inner_product_f32(x: &[f32], y: &[f32]) -> f32 {
1022 assert_eq!(x.len(), y.len());
1023 std::iter::zip(x.iter(), y.iter()).fold(0.0f32, |acc, (&a, &b)| ARCH.maybe_fma(a, b, acc))
1024 }
1025
1026 #[derive(Default)]
1031 struct XY<T> {
1032 xnorm: T,
1033 ynorm: T,
1034 xy: T,
1035 }
1036
1037 pub(super) fn cosine_i8(x: &[i8], y: &[i8]) -> f32 {
1038 assert_eq!(x.len(), y.len());
1039 let r: XY<i32> =
1040 std::iter::zip(x.iter(), y.iter()).fold(XY::<i32>::default(), |acc, (&vx, &vy)| {
1041 let vx: i32 = vx.into();
1042 let vy: i32 = vy.into();
1043 XY {
1044 xnorm: acc.xnorm + vx * vx,
1045 ynorm: acc.ynorm + vy * vy,
1046 xy: acc.xy + vx * vy,
1047 }
1048 });
1049
1050 if r.xnorm == 0 || r.ynorm == 0 {
1051 return 0.0;
1052 }
1053
1054 (r.xy as f32 / ((r.xnorm as f32).sqrt() * (r.ynorm as f32).sqrt())).clamp(-1.0, 1.0)
1055 }
1056
1057 pub(super) fn cosine_u8(x: &[u8], y: &[u8]) -> f32 {
1058 assert_eq!(x.len(), y.len());
1059 let r: XY<i32> =
1060 std::iter::zip(x.iter(), y.iter()).fold(XY::<i32>::default(), |acc, (&vx, &vy)| {
1061 let vx: i32 = vx.into();
1062 let vy: i32 = vy.into();
1063 XY {
1064 xnorm: acc.xnorm + vx * vx,
1065 ynorm: acc.ynorm + vy * vy,
1066 xy: acc.xy + vx * vy,
1067 }
1068 });
1069
1070 if r.xnorm == 0 || r.ynorm == 0 {
1071 return 0.0;
1072 }
1073
1074 (r.xy as f32 / ((r.xnorm as f32).sqrt() * (r.ynorm as f32).sqrt())).clamp(-1.0, 1.0)
1075 }
1076
1077 pub(super) fn cosine_f16(x: &[f16], y: &[f16]) -> f32 {
1078 assert_eq!(x.len(), y.len());
1079 let r: XY<f32> =
1080 std::iter::zip(x.iter(), y.iter()).fold(XY::<f32>::default(), |acc, (&vx, &vy)| {
1081 let vx: f32 = vx.into();
1082 let vy: f32 = vy.into();
1083 XY {
1084 xnorm: ARCH.maybe_fma(vx, vx, acc.xnorm),
1085 ynorm: ARCH.maybe_fma(vy, vy, acc.ynorm),
1086 xy: ARCH.maybe_fma(vx, vy, acc.xy),
1087 }
1088 });
1089
1090 if r.xnorm < f32::EPSILON || r.ynorm < f32::EPSILON {
1091 return 0.0;
1092 }
1093
1094 (r.xy / (r.xnorm.sqrt() * r.ynorm.sqrt())).clamp(-1.0, 1.0)
1095 }
1096
1097 pub(super) fn cosine_f32(x: &[f32], y: &[f32]) -> f32 {
1098 assert_eq!(x.len(), y.len());
1099 let r: XY<f32> =
1100 std::iter::zip(x.iter(), y.iter()).fold(XY::<f32>::default(), |acc, (&vx, &vy)| XY {
1101 xnorm: vx.mul_add(vx, acc.xnorm),
1102 ynorm: vy.mul_add(vy, acc.ynorm),
1103 xy: vx.mul_add(vy, acc.xy),
1104 });
1105
1106 if r.xnorm < f32::EPSILON || r.ynorm < f32::EPSILON {
1107 return 0.0;
1108 }
1109
1110 (r.xy / (r.xnorm.sqrt() * r.ynorm.sqrt())).clamp(-1.0, 1.0)
1111 }
1112}
1113
1114#[cfg(test)]
1119mod tests {
1120 use super::*;
1121
1122 use diskann_benchmark_runner::{
1123 benchmark::{PassFail, Regression},
1124 utils::percentiles::compute_percentiles,
1125 };
1126
1127 fn tiny_run(distance: SimilarityMeasure) -> Run {
1128 Run {
1129 distance,
1130 dim: NonZeroUsize::new(8).unwrap(),
1131 num_points: NonZeroUsize::new(1).unwrap(),
1132 loops_per_measurement: NonZeroUsize::new(1).unwrap(),
1133 num_measurements: NonZeroUsize::new(1).unwrap(),
1134 }
1135 }
1136
1137 fn tiny_op() -> SimdOp {
1138 SimdOp {
1139 query_type: DataType::Float32,
1140 data_type: DataType::Float32,
1141 arch: Arch::Scalar,
1142 runs: vec![tiny_run(SimilarityMeasure::SquaredL2)],
1143 }
1144 }
1145
1146 fn tiny_result(distance: SimilarityMeasure, minimum: u64) -> RunResult {
1147 let run = tiny_run(distance);
1148 let minimum = MicroSeconds::new(minimum);
1149 let mut latencies = vec![minimum];
1150 let percentiles = compute_percentiles(&mut latencies).unwrap();
1151 RunResult {
1152 run,
1153 latencies,
1154 percentiles,
1155 }
1156 }
1157
1158 fn tolerance(limit: f64) -> SimdTolerance {
1159 SimdTolerance {
1160 min_time_regression: NonNegativeFinite::new(limit).unwrap(),
1161 }
1162 }
1163
1164 #[test]
1165 fn check_rejects_mismatched_runs() {
1166 let kernel = Kernel::<diskann_wide::arch::Scalar, f32, f32>::new();
1167
1168 let err = kernel
1169 .check(
1170 &tolerance(0.0),
1171 &tiny_op(),
1172 &vec![tiny_result(SimilarityMeasure::SquaredL2, 100)],
1173 &vec![tiny_result(SimilarityMeasure::Cosine, 100)],
1174 )
1175 .unwrap_err();
1176
1177 assert_eq!(err.to_string(), "run 0 mismatched");
1178 }
1179
1180 #[test]
1181 fn check_allows_negative_relative_change() {
1182 let kernel = Kernel::<diskann_wide::arch::Scalar, f32, f32>::new();
1183
1184 let result = kernel
1185 .check(
1186 &tolerance(0.0),
1187 &tiny_op(),
1188 &vec![tiny_result(SimilarityMeasure::SquaredL2, 100)],
1189 &vec![tiny_result(SimilarityMeasure::SquaredL2, 95)],
1190 )
1191 .unwrap();
1192
1193 assert!(matches!(result, PassFail::Pass(_)));
1194 }
1195
1196 #[test]
1197 fn check_passes_on_tolerance_boundary() {
1198 let kernel = Kernel::<diskann_wide::arch::Scalar, f32, f32>::new();
1199
1200 let result = kernel
1201 .check(
1202 &tolerance(0.05),
1203 &tiny_op(),
1204 &vec![tiny_result(SimilarityMeasure::SquaredL2, 100)],
1205 &vec![tiny_result(SimilarityMeasure::SquaredL2, 105)],
1206 )
1207 .unwrap();
1208
1209 assert!(matches!(result, PassFail::Pass(_)));
1210 }
1211
1212 #[test]
1213 fn check_fails_above_tolerance_boundary() {
1214 let kernel = Kernel::<diskann_wide::arch::Scalar, f32, f32>::new();
1215
1216 let result = kernel
1217 .check(
1218 &tolerance(0.05),
1219 &tiny_op(),
1220 &vec![tiny_result(SimilarityMeasure::SquaredL2, 100)],
1221 &vec![tiny_result(SimilarityMeasure::SquaredL2, 106)],
1222 )
1223 .unwrap();
1224
1225 assert!(matches!(result, PassFail::Fail(_)));
1226 }
1227
1228 #[test]
1229 fn check_result_display_includes_failure_details() {
1230 let check = CheckResult {
1231 checks: vec![Comparison {
1232 run: tiny_run(SimilarityMeasure::SquaredL2),
1233 tolerance: tolerance(0.05),
1234 before_min: 100.0,
1235 after_min: 106.0,
1236 }],
1237 };
1238
1239 let rendered = check.to_string();
1240 assert!(rendered.contains("Distance"), "rendered = {rendered}");
1241 assert!(rendered.contains("squared_l2"), "rendered = {rendered}");
1242 assert!(rendered.contains("100.000"), "rendered = {rendered}");
1243 assert!(rendered.contains("106.000"), "rendered = {rendered}");
1244 assert!(rendered.contains("6.000 %"), "rendered = {rendered}");
1245 assert!(rendered.contains("FAIL"), "rendered = {rendered}");
1246 }
1247
1248 #[test]
1254 fn zero_values_rejected() {
1255 let kernel = Kernel::<diskann_wide::arch::Scalar, f32, f32>::new();
1256
1257 let result = kernel
1258 .check(
1259 &tolerance(0.05),
1260 &tiny_op(),
1261 &vec![tiny_result(SimilarityMeasure::SquaredL2, 0)],
1262 &vec![tiny_result(SimilarityMeasure::SquaredL2, 0)],
1263 )
1264 .unwrap();
1265
1266 assert!(matches!(result, PassFail::Fail(_)));
1267 }
1268}