Skip to content

Commit 03abd45

Browse files
committed
fix(builder): ensure cross-OS determinism using portable random distributions
1 parent 6eded88 commit 03abd45

12 files changed

Lines changed: 1043 additions & 349 deletions

‎cpp/benchmark/src/deglib_anns_bench.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ void filter_test() {
1717

1818
// Pre-generate random query IDs for is_valid tests
1919
std::mt19937 rng(std::random_device{}());
20-
std::uniform_int_distribution<int> dist(0, static_cast<int>(max_value));
20+
deglib::random::DeterministicUniformIntDistribution<int> dist(0, static_cast<int>(max_value));
2121
size_t num_queries = 100'000;
2222
std::vector<int> query_ids(num_queries);
2323
for (size_t i = 0; i < num_queries; ++i) {

‎cpp/benchmark/src/deglib_build_bench.cpp‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,13 +308,13 @@ void create_random_exploration_graph(const std::string repository_file, const st
308308

309309
// random order of vertices
310310
auto rnd = std::mt19937(7);
311-
auto rnd_neighbor = std::uniform_int_distribution<uint32_t>(0, edges_per_vertex - 1);
311+
auto rnd_neighbor = deglib::random::DeterministicUniformIntDistribution<uint32_t>(0, edges_per_vertex - 1);
312312

313313
// add the remaining vertices
314314
for (uint32_t label = edges_per_vertex + 1; label < vertex_count; label++) {
315315
const auto new_vertex_feature = repository.getFeature(label);
316316
const auto internal_index = graph.addVertex(label, new_vertex_feature);
317-
auto top_list = std::uniform_int_distribution<uint32_t>(0, label - 1);
317+
auto top_list = deglib::random::DeterministicUniformIntDistribution<uint32_t>(0, label - 1);
318318

319319
// remove the worst edge of the good neighbors and connect them with this new vertex
320320
auto new_neighbors = std::vector<std::pair<uint32_t, float>>();

‎cpp/deglib/include/builder.h‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "concurrent.h"
1717
#include "analysis.h"
1818
#include "graph.h"
19+
#include "random.h"
1920

2021
namespace deglib::builder
2122
{
@@ -516,7 +517,7 @@ class EvenRegularGraphBuilder {
516517
const auto edges_per_vertex = uint32_t(graph.getEdgesPerVertex());
517518

518519
// find good neighbor candidates for the new vertex
519-
auto distrib = std::uniform_int_distribution<uint32_t>(0, uint32_t(graph.size() - 1));
520+
auto distrib = deglib::random::DeterministicUniformIntDistribution<uint32_t>(0, uint32_t(graph.size() - 1));
520521
const std::vector<uint32_t> entry_vertex_indices = { distrib(this->rnd_) };
521522
auto top_list = graph.search(entry_vertex_indices, new_vertex_feature, this->extend_eps_, std::max(uint32_t(this->extend_k_), edges_per_vertex*2)); // need 2x otherwise it might lock during neighbor selection
522523
const auto candidates = topListAscending(top_list);
@@ -631,8 +632,7 @@ class EvenRegularGraphBuilder {
631632
const auto dist_func_param = graph.getFeatureSpace().get_dist_func_param();
632633

633634
// find good neighbors for the new vertex
634-
auto distrib = std::uniform_int_distribution<uint32_t>(0, uint32_t(graph.size() - 1));
635-
const std::vector<uint32_t> entry_vertex_indices = { distrib(this->rnd_) };
635+
const std::vector<uint32_t> entry_vertex_indices = { 0 };
636636
auto top_list = graph.search(entry_vertex_indices, new_vertex_feature, this->extend_eps_, std::max(uint32_t(this->extend_k_), edges_per_vertex));
637637
const auto results = topListAscending(top_list);
638638

@@ -1607,7 +1607,7 @@ class EvenRegularGraphBuilder {
16071607
auto& graph = this->graph_;
16081608

16091609
// Select a random vertex3
1610-
auto distrib = std::uniform_int_distribution<uint32_t>(0, uint32_t(graph.size() - 1));
1610+
auto distrib = deglib::random::DeterministicUniformIntDistribution<uint32_t>(0, uint32_t(graph.size() - 1));
16111611
uint32_t vertex3 = distrib(this->rnd_);
16121612

16131613
if (vertex3 == vertex1 || vertex3 == vertex2) return false;
@@ -1617,7 +1617,7 @@ class EvenRegularGraphBuilder {
16171617
const auto neighbor_weights = graph.getNeighborWeights(vertex3);
16181618
const auto edges_per_vertex = graph.getEdgesPerVertex();
16191619

1620-
auto distrib_neighbor = std::uniform_int_distribution<uint32_t>(0, edges_per_vertex - 1);
1620+
auto distrib_neighbor = deglib::random::DeterministicUniformIntDistribution<uint32_t>(0, edges_per_vertex - 1);
16211621
uint32_t neighbor_idx = distrib_neighbor(this->rnd_);
16221622

16231623
uint32_t vertex4 = neighbor_indices[neighbor_idx];
@@ -1687,7 +1687,7 @@ class EvenRegularGraphBuilder {
16871687
const auto edges_per_vertex = graph.getEdgesPerVertex();
16881688

16891689
// 1.1 select a random vertex
1690-
auto distrib = std::uniform_int_distribution<uint32_t>(0, uint32_t(graph.size() - 1));
1690+
auto distrib = deglib::random::DeterministicUniformIntDistribution<uint32_t>(0, uint32_t(graph.size() - 1));
16911691
uint32_t vertex1 = distrib(this->rnd_);
16921692

16931693
// 1.2 find the worst edge of this vertex
@@ -1696,7 +1696,7 @@ class EvenRegularGraphBuilder {
16961696
auto success = false;
16971697

16981698
// real random edges swaps
1699-
//auto distrib_neighbor = std::uniform_int_distribution<uint32_t>(0, edges_per_vertex - 1);
1699+
//auto distrib_neighbor = deglib::random::DeterministicUniformIntDistribution<uint32_t>(0, edges_per_vertex - 1);
17001700
//uint32_t neighbor_idx = distrib_neighbor(this->rnd_);
17011701
//success |= simpleEdgeSwaps(vertex1, neighbor_indices[neighbor_idx], neighbor_weights[neighbor_idx]);
17021702

‎cpp/deglib/include/deglib.h‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@
1010
#include "analysis.h"
1111
#include "graph/readonly_graph.h"
1212
#include "graph/sizebounded_graph.h"
13-
#include "builder.h"
13+
#include "builder.h"
14+
#include "random.h"

‎cpp/deglib/include/random.h‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#pragma once
2+
3+
#include <cmath>
4+
#include <cstdint>
5+
#include <random>
6+
#include <type_traits>
7+
8+
// Portable random distributions that produce identical sequences across
9+
// compilers and platforms (Windows/MSVC, Linux/GCC, macOS/Clang).
10+
//
11+
// std::uniform_int_distribution and std::normal_distribution are NOT portable:
12+
// - MSVC STL, libstdc++, and libc++ each use different algorithm implementations
13+
// - The same seed produces different sequences on different platforms
14+
// - This makes regression tests and graph builds non-deterministic across CI runners
15+
//
16+
// Both distributions operate on std::mt19937 (which IS portable — same seed
17+
// produces the same uniform sequence) and use deterministic algorithms:
18+
// - DeterministicUniformIntDistribution: bounded rejection sampling
19+
// - DeterministicNormalDistribution: Box-Muller transform
20+
//
21+
// Reference: Box, G.E.P. and Muller, M.E. (1958), "A Note on the Generation
22+
// of Random Normal Deviates", Annals of Mathematical Statistics, 29(2), pp.610-611.
23+
24+
namespace deglib::random {
25+
26+
/// Portable uniform integer distribution using bounded rejection sampling.
27+
/// Guarantees identical integer sequences across Windows, Linux, and macOS.
28+
template <typename IntType = int>
29+
class DeterministicUniformIntDistribution {
30+
public:
31+
DeterministicUniformIntDistribution(IntType min_val, IntType max_val)
32+
: min_val_(min_val), max_val_(max_val) {
33+
if (min_val_ < max_val_) {
34+
range_ = static_cast<uint64_t>(max_val_) - static_cast<uint64_t>(min_val_) + 1;
35+
max_valid_ = (4294967296ULL / range_) * range_ - 1;
36+
} else {
37+
range_ = 1;
38+
max_valid_ = 4294967295ULL;
39+
}
40+
}
41+
42+
template <typename Generator>
43+
IntType operator()(Generator& g) {
44+
if (range_ <= 1) {
45+
return min_val_;
46+
}
47+
48+
uint32_t val;
49+
do {
50+
val = g();
51+
} while (static_cast<uint64_t>(val) > max_valid_);
52+
53+
return static_cast<IntType>(min_val_ + (val % range_));
54+
}
55+
56+
private:
57+
IntType min_val_;
58+
IntType max_val_;
59+
uint64_t range_{1};
60+
uint64_t max_valid_{4294967295ULL};
61+
};
62+
63+
/// Portable normal distribution using the Box-Muller transform.
64+
/// Always consumes exactly 2 RNG values per call (no caching/spare state)
65+
/// to ensure deterministic behavior regardless of how multiple instances
66+
/// share the same RNG.
67+
class DeterministicNormalDistribution {
68+
public:
69+
explicit DeterministicNormalDistribution(float mean, float stddev)
70+
: mean_(mean), stddev_(stddev) {}
71+
72+
// Generate a single normally-distributed float using the provided RNG.
73+
// Always consumes exactly 2 RNG values per call (no caching/spare state)
74+
// to ensure deterministic behavior regardless of how multiple
75+
// DeterministicNormalDistribution instances share the same RNG.
76+
float operator()(std::mt19937& rng) {
77+
// Box-Muller transform: generate one standard normal from two uniforms.
78+
// We discard the second value (z1) to avoid cross-instance state issues.
79+
float u1, u2;
80+
do {
81+
u1 = static_cast<float>(static_cast<double>(rng()) / (static_cast<double>(rng.max()) + 1.0));
82+
} while (u1 <= 0.0f);
83+
u2 = static_cast<float>(static_cast<double>(rng()) / (static_cast<double>(rng.max()) + 1.0));
84+
85+
// Box-Muller formula: z0 = sqrt(-2 * ln(u1)) * cos(2 * pi * u2)
86+
float mag = stddev_ * std::sqrt(-2.0f * std::log(u1));
87+
return mean_ + mag * std::cos(2.0f * 3.14159265358979323846f * u2);
88+
}
89+
90+
private:
91+
float mean_;
92+
float stddev_;
93+
};
94+
95+
} // namespace deglib::random

‎cpp/test/src/regression/builder/test_l2_builder_regression.cpp‎

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// Tests all three deglib::builder::OptimizationTarget modes:
55
// - LowLID, HighLID, StreamingData
66

7-
TEST(DeglibBuilderRegressionL2, OptimizationTargetsBenchmark)
7+
TEST(DeglibBuilderRegressionL2, Benchmark_LowLID)
88
{
99
const size_t dim = 128;
1010
const size_t base_count = 100000;
@@ -17,17 +17,45 @@ TEST(DeglibBuilderRegressionL2, OptimizationTargetsBenchmark)
1717

1818
auto gt_data = compute_groundtruth_l2(base_data, base_count, query_data, query_count, dim, 10);
1919

20-
run_regression_test("LowLID", deglib::Metric::L2, 26000.0, 10.0, 0.959,
20+
run_regression_test("LowLID", deglib::Metric::L2, 30000.0, 9.3, 0.92,
2121
base_data.data(), query_data.data(), base_count, query_count, dim, gt_data,
2222
deglib::distances::fp32_l2::L2Float{}, 100,
2323
deglib::builder::OptimizationTarget::LowLID);
24+
}
25+
26+
TEST(DeglibBuilderRegressionL2, Benchmark_HighLID)
27+
{
28+
const size_t dim = 128;
29+
const size_t base_count = 100000;
30+
const size_t query_count = 100;
31+
const size_t num_clusters = 1000;
32+
33+
std::vector<float> base_data;
34+
std::vector<float> query_data;
35+
generate_synthetic_clustered_dataset(base_count, dim, base_data, query_data, query_count, num_clusters);
36+
37+
auto gt_data = compute_groundtruth_l2(base_data, base_count, query_data, query_count, dim, 10);
2438

25-
run_regression_test("HighLID", deglib::Metric::L2, 15000.0, 10.0, 0.888,
39+
run_regression_test("HighLID", deglib::Metric::L2, 15000.0, 10.0, 0.864,
2640
base_data.data(), query_data.data(), base_count, query_count, dim, gt_data,
2741
deglib::distances::fp32_l2::L2Float{}, 100,
2842
deglib::builder::OptimizationTarget::HighLID);
43+
}
44+
45+
TEST(DeglibBuilderRegressionL2, Benchmark_StreamingData)
46+
{
47+
const size_t dim = 128;
48+
const size_t base_count = 100000;
49+
const size_t query_count = 100;
50+
const size_t num_clusters = 1000;
51+
52+
std::vector<float> base_data;
53+
std::vector<float> query_data;
54+
generate_synthetic_clustered_dataset(base_count, dim, base_data, query_data, query_count, num_clusters);
55+
56+
auto gt_data = compute_groundtruth_l2(base_data, base_count, query_data, query_count, dim, 10);
2957

30-
run_regression_test("StreamingData", deglib::Metric::L2, 24000.0, 24.0, 0.96,
58+
run_regression_test("StreamingData", deglib::Metric::L2, 25000.0, 23.8, 0.95,
3159
base_data.data(), query_data.data(), base_count, query_count, dim, gt_data,
3260
deglib::distances::fp32_l2::L2Float{}, 100,
3361
deglib::builder::OptimizationTarget::StreamingData);

‎cpp/test/src/regression/deterministic_normal_distribution.h‎

Lines changed: 0 additions & 45 deletions
This file was deleted.

‎cpp/test/src/regression/deterministic_uniform_int_distribution.h‎

Lines changed: 0 additions & 48 deletions
This file was deleted.

0 commit comments

Comments
 (0)