Skip to content

Commit a83394f

Browse files
committed
Merge remote-tracking branch 'upstream/main' into HH-migrate-merge-dataset-to-take-inputs-concat-buffer-and-offset-26_10
2 parents 0996cb1 + ac85d6b commit a83394f

29 files changed

Lines changed: 842 additions & 238 deletions

File tree

‎c/include/cuvs/neighbors/cagra.h‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,10 @@ struct cuvsAceParams {
168168
*
169169
* Used when `use_disk` is true or when the graph does not fit in host and GPU
170170
* memory. This should be the fastest disk in the system and hold enough space
171-
* for twice the dataset, final graph, and label mapping.
171+
* for twice the dataset, final graph, and label mapping. The directory may
172+
* already exist, but ACE's named artifacts must not already exist. Simultaneous
173+
* builds must use different directories. On failure, ACE removes only artifacts
174+
* it created and never deletes unrelated directory contents.
172175
*/
173176
const char* build_dir;
174177
/**

‎c/include/cuvs/neighbors/hnsw.h‎

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

@@ -66,7 +66,11 @@ struct cuvsHnswAceParams {
6666
size_t npartitions;
6767
/**
6868
* Directory to store ACE build artifacts (e.g., KNN graph, optimized graph).
69-
* Used when `use_disk` is true or when the graph does not fit in memory.
69+
* Used when `use_disk` is true or when the graph does not fit in memory. The
70+
* directory may already exist, but ACE's named artifacts and `hnsw_index.bin`
71+
* must not already exist. Simultaneous builds must use different directories.
72+
* On failure, ACE removes only its uncommitted CAGRA artifacts; a completed
73+
* CAGRA stage is retained if creating the HNSW index fails.
7074
*/
7175
const char* build_dir;
7276
/**
@@ -109,7 +113,7 @@ CUVS_EXPORT cuvsError_t cuvsHnswAceParamsDestroy(cuvsHnswAceParams_t params);
109113
struct cuvsHnswIndexParams {
110114
/* hierarchy of the hnsw index */
111115
enum cuvsHnswHierarchy hierarchy;
112-
/** Size of the candidate list during hierarchy construction when hierarchy is `CPU`*/
116+
/** Maximum candidate list size used during index construction. */
113117
int ef_construction;
114118
/** Number of host threads to use to construct hierarchy when hierarchy is `CPU` or `GPU`.
115119
When the value is 0, the number of threads is automatically determined to the
@@ -119,15 +123,15 @@ struct cuvsHnswIndexParams {
119123
is parallelized with the help of CPU threads.
120124
*/
121125
int num_threads;
122-
/** HNSW M parameter: number of bi-directional links per node (used when building with ACE).
123-
* graph_degree = m * 2, intermediate_graph_degree = m * 3.
126+
/** HNSW M parameter: number of bi-directional links per node. When the graph is built on the GPU,
127+
* this parameter is used to derive the internal CAGRA graph build parameters.
124128
*/
125129
size_t M;
126130
/** Distance type for the index. */
127131
cuvsDistanceType metric;
128132
/**
129-
* Optional: specify ACE parameters for building HNSW index using ACE algorithm.
130-
* Set to nullptr for default behavior (from_cagra conversion).
133+
* Optional ACE parameters for out-of-core graph construction.
134+
* Set to nullptr to select the graph build algorithm automatically.
131135
*/
132136
cuvsHnswAceParams_t ace_params;
133137
};
@@ -285,22 +289,20 @@ CUVS_EXPORT cuvsError_t cuvsHnswFromCagraWithDataset(cuvsResources_t res,
285289
*/
286290

287291
/**
288-
* @defgroup hnsw_c_index_build Build HNSW index using ACE algorithm
292+
* @defgroup hnsw_c_index_build Build an HNSW index
289293
* @{
290294
*/
291295

292296
/**
293-
* @brief Build an HNSW index using ACE (Augmented Core Extraction) algorithm.
297+
* @brief Build an HNSW index from HNSW parameters.
294298
*
295-
* ACE enables building HNSW indexes for datasets too large to fit in GPU memory by:
296-
* 1. Partitioning the dataset using balanced k-means into core and augmented partitions
297-
* 2. Building sub-indexes for each partition independently
298-
* 3. Concatenating sub-graphs into a final unified index
299+
* The graph is built on the GPU and converted to an HNSW index that can be searched on the CPU.
300+
* The graph build algorithm is selected automatically unless explicit ACE parameters are provided.
299301
*
300302
* NOTE: This function requires CUDA to be available at runtime.
301303
*
302304
* @param[in] res cuvsResources_t opaque C handle
303-
* @param[in] params cuvsHnswIndexParams_t with ACE parameters configured
305+
* @param[in] params cuvsHnswIndexParams_t with HNSW build parameters
304306
* @param[in] dataset DLManagedTensor* host dataset to build index from
305307
* @param[out] index cuvsHnswIndex_t to return the built HNSW index
306308
*
@@ -314,18 +316,10 @@ CUVS_EXPORT cuvsError_t cuvsHnswFromCagraWithDataset(cuvsResources_t res,
314316
* cuvsResources_t res;
315317
* cuvsResourcesCreate(&res);
316318
*
317-
* // Create ACE parameters
318-
* cuvsHnswAceParams_t ace_params;
319-
* cuvsHnswAceParamsCreate(&ace_params);
320-
* ace_params->npartitions = 4;
321-
* ace_params->use_disk = true;
322-
* ace_params->build_dir = "/tmp/hnsw_ace_build";
323-
*
324319
* // Create index parameters
325320
* cuvsHnswIndexParams_t params;
326321
* cuvsHnswIndexParamsCreate(&params);
327322
* params->hierarchy = GPU;
328-
* params->ace_params = ace_params;
329323
* params->M = 32;
330324
* params->ef_construction = 120;
331325
*
@@ -340,7 +334,6 @@ CUVS_EXPORT cuvsError_t cuvsHnswFromCagraWithDataset(cuvsResources_t res,
340334
* cuvsHnswBuild(res, params, &dataset, hnsw_index);
341335
*
342336
* // Clean up
343-
* cuvsHnswAceParamsDestroy(ace_params);
344337
* cuvsHnswIndexParamsDestroy(params);
345338
* cuvsHnswIndexDestroy(hnsw_index);
346339
* cuvsResourcesDestroy(res);

‎c/src/neighbors/hnsw.cpp‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,17 @@ void _build(cuvsResources_t res,
3838
cpp_params.M = params->M;
3939
cpp_params.metric = static_cast<cuvs::distance::DistanceType>(params->metric);
4040

41-
// Configure ACE parameters
42-
RAFT_EXPECTS(params->ace_params != nullptr, "ACE parameters must be set for hnsw::build");
43-
auto ace_params = cuvs::neighbors::hnsw::graph_build_params::ace_params();
44-
ace_params.npartitions = params->ace_params->npartitions;
45-
ace_params.build_dir = params->ace_params->build_dir ? params->ace_params->build_dir : "/tmp/hnsw_ace_build";
46-
ace_params.use_disk = params->ace_params->use_disk;
47-
ace_params.max_host_memory_gb = params->ace_params->max_host_memory_gb;
48-
ace_params.max_gpu_memory_gb = params->ace_params->max_gpu_memory_gb;
49-
cpp_params.graph_build_params = ace_params;
41+
if (params->ace_params != nullptr) {
42+
auto ace_params = cuvs::neighbors::hnsw::graph_build_params::ace_params();
43+
ace_params.npartitions = params->ace_params->npartitions;
44+
ace_params.build_dir = params->ace_params->build_dir
45+
? params->ace_params->build_dir
46+
: "/tmp/hnsw_ace_build";
47+
ace_params.use_disk = params->ace_params->use_disk;
48+
ace_params.max_host_memory_gb = params->ace_params->max_host_memory_gb;
49+
ace_params.max_gpu_memory_gb = params->ace_params->max_gpu_memory_gb;
50+
cpp_params.graph_build_params = ace_params;
51+
}
5052

5153
using dataset_mdspan_type = raft::host_matrix_view<T const, int64_t, raft::row_major>;
5254
auto dataset_mds = cuvs::core::from_dlpack<dataset_mdspan_type>(dataset_tensor);

‎c/tests/neighbors/ann_hnsw_c.cu‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,37 @@ TEST(CagraHnswC, BuildSearch)
136136
cuvsResourcesDestroy(res);
137137
}
138138

139+
TEST(HnswC, BuildWithoutAce)
140+
{
141+
cuvsResources_t res;
142+
cuvsResourcesCreate(&res);
143+
144+
DLManagedTensor dataset_tensor;
145+
dataset_tensor.dl_tensor.data = dataset;
146+
dataset_tensor.dl_tensor.device.device_type = kDLCPU;
147+
dataset_tensor.dl_tensor.ndim = 2;
148+
dataset_tensor.dl_tensor.dtype.code = kDLFloat;
149+
dataset_tensor.dl_tensor.dtype.bits = 32;
150+
dataset_tensor.dl_tensor.dtype.lanes = 1;
151+
int64_t dataset_shape[2] = {4, 2};
152+
dataset_tensor.dl_tensor.shape = dataset_shape;
153+
dataset_tensor.dl_tensor.strides = nullptr;
154+
155+
cuvsHnswIndexParams_t hnsw_params;
156+
cuvsHnswIndexParamsCreate(&hnsw_params);
157+
hnsw_params->M = 2;
158+
hnsw_params->ef_construction = 100;
159+
160+
cuvsHnswIndex_t hnsw_index;
161+
cuvsHnswIndexCreate(&hnsw_index);
162+
163+
ASSERT_EQ(cuvsHnswBuild(res, hnsw_params, &dataset_tensor, hnsw_index), CUVS_SUCCESS);
164+
165+
cuvsHnswIndexParamsDestroy(hnsw_params);
166+
cuvsHnswIndexDestroy(hnsw_index);
167+
cuvsResourcesDestroy(res);
168+
}
169+
139170
TEST(HnswAceC, BuildSearch)
140171
{
141172
// create cuvsResources_t

‎cpp/include/cuvs/neighbors/cagra.hpp‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,10 @@ struct ace_params {
6767
*
6868
* Used when `use_disk` is true or when the graph does not fit in host and GPU
6969
* memory. This should be the fastest disk in the system and hold enough space
70-
* for twice the dataset, final graph, and label mapping.
70+
* for twice the dataset, final graph, and label mapping. The directory may
71+
* already exist, but ACE's named artifacts must not already exist. Simultaneous
72+
* builds must use different directories. On failure, ACE removes only artifacts
73+
* it created and never deletes unrelated directory contents.
7174
*/
7275
std::string build_dir = "/tmp/ace_build";
7376
/**

‎cpp/include/cuvs/neighbors/hnsw.hpp‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ struct index_params : cuvs::neighbors::index_params {
7979
* ace.use_disk = true;
8080
* ace.build_dir = "/tmp/hnsw_ace_build";
8181
* @endcode
82+
*
83+
* When ACE writes to disk, `build_dir` may already exist, but ACE's named CAGRA
84+
* artifacts and `hnsw_index.bin` must not already exist. Simultaneous builds
85+
* must use different directories. The HNSW output is published only after it
86+
* is fully serialized; however, the complete build is not transactional: if
87+
* HNSW conversion fails after CAGRA succeeds, the completed CAGRA artifacts
88+
* remain in the directory.
8289
*/
8390
std::variant<std::monostate, graph_build_params::ace_params> graph_build_params;
8491
};

‎cpp/include/cuvs/util/file_io.hpp‎

Lines changed: 43 additions & 32 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
#pragma once
@@ -180,47 +180,58 @@ class file_descriptor {
180180
* @tparam T Data type for the numpy array
181181
* @param path File path to create
182182
* @param shape Shape of the numpy array (e.g., {rows, cols} for 2D)
183+
* @param exclusive Fail when the file already exists instead of truncating it.
184+
* If creation succeeds but pre-allocation or header writing
185+
* fails, remove the newly created file.
183186
* @return Pair of (file_descriptor, header_size)
184187
*/
185188
template <typename T>
186189
std::pair<file_descriptor, size_t> create_numpy_file(const std::string& path,
187-
const std::vector<size_t>& shape)
190+
const std::vector<size_t>& shape,
191+
bool exclusive = false)
188192
{
189193
// Open file
190-
file_descriptor fd(path, O_CREAT | O_RDWR | O_TRUNC, 0644);
191-
192-
// Build header
193-
const auto dtype = raft::numpy_serializer::get_numpy_dtype<T>();
194-
const bool fortran_order = false;
195-
const raft::numpy_serializer::header_t header = {dtype, fortran_order, shape};
196-
197-
std::stringstream ss;
198-
raft::numpy_serializer::write_header(ss, header);
199-
std::string header_str = ss.str();
200-
size_t header_size = header_str.size();
201-
202-
// Calculate data size from shape
203-
size_t data_bytes = sizeof(T);
204-
for (auto dim : shape) {
205-
data_bytes *= dim;
206-
}
194+
const int flags = O_CREAT | O_RDWR | (exclusive ? O_EXCL : O_TRUNC);
195+
file_descriptor fd(path, flags, 0644);
196+
197+
try {
198+
// Build header
199+
const auto dtype = raft::numpy_serializer::get_numpy_dtype<T>();
200+
const bool fortran_order = false;
201+
const raft::numpy_serializer::header_t header = {dtype, fortran_order, shape};
202+
203+
std::stringstream ss;
204+
raft::numpy_serializer::write_header(ss, header);
205+
std::string header_str = ss.str();
206+
size_t header_size = header_str.size();
207+
208+
// Calculate data size from shape
209+
size_t data_bytes = sizeof(T);
210+
for (auto dim : shape) {
211+
data_bytes *= dim;
212+
}
207213

208-
// Pre-allocate file space
209-
if (posix_fallocate(fd.get(), 0, header_size + data_bytes) != 0) {
210-
RAFT_FAIL("Failed to pre-allocate space for file: %s", path.c_str());
211-
}
214+
// Pre-allocate file space
215+
if (posix_fallocate(fd.get(), 0, header_size + data_bytes) != 0) {
216+
RAFT_FAIL("Failed to pre-allocate space for file: %s", path.c_str());
217+
}
212218

213-
// Seek to beginning and write header
214-
if (lseek(fd.get(), 0, SEEK_SET) == -1) {
215-
RAFT_FAIL("Failed to seek to beginning of file: %s", path.c_str());
216-
}
219+
// Seek to beginning and write header
220+
if (lseek(fd.get(), 0, SEEK_SET) == -1) {
221+
RAFT_FAIL("Failed to seek to beginning of file: %s", path.c_str());
222+
}
217223

218-
ssize_t written = write(fd.get(), header_str.data(), header_str.size());
219-
if (written < 0 || static_cast<size_t>(written) != header_str.size()) {
220-
RAFT_FAIL("Failed to write numpy header to file: %s", path.c_str());
221-
}
224+
ssize_t written = write(fd.get(), header_str.data(), header_str.size());
225+
if (written < 0 || static_cast<size_t>(written) != header_str.size()) {
226+
RAFT_FAIL("Failed to write numpy header to file: %s", path.c_str());
227+
}
222228

223-
return {std::move(fd), header_size};
229+
return {std::move(fd), header_size};
230+
} catch (...) {
231+
fd.close();
232+
if (exclusive) { (void)::unlink(path.c_str()); }
233+
throw;
234+
}
224235
}
225236

226237
/**

0 commit comments

Comments
 (0)