Skip to content

Commit feb0dc4

Browse files
committed
refactor(graph): deduplicate search and pathfinding logic into InternalGraph
- Move `searchImpl`, `searchInternImpl`, and `hasPathImpl` templates to `InternalGraph` - Replace duplicated search implementations in `ReadOnlyGraph` and `SizeBoundedGraph` - Update QPS benchmark thresholds in `test_builder_regression.cpp`
1 parent 477f117 commit feb0dc4

4 files changed

Lines changed: 379 additions & 604 deletions

File tree

cpp/deglib/include/deglib/graph/internal_graph.h

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@
88
#include <string>
99
#include <algorithm>
1010

11+
#include <array>
12+
#include <unordered_map>
13+
#include <limits>
14+
1115
#include "deglib/distances.h"
1216
#include "deglib/filter.h"
17+
#include "deglib/graph/visited_list_pool.h"
18+
#include "deglib/utils/memory.h"
1319

1420
// Forward declaration for friend access
1521
namespace deglib::builder {
@@ -202,6 +208,10 @@ class InternalGraph
202208
return search_intern({ entry_vertex_index }, query_ptr, k, eps, include_entry, filter, max_distance_computation_count);
203209
}
204210

211+
protected:
212+
/**
213+
* Virtual internal search entry point implemented by derived graph classes.
214+
*/
205215
virtual deglib::graph::ResultSet search_intern(
206216
const std::vector<uint32_t>& entry_vertex_indices,
207217
const std::byte* query,
@@ -211,6 +221,332 @@ class InternalGraph
211221
const deglib::search::Filter* filter = nullptr,
212222
const uint32_t max_distance_computation_count = 0) const = 0;
213223

224+
/**
225+
* Statically dispatched exploration and k-NN search implementation.
226+
* Inlines graph member accesses (features, neighbors, labels) via GraphType to avoid vtable calls.
227+
*/
228+
template <typename GraphType, deglib::distances::DistanceFunction COMPARATOR, bool use_max_distance_count, bool use_filter>
229+
static deglib::graph::ResultSet searchImpl(
230+
const GraphType& self,
231+
const std::vector<uint32_t>& entry_vertex_indices,
232+
const std::byte* query,
233+
const uint32_t initial_k,
234+
const float eps,
235+
const bool include_entry,
236+
const deglib::search::Filter* filter,
237+
const uint32_t max_distance_computation_count)
238+
{
239+
uint32_t distance_computation_count = 0;
240+
const auto dist_func_param = self.feature_space_.get_dist_func_param();
241+
const auto feature_size = self.feature_space_.get_data_size();
242+
const size_t vertex_count = self.size();
243+
size_t k = std::min(vertex_count, static_cast<size_t>(initial_k));
244+
245+
// set of checked vertex ids
246+
const auto vl = self.visited_list_pool_->getFreeVisitedList();
247+
auto* checked_ids = vl->get_visited();
248+
const auto checked_ids_tag = vl->get_tag();
249+
250+
// items to traverse next
251+
auto next_vertices = deglib::graph::UncheckedSet();
252+
next_vertices.reserve(k * self.edges_per_vertex_);
253+
254+
// result set
255+
auto results = deglib::graph::ResultSet();
256+
results.reserve(k + 1);
257+
258+
// if the filter only contains few valid ids brute force them all
259+
if constexpr (use_filter) {
260+
if (vertex_count < 1'000 || (filter->get_inclusion_rate() * vertex_count) < 10'000 || filter->get_inclusion_rate() < 0.10f) {
261+
auto radius = std::numeric_limits<float>::max();
262+
filter->for_each_valid_label([&](uint32_t valid_label) {
263+
auto valid_index = self.getInternalIndex(valid_label);
264+
const auto feature = reinterpret_cast<const float*>(self.feature_by_index(valid_index));
265+
const auto distance = COMPARATOR::compare(query, feature, dist_func_param);
266+
267+
// remember the vertex, if its better than the worst in the result list
268+
if (distance < radius) {
269+
results.emplace(valid_index, distance);
270+
271+
// update the search radius
272+
if (results.size() > k) {
273+
results.pop();
274+
radius = results.top().getDistance();
275+
}
276+
}
277+
});
278+
return results;
279+
}
280+
}
281+
282+
// copy the initial entry vertices and their distances to the query into the three containers
283+
for (auto&& index : entry_vertex_indices) {
284+
if(checked_ids[index] != checked_ids_tag) {
285+
checked_ids[index] = checked_ids_tag;
286+
287+
const auto feature = self.feature_by_index(index);
288+
const auto distance = COMPARATOR::compare(query, feature, dist_func_param);
289+
next_vertices.emplace(index, distance);
290+
if (include_entry) {
291+
if constexpr (use_filter) {
292+
if(filter->is_valid(self.label_by_index(index))) {
293+
results.emplace(index, distance);
294+
}
295+
} else {
296+
results.emplace(index, distance);
297+
}
298+
}
299+
300+
// early stop after to many computations
301+
if constexpr (use_max_distance_count) {
302+
if(++distance_computation_count >= max_distance_computation_count) {
303+
return results;
304+
}
305+
}
306+
}
307+
}
308+
309+
// search radius
310+
auto radius = std::numeric_limits<float>::max();
311+
auto exploration_radius = radius;
312+
313+
// iterate as long as good elements are in the next_vertices queue
314+
auto good_neighbors = std::array<uint32_t, 256>();
315+
alignas(32) auto db_arr = std::array<const void*, 256>();
316+
alignas(32) auto dists = std::array<float, 256>();
317+
while (next_vertices.empty() == false)
318+
{
319+
// next vertex to check
320+
const auto next_vertex = next_vertices.top();
321+
next_vertices.pop();
322+
323+
// max distance reached
324+
if (next_vertex.getDistance() > exploration_radius)
325+
break;
326+
327+
size_t good_neighbor_count = 0;
328+
const auto neighbor_indices = self.neighbors_by_index(next_vertex.getIdentifier());
329+
for (size_t i = 0; i < self.edges_per_vertex_; i++) {
330+
const auto neighbor_index = neighbor_indices[i];
331+
if(checked_ids[neighbor_index] != checked_ids_tag) {
332+
checked_ids[neighbor_index] = checked_ids_tag;
333+
good_neighbors[good_neighbor_count++] = neighbor_index;
334+
}
335+
}
336+
337+
if (good_neighbor_count == 0)
338+
continue;
339+
340+
// Cap the neighbor count based on the remaining distance budget
341+
if constexpr (use_max_distance_count) {
342+
if (distance_computation_count + good_neighbor_count > max_distance_computation_count) {
343+
good_neighbor_count = max_distance_computation_count - distance_computation_count;
344+
}
345+
}
346+
347+
// Construct features pointer array
348+
for (size_t i = 0; i < good_neighbor_count; ++i) {
349+
db_arr[i] = self.feature_by_index(good_neighbors[i]);
350+
if (i < 8)
351+
memory::prefetch(reinterpret_cast<const char*>(db_arr[i]), feature_size);
352+
}
353+
354+
// Compute distances in batch
355+
COMPARATOR::compare_batch(query, db_arr.data(), good_neighbor_count, dist_func_param, dists.data());
356+
357+
// Process results sequentially
358+
for (size_t i = 0; i < good_neighbor_count; ++i) {
359+
const auto neighbor_index = good_neighbors[i];
360+
const auto neighbor_distance = dists[i];
361+
362+
// check the neighborhood of this vertex later, if its good enough
363+
if (neighbor_distance <= exploration_radius) {
364+
next_vertices.emplace(neighbor_index, neighbor_distance);
365+
366+
// remember the vertex, if its better than the worst in the result list
367+
if (neighbor_distance < radius) {
368+
if constexpr (use_filter) {
369+
if(filter->is_valid(self.label_by_index(neighbor_index))) {
370+
results.emplace(neighbor_index, neighbor_distance);
371+
}
372+
} else {
373+
results.emplace(neighbor_index, neighbor_distance);
374+
}
375+
376+
// update the search radius
377+
if (results.size() > k) {
378+
results.pop();
379+
radius = results.top().getDistance();
380+
exploration_radius = radius * ((radius < 0) ? (1 - eps) : (1 + eps));
381+
}
382+
}
383+
}
384+
}
385+
386+
if constexpr (use_max_distance_count) {
387+
distance_computation_count += good_neighbor_count;
388+
if (distance_computation_count >= max_distance_computation_count) {
389+
return results;
390+
}
391+
}
392+
}
393+
394+
return results;
395+
}
396+
397+
/**
398+
* Dispatches runtime metric and filter settings to compile-time specialized searchImpl instantiations.
399+
*/
400+
template <typename GraphType>
401+
static deglib::graph::ResultSet searchInternImpl(
402+
const GraphType& self,
403+
const std::vector<uint32_t>& entry_vertex_indices,
404+
const std::byte* query,
405+
const uint32_t k,
406+
const float eps = 0.0f,
407+
const bool include_entry = true,
408+
const deglib::search::Filter* filter = nullptr,
409+
const uint32_t max_distance_computation_count = 0)
410+
{
411+
return self.feature_space_.compute([&]<deglib::distances::DistanceFunction Dist>(Dist) -> deglib::graph::ResultSet {
412+
if(filter) {
413+
if(max_distance_computation_count == 0) {
414+
return searchImpl<GraphType, Dist, false, true>(self, entry_vertex_indices, query, k, eps, include_entry, filter, 0);
415+
} else {
416+
return searchImpl<GraphType, Dist, true, true>(self, entry_vertex_indices, query, k, eps, include_entry, filter, max_distance_computation_count);
417+
}
418+
} else {
419+
if(max_distance_computation_count == 0) {
420+
return searchImpl<GraphType, Dist, false, false>(self, entry_vertex_indices, query, k, eps, include_entry, nullptr, 0);
421+
} else {
422+
return searchImpl<GraphType, Dist, true, false>(self, entry_vertex_indices, query, k, eps, include_entry, nullptr, max_distance_computation_count);
423+
}
424+
}
425+
});
426+
}
427+
428+
/**
429+
* Greedy best-first reachability search.
430+
* Backtracks predecessors and returns an ordered path from to_vertex back to entry.
431+
*/
432+
template <typename GraphType>
433+
static std::vector<deglib::graph::ObjectDistance> hasPathImpl(
434+
const GraphType& self,
435+
const std::vector<uint32_t>& entry_vertex_indices,
436+
const uint32_t to_vertex,
437+
const float eps,
438+
const uint32_t k)
439+
{
440+
const auto query = self.feature_by_index(to_vertex);
441+
const auto dist_func = self.feature_space_.get_dist_func();
442+
const auto dist_func_param = self.feature_space_.get_dist_func_param();
443+
const auto feature_size = self.feature_space_.get_data_size();
444+
445+
// set of checked vertex ids
446+
const auto vl = self.visited_list_pool_->getFreeVisitedList();
447+
auto* checked_ids = vl->get_visited();
448+
const auto checked_ids_tag = vl->get_tag();
449+
450+
// items to traverse next
451+
auto next_vertices = deglib::graph::UncheckedSet();
452+
453+
// trackable information
454+
auto trackback = std::unordered_map<uint32_t, deglib::graph::ObjectDistance>();
455+
456+
// result set
457+
auto results = deglib::graph::ResultSet();
458+
459+
// copy the initial entry vertices and their distances to the query into the three containers
460+
for (auto&& index : entry_vertex_indices) {
461+
if(checked_ids[index] != checked_ids_tag) {
462+
checked_ids[index] = checked_ids_tag;
463+
464+
const auto feature = self.feature_by_index(index);
465+
const auto distance = dist_func(query, feature, dist_func_param);
466+
results.emplace(index, distance);
467+
next_vertices.emplace(index, distance);
468+
trackback.emplace(index, deglib::graph::ObjectDistance(index, distance));
469+
}
470+
}
471+
472+
// search radius
473+
auto radius = std::numeric_limits<float>::max();
474+
auto exploration_radius = radius;
475+
476+
// iterate as long as good elements are in the next_vertices queue
477+
auto good_neighbors = std::array<uint32_t, 256>();
478+
while (next_vertices.empty() == false)
479+
{
480+
// next vertex to check
481+
const auto next_vertex = next_vertices.top();
482+
next_vertices.pop();
483+
484+
// max distance reached
485+
if (next_vertex.getDistance() > exploration_radius)
486+
break;
487+
488+
size_t good_neighbor_count = 0;
489+
const auto neighbor_indices = self.neighbors_by_index(next_vertex.getIdentifier());
490+
for (size_t i = 0; i < self.edges_per_vertex_; i++) {
491+
const auto neighbor_index = neighbor_indices[i];
492+
493+
// found our target vertex, create a path back to the entry vertex
494+
if(neighbor_index == to_vertex) {
495+
auto path = std::vector<deglib::graph::ObjectDistance>();
496+
path.emplace_back(to_vertex, 0.f);
497+
path.emplace_back(next_vertex.getIdentifier(), next_vertex.getDistance());
498+
499+
auto last_vertex = trackback.find(next_vertex.getIdentifier());
500+
while(last_vertex != trackback.cend() && last_vertex->first != last_vertex->second.getIdentifier()) {
501+
path.emplace_back(last_vertex->second.getIdentifier(), last_vertex->second.getDistance());
502+
last_vertex = trackback.find(last_vertex->second.getIdentifier());
503+
}
504+
505+
return path;
506+
}
507+
508+
// collect
509+
if(checked_ids[neighbor_index] != checked_ids_tag) {
510+
checked_ids[neighbor_index] = checked_ids_tag;
511+
good_neighbors[good_neighbor_count++] = neighbor_index;
512+
}
513+
}
514+
515+
if (good_neighbor_count == 0)
516+
continue;
517+
518+
memory::prefetch(reinterpret_cast<const char*>(self.feature_by_index(good_neighbors[0])), feature_size);
519+
for (size_t i = 0; i < good_neighbor_count; i++) {
520+
memory::prefetch(reinterpret_cast<const char*>(self.feature_by_index(good_neighbors[std::min(i + 1, good_neighbor_count - 1)])), feature_size);
521+
522+
const auto neighbor_index = good_neighbors[i];
523+
const auto neighbor_feature_vector = self.feature_by_index(neighbor_index);
524+
const auto neighbor_distance = dist_func(query, neighbor_feature_vector, dist_func_param);
525+
526+
// check the neighborhood of this vertex later, if its good enough
527+
if (neighbor_distance <= exploration_radius) {
528+
next_vertices.emplace(neighbor_index, neighbor_distance);
529+
trackback.insert({neighbor_index, deglib::graph::ObjectDistance(next_vertex.getIdentifier(), next_vertex.getDistance())});
530+
531+
// remember the vertex, if its better than the worst in the result list
532+
if (neighbor_distance < radius) {
533+
results.emplace(neighbor_index, neighbor_distance);
534+
535+
// update the search radius
536+
if (results.size() > k) {
537+
results.pop();
538+
radius = results.top().getDistance();
539+
exploration_radius = radius * ((radius < 0) ? (1 - eps) : (1 + eps));
540+
}
541+
}
542+
}
543+
}
544+
}
545+
546+
// there is no path
547+
return std::vector<deglib::graph::ObjectDistance>();
548+
}
549+
214550
friend class deglib::builder::EvenRegularGraphBuilder;
215551
};
216552

0 commit comments

Comments
 (0)