Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1435,6 +1435,8 @@ if(NOT BUILD_CPU_ONLY)
src/cluster/spectral.cu
src/core/bitset.cu
src/core/bloom_filter.cu
src/core/roaring_allowlist.cu
src/neighbors/roaring_bitmap_filter.cu
src/core/omp_wrapper.cpp
src/util/file_io.cpp
src/util/host_memory.cpp
Expand Down
157 changes: 157 additions & 0 deletions cpp/include/cuvs/core/roaring_allowlist.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

#pragma once

#include <cuvs/core/export.hpp>

#include <raft/core/device_mdspan.hpp>
#include <raft/core/host_mdspan.hpp>
#include <raft/core/resources.hpp>

#include <cstddef>
#include <cstdint>
#include <memory>

namespace CUVS_EXPORT cuvs {
namespace core {

/**
* @brief Non-owning device view of one immutable Roaring allowlist.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a core vocab type. Do we want to consider putting this in raft at some point?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of putting it in RAFT as we are introducing several new filters as core vocabulary types that I consider are reusable.

*
* The view contains an opaque pointer to an already initialized device-side cuCollections
* reference plus immutable shape and cardinality metadata. Creating or copying it is O(1) and
* performs no allocation, parsing, kernel launch, or synchronization. The owning
* @ref roaring_allowlist must outlive the view and every operation that uses it.
*/
class CUVS_EXPORT roaring_allowlist_view {
public:
roaring_allowlist_view() = default;

[[nodiscard]] std::size_t dataset_rows() const noexcept { return dataset_rows_; }
[[nodiscard]] std::size_t cardinality() const noexcept { return cardinality_; }
[[nodiscard]] bool empty() const noexcept { return cardinality_ == 0; }
[[nodiscard]] bool valid() const noexcept { return valid_; }

/** @brief Opaque device pointer to the initialized cuCollections reference, or null if empty. */
[[nodiscard]] void const* device_reference() const noexcept { return device_reference_; }

private:
friend class roaring_allowlist;

roaring_allowlist_view(void const* device_reference,
std::size_t dataset_rows,
std::size_t cardinality) noexcept
: device_reference_(device_reference),
dataset_rows_(dataset_rows),
cardinality_(cardinality),
valid_(true)
{
}

void const* device_reference_{};
std::size_t dataset_rows_{};
std::size_t cardinality_{};
bool valid_{};
};

/**
* @brief Owning immutable exact Roaring allowlist over CAGRA dataset-row IDs.
*
* Build an allowlist through @ref from_ids, then pass its zero-copy @ref view to a
* cuvs::neighbors::filtering::roaring_bitmap_filter. A filter maps one such view to each query;
* owners remain independent and can therefore be reused across filters and queries.
*
* Construction sorts IDs on the GPU unless @p pre_sorted is true. Setting @p pre_sorted promises
* that IDs are already in strictly increasing order; this promise is not verified. IDs must be
* unique and smaller than @p dataset_rows. The encoded bytes and the initialized
* cuco::experimental::roaring_bitmap_ref<uint32_t> are retained on the device. Creating a view
* never copies or reparses them, and CAGRA search performs no Roaring initialization.
*
* ID-based construction emits the standard portable 32-bit Roaring array and bitmap container
* forms. Each ID is partitioned by its high 16 bits; the low 16 bits are stored as an array for at
* most 4,096 values in a partition and as an 8 KiB bitmap otherwise.
*
* @see https://github.com/RoaringBitmap/RoaringFormatSpec
* @see https://github.com/NVIDIA/cuCollections/pull/839
*/
class CUVS_EXPORT roaring_allowlist {
private:
struct impl;

public:
using key_type = std::uint32_t;

/**
* @brief Build one allowlist from host IDs.
*
* Host IDs are copied to the construction stream and then use the same device builder as the
* device overload. IDs must be smaller than dataset_rows; this precondition is not checked.
* Empty input is valid and rejects every candidate. The returned object is ready for same-stream
* use; cross-stream use requires an explicit dependency on the construction stream.
Comment thread
divyegala marked this conversation as resolved.
*
* @param[in] res RAFT resources containing the construction CUDA stream and memory resource.
* @param[in] dataset_rows Number of rows in the dataset whose IDs this allowlist addresses. Must
* be in the range [1, 2^32].
* @param[in] ids Host IDs to include in the allowlist. IDs must be unique and smaller than
* `dataset_rows`.
* @param[in] pre_sorted Whether `ids` are already in strictly increasing order. If false, the IDs
* are sorted during construction.
* @return An owning device-resident Roaring allowlist.
*/
static roaring_allowlist from_ids(raft::resources const& res,
std::size_t dataset_rows,
raft::host_vector_view<const key_type, std::int64_t> ids,
bool pre_sorted = false);

/**
* @brief Build one allowlist from device IDs.
*
* The input must remain valid until the construction stream reaches the enqueued work. IDs must
* be smaller than dataset_rows; this precondition is not checked.
* Temporary memory is O(cardinality + container count); no dataset-sized dense bitmap is used.
* The returned object is ready for same-stream use; cross-stream use requires an explicit
* dependency on the construction stream.
Comment thread
divyegala marked this conversation as resolved.
*
* @param[in] res RAFT resources containing the construction CUDA stream and memory resource.
* @param[in] dataset_rows Number of rows in the dataset whose IDs this allowlist addresses. Must
* be in the range [1, 2^32].
* @param[in] ids Device IDs to include in the allowlist. IDs must be unique and smaller than
* `dataset_rows`; the storage must remain valid until the construction stream reaches the
* enqueued work.
* @param[in] pre_sorted Whether `ids` are already in strictly increasing order. If false, the IDs
* are sorted during construction.
* @return An owning device-resident Roaring allowlist.
*/
static roaring_allowlist from_ids(raft::resources const& res,
std::size_t dataset_rows,
raft::device_vector_view<const key_type, std::int64_t> ids,
bool pre_sorted = false);

~roaring_allowlist();

roaring_allowlist(roaring_allowlist const&) = delete;
roaring_allowlist& operator=(roaring_allowlist const&) = delete;
roaring_allowlist(roaring_allowlist&&) noexcept;
roaring_allowlist& operator=(roaring_allowlist&&) noexcept;

[[nodiscard]] std::size_t dataset_rows() const noexcept;
[[nodiscard]] std::size_t cardinality() const noexcept;
[[nodiscard]] bool empty() const noexcept;

/** @brief Total device bytes retained by the encoded allowlist and initialized reference. */
[[nodiscard]] std::size_t size_bytes() const noexcept;

/** @brief Return a zero-copy view. */
[[nodiscard]] roaring_allowlist_view view() const noexcept;

private:
explicit roaring_allowlist(std::unique_ptr<impl> impl) noexcept;

std::unique_ptr<impl> impl_;
};

} // namespace core
} // namespace CUVS_EXPORT cuvs
1 change: 1 addition & 0 deletions cpp/include/cuvs/detail/jit_lto/common_fragments.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ struct tag_u8 {};
struct tag_filter_none {};
struct tag_filter_bitset {};
struct tag_filter_bloom_filter {};
struct tag_filter_roaring {};
struct tag_filter_udf {};

struct tag_bitset_u32 {};
Expand Down
96 changes: 94 additions & 2 deletions cpp/include/cuvs/neighbors/common.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include <cstring>
#include <memory>
#include <numeric>
#include <span>
#include <string>
#include <type_traits>
#include <utility>
Expand All @@ -43,7 +44,9 @@
namespace CUVS_EXPORT cuvs {
namespace core {
class bloom_filter;
}
class roaring_allowlist_view;
class roaring_allowlist;
} // namespace core
namespace neighbors {
/**
* @addtogroup cagra_cpp_index_params
Expand Down Expand Up @@ -1346,7 +1349,7 @@ namespace filtering {
* @{
*/

enum class FilterType : int { None = 0, Bitmap = 1, Bitset = 2, Bloom = 3, UDF = 100 };
enum class FilterType : int { None = 0, Bitmap = 1, Bitset = 2, Bloom = 3, Roaring = 4, UDF = 100 };

struct base_filter {
~base_filter() = default;
Expand Down Expand Up @@ -1501,6 +1504,95 @@ struct bloom_filter : public base_filter {
FilterType get_filter_type() const override { return FilterType::Bloom; }
};

/**
* @brief Reusable per-query mapping to immutable exact Roaring allowlists.
*
* Entry @c q selects view @c q. CAGRA retains candidate dataset row @c r when the selected
* allowlist contains @c r. Construction copies only already initialized device-reference pointers
* and empty flags into the filter payload; encoded bytes are neither copied nor parsed. Search
* therefore performs no Roaring allocation, initialization, synchronization, or preprocessing.
*
* @code{.cpp}
* auto first = cuvs::core::roaring_allowlist::from_ids(
* res, dataset_rows,
* raft::make_host_vector_view<const std::uint32_t, std::int64_t>(first_ids.data(),
* first_ids.size()));
* auto second = cuvs::core::roaring_allowlist::from_ids(
* res, dataset_rows,
* raft::make_host_vector_view<const std::uint32_t, std::int64_t>(second_ids.data(),
* second_ids.size()));
* std::array views{first.view(), second.view()};
* auto filter = cuvs::neighbors::filtering::roaring_bitmap_filter(res, views);
* @endcode
*
* Owners and views can be reused across filters and queries. This filter owns its mapping tables
* and device payload, but not the referenced owners, which must outlive the filter and all searches
* using it. Copies are cheap shared handles required by CAGRA query-offset wrappers.
*
* Roaring filters currently support direct @c cagra::search only. Dynamic batching can combine
* requests into a different query-row layout, and tiered search applies one filter to partitions
* with different row domains; both paths reject this filter type.
*
* @see cuvs::core::roaring_allowlist
* @see https://github.com/RoaringBitmap/RoaringFormatSpec
*/
struct roaring_bitmap_filter : public base_filter {
private:
struct impl;

public:
/** @brief Construct an invalid handle. It cannot be passed to CAGRA search. */
roaring_bitmap_filter() = default;

/**
* @brief Materialize the query-to-allowlist device pointer table.
*
* @p allowlists must be nonempty, every view must be valid, and every view must have the same
* `dataset_rows()`. Query count is inferred from the span length.
*/
explicit roaring_bitmap_filter(raft::resources const& res,
std::span<const cuvs::core::roaring_allowlist_view> allowlists);

[[nodiscard]] bool valid() const noexcept;
[[nodiscard]] std::size_t num_queries() const noexcept;
[[nodiscard]] std::size_t dataset_rows() const noexcept;
[[nodiscard]] std::size_t cardinality(std::size_t query_id) const;
[[nodiscard]] bool empty(std::size_t query_id) const;

/**
* @brief Conservative maximum rejected fraction among all query allowlists.
*
* CAGRA uses this precomputed value when `search_params::filtering_rate` is unset. Basing one
* batch-wide scalar on the sparsest query avoids under-provisioning that query, but a very sparse
* or empty allowlist can increase the search work performed for every query in the batch. Callers
* may set `search_params::filtering_rate` explicitly when another tradeoff is preferable.
*/
[[nodiscard]] float filtering_rate() const noexcept;

/** @brief Device bytes owned by this mapping, excluding the referenced allowlists. */
[[nodiscard]] std::size_t size_bytes() const noexcept;

/**
* @brief Replace one query's allowlist pointer outside the search path.
Comment thread
divyegala marked this conversation as resolved.
*
* The replacement must have the same `dataset_rows()`. Copies share the underlying mapping, so
* the replacement is visible through every copy of this filter. The method copies one pointer
* and one empty flag to the device and synchronizes @p res before returning. Do not call it
* concurrently with a search, and keep the replacement owner alive for all subsequent searches.
*/
void set_allowlist(raft::resources const& res,
std::size_t query_id,
cuvs::core::roaring_allowlist_view replacement);

/** @brief Internal device payload already prepared for the linked CAGRA predicate. */
[[nodiscard]] void* device_payload() const noexcept;

FilterType get_filter_type() const override { return FilterType::Roaring; }

private:
std::shared_ptr<impl> impl_;
};

/**
* @brief JIT-LTO user-defined filter predicate.
*
Expand Down
3 changes: 2 additions & 1 deletion cpp/include/cuvs/neighbors/dynamic_batching.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION.
* SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

Expand Down Expand Up @@ -173,6 +173,7 @@ struct index : cuvs::neighbors::index {
* @param[in] sample_filter
* filtering function, if any, must be the same for all requests in a batch
* (the pointer must be alive for the lifetime of the dynamic batching index)
* Roaring filters are not supported because batching changes the query-row mapping.
*/
template <typename Upstream>
index(const raft::resources& res,
Expand Down
3 changes: 2 additions & 1 deletion cpp/include/cuvs/neighbors/tiered_index.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ void compact(raft::resources const& res,
* @param[out] distances a device matrix view to the distances to the selected neighbors [n_queries,
* k]
* @param[in] sample_filter an optional device filter function object that greenlights samples
* for a given query. (none_sample_filter for no filtering)
* for a given query. (none_sample_filter for no filtering). Roaring filters are not supported
* because tiered partitions use different dataset-row domains.
*/
void search(raft::resources const& res,
const cagra::search_params& search_params,
Expand Down
Loading
Loading