Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bc6de04
Add per-dimension scalar quantized datasets
tarang-jain Nov 2, 2026
4aec0a9
Keep scalar reconstruction finite at FP32 limits
tarang-jain Nov 3, 2026
ce4d9f9
Unify scalar quantizer modes behind shared dataset APIs
tarang-jain Nov 3, 2026
9202b60
Document scalar quantizer field migration
tarang-jain Nov 3, 2026
9a356af
Align SQ builder arguments and generated API documentation
tarang-jain Nov 3, 2026
baecf69
Merge branch 'main' into scalar-quantized-dataset
tarang-jain Nov 3, 2026
e73cce0
Rename scalar quantizer offsets to shifts
tarang-jain Nov 3, 2026
e46013e
Construct scalar quantizers from bounds and align dataset tests
tarang-jain Nov 3, 2026
bfa2676
Merge branch 'main' into scalar-quantized-dataset
tarang-jain Nov 3, 2026
31081bd
Support floating input types and consolidate scalar quantization tests
tarang-jain Nov 3, 2026
715af3d
Merge branch 'main' into scalar-quantized-dataset
tarang-jain Nov 3, 2026
a038a48
Fix half scalar quantizer shared-library export
tarang-jain Nov 3, 2026
007a55c
Compute scalar quantization reconstruction bounds during training
tarang-jain Nov 4, 2026
8e0c620
Stream scalar quantized datasets from host memory
tarang-jain Nov 4, 2026
3c10b83
Remove scalar quantizer synchronization notes
tarang-jain Nov 4, 2026
a00df6c
Merge branch 'main' into scalar-quantized-dataset
tarang-jain Nov 4, 2026
053dce7
Unify scalar training and derive quantized limits from the code type
tarang-jain Nov 4, 2026
614a84b
Store global and per-dimension scalar shifts and scales
tarang-jain Nov 5, 2026
823726d
Restrict host scalar transforms to global parameters
tarang-jain Nov 5, 2026
6e0f0b0
Fix HNSW ACE example for global scalar transform API
tarang-jain Nov 5, 2026
4368cdc
Simplify scalar quantization tests into focused cases
tarang-jain Nov 6, 2026
1a0de8d
Restore quantizer arguments for host scalar transforms
tarang-jain Nov 6, 2026
3f9984c
Rename scalar quantizer shifts_type to per_dim_type
tarang-jain Nov 6, 2026
10728a7
Preserve original global scalar quantizer bounds
tarang-jain Nov 6, 2026
ea9e922
Store scalar quantizer bounds and align preprocessing views
tarang-jain Nov 6, 2026
9caac96
Simplify scalar quantization and add per-dimension C API
tarang-jain Nov 6, 2026
1f3237e
Use existing scalar C APIs for per-dimension quantization
tarang-jain Nov 6, 2026
ad742ea
Merge branch 'main' into scalar-quantized-dataset
tarang-jain Nov 6, 2026
643b31a
Simplify scalar quantization tests and reuse DLPack conversion
tarang-jain Nov 7, 2026
7d6341a
Merge branch 'main' into scalar-quantized-dataset
tarang-jain Nov 7, 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
35 changes: 29 additions & 6 deletions c/include/cuvs/preprocessing/quantize/scalar.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

Expand All @@ -19,15 +19,24 @@ extern "C" {
* @defgroup preprocessing_c_scalar C API for Scalar Quantizer
* @{
*/
/** Select global or per-dimension scalar quantization bounds. */
enum cuvsScalarQuantizationMode {
CUVS_SCALAR_GLOBAL = 0,
CUVS_SCALAR_PER_DIMENSION = 1
};

/**
* @brief Scalar quantizer parameters.
* @brief Scalar quantizer parameters. Allocate with cuvsScalarQuantizerParamsCreate.
*/
struct cuvsScalarQuantizerParams {
/*
/**
* specifies how many outliers at top & bottom will be ignored
* needs to be within range of (0, 1]
*/
float quantile;

/** Quantization mode. Defaults to CUVS_SCALAR_GLOBAL. */
enum cuvsScalarQuantizationMode mode;
};

typedef struct cuvsScalarQuantizerParams* cuvsScalarQuantizerParams_t;
Expand All @@ -53,6 +62,11 @@ CUVS_EXPORT cuvsError_t cuvsScalarQuantizerParamsDestroy(cuvsScalarQuantizerPara
*
* The quantization is performed by a linear mapping of an interval in the
* float data type to the full range of the quantized int type.
* Allocate with cuvsScalarQuantizerCreate and release with cuvsScalarQuantizerDestroy;
* do not copy the handle's contents. The implementation also owns any per-dimension bounds.
* In global mode, min_ and max_ are the trained bounds. In per-dimension mode, they are
* numeric_limits<T>::min() and numeric_limits<T>::max() for the training data type T,
* and are not used for transformation.
*/
typedef struct {
double min_;
Expand All @@ -70,7 +84,7 @@ typedef cuvsScalarQuantizer* cuvsScalarQuantizer_t;
CUVS_EXPORT cuvsError_t cuvsScalarQuantizerCreate(cuvsScalarQuantizer_t* quantizer);

/**
* @brief De-allocate Scalar Quantizer
* @brief De-allocate Scalar Quantizer after outstanding stream work completes.
*
* @param[in] quantizer
* @return cuvsError_t
Expand All @@ -80,10 +94,15 @@ CUVS_EXPORT cuvsError_t cuvsScalarQuantizerDestroy(cuvsScalarQuantizer_t quantiz
/**
* @brief Trains a scalar quantizer to be used later for quantizing the dataset.
*
* Supports float16, float32, and float64 in global or per-dimension mode.
* Per-dimension mode applies the quantile independently to each dimension using every row.
* Successful retraining replaces the previous bounds; failed training preserves them.
* Complete outstanding stream work using the quantizer before retraining.
*
* @param[in] res raft resource
* @param[in] params configure scalar quantizer, e.g. quantile
* @param[in] dataset a row-major host or device matrix
* @param[out] quantizer trained scalar quantizer
* @param[in,out] quantizer scalar quantizer allocated by cuvsScalarQuantizerCreate
*/
CUVS_EXPORT cuvsError_t cuvsScalarQuantizerTrain(cuvsResources_t res,
cuvsScalarQuantizerParams_t params,
Expand All @@ -93,6 +112,9 @@ CUVS_EXPORT cuvsError_t cuvsScalarQuantizerTrain(cuvsResources_t res,
/**
* @brief Applies quantization transform to given dataset
*
* Per-dimension mode requires device matrices with the same input type and width as training.
* Its output codes are int8 in [-127, 127]. The quantizer must outlive stream work using its bounds.
*
* @param[in] res raft resource
* @param[in] quantizer a scalar quantizer
* @param[in] dataset a row-major host or device matrix to transform
Expand All @@ -107,7 +129,8 @@ CUVS_EXPORT cuvsError_t cuvsScalarQuantizerTransform(cuvsResources_t res,
* @brief Perform inverse quantization step on previously quantized dataset
*
* Note that depending on the chosen data types train dataset the conversion is
* not lossless.
* not lossless. Per-dimension mode requires device matrices with the same output type and
* width as training. The quantizer must outlive stream work using its bounds.
*
* @param[in] res raft resource
* @param[in] quantizer a scalar quantizer
Expand Down
189 changes: 123 additions & 66 deletions c/src/preprocessing/quantize/scalar.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION.
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

#include <cstdint>
#include <cuda/std/limits>
#include <memory>
#include <dlpack/dlpack.h>

#include <cuvs/core/c_api.h>
Expand All @@ -15,49 +17,40 @@

namespace {

namespace scalar = cuvs::preprocessing::quantize::scalar;

// Keep the public min/max layout while owning per-dimension bounds behind the factory handle.
struct scalar_quantizer : cuvsScalarQuantizer {
uintptr_t addr = 0;
DLDataType dtype{};
};

template <typename T>
void _train(cuvsResources_t res,
cuvsScalarQuantizerParams params,
DLManagedTensor* dataset_tensor,
cuvsScalarQuantizer_t quantizer)
scalar::quantizer<T> _train(cuvsResources_t res,
scalar::params params,
DLManagedTensor* dataset_tensor)
{
auto dataset = dataset_tensor->dl_tensor;

auto res_ptr = reinterpret_cast<raft::resources*>(res);

auto quantizer_params = cuvs::preprocessing::quantize::scalar::params();
quantizer_params.quantile = params.quantile;

cuvs::preprocessing::quantize::scalar::quantizer<T> ret;

if (cuvs::core::is_dlpack_device_compatible(dataset)) {
using mdspan_type = raft::device_matrix_view<T const, int64_t, raft::row_major>;
auto mds = cuvs::core::from_dlpack<mdspan_type>(dataset_tensor);
ret = cuvs::preprocessing::quantize::scalar::train(*res_ptr, quantizer_params, mds);
return scalar::train(*res_ptr, params, cuvs::core::from_dlpack<mdspan_type>(dataset_tensor));
} else if (cuvs::core::is_dlpack_host_compatible(dataset)) {
using mdspan_type = raft::host_matrix_view<T const, int64_t, raft::row_major>;
auto mds = cuvs::core::from_dlpack<mdspan_type>(dataset_tensor);
ret = cuvs::preprocessing::quantize::scalar::train(*res_ptr, quantizer_params, mds);
return scalar::train(*res_ptr, params, cuvs::core::from_dlpack<mdspan_type>(dataset_tensor));
} else {
RAFT_FAIL("dataset must be accessible on host or device memory");
}

quantizer->min_ = ret.min_;
quantizer->max_ = ret.max_;
}

template <typename T, typename OutputT = int8_t>
void _transform(cuvsResources_t res,
cuvsScalarQuantizer_t quantizer_,
const scalar::quantizer<T>& quantizer,
DLManagedTensor* dataset_tensor,
DLManagedTensor* out_tensor)
{
auto res_ptr = reinterpret_cast<raft::resources*>(res);

cuvs::preprocessing::quantize::scalar::quantizer<T> quantizer;
quantizer.min_ = quantizer_->min_;
quantizer.max_ = quantizer_->max_;

auto dataset = dataset_tensor->dl_tensor;
if (cuvs::core::is_dlpack_device_compatible(dataset)) {
using mdspan_type = raft::device_matrix_view<T const, int64_t, raft::row_major>;
Expand Down Expand Up @@ -85,16 +78,12 @@ void _transform(cuvsResources_t res,

template <typename OutputT, typename InputT = int8_t>
void _inverse_transform(cuvsResources_t res,
cuvsScalarQuantizer_t quantizer_,
const scalar::quantizer<OutputT>& quantizer,
DLManagedTensor* dataset_tensor,
DLManagedTensor* out_tensor)
{
auto res_ptr = reinterpret_cast<raft::resources*>(res);

cuvs::preprocessing::quantize::scalar::quantizer<OutputT> quantizer;
quantizer.min_ = quantizer_->min_;
quantizer.max_ = quantizer_->max_;

auto dataset = dataset_tensor->dl_tensor;
if (cuvs::core::is_dlpack_device_compatible(dataset)) {
using mdspan_type = raft::device_matrix_view<InputT const, int64_t, raft::row_major>;
Expand All @@ -119,12 +108,59 @@ void _inverse_transform(cuvsResources_t res,
RAFT_FAIL("dataset must be accessible on host or device memory");
}
}

template <typename T>
void _transform(cuvsResources_t res,
cuvsScalarQuantizer_t quantizer,
DLManagedTensor* dataset,
DLManagedTensor* out)
{
_transform(res,
scalar::quantizer<T>{static_cast<T>(quantizer->min_), static_cast<T>(quantizer->max_)},
dataset,
out);
}

template <typename T>
void _inverse_transform(cuvsResources_t res,
cuvsScalarQuantizer_t quantizer,
DLManagedTensor* dataset,
DLManagedTensor* out)
{
_inverse_transform(res,
scalar::quantizer<T>{static_cast<T>(quantizer->min_), static_cast<T>(quantizer->max_)},
dataset,
out);
}

template <typename Fn>
void dispatch_float(DLDataType dtype, Fn&& fn)
{
RAFT_EXPECTS(dtype.code == kDLFloat && dtype.lanes == 1, "Expected a scalar floating-point dtype");
switch (dtype.bits) {
case 16: fn.template operator()<half>(); break;
case 32: fn.template operator()<float>(); break;
case 64: fn.template operator()<double>(); break;
default: RAFT_FAIL("Unsupported floating-point dtype bits: %d", dtype.bits);
}
}

void destroy_per_dim(scalar_quantizer* quantizer)
{
if (quantizer->addr) {
dispatch_float(quantizer->dtype, [&]<typename T>() {
delete reinterpret_cast<scalar::quantizer<T>*>(quantizer->addr);
});
quantizer->addr = 0;
}
}

} // namespace

extern "C" cuvsError_t cuvsScalarQuantizerParamsCreate(cuvsScalarQuantizerParams_t* params)
{
return cuvs::core::translate_exceptions(
[=] { *params = new cuvsScalarQuantizerParams{.quantile = 0.99}; });
[=] { *params = new cuvsScalarQuantizerParams{.quantile = 0.99, .mode = CUVS_SCALAR_GLOBAL}; });
}

extern "C" cuvsError_t cuvsScalarQuantizerParamsDestroy(cuvsScalarQuantizerParams_t params)
Expand All @@ -134,12 +170,21 @@ extern "C" cuvsError_t cuvsScalarQuantizerParamsDestroy(cuvsScalarQuantizerParam

extern "C" cuvsError_t cuvsScalarQuantizerCreate(cuvsScalarQuantizer_t* quantizer)
{
return cuvs::core::translate_exceptions([=] { *quantizer = new cuvsScalarQuantizer{}; });
return cuvs::core::translate_exceptions([=] {
RAFT_EXPECTS(quantizer != nullptr, "quantizer must not be null");
*quantizer = new scalar_quantizer{};
});
}

extern "C" cuvsError_t cuvsScalarQuantizerDestroy(cuvsScalarQuantizer_t quantizer)
{
return cuvs::core::translate_exceptions([=] { delete quantizer; });
return cuvs::core::translate_exceptions([=] {
if (quantizer) {
auto q = static_cast<scalar_quantizer*>(quantizer);
destroy_per_dim(q);
delete q;
}
});
}

extern "C" cuvsError_t cuvsScalarQuantizerTrain(cuvsResources_t res,
Expand All @@ -148,18 +193,34 @@ extern "C" cuvsError_t cuvsScalarQuantizerTrain(cuvsResources_t res,
cuvsScalarQuantizer_t quantizer)
{
return cuvs::core::translate_exceptions([=] {
auto dataset = dataset_tensor->dl_tensor;
if (dataset.dtype.code == kDLFloat && dataset.dtype.bits == 32) {
_train<float>(res, *params, dataset_tensor, quantizer);
} else if (dataset.dtype.code == kDLFloat && dataset.dtype.bits == 16) {
_train<half>(res, *params, dataset_tensor, quantizer);
} else if (dataset.dtype.code == kDLFloat && dataset.dtype.bits == 64) {
_train<double>(res, *params, dataset_tensor, quantizer);
} else {
RAFT_FAIL("Unsupported dataset DLtensor dtype: %d and bits: %d",
dataset.dtype.code,
dataset.dtype.bits);
RAFT_EXPECTS(params && dataset_tensor && quantizer,
"params, dataset, and quantizer must not be null");
scalar::params cpp_params;
cpp_params.quantile = params->quantile;
switch (params->mode) {
case CUVS_SCALAR_GLOBAL: cpp_params.range = scalar::quantization_mode::GLOBAL; break;
case CUVS_SCALAR_PER_DIMENSION:
cpp_params.range = scalar::quantization_mode::PER_DIMENSION;
break;
default: RAFT_FAIL("Unsupported scalar quantization mode");
}
auto q = static_cast<scalar_quantizer*>(quantizer);
dispatch_float(dataset_tensor->dl_tensor.dtype, [&]<typename T>() {
auto trained = _train<T>(res, cpp_params, dataset_tensor);
if (auto const* bounds =
std::get_if<typename scalar::quantizer<T>::global_type>(&trained.value)) {
destroy_per_dim(q);
q->min_ = bounds->min;
q->max_ = bounds->max;
} else {
auto owned = std::make_unique<scalar::quantizer<T>>(std::move(trained));
destroy_per_dim(q);
q->addr = reinterpret_cast<uintptr_t>(owned.release());
q->dtype = dataset_tensor->dl_tensor.dtype;
q->min_ = static_cast<double>(cuda::std::numeric_limits<T>::min());
q->max_ = static_cast<double>(cuda::std::numeric_limits<T>::max());
}
});
});
}

Expand All @@ -169,18 +230,16 @@ extern "C" cuvsError_t cuvsScalarQuantizerTransform(cuvsResources_t res,
DLManagedTensor* out_tensor)
{
return cuvs::core::translate_exceptions([=] {
auto dataset = dataset_tensor->dl_tensor;
if (dataset.dtype.code == kDLFloat && dataset.dtype.bits == 32) {
_transform<float>(res, quantizer, dataset_tensor, out_tensor);
} else if (dataset.dtype.code == kDLFloat && dataset.dtype.bits == 16) {
_transform<half>(res, quantizer, dataset_tensor, out_tensor);
} else if (dataset.dtype.code == kDLFloat && dataset.dtype.bits == 64) {
_transform<double>(res, quantizer, dataset_tensor, out_tensor);
} else {
RAFT_FAIL("Unsupported dataset DLtensor dtype: %d and bits: %d",
dataset.dtype.code,
dataset.dtype.bits);
}
RAFT_EXPECTS(quantizer && dataset_tensor && out_tensor,
"quantizer, dataset, and out must not be null");
auto q = static_cast<scalar_quantizer*>(quantizer);
dispatch_float(q->addr ? q->dtype : dataset_tensor->dl_tensor.dtype, [&]<typename T>() {
if (q->addr) {
_transform(res, *reinterpret_cast<scalar::quantizer<T>*>(q->addr), dataset_tensor, out_tensor);
} else {
_transform<T>(res, quantizer, dataset_tensor, out_tensor);
}
});
});
}

Expand All @@ -190,16 +249,14 @@ cuvsError_t cuvsScalarQuantizerInverseTransform(cuvsResources_t res,
DLManagedTensor* out)
{
return cuvs::core::translate_exceptions([=] {
auto dtype = out->dl_tensor.dtype;
if (dtype.code == kDLFloat && dtype.bits == 32) {
_inverse_transform<float>(res, quantizer, dataset, out);
} else if (dtype.code == kDLFloat && dtype.bits == 16) {
_inverse_transform<half>(res, quantizer, dataset, out);
} else if (dtype.code == kDLFloat && dtype.bits == 64) {
_inverse_transform<double>(res, quantizer, dataset, out);
} else {
RAFT_FAIL(
"Unsupported output dataset DLtensor dtype: %d and bits: %d", dtype.code, dtype.bits);
}
RAFT_EXPECTS(quantizer && dataset && out, "quantizer, dataset, and out must not be null");
auto q = static_cast<scalar_quantizer*>(quantizer);
dispatch_float(q->addr ? q->dtype : out->dl_tensor.dtype, [&]<typename T>() {
if (q->addr) {
_inverse_transform(res, *reinterpret_cast<scalar::quantizer<T>*>(q->addr), dataset, out);
} else {
_inverse_transform<T>(res, quantizer, dataset, out);
}
});
});
}
1 change: 1 addition & 0 deletions c/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ ConfigureTest(
)

ConfigureTest(NAME PCA_C_TEST PATH preprocessing/pca_c.cu)
ConfigureTest(NAME SCALAR_QUANTIZATION_C_TEST PATH preprocessing/scalar_c.cu)

if(BUILD_CAGRA_HNSWLIB)
ConfigureTest(NAME HNSW_C_TEST PATH neighbors/ann_hnsw_c.cu)
Expand Down
Loading
Loading