From a767068ae9452dfda993405f4b43c04c9528d7cd Mon Sep 17 00:00:00 2001 From: Dor Forer Date: Wed, 22 Jul 2026 16:21:01 +0800 Subject: [PATCH 1/5] [MOD-14952] Support normalization in QuantPreprocessor (#2) * Support norm in QuantPreprocessor * Remove unnecessary allocation and duplicated code (cherry picked from commit 027d5510b2bf7add6a45be9b0c07cda38c15b5ca) --- src/VecSim/spaces/computer/preprocessors.h | 118 +++++++++---- src/VecSim/types/sq8.h | 23 ++- tests/unit/test_components.cpp | 191 +++++++++++++++++++++ 3 files changed, 292 insertions(+), 40 deletions(-) diff --git a/src/VecSim/spaces/computer/preprocessors.h b/src/VecSim/spaces/computer/preprocessors.h index 988ba9dab..33f118cf0 100644 --- a/src/VecSim/spaces/computer/preprocessors.h +++ b/src/VecSim/spaces/computer/preprocessors.h @@ -1,6 +1,7 @@ /* * Copyright (c) 2006-Present, Redis Ltd. * All rights reserved. + * SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates * * Licensed under your choice of the Redis Source Available License 2.0 * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the @@ -17,12 +18,14 @@ #include #include #include +#include #include "VecSim/memory/vecsim_base.h" #include "VecSim/spaces/spaces.h" #include "VecSim/memory/memory_utils.h" #include "VecSim/types/float16.h" #include "VecSim/types/sq8.h" +#include "VecSim/utils/vecsim_stl.h" class PreprocessorInterface : public VecsimBaseObject { public: @@ -235,7 +238,7 @@ static inline float to_fp32(T x) { } } -template +template class QuantPreprocessor : public PreprocessorInterface { using OUTPUT_TYPE = uint8_t; using MetadataType = float; // SQ8 metadata is always FP32 (see class doc). @@ -244,13 +247,35 @@ class QuantPreprocessor : public PreprocessorInterface { static_assert(Metric == VecSimMetric_L2 || Metric == VecSimMetric_IP || Metric == VecSimMetric_Cosine, "QuantPreprocessor only supports L2, IP and Cosine metrics"); + static_assert(!WithNorm || Metric != VecSimMetric_Cosine, + "WithNorm does not support Cosine metric."); // Helper function to perform quantization. This function is used by the storage preprocessing // methods. void quantize(const DataType *input, OUTPUT_TYPE *quantized) const { assert(input && quantized); - // Find min and max values (computed in MetadataType regardless of DataType). - auto [min_val, max_val] = find_min_max(input); + + if constexpr (std::is_same_v && !WithNorm) { + quantize_impl(input, quantized); + } else { + float x_mean_ip = 0.0f; // only used for WithNorm + + // Get transformed values (convert to float, mean-subtracted for WithNorm) + vecsim_stl::vector values(this->dim, this->allocator); + for (size_t i = 0; i < this->dim; ++i) { + values[i] = to_fp32(input[i]); + if constexpr (WithNorm) { + x_mean_ip += values[i] * mean[i]; + values[i] -= mean[i]; + } + } + quantize_impl(values.data(), quantized, x_mean_ip); + } + } + + void quantize_impl(const float *values, OUTPUT_TYPE *quantized, float x_mean_ip = 0.0f) const { + // Find min and max values. + auto [min_val, max_val] = find_min_max(values); // Calculate scaling factor (typed as MetadataType because they end up as metadata). const MetadataType diff = (max_val - min_val); @@ -272,10 +297,10 @@ class QuantPreprocessor : public PreprocessorInterface { // Quantize the values for (; i < dim_round_down; i += 4) { // Load once (widened to FP32 if DataType is FP16). - const float x0 = to_fp32(input[i + 0]); - const float x1 = to_fp32(input[i + 1]); - const float x2 = to_fp32(input[i + 2]); - const float x3 = to_fp32(input[i + 3]); + const float x0 = values[i + 0]; + const float x1 = values[i + 1]; + const float x2 = values[i + 2]; + const float x3 = values[i + 3]; // We know (input - min) => 0 // If min == max, all values are the same and should be quantized to 0. // reconstruction will yield the same original value for all vectors. @@ -305,7 +330,7 @@ class QuantPreprocessor : public PreprocessorInterface { MetadataType sum_squares = (q0 + q1) + (q2 + q3); for (; i < this->dim; ++i) { - const float x = to_fp32(input[i]); + const float x = values[i]; quantized[i] = static_cast(std::round((x - min_val) * inv_delta)); sum += x; if constexpr (Metric == VecSimMetric_L2) { @@ -316,26 +341,29 @@ class QuantPreprocessor : public PreprocessorInterface { // Metadata uses MetadataType. Use memcpy because the metadata offset // (dim * sizeof(uint8_t)) is not guaranteed to be sizeof(MetadataType)-aligned. void *meta_dst = quantized + this->dim; - if constexpr (Metric == VecSimMetric_L2) { - const MetadataType buf[4] = {min_val, delta, sum, sum_squares}; - memcpy(meta_dst, buf, sizeof(buf)); - } else { - const MetadataType buf[3] = {min_val, delta, sum}; - memcpy(meta_dst, buf, sizeof(buf)); - } + MetadataType buf[5] = {min_val, delta, sum}; + size_t n = 3; + if constexpr (Metric == VecSimMetric_L2) buf[n++] = sum_squares; + if constexpr (WithNorm) buf[n++] = x_mean_ip; + memcpy(meta_dst, buf, n * sizeof(MetadataType)); } // Computes and writes query metadata (FP32) in a single pass over the input vector. - // For IP/Cosine: writes y_sum = Σy_i - // For L2: writes y_sum = Σy_i and y_sum_squares = Σy_i² + // Without norm: + // For IP/Cosine: writes y_sum = Σy_i + // For L2: writes y_sum = Σy_i and y_sum_squares = Σy_i² + // With norm: additionally appends y_mean_ip = Σ(mean_i * y_i). // The output pointer addresses the metadata region after the query body and may not be // 4-byte aligned (e.g. FP16 query body with odd dim), so writes go through memcpy. void assign_query_metadata(const DataType *input, void *output_metadata) const { + // Accumulators are FP32 to preserve precision for FP16 inputs. // 4 independent accumulators for sum float s0{}, s1{}, s2{}, s3{}; // 4 independent accumulators for sum of squares (only used for L2) float q0{}, q1{}, q2{}, q3{}; + // 4 independent accumulators for y_mean_ip (only used for WithNorm) + float m0{}, m1{}, m2{}, m3{}; size_t i = 0; // round dim down to the nearest multiple of 4 @@ -358,11 +386,19 @@ class QuantPreprocessor : public PreprocessorInterface { q2 += y2 * y2; q3 += y3 * y3; } + + if constexpr (WithNorm) { + m0 += mean[i + 0] * y0; + m1 += mean[i + 1] * y1; + m2 += mean[i + 2] * y2; + m3 += mean[i + 3] * y3; + } } - // Sum/sum_squares become metadata, so they are MetadataType. + // Sum/sum_squares/y_mean_ip become metadata, so they are MetadataType. MetadataType sum = (s0 + s1) + (s2 + s3); MetadataType sum_squares = (q0 + q1) + (q2 + q3); + MetadataType y_mean_ip = (m0 + m1) + (m2 + m3); // Tail: handle remaining elements for (; i < this->dim; ++i) { @@ -371,29 +407,41 @@ class QuantPreprocessor : public PreprocessorInterface { if constexpr (Metric == VecSimMetric_L2) { sum_squares += y * y; } + if constexpr (WithNorm) { + y_mean_ip += mean[i] * y; + } } // Metadata uses MetadataType. Use memcpy because the metadata offset (after the query // body of dim * sizeof(DataType)) is not guaranteed to be sizeof(MetadataType)-aligned // when DataType is float16 and dim is odd. - if constexpr (Metric == VecSimMetric_L2) { - const MetadataType buf[2] = {sum, sum_squares}; - memcpy(output_metadata, buf, sizeof(buf)); - } else { - const MetadataType buf[1] = {sum}; - memcpy(output_metadata, buf, sizeof(buf)); - } + MetadataType buf[3] = {sum}; + size_t n = 1; + if constexpr (Metric == VecSimMetric_L2) buf[n++] = sum_squares; + if constexpr (WithNorm) buf[n++] = y_mean_ip; + memcpy(output_metadata, buf, n * sizeof(MetadataType)); } public: - QuantPreprocessor(std::shared_ptr allocator, size_t dim) + // Standard constructor (WithNorm == false): no mean vector. + QuantPreprocessor(std::shared_ptr allocator, size_t dim) requires(!WithNorm) : PreprocessorInterface(allocator), dim(dim), storage_bytes_count(dim * sizeof(OUTPUT_TYPE) + - (vecsim_types::sq8::storage_metadata_count()) * + sq8::storage_metadata_count() * sizeof(MetadataType)), + query_bytes_count(dim * sizeof(DataType) + + sq8::query_metadata_count() * sizeof(MetadataType)) {} + + // WithNorm constructor: accepts a pre-computed mean vector (FP32, length == dim). + QuantPreprocessor(std::shared_ptr allocator, size_t dim, + const vecsim_stl::vector &mean_vec) requires(WithNorm) + : PreprocessorInterface(allocator), mean(mean_vec), dim(dim), + storage_bytes_count(dim * sizeof(OUTPUT_TYPE) + + sq8::storage_metadata_count() * sizeof(MetadataType)), query_bytes_count(dim * sizeof(DataType) + - (vecsim_types::sq8::query_metadata_count()) * - sizeof(MetadataType)) {} + sq8::query_metadata_count() * sizeof(MetadataType)) { + assert(this->mean.size() == dim && "mean vector size must equal dim"); + } /** * Preprocesses the original blob into separate storage and query blobs. @@ -495,15 +543,15 @@ class QuantPreprocessor : public PreprocessorInterface { } private: - // Returns (min, max) of the input vector evaluated in MetadataType. Both float and float16 - // expose a usable operator< (float16's overload delegates to FP32 semantics), so a single - // std::minmax_element call covers both DataTypes. The returned values are written verbatim - // into the metadata region. - std::pair find_min_max(const DataType *input) const { + std::pair find_min_max(const float *input) const { auto [min_it, max_it] = std::minmax_element(input, input + dim); - return {to_fp32(*min_it), to_fp32(*max_it)}; + return {*min_it, *max_it}; } + // Mean vector for WithNorm=true; zero-size placeholder otherwise (no runtime overhead). + [[no_unique_address]] std::conditional_t, std::monostate> + mean; + const size_t dim; const size_t storage_bytes_count; const size_t query_bytes_count; diff --git a/src/VecSim/types/sq8.h b/src/VecSim/types/sq8.h index dfe296321..28d6379d1 100644 --- a/src/VecSim/types/sq8.h +++ b/src/VecSim/types/sq8.h @@ -1,6 +1,7 @@ /* * Copyright (c) 2006-Present, Redis Ltd. * All rights reserved. + * SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates * * Licensed under your choice of the Redis Source Available License 2.0 * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the @@ -31,15 +32,27 @@ struct sq8 { SUM_SQUARES_QUERY = 1 // Only for L2 }; - // Template on metric — compile-time constant when metric is known - template + // Template on Metric and WithNorm — compile-time constants + // WithNorm: one extra metadata slot for x_mean_ip / y_mean_ip + template static constexpr size_t storage_metadata_count() { - return (Metric == VecSimMetric_L2) ? 4 : 3; + return ((Metric == VecSimMetric_L2) ? 4 : 3) + (WithNorm ? 1 : 0); } - template + template static constexpr size_t query_metadata_count() { - return (Metric == VecSimMetric_L2) ? 2 : 1; + return ((Metric == VecSimMetric_L2) ? 2 : 1) + (WithNorm ? 1 : 0); + } + + // Index of x_mean_ip / y_mean_ip in the last slot in metadata array + template + static constexpr size_t mean_ip_index() { + return storage_metadata_count() - 1; + } + + template + static constexpr size_t query_mean_ip_index() { + return query_metadata_count() - 1; } }; diff --git a/tests/unit/test_components.cpp b/tests/unit/test_components.cpp index c40d37e86..cf7d98901 100644 --- a/tests/unit/test_components.cpp +++ b/tests/unit/test_components.cpp @@ -1,6 +1,7 @@ /* * Copyright (c) 2006-Present, Redis Ltd. * All rights reserved. + * SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates * * Licensed under your choice of the Redis Source Available License 2.0 * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the @@ -1612,3 +1613,193 @@ TEST(QuantPreprocessorFP16Test, QuantizeReconstructRoundTripL2) { allocator->free_allocation(storage_blob); delete preprocessor; } + +// Shared parameterized fixture for QuantPreprocessor. +template +class QuantPreprocessorWithNormMetricTestBase : public testing::TestWithParam { +protected: + static constexpr size_t dim = 5; + static constexpr unsigned char alignment = 0; + static constexpr size_t original_blob_size = dim * sizeof(DataType); + + std::shared_ptr allocator; + DataType original_blob[dim]; + float widened_blob[dim]; + float mean_vec[dim] = {0.1f, 0.2f, 0.3f, 0.4f, 0.5f}; + + void SetUp() override { + allocator = VecSimAllocator::newVecsimAllocator(); + const float source[dim] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; + for (size_t i = 0; i < dim; ++i) { + if constexpr (std::is_same_v) { + original_blob[i] = vecsim_types::FP32_to_FP16(source[i]); + widened_blob[i] = vecsim_types::FP16_to_FP32(original_blob[i]); + } else { + original_blob[i] = source[i]; + widened_blob[i] = source[i]; + } + } + } + + static float load_meta(const void *base, size_t byte_offset) { + float value; + std::memcpy(&value, static_cast(base) + byte_offset, sizeof(value)); + return value; + } + + template + size_t getExpectedStorageSize() const { + return dim * sizeof(uint8_t) + sq8::storage_metadata_count() * sizeof(float); + } + + template + size_t getExpectedQuerySize() const { + return dim * sizeof(DataType) + sq8::query_metadata_count() * sizeof(float); + } + + template + void runQuantizationTest() { + const size_t expected_storage_size = getExpectedStorageSize(); + const size_t expected_query_size = getExpectedQuerySize(); + const size_t storage_meta_offset = dim * sizeof(uint8_t); + const size_t query_meta_offset = dim * sizeof(DataType); + float centered[dim]; + float expected_x_mean_ip = 0.0f; + float expected_y_sum = 0.0f; + float expected_y_sum_squares = 0.0f; + float expected_y_mean_ip = 0.0f; + for (size_t i = 0; i < dim; ++i) { + centered[i] = widened_blob[i] - mean_vec[i]; + expected_x_mean_ip += widened_blob[i] * mean_vec[i]; + expected_y_sum += widened_blob[i]; + expected_y_sum_squares += widened_blob[i] * widened_blob[i]; + expected_y_mean_ip += widened_blob[i] * mean_vec[i]; + } + + constexpr size_t max_storage_baseline = dim * sizeof(uint8_t) + 4 * sizeof(float); + uint8_t baseline_storage[max_storage_baseline]; + ComputeSQ8Quantization(centered, dim, baseline_storage); + + vecsim_stl::vector mean_vector(allocator); + for (size_t i = 0; i < dim; ++i) { + mean_vector.push_back(mean_vec[i]); + } + auto quant_preprocessor = + new (allocator) QuantPreprocessor(allocator, dim, mean_vector); + + { + void *storage_blob = nullptr; + void *query_blob = nullptr; + size_t storage_blob_size = original_blob_size; + size_t query_blob_size = original_blob_size; + quant_preprocessor->preprocess(original_blob, storage_blob, query_blob, + storage_blob_size, query_blob_size, alignment, + alignment); + + ASSERT_NE(storage_blob, nullptr); + ASSERT_EQ(storage_blob_size, expected_storage_size); + ASSERT_NE(query_blob, nullptr); + ASSERT_EQ(query_blob_size, expected_query_size); + + const size_t compare_size = (Metric == VecSimMetric_L2) + ? dim * sizeof(uint8_t) + 4 * sizeof(float) + : dim * sizeof(uint8_t) + 3 * sizeof(float); + EXPECT_NO_FATAL_FAILURE(CompareVectors( + static_cast(storage_blob), baseline_storage, compare_size)); + ASSERT_FLOAT_EQ( + load_meta(storage_blob, + storage_meta_offset + sq8::mean_ip_index() * sizeof(float)), + expected_x_mean_ip); + EXPECT_NO_FATAL_FAILURE(CompareVectors( + static_cast(query_blob), original_blob, dim)); + ASSERT_FLOAT_EQ( + load_meta(query_blob, query_meta_offset + sq8::SUM_QUERY * sizeof(float)), + expected_y_sum); + if constexpr (Metric == VecSimMetric_L2) { + ASSERT_FLOAT_EQ(load_meta(query_blob, query_meta_offset + + sq8::SUM_SQUARES_QUERY * sizeof(float)), + expected_y_sum_squares); + } + ASSERT_FLOAT_EQ( + load_meta(query_blob, + query_meta_offset + sq8::query_mean_ip_index() * sizeof(float)), + expected_y_mean_ip); + allocator->free_allocation(storage_blob); + allocator->free_allocation(query_blob); + } + + { + void *blob = nullptr; + size_t blob_size = original_blob_size; + quant_preprocessor->preprocessForStorage(original_blob, blob, blob_size, alignment); + ASSERT_NE(blob, nullptr); + ASSERT_EQ(blob_size, expected_storage_size); + allocator->free_allocation(blob); + } + + { + void *blob = nullptr; + size_t blob_size = original_blob_size; + quant_preprocessor->preprocessQuery(original_blob, blob, blob_size, alignment); + ASSERT_NE(blob, nullptr); + ASSERT_EQ(blob_size, expected_query_size); + EXPECT_NO_FATAL_FAILURE( + CompareVectors(static_cast(blob), original_blob, dim)); + ASSERT_FLOAT_EQ(load_meta(blob, query_meta_offset + sq8::SUM_QUERY * sizeof(float)), + expected_y_sum); + if constexpr (Metric == VecSimMetric_L2) { + ASSERT_FLOAT_EQ( + load_meta(blob, query_meta_offset + sq8::SUM_SQUARES_QUERY * sizeof(float)), + expected_y_sum_squares); + } + ASSERT_FLOAT_EQ(load_meta(blob, query_meta_offset + + sq8::query_mean_ip_index() * sizeof(float)), + expected_y_mean_ip); + allocator->free_allocation(blob); + } + + delete quant_preprocessor; + } +}; + +using QuantPreprocessorWithNormMetricTest = QuantPreprocessorWithNormMetricTestBase; + +TEST_P(QuantPreprocessorWithNormMetricTest, QuantizationBlobSizeAndMetadata) { + VecSimMetric metric = GetParam(); + switch (metric) { + case VecSimMetric_L2: + runQuantizationTest(); + break; + case VecSimMetric_IP: + runQuantizationTest(); + break; + } +} + +INSTANTIATE_TEST_SUITE_P(QuantPreprocessorWithNormTests, QuantPreprocessorWithNormMetricTest, + testing::Values(VecSimMetric_L2, VecSimMetric_IP), + [](const testing::TestParamInfo &info) { + return VecSimMetric_ToString(info.param); + }); + +using QuantPreprocessorFP16WithNormMetricTest = + QuantPreprocessorWithNormMetricTestBase; + +TEST_P(QuantPreprocessorFP16WithNormMetricTest, QuantizationBlobSizeAndMetadata) { + VecSimMetric metric = GetParam(); + switch (metric) { + case VecSimMetric_L2: + runQuantizationTest(); + break; + case VecSimMetric_IP: + runQuantizationTest(); + break; + } +} + +INSTANTIATE_TEST_SUITE_P(QuantPreprocessorFP16WithNormTests, + QuantPreprocessorFP16WithNormMetricTest, + testing::Values(VecSimMetric_L2, VecSimMetric_IP), + [](const testing::TestParamInfo &info) { + return VecSimMetric_ToString(info.param); + }); From 3c417fdc62fad06e0c81b4de5b77dba3396b05c3 Mon Sep 17 00:00:00 2001 From: Dor Forer Date: Wed, 22 Jul 2026 13:48:07 +0300 Subject: [PATCH 2/5] Format quantization normalization changes --- src/VecSim/spaces/computer/preprocessors.h | 21 ++++++++++++++------- src/VecSim/types/sq8.h | 3 ++- tests/unit/test_components.cpp | 3 ++- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/VecSim/spaces/computer/preprocessors.h b/src/VecSim/spaces/computer/preprocessors.h index 33f118cf0..2733bed44 100644 --- a/src/VecSim/spaces/computer/preprocessors.h +++ b/src/VecSim/spaces/computer/preprocessors.h @@ -1,7 +1,8 @@ /* * Copyright (c) 2006-Present, Redis Ltd. * All rights reserved. - * SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates + * SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates + * * * Licensed under your choice of the Redis Source Available License 2.0 * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the @@ -343,8 +344,10 @@ class QuantPreprocessor : public PreprocessorInterface { void *meta_dst = quantized + this->dim; MetadataType buf[5] = {min_val, delta, sum}; size_t n = 3; - if constexpr (Metric == VecSimMetric_L2) buf[n++] = sum_squares; - if constexpr (WithNorm) buf[n++] = x_mean_ip; + if constexpr (Metric == VecSimMetric_L2) + buf[n++] = sum_squares; + if constexpr (WithNorm) + buf[n++] = x_mean_ip; memcpy(meta_dst, buf, n * sizeof(MetadataType)); } @@ -417,14 +420,17 @@ class QuantPreprocessor : public PreprocessorInterface { // when DataType is float16 and dim is odd. MetadataType buf[3] = {sum}; size_t n = 1; - if constexpr (Metric == VecSimMetric_L2) buf[n++] = sum_squares; - if constexpr (WithNorm) buf[n++] = y_mean_ip; + if constexpr (Metric == VecSimMetric_L2) + buf[n++] = sum_squares; + if constexpr (WithNorm) + buf[n++] = y_mean_ip; memcpy(output_metadata, buf, n * sizeof(MetadataType)); } public: // Standard constructor (WithNorm == false): no mean vector. - QuantPreprocessor(std::shared_ptr allocator, size_t dim) requires(!WithNorm) + QuantPreprocessor(std::shared_ptr allocator, size_t dim) + requires(!WithNorm) : PreprocessorInterface(allocator), dim(dim), storage_bytes_count(dim * sizeof(OUTPUT_TYPE) + sq8::storage_metadata_count() * sizeof(MetadataType)), @@ -433,7 +439,8 @@ class QuantPreprocessor : public PreprocessorInterface { // WithNorm constructor: accepts a pre-computed mean vector (FP32, length == dim). QuantPreprocessor(std::shared_ptr allocator, size_t dim, - const vecsim_stl::vector &mean_vec) requires(WithNorm) + const vecsim_stl::vector &mean_vec) + requires(WithNorm) : PreprocessorInterface(allocator), mean(mean_vec), dim(dim), storage_bytes_count(dim * sizeof(OUTPUT_TYPE) + sq8::storage_metadata_count() * diff --git a/src/VecSim/types/sq8.h b/src/VecSim/types/sq8.h index 28d6379d1..34cf1de97 100644 --- a/src/VecSim/types/sq8.h +++ b/src/VecSim/types/sq8.h @@ -1,7 +1,8 @@ /* * Copyright (c) 2006-Present, Redis Ltd. * All rights reserved. - * SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates + * SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates + * * * Licensed under your choice of the Redis Source Available License 2.0 * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the diff --git a/tests/unit/test_components.cpp b/tests/unit/test_components.cpp index cf7d98901..129315041 100644 --- a/tests/unit/test_components.cpp +++ b/tests/unit/test_components.cpp @@ -1,7 +1,8 @@ /* * Copyright (c) 2006-Present, Redis Ltd. * All rights reserved. - * SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates + * SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates + * * * Licensed under your choice of the Redis Source Available License 2.0 * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the From 1995cebe94469c50182562ebb5e8283863de8ecb Mon Sep 17 00:00:00 2001 From: Dor Forer Date: Wed, 22 Jul 2026 18:21:59 +0300 Subject: [PATCH 3/5] Remove ARM copyright headers from modified files --- src/VecSim/spaces/computer/preprocessors.h | 2 -- src/VecSim/types/sq8.h | 2 -- tests/unit/test_components.cpp | 2 -- 3 files changed, 6 deletions(-) diff --git a/src/VecSim/spaces/computer/preprocessors.h b/src/VecSim/spaces/computer/preprocessors.h index 2733bed44..a4f88a7d7 100644 --- a/src/VecSim/spaces/computer/preprocessors.h +++ b/src/VecSim/spaces/computer/preprocessors.h @@ -1,8 +1,6 @@ /* * Copyright (c) 2006-Present, Redis Ltd. * All rights reserved. - * SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates - * * * Licensed under your choice of the Redis Source Available License 2.0 * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the diff --git a/src/VecSim/types/sq8.h b/src/VecSim/types/sq8.h index 34cf1de97..adf74a1c8 100644 --- a/src/VecSim/types/sq8.h +++ b/src/VecSim/types/sq8.h @@ -1,8 +1,6 @@ /* * Copyright (c) 2006-Present, Redis Ltd. * All rights reserved. - * SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates - * * * Licensed under your choice of the Redis Source Available License 2.0 * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the diff --git a/tests/unit/test_components.cpp b/tests/unit/test_components.cpp index 129315041..224388952 100644 --- a/tests/unit/test_components.cpp +++ b/tests/unit/test_components.cpp @@ -1,8 +1,6 @@ /* * Copyright (c) 2006-Present, Redis Ltd. * All rights reserved. - * SPDX-FileCopyrightText: Copyright 2026 Arm Limited and/or its affiliates - * * * Licensed under your choice of the Redis Source Available License 2.0 * (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the From 7eaa73f67da05ea127b5b853c9efc2a2b472e6c3 Mon Sep 17 00:00:00 2001 From: Dor Forer Date: Sun, 26 Jul 2026 09:33:01 +0300 Subject: [PATCH 4/5] Test HNSW stored-to-query dispatch --- tests/unit/test_hnsw.cpp | 46 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/unit/test_hnsw.cpp b/tests/unit/test_hnsw.cpp index e308a5d6c..2eb26786d 100644 --- a/tests/unit/test_hnsw.cpp +++ b/tests/unit/test_hnsw.cpp @@ -11,6 +11,8 @@ #include "VecSim/vec_sim.h" #include "VecSim/vec_sim_debug.h" #include "VecSim/algorithms/hnsw/hnsw_single.h" +#include "VecSim/index_factories/components/components_factory.h" +#include "VecSim/index_factories/factory_utils.h" #include "VecSim/index_factories/hnsw_factory.h" #include "unit_test_utils.h" #include "VecSim/utils/serializer.h" @@ -43,6 +45,50 @@ class HNSWTest : public ::testing::Test { TYPED_TEST_SUITE(HNSWTest, DataTypeSet); +namespace { + +float storedDispatchTestFunc(const void *, const void *, size_t) { return 11.0f; } + +float queryDispatchTestFunc(const void *, const void *, size_t) { return 22.0f; } + +} // namespace + +TEST(HNSWDistanceDispatchTest, UsesStoredToQueryDispatch) { + constexpr size_t dim = 4; + HNSWParams params = { + .type = VecSimType_FLOAT32, + .dim = dim, + .metric = VecSimMetric_L2, + .multi = false, + }; + auto abstract_params = VecSimFactory::NewAbstractInitParams(¶ms, nullptr, false); + auto components = CreateIndexComponents(abstract_params.allocator, params.metric, + params.dim, false); + delete components.indexCalculator; + components.indexCalculator = new (abstract_params.allocator) DistanceCalculatorCommon( + abstract_params.allocator, storedDispatchTestFunc, queryDispatchTestFunc); + + auto *index = new (abstract_params.allocator) + HNSWIndex_Single(¶ms, abstract_params, components); + float vector[dim] = {1.0f, 2.0f, 3.0f, 4.0f}; + float query[dim] = {4.0f, 3.0f, 2.0f, 1.0f}; + VecSimIndex_AddVector(index, vector, 7); + + ASSERT_FLOAT_EQ(index->calcDistance(vector, query), 11.0f); + auto verify_query_score = [](size_t id, double score, size_t) { + ASSERT_EQ(id, 7); + ASSERT_FLOAT_EQ(score, 22.0f); + }; + + runTopKSearchTest(index, query, 1, verify_query_score); + runRangeQueryTest(index, query, 23.0, verify_query_score, 1, BY_SCORE); + VecSimBatchIterator *batch_iterator = VecSimBatchIterator_New(index, query, nullptr); + runBatchIteratorSearchTest(batch_iterator, 1, verify_query_score); + VecSimBatchIterator_Free(batch_iterator); + + VecSimIndex_Free(index); +} + TYPED_TEST(HNSWTest, hnsw_vector_add_test) { size_t dim = 4; From d4e8f1c99242c26f7125b1a6cb5776aa1db24481 Mon Sep 17 00:00:00 2001 From: Dor Forer Date: Sun, 26 Jul 2026 11:18:29 +0300 Subject: [PATCH 5/5] Avoid allocation during normalized quantization --- src/VecSim/spaces/computer/preprocessors.h | 64 ++++++++++++---------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/src/VecSim/spaces/computer/preprocessors.h b/src/VecSim/spaces/computer/preprocessors.h index a4f88a7d7..c7db23c57 100644 --- a/src/VecSim/spaces/computer/preprocessors.h +++ b/src/VecSim/spaces/computer/preprocessors.h @@ -254,27 +254,8 @@ class QuantPreprocessor : public PreprocessorInterface { void quantize(const DataType *input, OUTPUT_TYPE *quantized) const { assert(input && quantized); - if constexpr (std::is_same_v && !WithNorm) { - quantize_impl(input, quantized); - } else { - float x_mean_ip = 0.0f; // only used for WithNorm - - // Get transformed values (convert to float, mean-subtracted for WithNorm) - vecsim_stl::vector values(this->dim, this->allocator); - for (size_t i = 0; i < this->dim; ++i) { - values[i] = to_fp32(input[i]); - if constexpr (WithNorm) { - x_mean_ip += values[i] * mean[i]; - values[i] -= mean[i]; - } - } - quantize_impl(values.data(), quantized, x_mean_ip); - } - } - - void quantize_impl(const float *values, OUTPUT_TYPE *quantized, float x_mean_ip = 0.0f) const { - // Find min and max values. - auto [min_val, max_val] = find_min_max(values); + float x_mean_ip = 0.0f; // only used for WithNorm + auto [min_val, max_val] = find_min_max(input, x_mean_ip); // Calculate scaling factor (typed as MetadataType because they end up as metadata). const MetadataType diff = (max_val - min_val); @@ -296,10 +277,10 @@ class QuantPreprocessor : public PreprocessorInterface { // Quantize the values for (; i < dim_round_down; i += 4) { // Load once (widened to FP32 if DataType is FP16). - const float x0 = values[i + 0]; - const float x1 = values[i + 1]; - const float x2 = values[i + 2]; - const float x3 = values[i + 3]; + const float x0 = transformed_value(input, i + 0); + const float x1 = transformed_value(input, i + 1); + const float x2 = transformed_value(input, i + 2); + const float x3 = transformed_value(input, i + 3); // We know (input - min) => 0 // If min == max, all values are the same and should be quantized to 0. // reconstruction will yield the same original value for all vectors. @@ -329,7 +310,7 @@ class QuantPreprocessor : public PreprocessorInterface { MetadataType sum_squares = (q0 + q1) + (q2 + q3); for (; i < this->dim; ++i) { - const float x = values[i]; + const float x = transformed_value(input, i); quantized[i] = static_cast(std::round((x - min_val) * inv_delta)); sum += x; if constexpr (Metric == VecSimMetric_L2) { @@ -548,9 +529,34 @@ class QuantPreprocessor : public PreprocessorInterface { } private: - std::pair find_min_max(const float *input) const { - auto [min_it, max_it] = std::minmax_element(input, input + dim); - return {*min_it, *max_it}; + inline float transformed_value(const DataType *input, size_t i) const { + float value = to_fp32(input[i]); + if constexpr (WithNorm) { + value -= mean[i]; + } + return value; + } + + std::pair find_min_max(const DataType *input, float &x_mean_ip) const { + if constexpr (!WithNorm) { + auto [min_it, max_it] = std::minmax_element(input, input + dim); + return {to_fp32(*min_it), to_fp32(*max_it)}; + } else { + const float first_input_value = to_fp32(input[0]); + float value = first_input_value - mean[0]; + float min_val = value; + float max_val = value; + x_mean_ip = first_input_value * mean[0]; + + for (size_t i = 1; i < dim; ++i) { + const float input_value = to_fp32(input[i]); + value = input_value - mean[i]; + min_val = std::min(min_val, value); + max_val = std::max(max_val, value); + x_mean_ip += input_value * mean[i]; + } + return {min_val, max_val}; + } } // Mean vector for WithNorm=true; zero-size placeholder otherwise (no runtime overhead).