mirror of
https://github.com/facebookresearch/faiss.git
synced 2026-10-12 02:13:36 +00:00
Avoid search stats contention during concurrent search (#5663)
Summary: Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5663 Follow-up to D121097648 (public PR: https://github.com/facebookresearch/faiss/pull/5657). Related public PR: https://github.com/facebookresearch/faiss/pull/4910 Use relaxed atomic additions for HNSW, RaBitQ, and IVFPQ stats aggregation. Builds with `std::atomic_ref` support use `fetch_add(..., std::memory_order_relaxed)`; older standard libraries fall back to `__atomic_fetch_add(..., __ATOMIC_RELAXED)`. This removes the global OpenMP critical sections that serialize concurrent searches while preserving exact counter updates. Benchmark design: mode/opt `bench_hnsw_stats` binary, one shared HNSW index, `N` external pthreads, one OpenMP thread per search (each worker calls `omp_set_num_threads(1)` once before its timed loop), five repetitions per point. Performance: | External pthreads | Critical M/s | Relaxed atomic M/s | Speedup | |---:|---:|---:|---:| | 1 | 2.228 | 2.503 | 1.12x | | 2 | 3.716 | 3.853 | 1.04x | | 4 | 5.662 | 7.357 | 1.30x | | 8 | 6.384 | 12.843 | 2.01x | | 16 | 2.697 | 15.804 | 5.86x | | 32 | 0.951 | 18.910 | 19.88x | | 64 | 0.117 | 21.866 | 186.89x | At 64 pthreads, the feature-detected `atomic_ref` path measured 21.918 M/s mean over 10 repetitions (21.907 M/s median, 2.59% CV). The `__atomic_fetch_add` fallback measured 22.563 M/s mean (22.511 M/s median, 3.24% CV). The 64-thread profile attributes 97.38% of cycles in the critical-section version to lock acquisition and waiting. With relaxed atomic aggregation, 74.14% is in the stats combine path instead. The remaining non-linear scaling, 21.866 M/s at 64 threads versus the 2.503 M/s single-thread baseline, comes from cache-line contention on the global counters and shared-index memory-bandwidth and cache pressure. Reviewed By: mnorris11 Differential Revision: D121368933 fbshipit-source-id: 8dd26313e55aa73c0cdd379c7648efacd4f5f1d8
This commit is contained in:
committed by
meta-codesync[bot]
parent
5e74b1bdde
commit
2496f63f76
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include <benchmark/benchmark.h>
|
||||
#include <faiss/IndexBinaryHNSW.h>
|
||||
#include <omp.h>
|
||||
|
||||
namespace faiss {
|
||||
namespace {
|
||||
|
||||
constexpr int kDimension = 8;
|
||||
constexpr idx_t kDatabaseSize = 1;
|
||||
|
||||
struct BenchmarkIndex {
|
||||
BenchmarkIndex() : index(kDimension, 2) {
|
||||
std::vector<uint8_t> database(kDatabaseSize * kDimension / 8);
|
||||
for (size_t i = 0; i < database.size(); ++i) {
|
||||
database[i] = static_cast<uint8_t>(i * 17 + 3);
|
||||
}
|
||||
|
||||
index.hnsw.efConstruction = 2;
|
||||
index.add(kDatabaseSize, database.data());
|
||||
index.hnsw.efSearch = 1;
|
||||
}
|
||||
|
||||
IndexBinaryHNSW index;
|
||||
};
|
||||
|
||||
const IndexBinaryHNSW& get_index() {
|
||||
static const BenchmarkIndex benchmark_index;
|
||||
return benchmark_index.index;
|
||||
}
|
||||
|
||||
void bench_binary_hnsw_single_query(benchmark::State& state) {
|
||||
omp_set_num_threads(1);
|
||||
constexpr std::array<uint8_t, kDimension / 8> query{};
|
||||
int32_t distance;
|
||||
idx_t label;
|
||||
|
||||
for (auto _ : state) {
|
||||
get_index().search(1, query.data(), 1, &distance, &label);
|
||||
benchmark::DoNotOptimize(distance);
|
||||
benchmark::DoNotOptimize(label);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations());
|
||||
}
|
||||
|
||||
BENCHMARK(bench_binary_hnsw_single_query)->ThreadRange(1, 64)->UseRealTime();
|
||||
|
||||
} // namespace
|
||||
} // namespace faiss
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
omp_set_num_threads(1);
|
||||
faiss::get_index();
|
||||
benchmark::Initialize(&argc, argv);
|
||||
benchmark::RunSpecifiedBenchmarks();
|
||||
benchmark::Shutdown();
|
||||
return 0;
|
||||
}
|
||||
@@ -121,10 +121,7 @@ void IndexBinaryHNSW::search(
|
||||
}
|
||||
}
|
||||
|
||||
#pragma omp critical
|
||||
{
|
||||
hnsw_stats.combine({n1, n2, ndis, nhops});
|
||||
}
|
||||
hnsw_stats.combine_atomic({n1, n2, ndis, nhops});
|
||||
|
||||
#pragma omp parallel for
|
||||
for (idx_t i = 0; i < n * k; ++i) {
|
||||
@@ -273,10 +270,7 @@ void IndexBinaryHNSWCagra::search(
|
||||
|
||||
res.end();
|
||||
}
|
||||
#pragma omp critical
|
||||
{
|
||||
hnsw_stats.combine(search_stats);
|
||||
}
|
||||
hnsw_stats.combine_atomic(search_stats);
|
||||
}
|
||||
|
||||
#pragma omp parallel for
|
||||
|
||||
+14
-18
@@ -32,6 +32,7 @@
|
||||
#include <faiss/impl/VisitedTable.h>
|
||||
#include <faiss/impl/hnsw/LockVector.h>
|
||||
#include <faiss/impl/hnsw/MinimaxHeap.h>
|
||||
#include <faiss/impl/platform_macros.h>
|
||||
#include <faiss/utils/random.h>
|
||||
#include <faiss/utils/sorting.h>
|
||||
|
||||
@@ -42,6 +43,13 @@ using NodeDistFarther = HNSW::NodeDistFarther;
|
||||
|
||||
HNSWStats hnsw_stats;
|
||||
|
||||
void HNSWStats::combine_atomic(const HNSWStats& other) {
|
||||
detail::atomic_fetch_add_relaxed(n1, other.n1);
|
||||
detail::atomic_fetch_add_relaxed(n2, other.n2);
|
||||
detail::atomic_fetch_add_relaxed(ndis, other.ndis);
|
||||
detail::atomic_fetch_add_relaxed(nhops, other.nhops);
|
||||
}
|
||||
|
||||
/**************************************************************
|
||||
* add / search blocks of descriptors
|
||||
**************************************************************/
|
||||
@@ -487,11 +495,8 @@ void hnsw_search(
|
||||
InterruptCallback::check();
|
||||
}
|
||||
|
||||
#pragma omp critical
|
||||
{
|
||||
hnsw_stats.combine({n1, n2, ndis, nhops});
|
||||
rabitq_stats.add({n_rabitq_1bit, n_rabitq_refine});
|
||||
}
|
||||
hnsw_stats.combine_atomic({n1, n2, ndis, nhops});
|
||||
rabitq_stats.add_atomic({n_rabitq_1bit, n_rabitq_refine});
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
@@ -702,11 +707,8 @@ void IndexHNSW::search_level_0(
|
||||
omp_capture_exception(ex, [&] { interrupt = true; });
|
||||
}
|
||||
}
|
||||
#pragma omp critical
|
||||
{
|
||||
hnsw_stats.combine(search_stats);
|
||||
rabitq_stats.add(rq_search_stats);
|
||||
}
|
||||
hnsw_stats.combine_atomic(search_stats);
|
||||
rabitq_stats.add_atomic(rq_search_stats);
|
||||
}
|
||||
omp_rethrow_if_exception(ex);
|
||||
};
|
||||
@@ -1233,10 +1235,7 @@ void IndexHNSW2Level::search(
|
||||
}
|
||||
omp_rethrow_if_exception(ex);
|
||||
|
||||
#pragma omp critical
|
||||
{
|
||||
hnsw_stats.combine({n1, n2, ndis, nhops});
|
||||
}
|
||||
hnsw_stats.combine_atomic({n1, n2, ndis, nhops});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1438,10 +1437,7 @@ void IndexHNSWCagra::range_search(
|
||||
result->do_allocation();
|
||||
pres.copy_result();
|
||||
|
||||
#pragma omp critical
|
||||
{
|
||||
hnsw_stats.combine({n1, n2, ndis, nhops});
|
||||
}
|
||||
hnsw_stats.combine_atomic({n1, n2, ndis, nhops});
|
||||
};
|
||||
|
||||
if (is_similarity_metric(metric_type)) {
|
||||
|
||||
@@ -315,6 +315,8 @@ struct HNSWStats {
|
||||
ndis += other.ndis;
|
||||
nhops += other.nhops;
|
||||
}
|
||||
|
||||
void combine_atomic(const HNSWStats& other);
|
||||
};
|
||||
|
||||
// global var that collects them all
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <faiss/impl/RaBitQUtils.h>
|
||||
#include <faiss/impl/RaBitQuantizerMultiBit.h>
|
||||
#include <faiss/impl/ResultHandler.h>
|
||||
#include <faiss/impl/platform_macros.h>
|
||||
#include <faiss/impl/simd_dispatch.h>
|
||||
#include <faiss/invlists/DirectMap.h>
|
||||
#include <faiss/utils/distances.h>
|
||||
@@ -27,6 +28,11 @@ namespace faiss {
|
||||
|
||||
RaBitQStats rabitq_stats;
|
||||
|
||||
void RaBitQStats::add_atomic(const RaBitQStats& other) {
|
||||
detail::atomic_fetch_add_relaxed(n_1bit, other.n_1bit);
|
||||
detail::atomic_fetch_add_relaxed(n_refine, other.n_refine);
|
||||
}
|
||||
|
||||
// Import shared utilities from RaBitQUtils
|
||||
using rabitq_utils::ExtraBitsFactors;
|
||||
using rabitq_utils::QueryFactorsData;
|
||||
|
||||
@@ -40,6 +40,8 @@ struct RaBitQStats {
|
||||
n_refine += other.n_refine;
|
||||
}
|
||||
|
||||
void add_atomic(const RaBitQStats& other);
|
||||
|
||||
double refine_ratio() const {
|
||||
return n_1bit ? double(n_refine) / double(n_1bit) : 0.0;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#pragma once
|
||||
|
||||
// basic int types and size_t
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
@@ -146,6 +147,21 @@ inline int __builtin_clzll(uint64_t x) {
|
||||
#define FAISS_DEPRECATED(msg)
|
||||
#endif // GCC or Clang
|
||||
|
||||
namespace faiss {
|
||||
namespace detail {
|
||||
|
||||
template <typename T>
|
||||
inline void atomic_fetch_add_relaxed(T& target, T value) {
|
||||
#if defined(__cpp_lib_atomic_ref) && __cpp_lib_atomic_ref >= 201806L
|
||||
std::atomic_ref<T>(target).fetch_add(value, std::memory_order_relaxed);
|
||||
#else
|
||||
__atomic_fetch_add(&target, value, __ATOMIC_RELAXED);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
} // namespace faiss
|
||||
|
||||
// Localized enablement of imprecise floating point operations
|
||||
// You need to use all 3 macros to cover all compilers.
|
||||
#if defined(_MSC_VER)
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <faiss/IndexIVFPQ.h>
|
||||
#include <faiss/impl/IDSelector.h>
|
||||
#include <faiss/impl/ResultHandler.h>
|
||||
#include <faiss/impl/platform_macros.h>
|
||||
#include <faiss/impl/pq_code_distance/IVFPQ_QueryTables.h>
|
||||
#include <faiss/impl/pq_code_distance/pq_code_distance-inl.h>
|
||||
#include <faiss/impl/simd_dispatch.h>
|
||||
@@ -411,10 +412,8 @@ struct IVFPQScannerT : QueryTables {
|
||||
}
|
||||
}
|
||||
|
||||
#pragma omp critical
|
||||
{
|
||||
indexIVFPQ_stats.n_hamming_pass += n_hamming_pass;
|
||||
}
|
||||
detail::atomic_fetch_add_relaxed(
|
||||
indexIVFPQ_stats.n_hamming_pass, n_hamming_pass);
|
||||
}
|
||||
|
||||
template <class SearchResultType>
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
#include <thread>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include <faiss/IndexBinaryHNSW.h>
|
||||
#include <faiss/IndexFlat.h>
|
||||
#include <faiss/IndexHNSW.h>
|
||||
#include <faiss/impl/FaissAssert.h>
|
||||
@@ -22,6 +24,36 @@
|
||||
#include <faiss/impl/VisitedTable.h>
|
||||
#include <faiss/impl/hnsw/MinimaxHeap.h>
|
||||
#include <faiss/utils/random.h>
|
||||
#include <omp.h>
|
||||
|
||||
TEST(HNSWStats, ConcurrentBinarySearchAggregatesExactly) {
|
||||
constexpr size_t num_threads = 16;
|
||||
constexpr size_t searches_per_thread = 100;
|
||||
|
||||
faiss::IndexBinaryHNSW index(8, 2);
|
||||
const uint8_t database = 0;
|
||||
index.add(1, &database);
|
||||
index.hnsw.efSearch = 1;
|
||||
faiss::hnsw_stats.reset();
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
threads.reserve(num_threads);
|
||||
for (size_t i = 0; i < num_threads; ++i) {
|
||||
threads.emplace_back([&] {
|
||||
omp_set_num_threads(1);
|
||||
int32_t distance;
|
||||
faiss::idx_t label;
|
||||
for (size_t j = 0; j < searches_per_thread; ++j) {
|
||||
index.search(1, &database, 1, &distance, &label);
|
||||
}
|
||||
});
|
||||
}
|
||||
for (auto& thread : threads) {
|
||||
thread.join();
|
||||
}
|
||||
|
||||
EXPECT_EQ(num_threads * searches_per_thread, faiss::hnsw_stats.n1);
|
||||
}
|
||||
|
||||
int reference_pop_min(faiss::MinimaxHeap& heap, float* vmin_out) {
|
||||
assert(heap.k > 0);
|
||||
|
||||
Reference in New Issue
Block a user