Skip to content
Open
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
52997da
Add HNSW layered hierarchy
julianmi May 26, 2026
a11d9ee
Improve deserialization logging
julianmi May 27, 2026
a95b0e0
Use ace prefix in benchmarking consistently
julianmi May 27, 2026
47e2d85
Validate metadata before allocating
julianmi May 27, 2026
21ce339
Store layered base topology by original node ID
julianmi Jun 1, 2026
4514bc8
Unify the ACE logging format
julianmi Jun 1, 2026
0e9accd
Merge branch 'main' into hnsw-layered-index
julianmi Jun 3, 2026
7a761cf
Address review feedback
julianmi Jun 9, 2026
ea1a96c
Replace JSON header with binary header
julianmi Jun 9, 2026
153a82d
Merge branch 'main' into hnsw-layered-index
julianmi Jun 9, 2026
265206b
Merge branch 'main' into hnsw-layered-index
julianmi Jun 12, 2026
ad3848a
Add GPU_LAYERED_ON_DISK support for HNSW materialization
julianmi Jun 12, 2026
2e0b01a
Merge remote-tracking branch 'upstream/main' into materialize-layered…
julianmi Sep 22, 2026
2d86fac
Add support for BBQ compressed datasets in NN-Descent and CAGRA (#2654)
lowener Sep 22, 2026
693f022
Increase CUDA 12 wheel size (#2676)
robertmaynard Sep 22, 2026
7757263
Merge branch 'main' into materialize-layered-index
julianmi Sep 23, 2026
4254eca
Update copyright notice
julianmi Sep 23, 2026
2f4f113
Merge branch 'main' into materialize-layered-index
julianmi Sep 29, 2026
7b437ff
Merge branch 'main' into materialize-layered-index
julianmi Sep 30, 2026
59e9aff
Merge branch 'main' into materialize-layered-index
julianmi Oct 2, 2026
d3abe61
Merge branch 'main' into materialize-layered-index
julianmi Oct 5, 2026
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
75 changes: 75 additions & 0 deletions c/include/cuvs/neighbors/hnsw.h
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,81 @@ CUVS_EXPORT cuvsError_t cuvsHnswDeserialize(cuvsResources_t res,
* @}
*/

/**
* @defgroup hnsw_c_index_materialize Materialize a layered HNSW artifact to an hnswlib index
* @{
*/

/**
* @brief Parameters for materializing a layered HNSW artifact into an hnswlib index on disk.
*/
struct cuvsHnswMaterializeParams {
/**
* Local dataset path holding the original-ID-ordered vectors used to build the artifact.
*
* Supported formats match layered deserialization: `.npy` and ANN benchmark `*.bin` files with a
* `[uint32 rows, uint32 cols]` header (`.fbin`, `.f16bin`, `.u8bin`, `.i8bin`).
*/
const char* dataset_path;
/**
* Upper bound on host memory (in GiB) used for the base-topology reorder buffer.
*
* When `<= 0`, the whole base topology is reordered in a single in-memory pass (no temporary
* files). When set, the base topology is reordered through bucketed temporary files so that peak
* host memory stays close to this budget.
*/
double max_host_memory_gb;
/** Number of host threads to use. When `0`, the maximum number of threads is used. */
int num_threads;
};

typedef struct cuvsHnswMaterializeParams* cuvsHnswMaterializeParams_t;

/**
* @brief Allocate HNSW materialize params, and populate with default values
*
* @param[in] params cuvsHnswMaterializeParams_t to allocate
* @return cuvsError_t
*/
CUVS_EXPORT cuvsError_t cuvsHnswMaterializeParamsCreate(cuvsHnswMaterializeParams_t* params);

/**
* @brief De-allocate HNSW materialize params
*
* @param[in] params cuvsHnswMaterializeParams_t to de-allocate
* @return cuvsError_t
*/
CUVS_EXPORT cuvsError_t cuvsHnswMaterializeParamsDestroy(cuvsHnswMaterializeParams_t params);

/**
* @brief Materialize a layered HNSW artifact into a standard hnswlib index file on disk.
*
* Materializes a `GRAPH_ONLY` artifact (graph topology only, stored in ACE order) plus a
* local dataset into a standard hnswlib index file, without ever holding the full materialized
* index in host memory. The resulting file is compatible with the original hnswlib library and can
* be read back through `cuvsHnswDeserialize` with `hierarchy == CPU`. The element data type
* (`float`, `half`, `uint8_t` or `int8_t`) is inferred from the external dataset. GRAPH_ONLY
* artifacts are currently produced through the C++ API.
*
* @param[in] res cuvsResources_t opaque C handle
* @param[in] params cuvsHnswMaterializeParams_t materialization parameters
* @param[in] layered_artifact_path path to the layered HNSW artifact
* @param[in] output_path path to the hnswlib index file to write
* @param[in] dim the dimension of the vectors in the index
* @param[in] metric the distance metric used to build the index
* @return cuvsError_t
*/
CUVS_EXPORT cuvsError_t cuvsHnswMaterializeToHnswlib(cuvsResources_t res,
cuvsHnswMaterializeParams_t params,
const char* layered_artifact_path,
const char* output_path,
int dim,
cuvsDistanceType metric);

/**
* @}
*/

#ifdef __cplusplus
}
#endif
38 changes: 38 additions & 0 deletions c/src/neighbors/hnsw.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -408,3 +408,41 @@ extern "C" cuvsError_t cuvsHnswDeserialize(cuvsResources_t res,
}
});
}

extern "C" cuvsError_t cuvsHnswMaterializeParamsCreate(cuvsHnswMaterializeParams_t* params)
{
return cuvs::core::translate_exceptions([=] {
*params = new cuvsHnswMaterializeParams{
.dataset_path = nullptr, .max_host_memory_gb = 0, .num_threads = 0};
});
}

extern "C" cuvsError_t cuvsHnswMaterializeParamsDestroy(cuvsHnswMaterializeParams_t params)
{
return cuvs::core::translate_exceptions([=] { delete params; });
}

extern "C" cuvsError_t cuvsHnswMaterializeToHnswlib(cuvsResources_t res,
cuvsHnswMaterializeParams_t params,
const char* layered_artifact_path,
const char* output_path,
int dim,
cuvsDistanceType metric)
{
return cuvs::core::translate_exceptions([=] {
auto res_ptr = reinterpret_cast<raft::resources*>(res);
auto cpp_params = cuvs::neighbors::hnsw::materialize_params();
if (params->dataset_path != nullptr) {
cpp_params.dataset_path = std::string(params->dataset_path);
}
cpp_params.max_host_memory_gb = params->max_host_memory_gb;
cpp_params.num_threads = params->num_threads;
auto metric_type = static_cast<cuvs::distance::DistanceType>(metric);
cuvs::neighbors::hnsw::materialize_to_hnswlib(*res_ptr,
cpp_params,
std::string(layered_artifact_path),
std::string(output_path),
dim,
metric_type);
});
}
70 changes: 70 additions & 0 deletions cpp/include/cuvs/neighbors/hnsw.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1451,6 +1451,76 @@ void deserialize(raft::resources const& res,
* @}
*/

/**
* @defgroup hnsw_cpp_index_materialize Materialize a layered HNSW artifact into an hnswlib index
* @{
*/

/**
* @brief Parameters for materializing a layered HNSW artifact into an hnswlib index on disk.
*/
struct materialize_params {
/** Local dataset path holding the original-ID-ordered vectors used to build the artifact.
*
* Supported formats match layered deserialization: `.npy` and ANN benchmark `*.bin` files with a
* `[uint32 rows, uint32 cols]` header (`.fbin`, `.f16bin`, `.u8bin`, `.i8bin`).
*/
std::string dataset_path;

/** Upper bound on host memory (in GiB) used for the base-topology reorder buffer.
*
* When `<= 0`, the whole base topology is reordered in a single in-memory pass (no temporary
* files). When set, the base topology is reordered through bucketed temporary files so that
* peak host memory stays close to this budget, at the cost of writing and re-reading the
* (small) base-topology section once.
*/
double max_host_memory_gb = 0;

/** Number of host threads to use. When `0`, the maximum number of threads is used. */
int num_threads = 0;
};

/**
* @brief Materialize a layered HNSW artifact into a standard hnswlib index file on disk.
*
* Materializes a `GRAPH_ONLY` artifact (graph topology only, stored in ACE order) plus a
* local dataset into a standard hnswlib index file, without ever holding the full materialized
* index in host memory. The materialization reorders the base topology from ACE order to
* original-id order and interleaves the vectors, emitting the output with sequential disk IO. The
* resulting file is compatible with the original hnswlib library (`loadIndex`) and can be read back
* through `cuvs::neighbors::hnsw::deserialize` with `hierarchy == HnswHierarchy::CPU`.
*
* The element data type (`float`, `half`, `uint8_t` or `int8_t`) is inferred from the external
* dataset, so materialization supports an original dataset dtype that differs from the graph's
* construction dtype.
*
* @param[in] res raft resources
* @param[in] params materialization parameters (dataset path, host-memory budget, threads)
* @param[in] layered_artifact_path path to the layered HNSW artifact
* @param[in] output_path path to the hnswlib index file to write
* @param[in] dim dimensions of the training dataset
* @param[in] metric distance metric. Supported metrics ("L2Expanded", "InnerProduct")
*
* Usage example:
* @code{.cpp}
* using namespace cuvs::neighbors;
* hnsw::materialize_params materialize_params;
* materialize_params.dataset_path = "dataset.fbin";
* hnsw::materialize_to_hnswlib(
* res, materialize_params, "layered_artifact.cuvs", "index.bin", dim, metric);
* @endcode
*/
void materialize_to_hnswlib(raft::resources const& res,
const materialize_params& params,
const std::string& layered_artifact_path,
const std::string& output_path,
int dim,
cuvs::distance::DistanceType metric);

/**
* @}
*/

} // namespace hnsw
} // namespace neighbors
} // namespace CUVS_EXPORT cuvs
Expand Down
13 changes: 13 additions & 0 deletions cpp/include/cuvs/util/file_io.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,19 @@ void write_large_file(const file_descriptor& fd,
const size_t total_bytes,
const uint64_t file_offset);

/**
* @brief Pre-size a file to `total_bytes` bytes.
*
* Prefers posix_fallocate (reserves blocks up-front, avoids later ENOSPC and fragmentation), but
* falls back to ftruncate on filesystems that do not support preallocation (tmpfs and some
* NFS/overlay mounts return EOPNOTSUPP/EINVAL/ENOSYS) so the operation still succeeds there. A
* `total_bytes` of 0 is a no-op. Throws on failure (uses the descriptor's path in the message).
*
* @param fd File descriptor to pre-size
* @param total_bytes Target file size in bytes
*/
void preallocate_file(const file_descriptor& fd, const size_t total_bytes);

/**
* @brief Sequential std::ostream backed by kvikio.
*
Expand Down
Loading
Loading