Skip to content

Commit a29019b

Browse files
committed
refactor(deglib): unify explore into search_intern and add std::span bounds-checked API
- Replace public raw-pointer search with bounds-checked std::span<const float> API. - Eliminate duplicated exploreImpl code across SizeBoundedGraph and ReadOnlyGraph. - Unify graph traversal into single searchImpl template - Standardize parameter ordering across search() and explore(): (target, k, eps, include_entry, filter, max_dist). - Grant friend access to EvenRegularGraphBuilder for protected search_intern calls.
1 parent 1daa370 commit a29019b

13 files changed

Lines changed: 150 additions & 325 deletions

File tree

cpp/bench/include/benchmark.h

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <fmt/ranges.h>
1212

1313
#include <atomic>
14+
#include <span>
1415
#include <filesystem>
1516
#include <fstream>
1617
#include <limits>
@@ -85,8 +86,8 @@ static float test_approx_anns(const deglib::search::SearchGraph& graph,
8586
const deglib::graph::Filter* filter = nullptr) {
8687
auto corrects = std::vector<float>(threads);
8788
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);
89+
auto query = reinterpret_cast<const float*>(query_repository.getFeature(uint32_t(i)));
90+
auto result_queue = graph.search(std::span<const float>(query, graph.getFeatureSpace().dim()), k, eps, filter);
9091

9192
if (result_queue.size() != k) {
9293
fmt::print(stderr, "ANNS with k={} got only {} results for query {}\n", k, result_queue.size(), i);
@@ -121,7 +122,7 @@ static float test_approx_explore(const deglib::search::SearchGraph& graph,
121122
auto corrects = std::vector<float>(threads);
122123
deglib::concurrent::parallel_for(0, entry_vertex_indices.size(), threads, [&](size_t i, size_t thread_id) {
123124
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+
auto result_queue = graph.explore(entry_vertex_index, k, max_distance_count, 0.0f, include_entry);
125126

126127
if (result_queue.size() != k) {
127128
fmt::print(stderr,
@@ -166,8 +167,8 @@ static std::vector<float> estimate_recall(const deglib::search::SearchGraph& gra
166167
std::atomic<size_t> correct{0};
167168

168169
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);
170+
auto query = reinterpret_cast<const float*>(query_repository.getFeature(uint32_t(i)));
171+
auto result_queue = graph.search(std::span<const float>(query, graph.getFeatureSpace().dim()), k, eps, nullptr, max_distance_count);
171172

172173
const auto& gt = answer[i];
173174
total += result_queue.size();

cpp/benchmark/include/benchmark.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <fmt/base.h>
77
#include <fmt/format.h>
88
#include <fmt/ranges.h>
9+
#include <span>
910

1011
#include <deglib/deglib.h>
1112
#include "stopwatch.h"
@@ -40,8 +41,8 @@ static float test_approx_anns(const deglib::search::SearchGraph& graph, const st
4041
auto corrects = std::vector<float>(threads);
4142
deglib::concurrent::parallel_for(0, test_size, threads, [&] (size_t i, size_t thread_id) {
4243

43-
auto query = reinterpret_cast<const std::byte*>(query_repository.getFeature(uint32_t(i)));
44-
auto result_queue = graph.search(entry_vertex_indices, query, eps, k, filter);
44+
auto query = reinterpret_cast<const float*>(query_repository.getFeature(uint32_t(i)));
45+
auto result_queue = graph.search(std::span<const float>(query, graph.getFeatureSpace().dim()), k, eps, filter);
4546

4647
if (result_queue.size() != k) {
4748
fmt::print(stderr, "ANNS with k={} got only {} results for query {}\n", k, result_queue.size(), i);
@@ -76,7 +77,7 @@ static float test_approx_explore(const deglib::search::SearchGraph& graph, const
7677
deglib::concurrent::parallel_for(0, entry_vertex_indices.size(), threads, [&] (size_t i, size_t thread_id) {
7778

7879
const auto entry_vertex_index = entry_vertex_indices[i][0];
79-
auto result_queue = graph.explore(entry_vertex_index, k, include_entry, max_distance_count); // TODO missing filter
80+
auto result_queue = graph.explore(entry_vertex_index, k, max_distance_count, 0.0f, include_entry); // TODO missing filter
8081

8182
if (result_queue.size() != k) {
8283
fmt::print(stderr, "Exploration with k={} got only {} results for query {}\n", k, result_queue.size(), i);

cpp/benchmark/src/deglib_build_bench.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <unordered_set>
44
#include <filesystem>
55
#include <atomic>
6+
#include <span>
67

78
#include <fmt/core.h>
89

@@ -632,8 +633,8 @@ static std::vector<float> estimate_recall(const deglib::search::SearchGraph& gra
632633
deglib::concurrent::parallel_for(0, query_repository.size(), threads, [&](size_t i_idx, size_t)
633634
{
634635
const int i = (int)i_idx;
635-
auto query = reinterpret_cast<const std::byte*>(query_repository.getFeature(uint32_t(i)));
636-
auto result_queue = graph.search(entry_vertex_indices, query, eps, k, nullptr, max_distance_count);
636+
auto query = reinterpret_cast<const float*>(query_repository.getFeature(uint32_t(i)));
637+
auto result_queue = graph.search(std::span<const float>(query, graph.getFeatureSpace().dim()), k, eps, nullptr, max_distance_count);
637638

638639
const auto& gt = answer[i];
639640
total.fetch_add(result_queue.size(), std::memory_order_relaxed);

cpp/deglib/include/deglib/builder.h

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ class EvenRegularGraphBuilder {
520520
// find good neighbor candidates for the new vertex
521521
auto distrib = deglib::random::DeterministicUniformIntDistribution<uint32_t>(0, uint32_t(graph.size() - 1));
522522
const std::vector<uint32_t> entry_vertex_indices = { distrib(this->rnd_) };
523-
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
523+
auto top_list = graph.search_intern(entry_vertex_indices, new_vertex_feature, std::max(uint32_t(this->extend_k_), edges_per_vertex*2), this->extend_eps_); // need 2x otherwise it might lock during neighbor selection
524524
const auto candidates = topListAscending(top_list);
525525

526526
// their should always be enough neighbors (search candidates), otherwise the graph would be broken
@@ -634,7 +634,7 @@ class EvenRegularGraphBuilder {
634634

635635
// find good neighbors for the new vertex
636636
const std::vector<uint32_t> entry_vertex_indices = { 0 };
637-
auto top_list = graph.search(entry_vertex_indices, new_vertex_feature, this->extend_eps_, std::max(uint32_t(this->extend_k_), edges_per_vertex));
637+
auto top_list = graph.search_intern(entry_vertex_indices, new_vertex_feature, std::max(uint32_t(this->extend_k_), edges_per_vertex), this->extend_eps_);
638638
const auto results = topListAscending(top_list);
639639

640640
// their should always be enough neighbors (search results), otherwise the graph would be broken
@@ -1153,7 +1153,7 @@ class EvenRegularGraphBuilder {
11531153
{
11541154
const auto vertex2_feature = graph.getFeatureVector(vertex2);
11551155
const std::vector<uint32_t> entry_vertex_indices = { vertex3, vertex4 };
1156-
auto top_list = graph.search(entry_vertex_indices, vertex2_feature, this->improve_eps_, improve_k_);
1156+
auto top_list = graph.search_intern(entry_vertex_indices, vertex2_feature, improve_k_, this->improve_eps_);
11571157

11581158
// find a good new vertex3
11591159
float best_gain = total_gain;
@@ -1228,7 +1228,7 @@ class EvenRegularGraphBuilder {
12281228
// find a good (not yet connected) vertex for vertex1/vertex4
12291229
const std::vector<uint32_t> entry_vertex_indices = { vertex2, vertex3 };
12301230
const auto vertex4_feature = graph.getFeatureVector(vertex4);
1231-
auto top_list = graph.search(entry_vertex_indices, vertex4_feature, this->improve_eps_, improve_k_);
1231+
auto top_list = graph.search_intern(entry_vertex_indices, vertex4_feature, improve_k_, this->improve_eps_);
12321232

12331233
float best_gain = 0;
12341234
uint32_t best_selected_neighbor = 0;
@@ -1420,7 +1420,7 @@ class EvenRegularGraphBuilder {
14201420
// RangeSearch from (a) to target (b): find good (e) and its neighbor (f) with
14211421
// e != b, f != b, and not adjacent to b: (N(b) ∩ {e,f} = ∅)
14221422
const auto b_feat = graph.getFeatureVector(b);
1423-
auto rs = graph.search({ s,t }, b_feat, eps, k);
1423+
auto rs = graph.search_intern({ s,t }, b_feat, k, eps);
14241424
float best_delta = std::numeric_limits<float>::lowest();
14251425
uint32_t best_e = 0, best_f = 0;
14261426
float best_w_ef = 0.f, best_w_eb = 0.f, best_w_fb = 0.f;
@@ -1498,7 +1498,7 @@ class EvenRegularGraphBuilder {
14981498
float best_score = std::numeric_limits<float>::lowest();
14991499
{
15001500
const auto b_feat = graph.getFeatureVector(b);
1501-
auto rs_bc = graph.search({ s,t }, b_feat, eps, k);
1501+
auto rs_bc = graph.search_intern({ s,t }, b_feat, k, eps);
15021502

15031503
// Choose the best (c,d) pair by maximizing (gain - d(b,c) + d(c,d))
15041504
float best_w_bc = 0.f, best_w_cd = 0.f;

cpp/deglib/include/deglib/graph/readonly_graph.h

Lines changed: 29 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -343,15 +343,13 @@ class ReadOnlyGraph : public deglib::search::SearchGraph {
343343
* The result set contains internal indices.
344344
*/
345345
template <deglib::DistanceFunction COMPARATOR, bool use_max_distance_count, bool use_filter>
346-
deglib::search::ResultSet searchImpl(const std::vector<uint32_t>& entry_vertex_indices, const std::byte* query, const float eps, const uint32_t initial_k, const deglib::graph::Filter* filter, const uint32_t max_distance_computation_count) const
346+
deglib::search::ResultSet searchImpl(const std::vector<uint32_t>& entry_vertex_indices, const std::byte* query, const uint32_t initial_k, const float eps, const bool include_entry, const deglib::graph::Filter* filter, const uint32_t max_distance_computation_count) const
347347
{
348+
uint32_t distance_computation_count = 0;
348349
const auto dist_func_param = this->feature_space_.get_dist_func_param();
349350
const auto feature_size = this->feature_space_.get_data_size();
350-
const size_t degree = this->edges_per_vertex_;
351351
const size_t vertex_count = this->size();
352-
353352
size_t k = std::min(vertex_count, static_cast<size_t>(initial_k));
354-
uint32_t distance_computation_count = 0;
355353

356354
// set of checked vertex ids
357355
const auto vl = visited_list_pool_->getFreeVisitedList();
@@ -360,12 +358,12 @@ class ReadOnlyGraph : public deglib::search::SearchGraph {
360358

361359
// items to traverse next
362360
auto next_vertices = deglib::search::UncheckedSet();
363-
next_vertices.reserve(k*degree);
361+
next_vertices.reserve(k*this->edges_per_vertex_);
364362

365363
// result set
366364
// TODO: custom priority queue with an internal Variable Length Array wrapped in a macro with linear-scan search and memcopy
367-
auto results = deglib::search::ResultSet();
368-
results.reserve(k);
365+
auto results = deglib::search::ResultSet();
366+
results.reserve(k+1);
369367

370368
// if the filter only contains few valid ids brute force them all
371369
if constexpr (use_filter) {
@@ -400,13 +398,15 @@ class ReadOnlyGraph : public deglib::search::SearchGraph {
400398
const auto feature = reinterpret_cast<const float*>(this->feature_by_index(index));
401399
const auto distance = COMPARATOR::compare(query, feature, dist_func_param);
402400
next_vertices.emplace(index, distance);
403-
if constexpr (use_filter) {
404-
if(filter->is_valid(this->label_by_index(index))) {
401+
if (include_entry) {
402+
if constexpr (use_filter) {
403+
if(filter->is_valid(this->label_by_index(index))) {
404+
results.emplace(index, distance);
405+
}
406+
} else {
405407
results.emplace(index, distance);
406-
}
407-
} else {
408-
results.emplace(index, distance);
409-
}
408+
}
409+
}
410410

411411
// early stop after to many computations
412412
if constexpr (use_max_distance_count) {
@@ -435,7 +435,7 @@ class ReadOnlyGraph : public deglib::search::SearchGraph {
435435

436436
size_t good_neighbor_count = 0;
437437
const auto neighbor_indices = this->neighbors_by_index(next_vertex.getInternalIndex());
438-
for (size_t i = 0; i < degree; i++) {
438+
for (size_t i = 0; i < this->edges_per_vertex_; i++) {
439439
const auto neighbor_index = neighbor_indices[i];
440440
if (checked_ids[neighbor_index] != checked_ids_tag) {
441441
checked_ids[neighbor_index] = checked_ids_tag;
@@ -491,124 +491,26 @@ class ReadOnlyGraph : public deglib::search::SearchGraph {
491491
return results;
492492
}
493493

494-
/**
495-
* The result set contains internal indices.
496-
*/
497-
deglib::search::ResultSet search(const std::vector<uint32_t>& entry_vertex_indices, const std::byte* query, const float eps, const uint32_t k, const deglib::graph::Filter* filter = nullptr, const uint32_t max_distance_computation_count = 0) const override
498-
{
499-
return feature_space_.compute([&]<deglib::DistanceFunction Dist>(Dist) {
500-
if(filter) {
501-
if(max_distance_computation_count == 0) {
502-
return searchImpl<Dist, false, true>(entry_vertex_indices, query, eps, k, filter, 0);
503-
} else {
504-
return searchImpl<Dist, true, true>(entry_vertex_indices, query, eps, k, filter, max_distance_computation_count);
505-
}
506-
} else {
507-
if(max_distance_computation_count == 0) {
508-
return searchImpl<Dist, false, false>(entry_vertex_indices, query, eps, k, nullptr, 0);
509-
} else {
510-
return searchImpl<Dist, true, false>(entry_vertex_indices, query, eps, k, nullptr, max_distance_computation_count);
511-
}
512-
}
513-
});
514-
}
515-
516-
/**
517-
* The result set contains internal indices.
518-
*/
519-
template <deglib::DistanceFunction COMPARATOR>
520-
deglib::search::ResultSet exploreImpl(const uint32_t entry_vertex_index, const uint32_t k, const bool include_entry, const uint32_t max_distance_computation_count) const
521-
{
522-
uint32_t distance_computation_count = 0;
523-
const auto dist_func_param = this->feature_space_.get_dist_func_param();
524-
const auto feature_size = this->feature_space_.get_data_size();
525-
526-
// set of checked vertex ids
527-
const auto vl = visited_list_pool_->getFreeVisitedList();
528-
auto* checked_ids = vl->get_visited();
529-
const auto checked_ids_tag = vl->get_tag();
530-
531-
// items to traverse next
532-
auto next_vertices = deglib::search::UncheckedSet();
533-
next_vertices.reserve(k*this->edges_per_vertex_);
534-
535-
// result set
536-
auto results = deglib::search::ResultSet();
537-
results.reserve(k);
538-
539-
// add the entry vertex index to the vertices which gets checked next and ignore it for further checks
540-
checked_ids[entry_vertex_index] = checked_ids_tag;
541-
next_vertices.emplace(entry_vertex_index, 0.0f);
542-
if(include_entry)
543-
results.emplace(entry_vertex_index, 0.0f);
544-
const auto query = this->feature_by_index(entry_vertex_index);
545-
546-
// search radius
547-
auto radius = std::numeric_limits<float>::max();
548-
549-
// iterate as long as good elements are in the next_vertices queue and max_calcs is not yet reached
550-
auto good_neighbors = std::array<uint32_t, 256>(); // this limits the neighbor count to 256 using Variable Length Array wrapped in a macro
551-
while (next_vertices.empty() == false)
494+
protected:
495+
deglib::search::ResultSet search_intern(const std::vector<uint32_t>& entry_vertex_indices, const std::byte* query, const uint32_t k, const float eps = 0.0f, const bool include_entry = true, const deglib::graph::Filter* filter = nullptr, const uint32_t max_distance_computation_count = 0) const override
552496
{
553-
// next vertex to check
554-
const auto next_vertex = next_vertices.top();
555-
next_vertices.pop();
556-
557-
uint8_t good_neighbor_count = 0;
558-
const auto neighbor_indices = this->neighbors_by_index(next_vertex.getInternalIndex());
559-
for (uint8_t i = 0; i < this->edges_per_vertex_; i++) {
560-
const auto neighbor_index = neighbor_indices[i];
561-
if (checked_ids[neighbor_index] != checked_ids_tag) {
562-
checked_ids[neighbor_index] = checked_ids_tag;
563-
good_neighbors[good_neighbor_count++] = neighbor_index;
564-
}
565-
}
566-
567-
if (good_neighbor_count == 0)
568-
continue;
569-
570-
memory::prefetch(reinterpret_cast<const char*>(this->feature_by_index(good_neighbors[0])), feature_size);
571-
for (uint8_t i = 0; i < good_neighbor_count; i++) {
572-
memory::prefetch(reinterpret_cast<const char*>(this->feature_by_index(good_neighbors[std::min(i + 1, good_neighbor_count - 1)])), feature_size);
573-
574-
const auto neighbor_index = good_neighbors[i];
575-
const auto neighbor_feature_vector = this->feature_by_index(neighbor_index);
576-
const auto neighbor_distance = COMPARATOR::compare(query, neighbor_feature_vector, dist_func_param);
577-
578-
if (neighbor_distance < radius) {
579-
580-
// check the neighborhood of this vertex later
581-
next_vertices.emplace(neighbor_index, neighbor_distance);
582-
583-
// remember the vertex, if its better than the worst in the result list
584-
results.emplace(neighbor_index, neighbor_distance);
585-
586-
// update the search radius
587-
if (results.size() > k) {
588-
results.pop();
589-
radius = results.top().getDistance();
497+
return feature_space_.compute([&]<deglib::DistanceFunction Dist>(Dist) {
498+
if(filter) {
499+
if(max_distance_computation_count == 0) {
500+
return searchImpl<Dist, false, true>(entry_vertex_indices, query, k, eps, include_entry, filter, 0);
501+
} else {
502+
return searchImpl<Dist, true, true>(entry_vertex_indices, query, k, eps, include_entry, filter, max_distance_computation_count);
503+
}
504+
} else {
505+
if(max_distance_computation_count == 0) {
506+
return searchImpl<Dist, false, false>(entry_vertex_indices, query, k, eps, include_entry, nullptr, 0);
507+
} else {
508+
return searchImpl<Dist, true, false>(entry_vertex_indices, query, k, eps, include_entry, nullptr, max_distance_computation_count);
590509
}
591510
}
592-
593-
// early stop after to many computations
594-
if(max_distance_computation_count > 0 && ++distance_computation_count >= max_distance_computation_count)
595-
return results;
596-
}
511+
});
597512
}
598513

599-
return results;
600-
}
601-
602-
/**
603-
* The result set contains internal indices.
604-
*/
605-
deglib::search::ResultSet explore(const uint32_t entry_vertex_index, const uint32_t k, const bool include_entry, const uint32_t max_distance_computation_count = 0) const override
606-
{
607-
return feature_space_.compute([&]<deglib::DistanceFunction Dist>(Dist) {
608-
return exploreImpl<Dist>(entry_vertex_index, k, include_entry, max_distance_computation_count);
609-
});
610-
}
611-
612514
};
613515

614516

0 commit comments

Comments
 (0)