1414#include < vector>
1515
1616// ============================================================================
17- // Integration Test Shared Utilities
17+ // Shared Test Utilities
1818// ============================================================================
19- // Centralized helpers for fast integration tests operating on 10,000 base
20- // vectors and 100 queries. These are adapted from test_regression.h but
21- // strip out QPS / build-time assertions so that only recall correctness is
22- // verified. All dataset generation uses the same bit-exact 32-bit PRNG so
23- // results are reproducible across platforms.
19+ // Centralized helpers for both integration and regression tests.
20+ // All dataset generation uses a bit-exact 32-bit PRNG so results are
21+ // reproducible across MSVC, GCC, and Clang on all CPU architectures.
2422// ============================================================================
2523
2624// ---------------------------------------------------------------------------
@@ -35,10 +33,20 @@ inline static uint32_t deglib_prng_next(uint32_t& state) {
3533 return state = x;
3634}
3735
36+ inline static float deglib_prng_float (uint32_t & state, float min_val, float max_val) {
37+ uint32_t val = deglib_prng_next (state) >> 8 ; // 24-bit integer
38+ float u = static_cast <float >(val) / 16777215 .0f ;
39+ float range = max_val - min_val;
40+ return min_val + u * range;
41+ }
42+
3843// ---------------------------------------------------------------------------
3944// Dataset generation (float, for L2 and InnerProduct)
4045// ---------------------------------------------------------------------------
4146
47+ // Generate cross-platform deterministic clustered dataset (Gaussian Mixture with fixed seed)
48+ // Uses pure 32-bit integer arithmetic and exact integer-to-float conversion to guarantee
49+ // 100% bit-exact float vectors across MSVC, GCC, and Clang on all CPU architectures.
4250inline static void generate_synthetic_clustered_dataset (size_t count, size_t dim, std::vector<float >& base,
4351 std::vector<float >& query, size_t query_count,
4452 size_t num_clusters = 20 )
@@ -85,6 +93,7 @@ inline static void generate_synthetic_clustered_dataset(size_t count, size_t dim
8593// Dataset generation (uint8, for L2_Uint8)
8694// ---------------------------------------------------------------------------
8795
96+ // Generate cross-platform deterministic uint8 clustered dataset
8897inline static void generate_synthetic_clustered_dataset_uint8 (size_t count, size_t dim, std::vector<uint8_t >& base,
8998 std::vector<uint8_t >& query, size_t query_count,
9099 size_t num_clusters = 20 )
@@ -133,6 +142,8 @@ inline static void generate_synthetic_clustered_dataset_uint8(size_t count, size
133142// Groundtruth computation
134143// ---------------------------------------------------------------------------
135144
145+ // Compute exact brute-force groundtruth for top-K neighbors using a custom distance evaluator.
146+ // Works for both float and uint8_t element types via the ElemType template parameter.
136147template <typename ElemType, typename DistFunc>
137148inline static std::vector<std::vector<uint32_t >> compute_groundtruth_custom (const std::vector<ElemType>& base, size_t base_count,
138149 const std::vector<ElemType>& query, size_t query_count,
@@ -163,6 +174,9 @@ inline static std::vector<std::vector<uint32_t>> compute_groundtruth_custom(cons
163174 return gt;
164175}
165176
177+ // Compute exact brute-force L2 groundtruth for top-K neighbors.
178+ // Uses the scalar L2Float::compare() implementation from deglib to ensure
179+ // the ground-truth distances match the actual distance computation exactly.
166180inline static std::vector<std::vector<uint32_t >> compute_groundtruth_l2 (const std::vector<float >& base, size_t base_count,
167181 const std::vector<float >& query, size_t query_count,
168182 size_t dim, uint32_t k)
@@ -174,6 +188,9 @@ inline static std::vector<std::vector<uint32_t>> compute_groundtruth_l2(const st
174188 });
175189}
176190
191+ // Compute exact brute-force InnerProduct groundtruth for top-K neighbors (distance = 1 - dot_product).
192+ // Uses the scalar InnerProductFloat::compare() implementation from deglib to ensure
193+ // the ground-truth distances match the actual distance computation exactly.
177194inline static std::vector<std::vector<uint32_t >> compute_groundtruth_innerproduct (const std::vector<float >& base, size_t base_count,
178195 const std::vector<float >& query, size_t query_count,
179196 size_t dim, uint32_t k)
@@ -185,6 +202,9 @@ inline static std::vector<std::vector<uint32_t>> compute_groundtruth_innerproduc
185202 });
186203}
187204
205+ // Compute exact brute-force L2 groundtruth for uint8 vectors.
206+ // Uses the scalar L2Uint8::compare() from deglib to ensure the ground-truth
207+ // distances match the actual distance computation exactly.
188208inline static std::vector<std::vector<uint32_t >> compute_groundtruth_l2_uint8 (const std::vector<uint8_t >& base, size_t base_count,
189209 const std::vector<uint8_t >& query, size_t query_count,
190210 size_t dim, uint32_t k)
@@ -240,7 +260,7 @@ inline static void check_distance_recall(const char* name, const std::vector<Ele
240260}
241261
242262// ---------------------------------------------------------------------------
243- // Graph builder + search recall runner (no QPS / build-time checks)
263+ // Graph builder + search recall runner (integration: no QPS / build-time checks)
244264// ---------------------------------------------------------------------------
245265
246266inline static void run_integration_test (const char * name, deglib::Metric metric, double min_recall,
@@ -426,10 +446,137 @@ inline static void run_builder_integration_test(const char* name, deglib::Metric
426446 }
427447}
428448
449+ // ---------------------------------------------------------------------------
450+ // Universal regression benchmark runner function for any metric
451+ // num_runs: number of measured search runs (averaged for QPS/recall).
452+ // Higher values extend the total search measurement window, reducing QPS noise.
453+ // optimization_target: controls the graph build strategy (LowLID, HighLID, StreamingData).
454+ // ---------------------------------------------------------------------------
455+
456+ inline static void run_regression_test (const char * name, deglib::Metric metric, double min_qps, double max_build_secs,
457+ double min_recall, const void * base_data,
458+ const void * query_data, size_t base_count, size_t query_count,
459+ size_t dim, const std::vector<std::vector<uint32_t >>& gt_data,
460+ std::optional<deglib::DistanceVariant> dist_variant = std::nullopt ,
461+ size_t num_runs = 5 ,
462+ deglib::builder::OptimizationTarget optimization_target = deglib::builder::OptimizationTarget::LowLID)
463+ {
464+ const uint32_t search_k = 10 ;
465+ const float search_eps = 0 .05f ;
466+
467+ const uint32_t edges_per_vertex = 32 ;
468+ const uint8_t extend_k = static_cast <uint8_t >(edges_per_vertex);
469+ const float extend_eps = 0 .1f ;
470+ const uint8_t improve_k = 0 ;
471+ const float improve_eps = 0 .0f ;
472+ const uint8_t max_path_length = 5 ;
473+ const uint32_t swap_tries = 0 ;
474+ const uint32_t additional_swap_tries = 0 ;
475+ const uint32_t thread_count = 1 ;
476+
477+ // Compute byte size per vector based on metric type (0x10 flag indicates 8-bit integer)
478+ const size_t feature_bytes = (static_cast <int >(metric) & 0x10 ) ? dim * sizeof (uint8_t ) : dim * sizeof (float );
479+
480+ // Build DEG Graph using the specified metric feature space
481+ const deglib::FloatSpace feature_space = dist_variant.has_value ()
482+ ? deglib::FloatSpace (dim, metric, dist_variant.value ())
483+ : deglib::FloatSpace (dim, metric);
484+
485+ deglib::graph::SizeBoundedGraph graph (static_cast <uint32_t >(base_count), edges_per_vertex,
486+ std::move (feature_space));
487+
488+ std::mt19937 rng (1337 );
489+ deglib::builder::EvenRegularGraphBuilder builder (graph, rng, optimization_target, extend_k, extend_eps, improve_k,
490+ improve_eps, max_path_length, swap_tries, additional_swap_tries);
491+ builder.setThreadCount (thread_count);
492+ auto t_build_start = std::chrono::high_resolution_clock::now ();
493+
494+ const std::byte* base_bytes = reinterpret_cast <const std::byte*>(base_data);
495+ for (size_t i = 0 ; i < base_count; ++i)
496+ {
497+ const std::byte* ptr = base_bytes + i * feature_bytes;
498+ std::vector<std::byte> feat_vec (ptr, ptr + feature_bytes);
499+ builder.addEntry (static_cast <uint32_t >(i), std::move (feat_vec));
500+ }
501+
502+ auto build_callback = [](deglib::builder::BuilderStatus& status) {};
503+ builder.build (build_callback);
504+
505+ auto t_build_end = std::chrono::high_resolution_clock::now ();
506+ double build_secs = std::chrono::duration<double >(t_build_end - t_build_start).count ();
507+
508+ auto entry_vertex_indices = graph.getEntryVertexIndices ();
509+ const std::byte* query_bytes = reinterpret_cast <const std::byte*>(query_data);
510+
511+ auto run_search = [&]() -> std::pair<double , double >
512+ {
513+ size_t total_correct = 0 ;
514+ auto t_search_start = std::chrono::high_resolution_clock::now ();
515+
516+ for (size_t q = 0 ; q < query_count; ++q)
517+ {
518+ const std::byte* q_ptr = query_bytes + q * feature_bytes;
519+ auto result = graph.search (entry_vertex_indices, q_ptr, search_eps, search_k, nullptr , 0 );
520+
521+ std::unordered_set<uint32_t > gt_set;
522+ if (!gt_data.empty () && q < gt_data.size ())
523+ {
524+ size_t eval_k = std::min (static_cast <size_t >(search_k), gt_data[q].size ());
525+ for (size_t i = 0 ; i < eval_k; ++i)
526+ {
527+ gt_set.insert (gt_data[q][i]);
528+ }
529+ }
530+
531+ while (!result.empty ())
532+ {
533+ auto top_item = result.top ();
534+ result.pop ();
535+ uint32_t ext_label = graph.getExternalLabel (top_item.getInternalIndex ());
536+ if (gt_set.count (ext_label))
537+ {
538+ total_correct++;
539+ }
540+ }
541+ }
542+
543+ auto t_search_end = std::chrono::high_resolution_clock::now ();
544+ double search_secs = std::chrono::duration<double >(t_search_end - t_search_start).count ();
545+ double qps = static_cast <double >(query_count) / search_secs;
546+ double recall = static_cast <double >(total_correct) / static_cast <double >(query_count * search_k);
547+ return {qps, recall};
548+ };
549+
550+ // Warm-up run
551+ run_search ();
552+
553+ // Measured average runs — higher num_runs extends the measurement window,
554+ // reducing QPS noise from OS jitter and CPU power-state transitions.
555+ double total_qps = 0.0 ;
556+ double total_recall = 0.0 ;
557+ for (size_t r = 0 ; r < num_runs; ++r)
558+ {
559+ auto [qps, recall] = run_search ();
560+ total_qps += qps;
561+ total_recall += recall;
562+ }
563+ double qps = total_qps / num_runs;
564+ double recall = total_recall / num_runs;
565+
566+ std::cout << " [" << name << " ] build_secs=" << build_secs << " qps=" << qps << " recall=" << recall << std::endl;
567+
568+ EXPECT_GE (recall + 1e-5 , min_recall);
569+ if (std::getenv (" SKIP_PERFORMANCE_TESTS" ) == nullptr ) {
570+ EXPECT_GT (qps, min_qps);
571+ EXPECT_LE (build_secs, max_build_secs);
572+ }
573+ }
574+
429575// ---------------------------------------------------------------------------
430576// Checksums for dataset determinism verification
431577// ---------------------------------------------------------------------------
432578
579+ // FNV-1a 64-bit hash for byte buffers
433580inline static uint64_t fnv1a_64 (const void * data, size_t bytes) {
434581 const uint8_t * ptr = static_cast <const uint8_t *>(data);
435582 uint64_t hash = 14695981039346656037ULL ;
@@ -440,10 +587,12 @@ inline static uint64_t fnv1a_64(const void* data, size_t bytes) {
440587 return hash;
441588}
442589
590+ // Compute checksum of a float vector
443591inline static uint64_t float_vector_checksum (const std::vector<float >& vec) {
444592 return fnv1a_64 (vec.data (), vec.size () * sizeof (float ));
445593}
446594
595+ // Compute checksum of groundtruth 2D vector
447596inline static uint64_t groundtruth_checksum (const std::vector<std::vector<uint32_t >>& gt) {
448597 uint64_t hash = 14695981039346656037ULL ;
449598 for (const auto & row : gt) {
0 commit comments