[REVIEW] New Dataset API Clarifying Ownership#1846
Conversation
|
/ok to test 5447a4c |
|
/ok to test 17ab09d |
|
NB: I updated the label to |
@achirkin The problem w/ using mdspan/mdarray for this is that it's not carrying along the proper information to either the algorithms nor the user (which is why we created this specialized class for this in the first place!). Two immediate reasons why this API is necessary:
This new API solves both of these problems while leaving the control over the memory ownership entirely in the user's hands. We've discussed this for a long time. We've known this is needed for a long time. it's time to prioritize this and get it done. I agree that an anstract class might make more sense, but ultimately we should not be moving any owneship over to the algorithm (the user should maintain ownership over the class and underlying memory the entire time). |
…tion between make host/device padded dataset in factory
… of dataset + create build_result struct which returns both index and vpq_dataset to prevent automatic out of scope destruction of dataset for vpq case
…rt for cases where we DO need to own the dataset (in order to keep view alive for index). All cases where we build() from dataset already on device --> we don't need to own. Merge + All cases when data is on host --> we DO need to own the device copy we create. This includes within ACE build and C API build from host and from_args with host dataset
|
The doc that outlines some of the API design choices can be found in slack. Let me know if there are any parts of the design that can be altered to better suit our users' needs. The following files are test case files I've added and can be ignored for now. They will be removed before the final merge with upstream repo:
|
…d to declare the output index type passed into deserialize(). User allocates memory outside this call and owns this memory since index doesn't own dataset anymore. Since memory is passed into deserialize, deserialize will already know the type of index it should deserialize at compile time because the user specifies it. This is the new API contract and is done in iface.hpp. Adding additional index types for deserialize will just require an additional dispatch branch in the iface in the future
…hit 2 argument index not recognized error. This is because index used to be untyped with 2 args but now we added DatasetViewT template param on index which made index 3 args so FAISS call sites for index which were still 2 args were no longer recognized.
|
/ok to test 4258dc5 |
…_view for ACE build The new cagra::build() API requires dataset_view types (host_padded_dataset_view or device_padded_dataset_view) rather than raw raft::mdspan. This fixes the following build errors: - "no instance of function template cagra::attach_device_dataset_on_host_index matches" (caused by cagra::build resolving to mg_index overload when passed raw mdspan with uint32_t extents, since index_params implicitly converts to mg_index_params) - "no operator = matches these operands" for *dataset_ = std::move(padded->data_) (caused by uint32_t vs int64_t mdarray extents mismatch) Fix: use int64_t extents for dataset_view_host so make_device_padded_dataset produces data_ with matching int64_t extents, and explicitly create an owning host_padded_dataset (or zero-copy view when stride already matches) for the ACE cagra::build calls.
Wrap device dataset with make_device_padded_dataset_view before passing to cuvs::neighbors::cagra::build(), and use make_device_padded_dataset with as_dataset_view() for the host-data path, satisfying the is_device_dataset_view constraint required by the new build() API.
…isk-mode hnsw.build Three related bugs caused test_cagra_ace.py and test_hnsw_ace.py failures: 1. Use-after-free in cagra C API (in-memory ACE, ~1% recall): convert_host_to_device_index stores a VIEW of host_idx.graph_ (no ownership transfer). When host_idx goes out of scope, device_idx.graph_view_ becomes dangling. Fix: copy device graph to host then back to a new owned device copy before host_idx is destroyed. 2. Missing graph_fd/mapping_fd transfer in cagra C API (disk-mode ACE): After convert_host_to_device_index, only dataset_fd was stolen from host_idx. The graph_fd and mapping_fd were never transferred, so hnsw::from_cagra's disk-path check (dataset_fd && graph_fd) always fell through to the GPU path which failed with "No dataset provided". Fix: add steal_graph_fd()/steal_mapping_fd() to the index class and transfer all three FDs in the C API disk-mode block. 3. hnsw::build with use_disk=True never wrote hnsw_index.bin: The ACE host index had dataset/graph/mapping FDs, but the code created an in-memory device padded dataset and called attach_device_dataset_on_host_index, discarding the FDs. from_cagra never saw a disk-backed index and never called serialize_to_hnswlib_from_disk. Fix: detect disk mode (dataset_fd present), transfer all FDs to a fresh device_padded_index, then call from_cagra which takes the disk path and serializes the HNSW index to build_dir/hnsw_index.bin.
The CuvsCagra<data_t>::train() hunk had wrong counts in its @@ header: @@ -204,14 +207,22 @@ (declared 14 before, 22 after) @@ -204,15 +207,21 @@ (actual 15 before, 21 after) The off-by-one counts in opposite directions caused `git apply` to report "corrupt patch at line 301", silently skipping the entire 26.06 diff. This left GpuResources.h with the old rmm/mr/device_memory_resource.hpp include that was removed in RMM 26.06, causing CI build failures. Also removed a spurious blank line between the train() and search() hunks that caused a "patch fragment without header" error.
|
/ok to test 9c024c6 |
…ce path. Host_padded_index cannot have attach_dataset_on_build=true since we can't search on a host dataset regardless. ACE non-disk, ACE sub-builds, and merge no longer need attach_dataset_on_build in the new design
…taset_view The device path in BinaryCuvsCagra::train() was passing a raw mdspan to cagra::build() instead of a device_padded_dataset_view, causing: error: no instance of overloaded function "cuvs::neighbors::cagra::build" matches the argument list Fix by wrapping the device matrix view with make_device_padded_dataset_view before passing to build(), matching the pattern used in CuvsCagra<data_t>::train(). Also corrects the downstream hunk offsets (+183/+225/+297) in the BinaryCuvsCagra search() and reset() sections that were shifted by the 3-line addition.
update_graph(device_matrix_view) stores only a VIEW with no ownership transfer. When convert_host_to_device_index is called with a temporary host_idx (e.g. inside iface.hpp for the MG host-data build path), the returned device index ends up with a dangling graph_view_ once host_idx is destroyed, causing a segfault on first access. Fix: copy the graph device→host→device inside convert_host_to_device_index so the returned index always owns its graph memory. Remove the now-redundant workaround that was doing the same extra copy in cagra.cpp.
|
/ok to test e795819 |
…ed update_dataset() supporting host_matrix and device_matrix views were removed
|
/ok to test 26b87e5 |
…iew. All call sites updated to use Dataset API instead
…The main library already uses C++20
…ncepts. The main library already uses C++20" This reverts commit 48fc8d5.
|
/ok to test 217e5bd |
… to use the new Dataset API
|
/ok to test 8001bf4 |
|
/ok to test 3803e00 |
|
/ok to test c31ea7c |
Overview
Addressing #1574 and #1571.
Replaced strided_dataset with padded_dataset class. Added support all the way up to CAGRA code.
Old class structure (Classes + Inheritance):
Intermediate Class Structure (Classes + Inheritance):
dataset and dataset_view are now 2 separate parent classes.
Intermediate Class Structure (ContainerType Tags + Composition + Variants):
New Class Structure (ContainerType Tags + Composition):
Inheritance is removed entirely and all dataset types are on the same level of the inheritance tree.
Ownership
The index and cagra::build / cagra::index do not own raw vector storage, they only take views.
The old code had a type-erased std::unique_ptr<dataset_view<...>>, i.e. non-owning view handles. The new code uses templates on the index type which determines the type of dataset_view the index holds.
ACE v.s. non-ACE paths on Host
ACE path copies datasets that can't entirely fit in CPU memory in chunks onto GPU memory by calling make_padded_dataset. This is 1x memory on CPU and 1x memory on GPU.
Return types:
Used mainly to maintain lifetime of dataset.
cuvs_cagra_c_api_lifetime_holder
It is a single C++ struct in cagra.cpp that groups the real cagra::index with any extra heap-owned things the C API had to create so the index’s non-owning views stay valid.
cuvs_cagra_c_api_lifetime_holder is a separate heap object from cagra::index. It is heap-allocated in cagra.cpp with new cuvs_cagra_c_api_lifetime_holder<...>. The C API keeps a raw pointer to it in cuvsCagraIndex.cuvs_cagra_c_api_lifetime_holder It is not embedded in the index, which is why the C layer needs that second field to delete the holder on destroy.
Heap-allocated bundle for the C API: owns
cagra::indexand any co-owned device storage (VPQ, padded dataset copy, merge/de-serialize/extend buffers) when the index is not standalone.cuvsCagraIndex.c_api_lifetime_ownerpoints at this. Used for merge, build, deserialize, from_args, extend.The holder moves the owning device_padded_dataset (as unique_ptr<dataset<>> in padded_dataset_owner) to the heap, and cuvsCagraIndex.merged_owner points at the holder. Destroying the C index later destroys the holder first, so the dataset outlives the index’s use of the view, or the ordering is set up so the view is not used after free.
In cuvsCagraIndexFromArgs in cagra.cpp (C API) where callers are things like the Python cagra.from_graph (via Cython) and the Java CagraIndexImpl, and any C code that uses that function:
The flow is: caller → cuvsCagraIndexFromArgs → _from_args, which writes into the cuvsCagraIndex struct the user passed
The holder is not returned as a separate C return value. It is allocated on the heap and its address is stored in output_index->merged_owner, and output_index->addr points at the index inside that holder (or at a freestanding index when merged_owner == 0).
So when _from_args returns, the user’s cuvsCagraIndex already holds the pointers that describe where everything lives.
The unique_ptr to the copy of the dataset from make_padded_dataset is not local to _from_args—it is a member of the holder, which is on the heap and stays alive.
Miscellaneous: Extend Serialize Deserialize
Will fill in later
Factories:
Places where make_padded_dataset/view are called internally (not by user):
Host non-ACE path
Tiered CAGRA
Ownership in Downstream Functions:
Improvements:
Breaking Changes for Dataset API:
The following functions are removed since index no longer owns the dataset, index only takes views:
Removed old functions that took mdspan or derivatives of mdspan.
3 cases where index previously owned dataset [all deprecated paths]:
2 edge case build() paths when attach_dataset_on_build == true and a successful dense attach:
Merge:
These paths have since been removed.
Attach Dataset
Compressed Dataset
Merged Dataset
Deserialize
Helpers
How to attach a compressed dataset onto an uncompressed index?
How to attach a searchable device dataset onto an index built with host build?
a. Utilizes map of host dataset type to device dataset type counterpart
TODOs:
Recent Updates:
Future PRs:
PR#2: Add Support for Compressed Datasets
PR#3: Migrate Rest of Algorithms to use Dataset API