Skip to content
Open
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 .github/workflows/03-macos-linux-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ jobs:
--no-build-isolation \
--config-settings='cmake.define.BUILD_TOOLS=ON' \
--config-settings='cmake.define.ENABLE_WERROR=ON' \
--config-settings='cmake.define.USE_OSS_MIRROR=ON' \
--config-settings='cmake.define.CMAKE_C_COMPILER_LAUNCHER=ccache' \
--config-settings='cmake.define.CMAKE_CXX_COMPILER_LAUNCHER=ccache' \
${{ matrix.arch_flag }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/04-android-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ jobs:
-DENABLE_NATIVE=OFF \
-DAUTO_DETECT_ARCH=OFF \
-DENABLE_WERROR=ON \
-DUSE_OSS_MIRROR=ON \
-DCMAKE_INSTALL_PREFIX="$BUILD_DIR/install" \
-DGLOBAL_CC_PROTOBUF_PROTOC="$GITHUB_WORKSPACE/build_host/bin/protoc" \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/05-windows-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ jobs:
--no-build-isolation `
--config-settings='cmake.define.BUILD_TOOLS=ON' `
--config-settings='cmake.define.ENABLE_WERROR=ON' `
--config-settings='cmake.define.USE_OSS_MIRROR=ON' `
--config-settings='cmake.define.CMAKE_C_COMPILER_LAUNCHER=sccache' `
--config-settings='cmake.define.CMAKE_CXX_COMPILER_LAUNCHER=sccache'
shell: powershell
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/06-ios-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ jobs:
-DBUILD_PYTHON_BINDINGS=OFF \
-DBUILD_TOOLS=OFF \
-DENABLE_WERROR=ON \
-DUSE_OSS_MIRROR=ON \
-DCMAKE_INSTALL_PREFIX="./install" \
-DGLOBAL_CC_PROTOBUF_PROTOC="$GITHUB_WORKSPACE/build_host/bin/protoc" \
-DIOS=ON \
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/07-linux-riscv-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ jobs:
--no-build-isolation \
--config-settings='cmake.define.BUILD_TOOLS="ON"' \
--config-settings='cmake.define.ENABLE_WERROR=ON' \
--config-settings='cmake.define.USE_OSS_MIRROR=ON' \
--config-settings='cmake.define.CMAKE_C_COMPILER_LAUNCHER=ccache' \
--config-settings='cmake.define.CMAKE_CXX_COMPILER_LAUNCHER=ccache'
shell: bash
Expand Down
40 changes: 34 additions & 6 deletions src/core/interface/index.cc
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ int Index::CreateAndInitConverterReformer(const QuantizerParam &param,
case QuantizerType::kRabitq:
// no converter here
return 0;
case QuantizerType::kUniformInt8:
converter_name = "UniformInt8StreamingConverter";
break;
default:
LOG_ERROR("Unsupported quantizer type: ");
return core::IndexError_Unsupported;
Expand All @@ -187,13 +190,17 @@ int Index::CreateAndInitConverterReformer(const QuantizerParam &param,
}

proxima_index_meta_ = converter_->meta();
reformer_ =
core::IndexFactory::CreateReformer(proxima_index_meta_.reformer_name());
if (reformer_ == nullptr ||
reformer_->init(proxima_index_meta_.reformer_params()) != 0) {
LOG_ERROR("Failed to create and init reformer");
return core::IndexError_Runtime;

if (!proxima_index_meta_.reformer_name().empty()) {
reformer_ =
core::IndexFactory::CreateReformer(proxima_index_meta_.reformer_name());
if (reformer_ == nullptr ||
reformer_->init(proxima_index_meta_.reformer_params()) != 0) {
LOG_ERROR("Failed to create and init reformer");
return core::IndexError_Runtime;
}
}

streamer_vector_meta_.set_meta(proxima_index_meta_.data_type(),
proxima_index_meta_.dimension());
streamer_vector_meta_.set_meta_type(proxima_index_meta_.meta_type());
Expand Down Expand Up @@ -294,6 +301,27 @@ int Index::Open(const std::string &file_path, StorageOptions storage_options) {
return core::IndexError_Runtime;
}

// If a converter exists but reformer was not created during Init()
// (converters like UniformInt8 whose reformer params are only available
// after train()), create it now from the persisted meta that the streamer
// has loaded. When there is no converter (QuantizerType::kNone), reformer_
// is nullptr by design — skip this block entirely.
if (converter_ != nullptr && reformer_ == nullptr) {
const auto &meta = streamer_->meta();
if (meta.reformer_name().empty()) {
LOG_ERROR(
"Index::Open: converter exists but reformer not initialized and "
"no reformer in persisted meta");
return core::IndexError_Runtime;
}
reformer_ = core::IndexFactory::CreateReformer(meta.reformer_name());
if (!reformer_ || reformer_->init(meta.reformer_params()) != 0) {
LOG_ERROR("Failed to create reformer '%s' from persisted meta",
meta.reformer_name().c_str());
return core::IndexError_Runtime;
}
}

// converter/reformer/metric are created in IndexFactory::CreateIndex
// TODO: init

Expand Down
4 changes: 4 additions & 0 deletions src/core/metric/metric_params.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,9 @@ static const std::string QUANTIZED_INTEGER_METRIC_ORIGIN_METRIC_NAME =
static const std::string QUANTIZED_INTEGER_METRIC_ORIGIN_METRIC_PARAMS =
"proxima.quantized_integer.metric.origin_metric_params";

//! UniformInt8 Metric
static const std::string UNIFORM_INT8_METRIC_ORIGIN_METRIC_NAME =
"proxima.uniform_int8.metric.origin_metric_name";

} // namespace core
} // namespace zvec
158 changes: 158 additions & 0 deletions src/core/metric/uniform_int8_metric.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Copyright 2025-present the zvec project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <ailego/math/euclidean_distance_matrix.h>
#include <ailego/math_batch/euclidean_distance_batch.h>
#include <zvec/core/framework/index_error.h>
#include <zvec/core/framework/index_factory.h>
#include <zvec/turbo/turbo.h>
#include "metric_params.h"

namespace zvec {
namespace core {

/*! Index Metric for Uniform Int8 Quantization (Global Scale)
*
* Uses direct int8 L2 distance computation. Since all vectors share
* a single global scale/bias, no per-vector reconstruction is needed.
* This is the key benefit: distance = sum((a[i] - b[i])^2) on raw int8
* values, with optional post-scaling by 1/scale^2 for real L2 distances.
*/
class UniformInt8Metric : public IndexMetric {
public:
//! Initialize Metric
int init(const IndexMeta &meta, const ailego::Params &index_params) override {
if (meta.data_type() != IndexMeta::DataType::DT_INT8) {
LOG_ERROR("UniformInt8Metric: unsupported type %d", meta.data_type());
return IndexError_Unsupported;
}

std::string metric_name;
index_params.get(UNIFORM_INT8_METRIC_ORIGIN_METRIC_NAME, &metric_name);
if (metric_name.empty()) {
LOG_ERROR("UniformInt8Metric: param %s is required",
UNIFORM_INT8_METRIC_ORIGIN_METRIC_NAME.c_str());
return IndexError_InvalidArgument;
}

if (metric_name != "SquaredEuclidean") {
LOG_ERROR("UniformInt8Metric: only SquaredEuclidean supported, got %s",
metric_name.c_str());
return IndexError_Unsupported;
}

meta_ = meta;
params_ = index_params;

LOG_INFO("UniformInt8Metric initialized: dimension=%u", meta_.dimension());
return 0;
}

//! Cleanup Metric
int cleanup(void) override {
return 0;
}

//! Retrieve if it matched
bool is_matched(const IndexMeta &meta) const override {
return meta.data_type() == meta_.data_type() &&
meta.unit_size() == meta_.unit_size();
}

//! Retrieve if it matched
bool is_matched(const IndexMeta &meta,
const IndexQueryMeta &qmeta) const override {
return qmeta.data_type() == meta_.data_type() &&
qmeta.unit_size() == meta_.unit_size() &&
qmeta.dimension() == meta.dimension();
}

//! Retrieve distance function for query (1x1)
MatrixDistance distance(void) const override {
return distance_matrix(1, 1);
}

//! Retrieve matrix distance function
//! Uses direct int8 L2: sum((a[i]-b[i])^2) — no reconstruction needed
MatrixDistance distance_matrix(size_t m, size_t n) const override {
if (m == 1 && n == 1) {
auto turbo_ret = turbo::get_distance_func(
turbo::MetricType::kSquaredEuclidean, turbo::DataType::kInt8,
turbo::QuantizeType::kUniform);
if (turbo_ret) {
return turbo_ret;
}
return reinterpret_cast<MatrixDistanceHandle>(
ailego::SquaredEuclideanDistanceMatrix<int8_t, 1, 1>::Compute);
}
// Only 1x1 is available for int8 in ailego
return nullptr;
}

//! Retrieve batch distance function
//! Uses direct int8 batch L2 with prefetching
MatrixBatchDistance batch_distance(void) const override {
auto turbo_ret = turbo::get_batch_distance_func(
turbo::MetricType::kSquaredEuclidean, turbo::DataType::kInt8,
turbo::QuantizeType::kUniform);
if (turbo_ret) {
return turbo_ret;
}
return reinterpret_cast<IndexMetric::MatrixBatchDistanceHandle>(
ailego::DistanceBatch::SquaredEuclideanDistanceBatch<int8_t, 12,
2>::ComputeBatch);
}

//! Retrieve params of Metric
const ailego::Params &params(void) const override {
return params_;
}

//! Train the metric (no training needed)
int train(const void * /*vec*/, size_t /*dim*/) override {
return 0;
}

//! Retrieve if it supports training
bool support_train(void) const override {
return false;
}

//! Normalize result (no-op: normalization is handled by reformer)
void normalize(float * /*score*/) const override {}

//! Retrieve if it supports normalization
bool support_normalize(void) const override {
return false;
}

//! Retrieve query metric object of this index metric
Pointer query_metric(void) const override {
return nullptr;
}

//! No query preprocessing needed for direct int8 L2
DistanceBatchQueryPreprocessFunc get_query_preprocess_func() const override {
return nullptr;
}

private:
IndexMeta meta_{};
ailego::Params params_{};
};

INDEX_FACTORY_REGISTER_METRIC_ALIAS(UniformInt8, UniformInt8Metric);

} // namespace core
} // namespace zvec
2 changes: 1 addition & 1 deletion src/core/quantizer/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ cc_library(
NAME core_quantizer
STATIC SHARED STRICT ALWAYS_LINK
SRCS *.cc
LIBS zvec_ailego core_framework
LIBS zvec_ailego zvec_turbo core_framework
INCS . ${PROJECT_ROOT_DIR}/src/core
LDFLAGS "${CORE_QUANTIZER_LDFLAGS}"
VERSION "${PROXIMA_ZVEC_VERSION}"
Expand Down
6 changes: 6 additions & 0 deletions src/core/quantizer/quantizer_params.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ static const std::string INTEGER_STREAMING_REFORMER_ENABLE_NORMALIZE =
static const std::string INTEGER_STREAMING_REFORMER_IS_EUCLIDEAN =
"integer_streaming.reformer.is_euclidean";

//! UniformInt8StreamingConverter / Reformer
static const std::string UNIFORM_INT8_REFORMER_SCALE =
"uniform_int8.reformer.scale";
static const std::string UNIFORM_INT8_REFORMER_BIAS =
"uniform_int8.reformer.bias";

//! DoubleBitConverter
static const std::string DOUBLE_BIT_CONVERTER_TRAIN_SAMPLE_COUNT =
"double_bit.converter.train_sample_count";
Expand Down
Loading
Loading