Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions c/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ add_library(
src/neighbors/all_neighbors.cpp
src/preprocessing/pca.cpp
src/preprocessing/quantize/binary.cpp
src/preprocessing/quantize/bbq.cpp
src/preprocessing/quantize/pq.cpp
src/preprocessing/quantize/scalar.cpp
src/distance/pairwise_distance.cpp
Expand Down
1 change: 1 addition & 0 deletions c/include/cuvs/core/all.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

#include <cuvs/preprocessing/pca.h>
#include <cuvs/preprocessing/quantize/binary.h>
#include <cuvs/preprocessing/quantize/bbq.h>
#include <cuvs/preprocessing/quantize/pq.h>
#include <cuvs/preprocessing/quantize/scalar.h>

Expand Down
3 changes: 2 additions & 1 deletion c/include/cuvs/core/dataset.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ extern "C" {
typedef enum {
CUVS_DATASET_LAYOUT_STANDARD = 0,
CUVS_DATASET_LAYOUT_PADDED = 1,
CUVS_DATASET_LAYOUT_PQ = 2
CUVS_DATASET_LAYOUT_PQ = 2,
CUVS_DATASET_LAYOUT_BBQ = 3
} cuvsDatasetLayout_t;

/**
Expand Down
17 changes: 11 additions & 6 deletions c/include/cuvs/neighbors/cagra.h
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,8 @@ CUVS_EXPORT cuvsError_t cuvsCagraUpdateDataset(cuvsResources_t res,
*
* The memory space and layout \p dataset was constructed with select the C++ build overload.
* Build the handle with an owning factory or the matching dataset view factory
* (`cuvsDatasetMakePaddedView` / `cuvsDatasetMakeStandardView`).
* (`cuvsDatasetMakePaddedView`, `cuvsDatasetMakeStandardView`, or
* `cuvsDatasetMakeBbqView`).
*
* Note that a dataset residing in host memory produces a host-backed index, which
* must be made search-ready with `cuvsCagraUpdateDataset` (using a device-padded
Expand Down Expand Up @@ -696,6 +697,8 @@ CUVS_EXPORT cuvsError_t cuvsCagraUpdateDataset(cuvsResources_t res,
* A `CUVS_DATASET_LAYOUT_PQ` dataset created by `cuvsDatasetMakePQ` builds an iterative CAGRA-Q
* index. VPQ input requires `L2Expanded` and `ITERATIVE_CAGRA_SEARCH` (or `AUTO_SELECT`), and the
* VPQ dataset must outlive the index because the index stores a non-owning view.
* A `CUVS_DATASET_LAYOUT_BBQ` dataset builds a graph-only index; attach a searchable dataset with
* `cuvsCagraUpdateDataset` before search.
*
* @param[in] res cuvsResources_t opaque C handle
* @param[in] params cuvsCagraIndexParams_t used to build CAGRA index
Expand Down Expand Up @@ -860,8 +863,9 @@ CUVS_EXPORT cuvsError_t cuvsCagraSearchMultiPartition(cuvsResources_t res,
/**
* Save the CAGRA graph to file without its dataset.
*
* This supports dense and PQ-backed indexes. The dataset must be attached separately after loading
* the graph.
* This supports dense, PQ-backed, and BBQ-built indexes. The serialized file does not contain
* vector data. After deserialization the index cannot be searched until a compatible dataset is
* attached with `cuvsCagraUpdateDataset`.
*
* Experimental, both the API and the serialization format are subject to change.
*
Expand All @@ -876,9 +880,10 @@ CUVS_EXPORT cuvsError_t cuvsCagraSerializeGraph(cuvsResources_t res,
/**
* Save the CAGRA graph and its attached dataset to file.
*
* The index stores a non-owning dataset view. The caller must keep the dataset backing that view
* alive while this function runs. Returns CUVS_ERROR without modifying the destination file if
* the index has no attached dataset. PQ datasets are not serialized by this function.
* The index stores a non-owning dataset view. The caller must keep the memory of the dataset
* backing that view alive while this function runs. Returns CUVS_ERROR without modifying the
* destination file if the index has no attached dataset. PQ and BBQ datasets are not serialized
* by this function.
*
* Experimental, both the API and the serialization format are subject to change.
*
Expand Down
142 changes: 142 additions & 0 deletions c/include/cuvs/preprocessing/quantize/bbq.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

#pragma once

#include <cuvs/core/c_api.h>
#include <cuvs/core/dataset.h>
#include <cuvs/distance/distance.h>

#include <dlpack/dlpack.h>
#include <stddef.h>
#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

/**
* @defgroup preprocessing_c_bbq C API for Better Binary Quantization datasets
* @{
*/

/**
* Storage layout of BBQ/OSQ quantized component codes in each dataset row.
* CUVS_BBQ_CODE_LAYOUT_PACKED_1B: Each dimension is quantized to a single bit and packed into bytes. Reflects
* Lucene's OptimizedScalarQuantizer.packAsBinary.
* CUVS_BBQ_CODE_LAYOUT_TRANSPOSED_2B: Each dimension is quantized to 2 bits, stored as 2 bitplanes.
* Reflects Lucene's OptimizedScalarQuantizer.transposeDibit. SIMT popc path only
* (paired with a transposed_4b or packed_1b operand);
* CUVS_BBQ_CODE_LAYOUT_TRANSPOSED_4B: Each dimension is quantized to 4 bits, optimized for bitwise operations.
* Reflects Lucene's OptimizedScalarQuantizer.transposeHalfByte. the first bit of
* every dimension is in the first set dimensions bits, or (dimensions/8)
* bytes. The second, third, and fourth bits are in the second, third, and
* fourth set of dimensions bits, respectively. Format used for queries.
* CUVS_BBQ_CODE_LAYOUT_PACKED_4B: Each dimension is quantized to 4 bits, two values are packed into each output
* byte.
* CUVS_BBQ_CODE_LAYOUT_PACKED_7B: Each dimension is quantized to 7 bits and treated as a signed value.
* CUVS_BBQ_CODE_LAYOUT_PACKED_8B: Each dimension is quantized to 8 bits and treated as an unsigned value.
*/
typedef enum {
CUVS_BBQ_CODE_LAYOUT_PACKED_1B = 0,
CUVS_BBQ_CODE_LAYOUT_TRANSPOSED_2B,
CUVS_BBQ_CODE_LAYOUT_TRANSPOSED_4B,
CUVS_BBQ_CODE_LAYOUT_PACKED_4B,
CUVS_BBQ_CODE_LAYOUT_PACKED_7B,
CUVS_BBQ_CODE_LAYOUT_PACKED_8B
} cuvsBbqCodeLayout_t;


/**
* @brief Better Binary Quantization
* ([BBQ](https://www.elastic.co/search-labs/blog/better-binary-quantization-lucene-elasticsearch))
* is a vector-quantization approach used in Elasticsearch and Apache Lucene. It builds on ideas
* introduced in RaBitQ([Gao and Long](https://arxiv.org/pdf/2405.12497, [Gao et
* al.](https://arxiv.org/pdf/2409.09913)): residual binary codes around a centroid, corrective
* factors, and efficient bitwise comparison of codes at different bit widths. Lucene implements
* this as optimized scalar quantization (OSQ) with packed and bit-plane layouts; Elasticsearch
* exposes it as BBQ.
*
* BBQ in cuVS designed to be compatible with the Lucene/Elasticsearch dataset: a single shared
* centroid, no random rotation, and OSQ codes.
*
* RaBitQ and BBQ in cuVS both compress centroid-relative vectors to low-bit codes and retain
* additional per-vector information so search is better than naïve sign-bit comparison. They differ
* in transformation and scale representation. RaBitQ commonly separates residual magnitude from
* direction, then applies a random orthogonal rotation before binary coding; BBQ uses per-vector
* scalar intervals to interpret the compressed residual codes.
*/
typedef struct cuvsBbqQuantizer {
uintptr_t addr;
void (*destroy_addr)(void*);
DLDataType dtype;
bool is_owning;
} cuvsBbqQuantizer;
typedef cuvsBbqQuantizer* cuvsBbqQuantizer_t;

/**
* @brief Create a BBQ quantizer view from caller-owned device tensors.
*
* Tensors are not copied and must remain valid while a derived dataset is in use.
*
* @param[in] codes uint8 device matrix containing encoded rows
* @param[in] lower_intervals float32 device vector with one lower interval per row
* @param[in] upper_intervals float32 device vector with one upper interval per row
* @param[in] additional_corrections float32 device vector with one correction per row
* @param[in] quantized_component_sums int32 device vector with one component sum per row
* @param[in] centroid device vector containing the dataset centroid
* @param[in] dequant_delta float32 device vector with one dequantization delta per row
* @param[in] dequant_sum_delta float32 device vector with one delta-times-sum value per row
* @param[in] row_norm float32 device vector with one original-space squared norm per row
* @param[in] layout encoded code layout
* @param[in] metric distance metric associated with the encoded dataset
* @param[in] centroid_norm_sq squared norm of the centroid
* @param[out] quantizer newly allocated non-owning quantizer handle
* @return cuvsError_t
*/
CUVS_EXPORT cuvsError_t cuvsBbqQuantizerCreateView(
DLManagedTensor* codes,
DLManagedTensor* lower_intervals,
DLManagedTensor* upper_intervals,
DLManagedTensor* additional_corrections,
DLManagedTensor* quantized_component_sums,
DLManagedTensor* centroid,
DLManagedTensor* dequant_delta,
DLManagedTensor* dequant_sum_delta,
DLManagedTensor* row_norm,
cuvsBbqCodeLayout_t layout,
cuvsDistanceType metric,
float centroid_norm_sq,
cuvsBbqQuantizer_t* quantizer);

/**
* @brief Destroy a BBQ quantizer without destroying its caller-owned tensors.
*
* @param[in] quantizer quantizer handle to destroy
* @return cuvsError_t
*/
CUVS_EXPORT cuvsError_t cuvsBbqQuantizerDestroy(cuvsBbqQuantizer_t quantizer);

/**
* @brief Create a non-owning device BBQ dataset view.
*
* Accepts one symmetric quantizer or two compatible asymmetric quantizers.
*
* @param[in] res cuVS resources
* @param[in] quantizers array containing one or two BBQ quantizer handles
* @param[in] num_quantizers number of elements in `quantizers`
* @param[out] dataset newly allocated non-owning BBQ dataset handle
* @return cuvsError_t
*/
CUVS_EXPORT cuvsError_t cuvsDatasetMakeBbqView(cuvsResources_t res,
cuvsBbqQuantizer_t* quantizers,
size_t num_quantizers,
cuvsDataset_t* dataset);

/** @} */

#ifdef __cplusplus
}
#endif
Loading
Loading