Skip to content

Commit 71decfc

Browse files
maxwbuckleyclaude
andcommitted
Promote filtering_rate to base search_params; honor it in brute_force
`filtering_rate` (a hint of the fraction of items filtered out by the sample filter) previously lived on `cagra::search_params` only. CAGRA used it to size `itopk_size` and, when the user left it at its default of `-1.0`, called `bitset_view::count(res)` on every search — a GPU popcount reduction + host sync that adds measurable latency. `brute_force` filtered search also called `bitset_view::count(res)` on every search to compute `sparsity` (used to choose between the dense tiled GEMM path and the sparse CSR path), but had no user-facing knob to skip the auto-detection. This change: - Moves `float filtering_rate = -1.0` from `cagra::search_params` to the base `cuvs::neighbors::search_params`. `cagra::search_params` inherits it; existing code that accesses `params.filtering_rate` is unaffected. - Plumbs `brute_force::search_params` through `detail::search` and `brute_force_search_filtered`. When `params.filtering_rate >= 0`, the hint is used directly as `sparsity` and the per-search popcount is skipped on the dense path. The CSR path still needs an exact non-zero count to size the matrix, so popcount runs lazily there. - Algorithms that don't use the hint (`ivf_flat`, `ivf_pq`, `hnsw`) ignore the new base field; their `search_params` inherit it but nothing reads it. Tests: extends `brute_force_prefiltered.cu` with `ResultWithFilteringRateHint` cases for both bitmap and bitset fixtures (float and half), reusing the existing parameter matrix and asserting that results match the auto-detect path. Closes #1960. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4addf4e commit 71decfc

5 files changed

Lines changed: 163 additions & 19 deletions

File tree

cpp/include/cuvs/neighbors/cagra.hpp

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -343,12 +343,9 @@ struct search_params : cuvs::neighbors::search_params {
343343
*/
344344
float persistent_device_usage = 1.0;
345345

346-
/**
347-
* A parameter indicating the rate of nodes to be filtered-out, when filtering is used.
348-
* The value must be equal to or greater than 0.0 and less than 1.0. Default value is
349-
* negative, in which case the filtering rate is automatically calculated.
350-
*/
351-
float filtering_rate = -1.0;
346+
// `filtering_rate` is inherited from `cuvs::neighbors::search_params`. CAGRA uses it to
347+
// size `itopk_size`; supplying a non-negative value avoids a per-search popcount + host sync
348+
// on the `bitset_filter` path.
352349
};
353350

354351
/**

cpp/include/cuvs/neighbors/common.hpp

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,23 @@ struct index_params {
116116
float metric_arg = 2.0f;
117117
};
118118

119-
struct search_params {};
119+
struct search_params {
120+
/**
121+
* A hint indicating the rate at which the sample filter is expected to filter out items
122+
* (i.e. `(n_dataset - n_set_bits) / n_dataset` for a `bitset_filter`).
123+
*
124+
* Algorithms that benefit from knowing the filter's selectivity may use this hint to tune
125+
* internal parameters or skip a hidden popcount kernel + host sync that would otherwise be
126+
* required to derive it from the filter on every search call.
127+
*
128+
* - Negative (default): the algorithm auto-detects the rate from the filter when needed.
129+
* This launches a GPU popcount reduction and synchronizes the stream per search call.
130+
* - In `[0.0, 1.0)`: the algorithm trusts the supplied value and skips the auto-detection.
131+
*
132+
* Algorithms that do not use this hint (e.g. `ivf_flat`, `ivf_pq`) ignore it.
133+
*/
134+
float filtering_rate = -1.0;
135+
};
120136

121137
/**
122138
* @brief Strategy for merging indices.

cpp/src/neighbors/brute_force.cu

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ void index<T, DistT>::update_dataset(
199199
const cuvs::neighbors::filtering::base_filter& sample_filter) \
200200
{ \
201201
detail::search<T, int64_t, DistT, raft::row_major>( \
202-
res, idx, queries, neighbors, distances, sample_filter); \
202+
res, params, idx, queries, neighbors, distances, sample_filter); \
203203
} \
204204
void search(raft::resources const& res, \
205205
const cuvs::neighbors::brute_force::index<T, DistT>& idx, \
@@ -209,7 +209,7 @@ void index<T, DistT>::update_dataset(
209209
const cuvs::neighbors::filtering::base_filter& sample_filter) \
210210
{ \
211211
detail::search<T, int64_t, DistT, raft::row_major>( \
212-
res, idx, queries, neighbors, distances, sample_filter); \
212+
res, {}, idx, queries, neighbors, distances, sample_filter); \
213213
} \
214214
void search(raft::resources const& res, \
215215
const cuvs::neighbors::brute_force::search_params& params, \
@@ -220,7 +220,7 @@ void index<T, DistT>::update_dataset(
220220
const cuvs::neighbors::filtering::base_filter& sample_filter) \
221221
{ \
222222
detail::search<T, int64_t, DistT, raft::col_major>( \
223-
res, idx, queries, neighbors, distances, sample_filter); \
223+
res, params, idx, queries, neighbors, distances, sample_filter); \
224224
} \
225225
void search(raft::resources const& res, \
226226
const cuvs::neighbors::brute_force::index<T, DistT>& idx, \
@@ -230,7 +230,7 @@ void index<T, DistT>::update_dataset(
230230
const cuvs::neighbors::filtering::base_filter& sample_filter) \
231231
{ \
232232
detail::search<T, int64_t, DistT, raft::col_major>( \
233-
res, idx, queries, neighbors, distances, sample_filter); \
233+
res, {}, idx, queries, neighbors, distances, sample_filter); \
234234
}
235235

236236
CUVS_INST_BFKNN(float, float);

cpp/src/neighbors/detail/knn_brute_force.cuh

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,7 @@ void brute_force_search(
584584
template <typename T, typename IdxT, typename BitsT, typename DistanceT = float>
585585
void brute_force_search_filtered(
586586
raft::resources const& res,
587+
const cuvs::neighbors::brute_force::search_params& params,
587588
const cuvs::neighbors::brute_force::index<T, DistanceT>& idx,
588589
raft::device_matrix_view<const T, IdxT, raft::row_major> queries,
589590
const cuvs::neighbors::filtering::base_filter* filter,
@@ -619,23 +620,35 @@ void brute_force_search_filtered(
619620
const cuvs::core::bitset_view<BitsT, IdxT>>>
620621
filter_view;
621622

622-
IdxT nnz_h = 0;
623-
float sparsity = 0.0f;
623+
IdxT nnz_h = 0;
624+
float sparsity = 0.0f;
625+
bool nnz_h_is_set = false;
626+
const bool use_hint = params.filtering_rate >= 0.0f;
624627

625628
const BitsT* filter_data = nullptr;
626629

627630
if (filter_type == cuvs::neighbors::filtering::FilterType::Bitmap) {
628631
auto actual_filter =
629632
dynamic_cast<const cuvs::neighbors::filtering::bitmap_filter<BitsT, int64_t>*>(filter);
630633
filter_view.emplace(actual_filter->view());
631-
nnz_h = actual_filter->view().count(res);
632-
sparsity = 1.0 - nnz_h / (1.0 * n_queries * n_dataset);
634+
if (use_hint) {
635+
sparsity = params.filtering_rate;
636+
} else {
637+
nnz_h = actual_filter->view().count(res);
638+
sparsity = 1.0 - nnz_h / (1.0 * n_queries * n_dataset);
639+
nnz_h_is_set = true;
640+
}
633641
} else if (filter_type == cuvs::neighbors::filtering::FilterType::Bitset) {
634642
auto actual_filter =
635643
dynamic_cast<const cuvs::neighbors::filtering::bitset_filter<BitsT, int64_t>*>(filter);
636644
filter_view.emplace(actual_filter->view());
637-
nnz_h = n_queries * actual_filter->view().count(res);
638-
sparsity = 1.0 - nnz_h / (1.0 * n_queries * n_dataset);
645+
if (use_hint) {
646+
sparsity = params.filtering_rate;
647+
} else {
648+
nnz_h = n_queries * actual_filter->view().count(res);
649+
sparsity = 1.0 - nnz_h / (1.0 * n_queries * n_dataset);
650+
nnz_h_is_set = true;
651+
}
639652
} else {
640653
RAFT_FAIL("Unsupported sample filter type");
641654
}
@@ -666,6 +679,19 @@ void brute_force_search_filtered(
666679
raft::identity_op(),
667680
filter_type);
668681
} else {
682+
// The CSR path needs an exact non-zero count to size the matrix. If the hint was used,
683+
// popcount lazily here — we still save the kernel + sync on the (more common) dense path.
684+
if (!nnz_h_is_set) {
685+
if (filter_type == cuvs::neighbors::filtering::FilterType::Bitmap) {
686+
auto actual_filter =
687+
dynamic_cast<const cuvs::neighbors::filtering::bitmap_filter<BitsT, int64_t>*>(filter);
688+
nnz_h = actual_filter->view().count(res);
689+
} else {
690+
auto actual_filter =
691+
dynamic_cast<const cuvs::neighbors::filtering::bitset_filter<BitsT, int64_t>*>(filter);
692+
nnz_h = n_queries * actual_filter->view().count(res);
693+
}
694+
}
669695
auto csr = raft::make_device_csr_matrix<DistanceT, IdxT>(res, n_queries, n_dataset, nnz_h);
670696
std::visit([&](const auto& actual_view) { actual_view.to_csr(res, csr); }, *filter_view);
671697

@@ -739,6 +765,7 @@ void brute_force_search_filtered(
739765

740766
template <typename T, typename IdxT, typename DistT, typename LayoutT>
741767
void search(raft::resources const& res,
768+
const cuvs::neighbors::brute_force::search_params& params,
742769
const cuvs::neighbors::brute_force::index<T, DistT>& idx,
743770
raft::device_matrix_view<const T, int64_t, LayoutT> queries,
744771
raft::device_matrix_view<int64_t, int64_t, raft::row_major> neighbors,
@@ -759,7 +786,7 @@ void search(raft::resources const& res,
759786
dynamic_cast<const cuvs::neighbors::filtering::bitmap_filter<uint32_t, int64_t>&>(
760787
sample_filter_ref);
761788
return brute_force_search_filtered<T, int64_t, uint32_t, DistT>(
762-
res, idx, queries, &sample_filter, neighbors, distances);
789+
res, params, idx, queries, &sample_filter, neighbors, distances);
763790
} catch (const std::bad_cast&) {
764791
}
765792

@@ -768,7 +795,7 @@ void search(raft::resources const& res,
768795
dynamic_cast<const cuvs::neighbors::filtering::bitset_filter<uint32_t, int64_t>&>(
769796
sample_filter_ref);
770797
return brute_force_search_filtered<T, int64_t, uint32_t, DistT>(
771-
res, idx, queries, &sample_filter, neighbors, distances);
798+
res, params, idx, queries, &sample_filter, neighbors, distances);
772799
} catch (const std::bad_cast&) {
773800
RAFT_FAIL("Unsupported sample filter type");
774801
}

cpp/tests/neighbors/brute_force_prefiltered.cu

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,50 @@ class PrefilteredBruteForceOnBitmapTest
520520
true));
521521
}
522522

523+
// Same as Run(), but passes the true sparsity as a filtering_rate hint to the
524+
// params-taking search overload. Confirms results match auto-detection.
525+
void RunWithFilteringRateHint()
526+
{
527+
auto dataset_raw = raft::make_device_matrix_view<const value_t, index_t, raft::row_major>(
528+
(const value_t*)dataset_d.data(), params.n_dataset, params.dim);
529+
530+
auto queries = raft::make_device_matrix_view<const value_t, index_t, raft::row_major>(
531+
(const value_t*)queries_d.data(), params.n_queries, params.dim);
532+
533+
auto dataset = brute_force::build(handle, dataset_raw, params.metric);
534+
535+
auto filter = cuvs::core::bitmap_view<bitmap_t, index_t>(
536+
(bitmap_t*)filter_d.data(), params.n_queries, params.n_dataset);
537+
538+
auto out_val = raft::make_device_matrix_view<dist_t, index_t, raft::row_major>(
539+
out_val_d.data(), params.n_queries, params.top_k);
540+
auto out_idx = raft::make_device_matrix_view<index_t, index_t, raft::row_major>(
541+
out_idx_d.data(), params.n_queries, params.top_k);
542+
543+
cuvs::neighbors::brute_force::search_params search_params;
544+
search_params.filtering_rate = params.sparsity;
545+
546+
brute_force::search(handle,
547+
search_params,
548+
dataset,
549+
queries,
550+
out_idx,
551+
out_val,
552+
cuvs::neighbors::filtering::bitmap_filter(filter));
553+
554+
raft::resource::sync_stream(handle);
555+
556+
ASSERT_TRUE(cuvs::neighbors::devArrMatchKnnPair(out_idx_expected_d.data(),
557+
out_idx.data_handle(),
558+
out_val_expected_d.data(),
559+
out_val.data_handle(),
560+
params.n_queries,
561+
params.top_k,
562+
0.001f,
563+
stream,
564+
true));
565+
}
566+
523567
protected:
524568
raft::resources handle;
525569
cudaStream_t stream;
@@ -941,6 +985,50 @@ class PrefilteredBruteForceOnBitsetTest
941985
true));
942986
}
943987

988+
// Same as Run(), but passes the true sparsity as a filtering_rate hint to the
989+
// params-taking search overload. Confirms results match auto-detection.
990+
void RunWithFilteringRateHint()
991+
{
992+
auto dataset_raw = raft::make_device_matrix_view<const value_t, index_t, raft::row_major>(
993+
(const value_t*)dataset_d.data(), params.n_dataset, params.dim);
994+
995+
auto queries = raft::make_device_matrix_view<const value_t, index_t, raft::row_major>(
996+
(const value_t*)queries_d.data(), params.n_queries, params.dim);
997+
998+
auto dataset = brute_force::build(handle, dataset_raw, params.metric);
999+
1000+
auto filter =
1001+
cuvs::core::bitset_view<bitset_t, index_t>((bitset_t*)filter_d.data(), params.n_dataset);
1002+
1003+
auto out_val = raft::make_device_matrix_view<dist_t, index_t, raft::row_major>(
1004+
out_val_d.data(), params.n_queries, params.top_k);
1005+
auto out_idx = raft::make_device_matrix_view<index_t, index_t, raft::row_major>(
1006+
out_idx_d.data(), params.n_queries, params.top_k);
1007+
1008+
cuvs::neighbors::brute_force::search_params search_params;
1009+
search_params.filtering_rate = params.sparsity;
1010+
1011+
brute_force::search(handle,
1012+
search_params,
1013+
dataset,
1014+
queries,
1015+
out_idx,
1016+
out_val,
1017+
cuvs::neighbors::filtering::bitset_filter(filter));
1018+
1019+
raft::resource::sync_stream(handle);
1020+
1021+
ASSERT_TRUE(cuvs::neighbors::devArrMatchKnnPair(out_idx_expected_d.data(),
1022+
out_idx.data_handle(),
1023+
out_val_expected_d.data(),
1024+
out_val.data_handle(),
1025+
params.n_queries,
1026+
params.top_k,
1027+
0.001f,
1028+
stream,
1029+
true));
1030+
}
1031+
9441032
protected:
9451033
raft::resources handle;
9461034
cudaStream_t stream;
@@ -963,18 +1051,34 @@ class PrefilteredBruteForceOnBitsetTest
9631051
using PrefilteredBruteForceTestOnBitmap_float_int64 =
9641052
PrefilteredBruteForceOnBitmapTest<float, float, int64_t>;
9651053
TEST_P(PrefilteredBruteForceTestOnBitmap_float_int64, Result) { Run(); }
1054+
TEST_P(PrefilteredBruteForceTestOnBitmap_float_int64, ResultWithFilteringRateHint)
1055+
{
1056+
RunWithFilteringRateHint();
1057+
}
9661058

9671059
using PrefilteredBruteForceTestOnBitmap_half_int64 =
9681060
PrefilteredBruteForceOnBitmapTest<half, float, int64_t>;
9691061
TEST_P(PrefilteredBruteForceTestOnBitmap_half_int64, Result) { Run(); }
1062+
TEST_P(PrefilteredBruteForceTestOnBitmap_half_int64, ResultWithFilteringRateHint)
1063+
{
1064+
RunWithFilteringRateHint();
1065+
}
9701066

9711067
using PrefilteredBruteForceTestOnBitset_float_int64 =
9721068
PrefilteredBruteForceOnBitsetTest<float, float, int64_t>;
9731069
TEST_P(PrefilteredBruteForceTestOnBitset_float_int64, Result) { Run(); }
1070+
TEST_P(PrefilteredBruteForceTestOnBitset_float_int64, ResultWithFilteringRateHint)
1071+
{
1072+
RunWithFilteringRateHint();
1073+
}
9741074

9751075
using PrefilteredBruteForceTestOnBitset_half_int64 =
9761076
PrefilteredBruteForceOnBitsetTest<half, float, int64_t>;
9771077
TEST_P(PrefilteredBruteForceTestOnBitset_half_int64, Result) { Run(); }
1078+
TEST_P(PrefilteredBruteForceTestOnBitset_half_int64, ResultWithFilteringRateHint)
1079+
{
1080+
RunWithFilteringRateHint();
1081+
}
9781082

9791083
template <typename index_t>
9801084
const std::vector<PrefilteredBruteForceInputs<index_t>> selectk_inputs = {

0 commit comments

Comments
 (0)