Skip to content

[WIP] Vpq dataset serialisation - #2480

Open
irina-resh-nvda wants to merge 4 commits into
NVIDIA:mainfrom
irina-resh-nvda:vpq_dataset_serialisation
Open

[WIP] Vpq dataset serialisation#2480
irina-resh-nvda wants to merge 4 commits into
NVIDIA:mainfrom
irina-resh-nvda:vpq_dataset_serialisation

Conversation

@irina-resh-nvda

@irina-resh-nvda irina-resh-nvda commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Main functionality: A VPQ dataset could be built and loaded but not saved: serialize static_asserts on any dataset view that is not dense, so compressed rows were requantized on every run and a CAGRA-Q index could not be persisted at all.
This restores the VPQ writer in the dataset serializer and exposes it twice: pq::serialize / pq::deserialize for a device_vpq_dataset<half, int64_t>, over a stream or a filename, so a dataset can be compressed once offline and reloaded; and cagra::serialize / deserialize for vpq_f16_index under a new device_vpq_f16 dataset kind, so the compressed rows travel inside the index file.
There out_dataset is required rather than optional, since those rows exist nowhere else and a graph-only load would hand back an index that cannot answer a query.
A standalone .vpq carries the preamble the CAGRA index files already use, a NumPy dtype prefix then a version, so a future change to the encoded row layout fails cleanly instead of being misread; the index format version is unchanged, as the new kind is only an added enumerator.

Also here: the C API names the VPQ kind instead of reporting such a file as invalid, and MG_C_TEST is registered only when BUILD_MG_ALGOS is on, where it previously failed to link.

Tests: PREPROCESSING_TEST gains *VpqSerialization*, which round-trips the pq_bits, pq_dim combinations CAGRA-Q accepts, comparing both raw bytes and decoded vectors, and covers the refusals: an empty stream, a foreign dtype prefix, a version from the future, a truncated payload and a null out-parameter.
NEIGHBORS_ANN_CAGRA_FLOAT_UINT32_TEST gains *CagraVpqSerialize*, which saves a CAGRA-Q index, loads it back, and checks that it returns the same neighbours with the dense rows gone. It also covers saving the graph on its own, and refusing to load a compressed index as a dense one.

@irina-resh-nvda
irina-resh-nvda requested review from a team as code owners August 18, 2026 12:38
@irina-resh-nvda irina-resh-nvda self-assigned this Aug 18, 2026
Comment thread c/src/neighbors/cagra.cpp
// cuvsDatasetLayout_t covers standard and padded only, and every C entry point dispatches
// on that layout, so there is nothing here to hand a VPQ index to yet.
RAFT_FAIL("File holds a VPQ-compressed (CAGRA-Q) dataset, which the C API has no dataset "
"layout for; load it through the C++ API");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not an acceptable way to handle things tha are not yet exposed to C. Folks use C because they have to (for C ABI stability guarantees, for example). We can't just throw an error and forward them to C++

/** Host-resident dataset using its standard row layout. */
host_standard = 4,
/** Device-resident VPQ-compressed dataset with f16 codebooks (CAGRA-Q). */
device_vpq_f16 = 5,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're adding new things, please rename to device_pq. VPQ is not a proper term it accidentally made its way into the public APIs (which was an oversight). Also- why the "f16"? Can we drop that?

* error rather than a silently unusable index. For the same reason `include_dataset = false`
* produces an index that only `update_dataset` can make searchable again.
*/
void serialize(raft::resources const& handle,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Order of argument should be handle, params (if applicable), input, output.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the other functions did not follow the proper order. Let's at least make sure the new functions are in the proper order (we can have someone update the others in a follow-up).

* Unlike the dense overloads, `out_dataset` is required. Nothing can be searched in a VPQ index
* whose rows were dropped, so there is no use for a graph-only load, and asking for one is an
* error rather than a silently unusable index. For the same reason `include_dataset = false`
* produces an index that only `update_dataset` can make searchable again.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use proper doxygen tags to denote which arguments are inputs and outputs.

std::unique_ptr<cuvs::neighbors::device_standard_dataset<uint8_t, int64_t>>*
out_dataset = nullptr);

/* vpq_f16_index overloads (CAGRA-Q).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vpq_f16_index doesn't mean anything to a user reading the docs. Please opt to use more descriptive summaries for comments. Please also don't call this "cagra-q" anymore. It's just cagra with quantized vectors (the vector quantization should be completely decoupled from the graph).

void deserialize(
raft::resources const& handle,
const std::string& filename,
cuvs::neighbors::cagra::vpq_f16_index<float>* index,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make the output dataset optional. The graph does not need to be deserialized with the vectors and we should not enforce this. Also- we would like to not require the dataset be known up front wen the user is deserializing a graph that might happen to have a dataset. Goal is to have it deserialize to some dataset owned by the user, but the user shouldn't have to care about its concrete type.

In the C layer, we play to make this even more abstract.

std::istream& is,
std::unique_ptr<cuvs::neighbors::device_vpq_dataset<half, int64_t>>* out_dataset)
{
RAFT_EXPECTS(out_dataset != nullptr, "pq::deserialize: out_dataset must not be null");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't do this- the whole point in decoupling the dataset from the index is to satisfy use-cases where the user has already pq quantized their dataset and just wants to construct a graph. They should not be forced to store the pq vectors with the index.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants