Skip to content

Commit 61d6240

Browse files
committed
feat(bench): add bench_static_data and bench_dynamic_data benchmarks
1 parent d80e787 commit 61d6240

23 files changed

Lines changed: 2967 additions & 175 deletions

cpp/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,5 @@ endif()
104104
if (ENABLE_BENCHMARKS)
105105
add_subdirectory(external/fmt)
106106
add_subdirectory(benchmark)
107+
add_subdirectory(bench)
107108
endif()

cpp/bench/CMakeLists.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
cmake_minimum_required(VERSION 3.19)
2+
3+
project(benchmarks)
4+
5+
include_directories(${PROJECT_SOURCE_DIR}/include)
6+
7+
set(DATA_PATH "" CACHE PATH "Path to benchmark data sets")
8+
if(DATA_PATH STREQUAL "")
9+
message(WARNING "DATA_PATH is empty for benchmarks")
10+
endif()
11+
12+
add_executable(bench_static_data ${PROJECT_SOURCE_DIR}/src/bench_static_data.cpp)
13+
target_include_directories(bench_static_data PRIVATE ${PROJECT_SOURCE_DIR}/include)
14+
target_link_libraries(bench_static_data PUBLIC DEG_LIB compile-options fmt::fmt-header-only)
15+
target_compile_definitions(bench_static_data PUBLIC "DATA_PATH=\"${DATA_PATH}\"")
16+
17+
add_executable(bench_dynamic_data ${PROJECT_SOURCE_DIR}/src/bench_dynamic_data.cpp)
18+
target_include_directories(bench_dynamic_data PRIVATE ${PROJECT_SOURCE_DIR}/include)
19+
target_link_libraries(bench_dynamic_data PUBLIC DEG_LIB compile-options fmt::fmt-header-only)
20+
target_compile_definitions(bench_dynamic_data PUBLIC "DATA_PATH=\"${DATA_PATH}\"")

cpp/bench/include/benchmark.h

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
#pragma once
2+
3+
/**
4+
* @file benchmark.h
5+
* @brief Main benchmark utilities for deglib without OpenMP dependency.
6+
*/
7+
8+
#include <fmt/base.h>
9+
#include <fmt/format.h>
10+
#include <fmt/ostream.h>
11+
#include <fmt/ranges.h>
12+
13+
#include <atomic>
14+
#include <filesystem>
15+
#include <fstream>
16+
#include <limits>
17+
#include <random>
18+
19+
#include "dataset.h"
20+
#include "deglib.h"
21+
#include "file_io.h"
22+
#include "logging.h"
23+
#include "stats.h"
24+
#include "stopwatch.h"
25+
26+
namespace deglib::benchmark {
27+
28+
/**
29+
* @brief Compute baseline time per query using linear distance computation.
30+
*/
31+
static uint64_t compute_linear_search_baseline(const deglib::FeatureRepository& base_repository,
32+
const deglib::Metric metric,
33+
const uint32_t sample_size = 100,
34+
const deglib::cpu::InstructionSet instruction = deglib::cpu::InstructionSet::Auto) {
35+
const auto dims = base_repository.dims();
36+
const auto base_size = base_repository.size();
37+
const auto feature_space = deglib::FloatSpace(dims, metric, instruction);
38+
const auto dist_func = feature_space.get_dist_func();
39+
const auto dist_func_param = feature_space.get_dist_func_param();
40+
41+
const auto query_count = std::min(sample_size, (uint32_t)base_size);
42+
43+
std::vector<uint32_t> query_indices(query_count);
44+
std::mt19937 rng(7);
45+
std::uniform_int_distribution<uint32_t> dist(0, (uint32_t)base_size - 1);
46+
for (uint32_t i = 0; i < query_count; i++) {
47+
query_indices[i] = dist(rng);
48+
}
49+
50+
log("Computing linear search baseline with {} random queries on {} base vectors...\n", query_count, base_size);
51+
52+
StopW stopw = StopW();
53+
54+
float min_dist = std::numeric_limits<float>::max();
55+
for (uint32_t q = 0; q < query_count; q++) {
56+
const auto query = base_repository.getFeature(query_indices[q]);
57+
58+
for (uint32_t i = 0; i < base_size; i++) {
59+
const auto base_feature = base_repository.getFeature(i);
60+
const auto d = dist_func(query, base_feature, dist_func_param);
61+
if (d < min_dist) min_dist = d;
62+
}
63+
}
64+
65+
const uint64_t total_time_us = stopw.getElapsedTimeMicro();
66+
const uint64_t time_per_query_us = total_time_us / query_count;
67+
68+
log("Linear search baseline: {}us per query (total: {}ms for {} queries) with min distance {}\n",
69+
time_per_query_us,
70+
total_time_us / 1000,
71+
query_count,
72+
min_dist);
73+
74+
return time_per_query_us;
75+
}
76+
77+
static float test_approx_anns(const deglib::search::SearchGraph& graph,
78+
const std::vector<uint32_t>& entry_vertex_indices,
79+
const deglib::FeatureRepository& query_repository,
80+
const std::vector<std::vector<uint32_t>>& ground_truth,
81+
const float eps,
82+
const uint32_t k,
83+
const uint32_t test_size,
84+
const uint32_t threads,
85+
const deglib::graph::Filter* filter = nullptr) {
86+
auto corrects = std::vector<float>(threads);
87+
deglib::concurrent::parallel_for(0, test_size, threads, [&](size_t i, size_t thread_id) {
88+
auto query = reinterpret_cast<const std::byte*>(query_repository.getFeature(uint32_t(i)));
89+
auto result_queue = graph.search(entry_vertex_indices, query, eps, k, filter);
90+
91+
if (result_queue.size() != k) {
92+
fmt::print(stderr, "ANNS with k={} got only {} results for query {}\n", k, result_queue.size(), i);
93+
abort();
94+
}
95+
96+
uint32_t correct = 0;
97+
const auto& gt = ground_truth[i];
98+
while (result_queue.empty() == false) {
99+
const auto& result = result_queue.top();
100+
const auto external_id = graph.getExternalLabel(result.getInternalIndex());
101+
if (std::binary_search(gt.begin(), gt.end(), external_id)) correct++;
102+
result_queue.pop();
103+
}
104+
105+
corrects[thread_id] += correct;
106+
});
107+
108+
float total_correct = 0;
109+
for (size_t i = 0; i < threads; i++) total_correct += corrects[i];
110+
return total_correct / (test_size * k);
111+
}
112+
113+
static float test_approx_explore(const deglib::search::SearchGraph& graph,
114+
const std::vector<std::vector<uint32_t>>& entry_vertex_indices,
115+
const bool include_entry,
116+
const std::vector<std::vector<uint32_t>>& ground_truth,
117+
const uint32_t k,
118+
const uint32_t max_distance_count,
119+
const uint32_t threads,
120+
const deglib::graph::Filter* filter = nullptr) {
121+
auto corrects = std::vector<float>(threads);
122+
deglib::concurrent::parallel_for(0, entry_vertex_indices.size(), threads, [&](size_t i, size_t thread_id) {
123+
const auto entry_vertex_index = entry_vertex_indices[i][0];
124+
auto result_queue = graph.explore(entry_vertex_index, k, include_entry, max_distance_count);
125+
126+
if (result_queue.size() != k) {
127+
fmt::print(stderr,
128+
"Exploration with k={} got only {} results for query {} and max_distance_count {}\n",
129+
k,
130+
result_queue.size(),
131+
i,
132+
max_distance_count);
133+
abort();
134+
}
135+
136+
uint32_t correct = 0;
137+
const auto& gt = ground_truth[i];
138+
while (result_queue.empty() == false) {
139+
const auto& result = result_queue.top();
140+
const auto external_id = graph.getExternalLabel(result.getInternalIndex());
141+
if (std::binary_search(gt.begin(), gt.end(), external_id)) correct++;
142+
result_queue.pop();
143+
}
144+
145+
corrects[thread_id] += correct;
146+
});
147+
148+
float total_correct = 0;
149+
for (size_t i = 0; i < threads; i++) total_correct += corrects[i];
150+
return total_correct / (entry_vertex_indices.size() * k);
151+
}
152+
153+
static std::vector<float> estimate_recall(const deglib::search::SearchGraph& graph,
154+
const deglib::FeatureRepository& query_repository,
155+
const std::vector<std::vector<uint32_t>>& answer,
156+
const uint32_t max_distance_count,
157+
const uint32_t k) {
158+
const auto entry_vertex_indices = std::vector<uint32_t>{graph.getInternalIndex(0)};
159+
160+
std::vector<float> recalls;
161+
std::vector<float> eps_parameter = {0.1f, 0.2f};
162+
const uint32_t threads = std::thread::hardware_concurrency() / 2;
163+
164+
for (float eps : eps_parameter) {
165+
std::atomic<size_t> total{0};
166+
std::atomic<size_t> correct{0};
167+
168+
deglib::concurrent::parallel_for(0, query_repository.size(), threads, [&](size_t i, size_t thread_id) {
169+
auto query = reinterpret_cast<const std::byte*>(query_repository.getFeature(uint32_t(i)));
170+
auto result_queue = graph.search(entry_vertex_indices, query, eps, k, nullptr, max_distance_count);
171+
172+
const auto& gt = answer[i];
173+
total += result_queue.size();
174+
175+
size_t local_correct = 0;
176+
while (result_queue.empty() == false) {
177+
const auto internal_index = result_queue.top().getInternalIndex();
178+
const auto external_id = graph.getExternalLabel(internal_index);
179+
if (std::binary_search(gt.begin(), gt.end(), external_id)) local_correct++;
180+
result_queue.pop();
181+
}
182+
correct += local_correct;
183+
});
184+
185+
const auto precision = ((float)correct.load()) / total.load();
186+
recalls.push_back(precision);
187+
}
188+
189+
return recalls;
190+
}
191+
192+
static void test_graph_anns(const deglib::search::SearchGraph& graph,
193+
const deglib::FeatureRepository& query_repository,
194+
const std::vector<std::vector<uint32_t>>& ground_truth,
195+
const uint32_t repeat,
196+
const uint32_t threads,
197+
const uint32_t k,
198+
const std::vector<float>& eps_parameter,
199+
const deglib::graph::Filter* filter = nullptr,
200+
const uint64_t linear_baseline_us = 0,
201+
const uint32_t abort_sample_size = 100) {
202+
const auto entry_vertex_indices = graph.getEntryVertexIndices();
203+
204+
std::vector<float> eps_parameter_sorted = eps_parameter;
205+
std::sort(eps_parameter_sorted.begin(), eps_parameter_sorted.end());
206+
log("Compute TOP{} for eps {}\n", k, fmt::join(eps_parameter_sorted, ", "));
207+
if (linear_baseline_us > 0) {
208+
log("Early abort enabled: baseline {}us/query, checking after {} queries\n", linear_baseline_us, abort_sample_size);
209+
}
210+
211+
log("Internal seed id {} \n", entry_vertex_indices[0]);
212+
log("Actual memory usage: {} Mb\n", getCurrentRSS() / 1000000);
213+
log("Max memory usage: {} Mb\n", getPeakRSS() / 1000000);
214+
215+
const auto test_size = uint32_t(query_repository.size());
216+
for (float eps : eps_parameter_sorted) {
217+
if (linear_baseline_us > 0 && test_size > abort_sample_size) {
218+
const auto sample_size = std::min(abort_sample_size, test_size);
219+
StopW sample_stopw = StopW();
220+
deglib::benchmark::test_approx_anns(
221+
graph, entry_vertex_indices, query_repository, ground_truth, eps, k, sample_size, threads, filter);
222+
const uint64_t sample_time_us = sample_stopw.getElapsedTimeMicro();
223+
const uint64_t sample_time_per_query = sample_time_us / sample_size;
224+
225+
if (sample_time_per_query > linear_baseline_us) {
226+
log("eps {:.3f} \t ABORTED ({}us/query > {}us baseline after {} queries)\n",
227+
eps,
228+
sample_time_per_query,
229+
linear_baseline_us,
230+
sample_size);
231+
return;
232+
}
233+
}
234+
235+
StopW stopw = StopW();
236+
float recall = 0;
237+
for (size_t i = 0; i < repeat; i++)
238+
recall = deglib::benchmark::test_approx_anns(
239+
graph, entry_vertex_indices, query_repository, ground_truth, eps, k, test_size, threads, filter);
240+
uint64_t search_time_us = stopw.getElapsedTimeMicro();
241+
uint64_t time_us_per_query = (search_time_us / test_size) / repeat;
242+
243+
log("eps {:.3f} \t recall {:.5f} \t time_us_per_query {:6}us \t search time: {:6}ms\n",
244+
eps,
245+
recall,
246+
time_us_per_query,
247+
search_time_us / 1000);
248+
if (recall > 0.997) {
249+
log("Reached recall > 0.997, stopping further tests.\n");
250+
return;
251+
}
252+
}
253+
}
254+
255+
static void test_graph_explore(const deglib::search::SearchGraph& graph,
256+
const std::vector<uint32_t>& entry_vertex_labels,
257+
const std::vector<std::vector<uint32_t>>& ground_truth,
258+
const bool include_entry,
259+
const uint32_t repeat,
260+
const uint32_t k,
261+
const uint32_t threads,
262+
const deglib::graph::Filter* filter = nullptr,
263+
const uint32_t explore_depth = 3,
264+
const uint64_t linear_baseline_us = 0,
265+
const uint32_t abort_sample_size = 100,
266+
const float recall_target = 0.997f) {
267+
if (entry_vertex_labels.size() != ground_truth.size()) {
268+
fmt::print(stderr, "Entry vertex count {} does not match ground truth count {}\n", entry_vertex_labels.size(), ground_truth.size());
269+
abort();
270+
}
271+
272+
const uint32_t query_count = (uint32_t)entry_vertex_labels.size();
273+
274+
auto entry_vertex_indices = std::vector<std::vector<uint32_t>>(query_count);
275+
for (size_t i = 0; i < query_count; i++) {
276+
entry_vertex_indices[i].push_back(graph.getInternalIndex(entry_vertex_labels[i]));
277+
}
278+
279+
if (linear_baseline_us > 0) {
280+
log("Early abort enabled: baseline {}us/query, checking after {} queries\n", linear_baseline_us, abort_sample_size);
281+
}
282+
283+
log("Actual memory usage: {} Mb\n", getCurrentRSS() / 1000000);
284+
log("Max memory usage: {} Mb\n", getPeakRSS() / 1000000);
285+
286+
uint32_t k_factor = 100;
287+
float last_recall = -1.0f;
288+
for (uint32_t f = 0; f <= explore_depth; f++, k_factor *= 10) {
289+
for (uint32_t i = (f == 0) ? 1 : 2; i < 11; i++) {
290+
const auto max_distance_count = ((f == 0) ? (k + k_factor * (i - 1)) : (k_factor * i));
291+
292+
if (linear_baseline_us > 0 && query_count > abort_sample_size) {
293+
const auto sample_size = std::min(abort_sample_size, query_count);
294+
295+
auto sample_entry_indices =
296+
std::vector<std::vector<uint32_t>>(entry_vertex_indices.begin(), entry_vertex_indices.begin() + sample_size);
297+
auto sample_ground_truth = std::vector<std::vector<uint32_t>>(ground_truth.begin(), ground_truth.begin() + sample_size);
298+
StopW sample_stopw = StopW();
299+
deglib::benchmark::test_approx_explore(
300+
graph, sample_entry_indices, include_entry, sample_ground_truth, k, max_distance_count, threads, filter);
301+
const uint64_t sample_time_us = sample_stopw.getElapsedTimeMicro();
302+
const uint64_t sample_time_per_query = sample_time_us / sample_size;
303+
304+
if (sample_time_per_query > linear_baseline_us) {
305+
log("max_distance_count {:5}, k {:4}, ABORTED ({}us/query > {}us baseline after {} queries)\n",
306+
max_distance_count,
307+
k,
308+
sample_time_per_query,
309+
linear_baseline_us,
310+
sample_size);
311+
return;
312+
}
313+
}
314+
315+
StopW stopw = StopW();
316+
float recall = 0;
317+
for (size_t r = 0; r < repeat; r++)
318+
recall = deglib::benchmark::test_approx_explore(
319+
graph, entry_vertex_indices, include_entry, ground_truth, k, max_distance_count, threads, filter);
320+
uint64_t search_time_us = stopw.getElapsedTimeMicro();
321+
uint64_t time_us_per_query = search_time_us / (query_count * repeat);
322+
323+
log("k {:5}, max_distance_count {:6}, recall {:.4f}, time_us_per_query {:6}\n",
324+
k,
325+
max_distance_count,
326+
recall,
327+
time_us_per_query);
328+
329+
if (recall == last_recall) {
330+
log("Recall stabilized at {:.4f}, stopping exploration sweep\n", recall);
331+
return;
332+
}
333+
last_recall = recall;
334+
335+
if (recall >= recall_target) {
336+
log("Recall target {:.3f} reached, stopping exploration sweep\n", recall_target);
337+
return;
338+
}
339+
}
340+
}
341+
}
342+
343+
} // namespace deglib::benchmark

0 commit comments

Comments
 (0)