Skip to content

Commit 8ea8f09

Browse files
authored
All-neighbors graph on host memory (#2313)
Closes #1903 This PR adds support for CPU kNN graph for batched all-neighbors. Passing host matrices for the indices and distances avoids having to fit the entire graph on GPU. Small perf gap and expected peak GPU memory reductions below: <img width="2384" height="1487" alt="results" src="https://github.com/user-attachments/assets/884f9716-18f1-4a1f-9993-4d4604761c6a" /> Authors: - Jinsol Park (https://github.com/jinsolp) Approvers: - Dante Gama Dessavre (https://github.com/dantegd) URL: #2313
1 parent ab79f9a commit 8ea8f09

11 files changed

Lines changed: 1124 additions & 468 deletions

File tree

c/include/cuvs/neighbors/all_neighbors.h

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
2+
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

@@ -28,8 +28,11 @@ extern "C" {
2828
* provide the dataset on host.
2929
*
3030
* Notes:
31-
* - Outputs (indices, distances, core_distances) are expected to be on device memory.
32-
* - Host variant accepts host-resident dataset; device variant accepts device-resident dataset.
31+
* - A host-resident dataset accepts either host- or device-resident outputs (indices, distances,
32+
* core_distances); a device-resident dataset requires device-resident outputs. All provided
33+
* outputs must share the same memory space.
34+
* - With batched builds (`n_clusters > 1`), host-resident outputs avoid materializing the full
35+
* [num_rows x k] graph on the GPU at once.
3336
* - For batching, `overlap_factor < n_clusters` must hold.
3437
* - When `core_distances` is provided, mutual-reachability distances are produced (see alpha).
3538
*/
@@ -94,16 +97,19 @@ CUVS_EXPORT cuvsError_t cuvsAllNeighborsIndexParamsDestroy(cuvsAllNeighborsIndex
9497
* resources
9598
* @param[in] params Build parameters (see cuvsAllNeighborsIndexParams)
9699
* @param[in] dataset 2D tensor [num_rows x dim] on host or device (auto-detected)
97-
* @param[out] indices 2D tensor [num_rows x k] on device (int64)
98-
* @param[out] distances Optional 2D tensor [num_rows x k] on device (float32); can be NULL
99-
* @param[out] core_distances Optional 1D tensor [num_rows] on device (float32); can be NULL
100+
* @param[out] indices 2D tensor [num_rows x k] (int64), host or device
101+
* @param[out] distances Optional 2D tensor [num_rows x k] (float32), host or device; can be
102+
* NULL
103+
* @param[out] core_distances Optional 1D tensor [num_rows] (float32), host or device; can be NULL
100104
* @param[in] alpha Mutual-reachability scaling; used only when core_distances is provided
101105
*
102106
* The function automatically detects whether the dataset is host-resident or device-resident
103107
* and calls the appropriate implementation. For host datasets, it partitions data into
104108
* `n_clusters` clusters and assigns each row to `overlap_factor` nearest clusters. For device
105109
* datasets, `n_clusters` must be 1 (no batching); `overlap_factor` is ignored.
106-
* Outputs always reside in device memory.
110+
*
111+
* Output memory space: a host dataset supports host- or device-resident outputs; a device dataset
112+
* requires device-resident outputs. All provided outputs must share the same memory space.
107113
*/
108114
CUVS_EXPORT cuvsError_t cuvsAllNeighborsBuild(cuvsResources_t res,
109115
cuvsAllNeighborsIndexParams_t params,

c/src/neighbors/all_neighbors.cpp

Lines changed: 63 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
/*
2-
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
2+
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

66
#include <cstdint>
77
#include <dlpack/dlpack.h>
8+
#include <type_traits>
89

910
#include <raft/core/error.hpp>
1011
#include <raft/core/mdspan_types.hpp>
@@ -80,36 +81,50 @@ static cuvs::neighbors::all_neighbors::all_neighbors_params convert_params(
8081
return out;
8182
}
8283

83-
static void ensure_indices_dtype_and_device_compatibility(DLManagedTensor* indices)
84+
static void ensure_indices_dtype(DLManagedTensor* indices)
8485
{
8586
auto dtype = indices->dl_tensor.dtype;
8687
RAFT_EXPECTS(dtype.code == kDLInt && dtype.bits == 64, "indices must be int64 output tensor");
87-
RAFT_EXPECTS(cuvs::core::is_dlpack_device_compatible(indices->dl_tensor),
88-
"indices tensor must be device-compatible");
8988
}
9089

91-
static void ensure_optional_distance_dtype_and_device_compatibility(DLManagedTensor* distances)
90+
static void ensure_optional_distance_dtype(DLManagedTensor* distances)
9291
{
9392
if (distances == nullptr) { return; }
9493
auto dtype = distances->dl_tensor.dtype;
9594
RAFT_EXPECTS(dtype.code == kDLFloat && dtype.bits == 32,
9695
"distances must be float32 output tensor");
97-
RAFT_EXPECTS(cuvs::core::is_dlpack_device_compatible(distances->dl_tensor),
98-
"distances tensor must be device-compatible");
9996
}
10097

101-
static void ensure_optional_core_distance_dtype_and_device_compatibility(
102-
DLManagedTensor* core_distances)
98+
static void ensure_optional_core_distance_dtype(DLManagedTensor* core_distances)
10399
{
104100
if (core_distances == nullptr) { return; }
105101
auto dtype = core_distances->dl_tensor.dtype;
106102
RAFT_EXPECTS(dtype.code == kDLFloat && dtype.bits == 32,
107103
"core_distances must be float32 output tensor");
108-
RAFT_EXPECTS(cuvs::core::is_dlpack_device_compatible(core_distances->dl_tensor),
109-
"core_distances tensor must be device-compatible");
110104
}
111105

112-
template <typename T>
106+
// Validate that the outputs (indices/distances/core_distances) all live in the same memory space
107+
static bool validate_output_memory_space(DLManagedTensor* indices,
108+
DLManagedTensor* distances,
109+
DLManagedTensor* core_distances)
110+
{
111+
const bool host = cuvs::core::is_dlpack_host_compatible(indices->dl_tensor);
112+
RAFT_EXPECTS(host || cuvs::core::is_dlpack_device_compatible(indices->dl_tensor),
113+
"indices tensor must be host- or device-compatible");
114+
auto same_space = [&](DLManagedTensor* t, const char* name) {
115+
if (t == nullptr) { return; }
116+
RAFT_EXPECTS(cuvs::core::is_dlpack_host_compatible(t->dl_tensor) == host,
117+
"%s tensor must be in the same memory space (host or device) as indices",
118+
name);
119+
};
120+
same_space(distances, "distances");
121+
same_space(core_distances, "core_distances");
122+
return host;
123+
}
124+
125+
// Build with a host-resident dataset. HostOutput selects whether the outputs live on host or
126+
// device.
127+
template <typename T, bool HostOutput>
113128
void _build_host(cuvsResources_t res,
114129
cuvsAllNeighborsIndexParams_t params,
115130
DLManagedTensor* dataset_tensor,
@@ -124,24 +139,23 @@ void _build_host(cuvsResources_t res,
124139
RAFT_EXPECTS(cuvs::core::is_dlpack_host_compatible(dlt),
125140
"Host build expects host-compatible dataset tensor");
126141

127-
ensure_indices_dtype_and_device_compatibility(indices_tensor);
128-
ensure_optional_distance_dtype_and_device_compatibility(distances_tensor);
129-
ensure_optional_core_distance_dtype_and_device_compatibility(core_distances_tensor);
130-
131-
// Check dependencies between parameters
132-
if (core_distances_tensor != nullptr && distances_tensor == nullptr) {
133-
RAFT_FAIL("distances tensor must be provided when core_distances tensor is provided");
134-
}
135-
136142
int64_t n_rows = dlt.shape[0];
137143
int64_t n_cols = dlt.shape[1];
138144

139145
auto cpp_params = convert_params(params, n_rows, n_cols);
140146

141-
using dataset_mdspan_t = raft::host_matrix_view<const T, int64_t, raft::row_major>;
142-
using indices_mdspan_t = raft::device_matrix_view<int64_t, int64_t, raft::row_major>;
143-
using distances_mdspan_t = raft::device_matrix_view<float, int64_t, raft::row_major>;
144-
using core_mdspan_t = raft::device_vector_view<float, int64_t>;
147+
using dataset_mdspan_t = raft::host_matrix_view<const T, int64_t, raft::row_major>;
148+
using indices_mdspan_t =
149+
std::conditional_t<HostOutput,
150+
raft::host_matrix_view<int64_t, int64_t, raft::row_major>,
151+
raft::device_matrix_view<int64_t, int64_t, raft::row_major>>;
152+
using distances_mdspan_t =
153+
std::conditional_t<HostOutput,
154+
raft::host_matrix_view<float, int64_t, raft::row_major>,
155+
raft::device_matrix_view<float, int64_t, raft::row_major>>;
156+
using core_mdspan_t = std::conditional_t<HostOutput,
157+
raft::host_vector_view<float, int64_t>,
158+
raft::device_vector_view<float, int64_t>>;
145159

146160
auto dataset = cuvs::core::from_dlpack<dataset_mdspan_t>(dataset_tensor);
147161
auto indices = cuvs::core::from_dlpack<indices_mdspan_t>(indices_tensor);
@@ -160,6 +174,7 @@ void _build_host(cuvsResources_t res,
160174
cpp_res, cpp_params, dataset, indices, distances, core_distances, alpha);
161175
}
162176

177+
// Build with a device-resident dataset. Outputs are always device-resident.
163178
template <typename T>
164179
void _build_device(cuvsResources_t device_res,
165180
cuvsAllNeighborsIndexParams_t params,
@@ -175,15 +190,6 @@ void _build_device(cuvsResources_t device_res,
175190
RAFT_EXPECTS(cuvs::core::is_dlpack_device_compatible(dlt),
176191
"Device build expects device-compatible dataset tensor");
177192

178-
ensure_indices_dtype_and_device_compatibility(indices_tensor);
179-
ensure_optional_distance_dtype_and_device_compatibility(distances_tensor);
180-
ensure_optional_core_distance_dtype_and_device_compatibility(core_distances_tensor);
181-
182-
// Check dependencies between parameters
183-
if (core_distances_tensor != nullptr && distances_tensor == nullptr) {
184-
RAFT_FAIL("distances tensor must be provided when core_distances tensor is provided");
185-
}
186-
187193
int64_t n_rows = dlt.shape[0];
188194
int64_t n_cols = dlt.shape[1];
189195

@@ -250,17 +256,33 @@ extern "C" cuvsError_t cuvsAllNeighborsBuild(cuvsResources_t res,
250256
return cuvs::core::translate_exceptions([=] {
251257
auto dataset = dataset_tensor->dl_tensor;
252258

259+
ensure_indices_dtype(indices_tensor);
260+
ensure_optional_distance_dtype(distances_tensor);
261+
ensure_optional_core_distance_dtype(core_distances_tensor);
262+
if (core_distances_tensor != nullptr && distances_tensor == nullptr) {
263+
RAFT_FAIL("distances tensor must be provided when core_distances tensor is provided");
264+
}
265+
266+
const bool host_output =
267+
validate_output_memory_space(indices_tensor, distances_tensor, core_distances_tensor);
268+
253269
if (dataset.dtype.code == kDLFloat && dataset.dtype.bits == 32) {
254270
// Check if dataset is host-compatible or device-compatible
255271
if (cuvs::core::is_dlpack_host_compatible(dataset)) {
256-
_build_host<float>(res,
257-
params,
258-
dataset_tensor,
259-
indices_tensor,
260-
distances_tensor,
261-
core_distances_tensor,
262-
alpha);
272+
// Host dataset supports both host- and device-resident outputs.
273+
if (host_output) {
274+
_build_host<float, true>(
275+
res, params, dataset_tensor, indices_tensor, distances_tensor, core_distances_tensor,
276+
alpha);
277+
} else {
278+
_build_host<float, false>(
279+
res, params, dataset_tensor, indices_tensor, distances_tensor, core_distances_tensor,
280+
alpha);
281+
}
263282
} else if (cuvs::core::is_dlpack_device_compatible(dataset)) {
283+
RAFT_EXPECTS(!host_output,
284+
"A device-resident dataset requires device-resident outputs; put the dataset "
285+
"on host to produce host-resident outputs.");
264286
_build_device<float>(res,
265287
params,
266288
dataset_tensor,

cpp/include/cuvs/neighbors/all_neighbors.hpp

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
2+
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

@@ -119,8 +119,8 @@ struct all_neighbors_params {
119119
* to build all-neighbors knn graph
120120
* @param[in] dataset raft::host_matrix_view input dataset expected to be located
121121
* in host memory
122-
* @param[out] indices nearest neighbor indices of shape [n_row x k]
123-
* @param[out] distances nearest neighbor distances [n_row x k]
122+
* @param[out] indices nearest neighbor indices of shape [n_row x k] on device memory
123+
* @param[out] distances nearest neighbor distances [n_row x k] on device memory
124124
* @param[out] core_distances array for core distances of size [n_row]. Requires distances matrix to
125125
* compute core_distances. If core_distances is given, the resulting indices and distances will be
126126
* mutual reachability space.
@@ -135,6 +135,40 @@ void build(
135135
std::optional<raft::device_vector_view<float, int64_t, row_major>> core_distances = std::nullopt,
136136
float alpha = 1.0);
137137

138+
/**
139+
* @brief Builds an approximate all-neighbors knn graph (find nearest neighbors for all the
140+
* training vectors)
141+
*
142+
* Usage example:
143+
* @code{.cpp}
144+
* using namespace cuvs::neighbors;
145+
* all_neighbors::all_neighbors_params params;
146+
* params.n_clusters = 4;
147+
* auto indices = raft::make_host_matrix<int64_t, int64_t>(n_row, k);
148+
* auto distances = raft::make_host_matrix<float, int64_t>(n_row, k);
149+
* all_neighbors::build(res, params, dataset, indices.view(), distances.view());
150+
* @endcode
151+
*
152+
* @param[in] handle raft::resources is an object managing resources
153+
* @param[in] params an instance of all_neighbors::all_neighbors_params that are parameters
154+
* to build all-neighbors knn graph
155+
* @param[in] dataset raft::host_matrix_view input dataset expected to be located in host memory
156+
* @param[out] indices nearest neighbor indices of shape [n_row x k] on host memory
157+
* @param[out] distances nearest neighbor distances [n_row x k] on host memory
158+
* @param[out] core_distances array for core distances of size [n_row] on host memory. Requires
159+
* distances matrix to compute core_distances. If core_distances is given, the resulting indices and
160+
* distances will be mutual reachability space.
161+
* @param[in] alpha distance scaling parameter as used in robust single linkage.
162+
*/
163+
void build(
164+
const raft::resources& handle,
165+
const all_neighbors_params& params,
166+
raft::host_matrix_view<const float, int64_t, row_major> dataset,
167+
raft::host_matrix_view<int64_t, int64_t, row_major> indices,
168+
std::optional<raft::host_matrix_view<float, int64_t, row_major>> distances = std::nullopt,
169+
std::optional<raft::host_vector_view<float, int64_t, row_major>> core_distances = std::nullopt,
170+
float alpha = 1.0);
171+
138172
/**
139173
* @brief Builds an approximate all-neighbors knn graph (find nearest neighbors for all the training
140174
* vectors) params.n_clusters should be 1 for data on device. To use a larger params.n_clusters for
@@ -155,8 +189,8 @@ void build(
155189
* to build all-neighbors knn graph
156190
* @param[in] dataset raft::device_matrix_view input dataset expected to be located
157191
* in device memory
158-
* @param[out] indices nearest neighbor indices of shape [n_row x k]
159-
* @param[out] distances nearest neighbor distances [n_row x k]
192+
* @param[out] indices nearest neighbor indices of shape [n_row x k] on device memory
193+
* @param[out] distances nearest neighbor distances [n_row x k] on device memory
160194
* @param[out] core_distances array for core distances of size [n_row]. Requires distances matrix to
161195
* compute core_distances. If core_distances is given, the resulting indices and distances will be
162196
* mutual reachability space.

cpp/src/neighbors/all_neighbors/all_neighbors.cu

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION.
2+
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

@@ -30,6 +30,18 @@ namespace cuvs::neighbors::all_neighbors {
3030
{ \
3131
return all_neighbors::detail::build<T, IdxT>( \
3232
handle, params, dataset, indices, distances, core_distances, alpha); \
33+
} \
34+
\
35+
void build(const raft::resources& handle, \
36+
const all_neighbors_params& params, \
37+
raft::host_matrix_view<const T, IdxT, row_major> dataset, \
38+
raft::host_matrix_view<IdxT, IdxT, row_major> indices, \
39+
std::optional<raft::host_matrix_view<T, IdxT, row_major>> distances, \
40+
std::optional<raft::host_vector_view<T, IdxT, row_major>> core_distances, \
41+
T alpha) \
42+
{ \
43+
return all_neighbors::detail::build<T, IdxT>( \
44+
handle, params, dataset, indices, distances, core_distances, alpha); \
3345
}
3446

3547
CUVS_INST_ALL_NEIGHBORS(float, int64_t);

0 commit comments

Comments
 (0)