Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions c/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ add_library(
src/preprocessing/quantize/pq.cpp
src/preprocessing/quantize/scalar.cpp
src/distance/pairwise_distance.cpp
src/selection/select_k.cpp
)
add_library(cuvs::c_api ALIAS cuvs_c)
set_target_properties(
Expand Down
2 changes: 2 additions & 0 deletions c/include/cuvs/core/all.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,5 @@
#include <cuvs/preprocessing/quantize/binary.h>
#include <cuvs/preprocessing/quantize/pq.h>
#include <cuvs/preprocessing/quantize/scalar.h>

#include <cuvs/selection/select_k.h>
30 changes: 30 additions & 0 deletions c/include/cuvs/core/c_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,23 @@ CUVS_EXPORT cuvsError_t cuvsStreamSync(cuvsResources_t res);
*/
CUVS_EXPORT cuvsError_t cuvsDeviceIdGet(cuvsResources_t res, int* device_id);

/**
* @brief Configure the temporary workspace on this resources object as an uncapped pool, backed
* by the current device memory resource. After the initial reservation is allocated on
* first use, subsequent calls to cuvsRMMAlloc / cuvsRMMFree on the same resources handle
* hit the pool cache rather than calling cudaMallocAsync / cudaFreeAsync, reducing CUDA
* context lock contention under concurrent query threads. The pool grows without shrinking:
* freed allocations are returned to the pool rather than to the device, so the pool's
* high-water mark only increases until the resources object is destroyed.
*
* @param[in] res cuvsResources_t opaque C handle
* @param[in] initial_size_bytes initial pool reservation in bytes; size to cover the
* steady-state working set to avoid growth after warmup
* @return cuvsError_t
*/
CUVS_EXPORT cuvsError_t cuvsResourcesSetWorkspacePool(cuvsResources_t res,
size_t initial_size_bytes);

/**
* @brief Create an Initialized opaque C handle for C++ type `raft::device_resources_snmg`
* for multi-GPU operations
Expand Down Expand Up @@ -239,6 +256,19 @@ CUVS_EXPORT cuvsError_t cuvsRMMFree(cuvsResources_t res, void* ptr, size_t bytes
CUVS_EXPORT cuvsError_t cuvsRMMPoolMemoryResourceEnable(int initial_pool_size_percent,
int max_pool_size_percent,
bool managed);
/**
* @brief Switches the working memory resource to use stream-ordered asynchronous allocation
* (cudaMallocAsync / cudaFreeAsync). Unlike the pool resource, this resource returns memory to
* the stream immediately without blocking the CPU, eliminating device-wide synchronization on
* deallocation. This is especially beneficial when multiple CAGRA searches run concurrently on
* separate CUDA streams, because the internal workspace allocations no longer serialize kernel
* launches. Be aware that this function will change the memory resource for the whole process
* and the new memory resource will be used until explicitly changed.
*
* @return cuvsError_t
*/
CUVS_EXPORT cuvsError_t cuvsRMMAsyncMemoryResourceEnable();

/**
* @brief Resets the memory resource to use the default memory resource (cuda_memory_resource)
* @return cuvsError_t
Expand Down
43 changes: 43 additions & 0 deletions c/include/cuvs/neighbors/cagra.h
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,49 @@ CUVS_EXPORT cuvsError_t cuvsCagraSearch(cuvsResources_t res,
DLManagedTensor* distances,
cuvsFilter filter);

/**
* @brief Search multiple CAGRA index partitions concurrently and return the global top-k per
* query.
*
* For each query row, the function searches all partitions in parallel into an internal
* intermediate buffer, applies per-partition distance post-processing, runs a batched top-k
* merge across partitions, and writes the final outputs to the caller-supplied device tensors.
* All work is submitted to the CUDA stream associated with @p res; use @c cuvsStreamSync to
* wait for completion.
*
* The index element type may be float32, float16, int8, or uint8. All partitions must share the
* same element type, and the queries must use that same type.
*
* @param[in] res cuvsResources_t opaque C handle
* @param[in] params search parameters (shared across partitions)
* @param[in] num_partitions number of index partitions
* @param[in] indices array of num_partitions cuvsCagraIndex_t pointers, all of the same
* element type
* @param[in] queries DLManagedTensor* (device, same dtype as the indices, [n_queries,
* dim]); the queries matrix is searched against every partition
* @param[out] partition_ids DLManagedTensor* (device, uint32, [n_queries, k]); which partition
* each returned neighbor came from
* @param[out] neighbors DLManagedTensor* (device, uint32 or int64, [n_queries, k]); ordinal
* in the corresponding partition's dataset
* @param[out] distances DLManagedTensor* (device, float32, [n_queries, k]); post-processed
* distance for each (query, neighbor)
* @param[in] filters array of `num_partitions` filters, one per partition (or NULL for a
* fully unfiltered search). `filters[i]` applies to partition `i`: use
* {.type=NO_FILTER, .addr=0} for no filter on that partition, or
* {.type=BITSET, .addr=ptr} where ptr is a uintptr_t-cast
* DLManagedTensor* holding that partition's own bitset (one bit per
* vector in that partition; standard 32-bit packing).
*/
CUVS_EXPORT cuvsError_t cuvsCagraSearchMultiPartition(cuvsResources_t res,
cuvsCagraSearchParams_t params,
uint32_t num_partitions,
cuvsCagraIndex_t* indices,
DLManagedTensor* queries,
DLManagedTensor* partition_ids,
DLManagedTensor* neighbors,
DLManagedTensor* distances,
cuvsFilter* filters);

/**
* @}
*/
Expand Down
8 changes: 7 additions & 1 deletion c/include/cuvs/neighbors/common.h
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 @@ -34,6 +34,12 @@ enum cuvsFilterType {
/**
* @brief Struct to hold address of cuvs::neighbors::prefilter and its type
*
* `addr` points to a filter object owned by the caller; the library performs no caching of the
* underlying bitset across search calls. Allocating and populating the device bitset may be more
* expensive than a single filtered search, so callers that issue repeated searches against the same
* filter (e.g. many queries over one index) should build the bitset once and reuse the same
* cuvsFilter across those calls rather than rebuild it per search. Reusing the bitset is essential
* for realizing the full throughput of filtered search.
*/
typedef struct {
uintptr_t addr;
Expand Down
37 changes: 37 additions & 0 deletions c/include/cuvs/selection/select_k.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once

#include <cuvs/core/c_api.h>
#include <dlpack/dlpack.h>

#ifdef __cplusplus
extern "C" {
#endif

/**
* @brief Select the k smallest values from a flat device array of n candidates.
*
* Treats `in_val` as a matrix of shape [1, n] and selects the `k` smallest
* float values. `out_idx` receives the int64 column positions of the selected
* values in [0, n), so the caller can recover per-segment identity as:
*
* segment_index = out_idx[j] / segment_k
* position_in_segment = out_idx[j] % segment_k
*
* @param[in] res cuvsResources_t handle
* @param[in] in_val DLManagedTensor* shape [1, n], float32, device memory
* @param[out] out_val DLManagedTensor* shape [1, k], float32, device memory
* @param[out] out_idx DLManagedTensor* shape [1, k], int64, device memory
* @return cuvsError_t
*/
CUVS_EXPORT cuvsError_t cuvsSelectK(cuvsResources_t res,
DLManagedTensor* in_val,
DLManagedTensor* out_val,
DLManagedTensor* out_idx);

#ifdef __cplusplus
}
#endif
34 changes: 29 additions & 5 deletions c/src/core/c_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
#include <raft/core/memory_tracking_resources.hpp>
#include <raft/core/resource/cuda_stream.hpp>
#include <raft/core/resource/device_id.hpp>
#include <raft/core/resource/device_memory_resource.hpp>
#include <raft/core/resource/resource_types.hpp>
#include <raft/core/resources.hpp>
#include <raft/util/cudart_utils.hpp>
#include <rapids_logger/logger.hpp>
#include <rmm/cuda_stream_view.hpp>
#include <rmm/mr/cuda_async_memory_resource.hpp>
#include <rmm/mr/cuda_memory_resource.hpp>
#include <rmm/mr/managed_memory_resource.hpp>
#include <rmm/mr/per_device_resource.hpp>
Expand All @@ -39,6 +41,19 @@ extern "C" cuvsError_t cuvsResourcesCreate(cuvsResources_t* res)
});
}

extern "C" cuvsError_t cuvsResourcesSetWorkspacePool(cuvsResources_t res, size_t initial_size_bytes)
{
return cuvs::core::translate_exceptions([=] {
auto res_ptr = reinterpret_cast<raft::resources*>(res);
// Create an uncapped pool: pre-warms with initial_size_bytes to avoid cudaMalloc on every
// query, but can grow beyond that if an allocation exceeds the initial reservation.
raft::resource::set_workspace_resource(
*res_ptr,
rmm::mr::pool_memory_resource{rmm::mr::get_current_device_resource_ref(),
initial_size_bytes});
});
}

extern "C" cuvsError_t cuvsResourcesCreateWithMemoryTracking(cuvsResources_t* res,
const char* csv_path,
int64_t sample_interval_ms)
Expand Down Expand Up @@ -153,17 +168,17 @@ extern "C" cuvsError_t cuvsRMMAlloc(cuvsResources_t res, void** ptr, size_t byte
{
return cuvs::core::translate_exceptions([=] {
auto res_ptr = reinterpret_cast<raft::resources*>(res);
auto mr = rmm::mr::get_current_device_resource_ref();
*ptr = mr.allocate(raft::resource::get_cuda_stream(*res_ptr), bytes);
auto stream = raft::resource::get_cuda_stream(*res_ptr);
*ptr = raft::resource::get_workspace_resource_ref(*res_ptr).allocate(stream, bytes);
});
}

extern "C" cuvsError_t cuvsRMMFree(cuvsResources_t res, void* ptr, size_t bytes)
{
return cuvs::core::translate_exceptions([=] {
auto res_ptr = reinterpret_cast<raft::resources*>(res);
auto mr = rmm::mr::get_current_device_resource_ref();
mr.deallocate(raft::resource::get_cuda_stream(*res_ptr), ptr, bytes);
auto stream = raft::resource::get_cuda_stream(*res_ptr);
raft::resource::get_workspace_resource_ref(*res_ptr).deallocate(stream, ptr, bytes);
});
}

Expand All @@ -185,9 +200,18 @@ extern "C" cuvsError_t cuvsRMMPoolMemoryResourceEnable(int initial_pool_size_per
});
}

extern "C" cuvsError_t cuvsRMMAsyncMemoryResourceEnable()
{
return cuvs::core::translate_exceptions([=] {
rmm::mr::set_current_device_resource(rmm::mr::cuda_async_memory_resource{});
});
}

extern "C" cuvsError_t cuvsRMMMemoryResourceReset()
{
return cuvs::core::translate_exceptions([=] { rmm::mr::reset_current_device_resource(); });
return cuvs::core::translate_exceptions([=] {
rmm::mr::reset_current_device_resource();
});
}

thread_local std::unique_ptr<rmm::mr::pinned_host_memory_resource> pinned_mr;
Expand Down
138 changes: 138 additions & 0 deletions c/src/neighbors/cagra.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,96 @@ void _search(cuvsResources_t res,
}
}

template <typename T, typename OutIdxT>
void _search_multi_partition(cuvsResources_t res,
cuvsCagraSearchParams params,
uint32_t num_partitions,
cuvsCagraIndex_t* indices,
DLManagedTensor* queries,
DLManagedTensor* partition_ids,
DLManagedTensor* neighbors,
DLManagedTensor* distances,
cuvsFilter* filters)
{
using IdxT = uint32_t;
using DistanceT = float;
using IndexT = cuvs::neighbors::cagra::index<T, IdxT>;

auto res_ptr = reinterpret_cast<raft::resources*>(res);
auto search_params = cuvs::neighbors::cagra::search_params();
convert_c_search_params(params, &search_params);

std::vector<const IndexT*> idx_vec(num_partitions);
for (uint32_t i = 0; i < num_partitions; i++) {
idx_vec[i] = reinterpret_cast<const IndexT*>(indices[i]->addr);
}

using queries_view_t = raft::device_matrix_view<const T, int64_t, raft::row_major>;
using pid_view_t = raft::device_matrix_view<uint32_t, int64_t, raft::row_major>;
using nbrs_view_t = raft::device_matrix_view<OutIdxT, int64_t, raft::row_major>;
using dist_view_t = raft::device_matrix_view<DistanceT, int64_t, raft::row_major>;

auto queries_view = cuvs::core::from_dlpack<queries_view_t>(queries);
auto partition_ids_view = cuvs::core::from_dlpack<pid_view_t>(partition_ids);
auto neighbors_view = cuvs::core::from_dlpack<nbrs_view_t>(neighbors);
auto distances_view = cuvs::core::from_dlpack<dist_view_t>(distances);

// One bitset view per partition; an empty view (null ptr) means "no filter for that partition".
using bitset_view_t = cuvs::core::bitset_view<std::uint32_t, int64_t>;
using bitset_mdspan_t = raft::device_vector_view<std::uint32_t, int64_t, raft::row_major>;
std::vector<bitset_view_t> partition_bitsets(
num_partitions, bitset_view_t(static_cast<std::uint32_t*>(nullptr), static_cast<int64_t>(0)));
if (filters != nullptr) {
for (uint32_t i = 0; i < num_partitions; i++) {
if (filters[i].type == NO_FILTER) {
continue; // leave the empty (accept-all) view for this partition
} else if (filters[i].type == BITSET) {
auto* bitset_tensor = reinterpret_cast<DLManagedTensor*>(filters[i].addr);
RAFT_EXPECTS(
bitset_tensor != nullptr, "BITSET filter addr must be non-null (partition %u)", i);
auto bitset_mds = cuvs::core::from_dlpack<bitset_mdspan_t>(bitset_tensor);
partition_bitsets[i] =
bitset_view_t(bitset_mds, static_cast<int64_t>(bitset_mds.size()) * 32);
} else {
RAFT_FAIL("Unsupported filter type for multi-partition search (partition %u): %d",
i,
(int)filters[i].type);
}
}
}

cuvs::neighbors::cagra::search(*res_ptr,
search_params,
idx_vec,
queries_view,
partition_ids_view,
neighbors_view,
distances_view,
partition_bitsets);
}

template <typename T>
void _search_multi_partition(cuvsResources_t res,
cuvsCagraSearchParams params,
uint32_t num_partitions,
cuvsCagraIndex_t* indices,
DLManagedTensor* queries,
DLManagedTensor* partition_ids,
DLManagedTensor* neighbors,
DLManagedTensor* distances,
cuvsFilter* filters)
{
if (neighbors->dl_tensor.dtype.code == kDLUInt && neighbors->dl_tensor.dtype.bits == 32) {
_search_multi_partition<T, uint32_t>(
res, params, num_partitions, indices, queries, partition_ids, neighbors, distances, filters);
} else if (neighbors->dl_tensor.dtype.code == kDLInt && neighbors->dl_tensor.dtype.bits == 64) {
_search_multi_partition<T, int64_t>(
res, params, num_partitions, indices, queries, partition_ids, neighbors, distances, filters);
} else {
RAFT_FAIL("neighbors should be of type uint32_t or int64_t");
}
}

template <typename T>
void _serialize(cuvsResources_t res,
const char* filename,
Expand Down Expand Up @@ -690,6 +780,54 @@ extern "C" cuvsError_t cuvsCagraSearch(cuvsResources_t res,
});
}

extern "C" cuvsError_t cuvsCagraSearchMultiPartition(cuvsResources_t res,
cuvsCagraSearchParams_t params,
uint32_t num_partitions,
cuvsCagraIndex_t* indices,
DLManagedTensor* queries,
DLManagedTensor* partition_ids,
DLManagedTensor* neighbors,
DLManagedTensor* distances,
cuvsFilter* filters)
{
return cuvs::core::translate_exceptions([=] {
RAFT_EXPECTS(num_partitions > 0, "num_partitions must be > 0");
RAFT_EXPECTS(indices != nullptr && queries != nullptr && partition_ids != nullptr &&
neighbors != nullptr && distances != nullptr,
"All pointer arguments must be non-null");

// Every partition index must be present, built, and share one dtype; the search dispatches on
// that common dtype. The queries dtype is validated against T inside from_dlpack.
RAFT_EXPECTS(indices[0] != nullptr, "Index at position 0 is null");
auto index_dtype = indices[0]->dtype;
for (uint32_t i = 0; i < num_partitions; i++) {
RAFT_EXPECTS(indices[i] != nullptr && indices[i]->addr != 0,
"Index at position %u is null or not built", i);
RAFT_EXPECTS(
indices[i]->dtype.code == index_dtype.code && indices[i]->dtype.bits == index_dtype.bits,
"All partition indices must share the same dtype; position %u differs from position 0", i);
}

if (index_dtype.code == kDLFloat && index_dtype.bits == 32) {
_search_multi_partition<float>(
res, *params, num_partitions, indices, queries, partition_ids, neighbors, distances, filters);
} else if (index_dtype.code == kDLFloat && index_dtype.bits == 16) {
_search_multi_partition<half>(
res, *params, num_partitions, indices, queries, partition_ids, neighbors, distances, filters);
} else if (index_dtype.code == kDLInt && index_dtype.bits == 8) {
_search_multi_partition<int8_t>(
res, *params, num_partitions, indices, queries, partition_ids, neighbors, distances, filters);
} else if (index_dtype.code == kDLUInt && index_dtype.bits == 8) {
_search_multi_partition<uint8_t>(
res, *params, num_partitions, indices, queries, partition_ids, neighbors, distances, filters);
} else {
RAFT_FAIL("Unsupported multi-partition index DLtensor dtype: %d and bits: %d",
index_dtype.code,
index_dtype.bits);
}
});
}

extern "C" cuvsError_t cuvsCagraMerge(cuvsResources_t res,
cuvsCagraIndexParams_t params,
cuvsCagraIndex_t* indices,
Expand Down
Loading
Loading