From 7155105e7b6ec0f1f1bd08fdf153356e442b4b24 Mon Sep 17 00:00:00 2001 From: Dor Forer Date: Thu, 1 Oct 2026 13:15:53 +0300 Subject: [PATCH 1/5] MOD-19169: Quantize FP16 SQ8 queries once for symmetric distance kernels --- .github/workflows/benchmark-runner.yml | 7 ++++ .../components/components_factory.h | 34 ++++++++++++------- src/VecSim/spaces/computer/calculator.h | 19 +++++++++++ src/VecSim/spaces/computer/preprocessors.h | 25 ++++++++++++++ 4 files changed, 72 insertions(+), 13 deletions(-) diff --git a/.github/workflows/benchmark-runner.yml b/.github/workflows/benchmark-runner.yml index 86c5739f9..c4bde4a15 100644 --- a/.github/workflows/benchmark-runner.yml +++ b/.github/workflows/benchmark-runner.yml @@ -98,6 +98,13 @@ jobs: timeout-minutes: 300 run: | make benchmark BM_FILTER=${{ inputs.setup }} + - name: Upload benchmark results + if: always() + uses: actions/upload-artifact@v7 + with: + name: benchmark-${{ inputs.setup }}-${{ inputs.architecture }} + path: '**/*_results.json' + if-no-files-found: warn - name: Collect results run: | ./tests/benchmark/benchmarks.sh ${{ inputs.setup }} | xargs -P 0 -I {} redisbench-admin export \ diff --git a/src/VecSim/index_factories/components/components_factory.h b/src/VecSim/index_factories/components/components_factory.h index 9b77b9b15..0f9fa824b 100644 --- a/src/VecSim/index_factories/components/components_factory.h +++ b/src/VecSim/index_factories/components/components_factory.h @@ -44,17 +44,26 @@ template IndexComponents CreateSQ8IndexComponents(const std::shared_ptr &allocator, size_t dim, const float *mean_ptr) { - using Preprocessor = QuantPreprocessor; - using QueryType = typename Preprocessor::QueryType; - unsigned char storage_alignment = 0, asym_storage_alignment = 0; - - // Graph construction compares two stored SQ8 blobs; search compares a stored blob with a - // QueryType query. Both dispatchers report alignment for the stored operand. + constexpr bool quantize_query = std::is_same_v; + using Preprocessor = + std::conditional_t, + QuantPreprocessor>; + using Calculator = + std::conditional_t, + DistanceCalculatorWithNorm>; + unsigned char storage_alignment = 0; auto sym_func = spaces::GetDistFunc(Metric, dim, &storage_alignment); - auto asym_func = spaces::GetDistFunc( - Metric, dim, &asym_storage_alignment); - storage_alignment = spaces::combineAlignments(storage_alignment, asym_storage_alignment); - const unsigned char query_alignment = GetQueryAlignment(Metric, dim); + auto query_func = sym_func; + unsigned char query_alignment = storage_alignment; + if constexpr (!quantize_query) { + using QueryType = typename Preprocessor::QueryType; + unsigned char asym_storage_alignment = 0; + query_func = spaces::GetDistFunc( + Metric, dim, &asym_storage_alignment); + storage_alignment = spaces::combineAlignments(storage_alignment, asym_storage_alignment); + query_alignment = GetQueryAlignment(Metric, dim); + } PreprocessorInterface *pp = nullptr; IndexCalculatorInterface *calc = nullptr; @@ -69,11 +78,10 @@ CreateSQ8IndexComponents(const std::shared_ptr &allocator, size } pp = new (allocator) Preprocessor(allocator, dim, mean_vec); - calc = new (allocator) DistanceCalculatorWithNorm( - allocator, asym_func, sym_func, mean_sum_squares); + calc = new (allocator) Calculator(allocator, query_func, sym_func, mean_sum_squares); } else { pp = new (allocator) Preprocessor(allocator, dim); - calc = new (allocator) DistanceCalculatorCommon(allocator, sym_func, asym_func); + calc = new (allocator) DistanceCalculatorCommon(allocator, sym_func, query_func); } auto *container = new (allocator) diff --git a/src/VecSim/spaces/computer/calculator.h b/src/VecSim/spaces/computer/calculator.h index 793402ad1..5de960788 100644 --- a/src/VecSim/spaces/computer/calculator.h +++ b/src/VecSim/spaces/computer/calculator.h @@ -251,3 +251,22 @@ class DistanceCalculatorWithNorm : public IndexCalculatorInterface { return DistanceDispatch::stateful(&context_, calcQueryWithContext); } }; + +// Both operands are centered SQ8 blobs, so queries need the symmetric mean correction. +template +class QuantizedQueryDistanceCalculatorWithNorm + : public DistanceCalculatorWithNorm { + using Base = DistanceCalculatorWithNorm; + +public: + using Base::Base; + + DistType calcDistanceForQuery(const void *candidate, const void *query, + size_t dim) const override { + return Base::calcDistance(candidate, query, dim); + } + + DistanceDispatch getDistanceDispatch(DistanceMode) const override { + return Base::getDistanceDispatch(DistanceMode::StoredToStored); + } +}; diff --git a/src/VecSim/spaces/computer/preprocessors.h b/src/VecSim/spaces/computer/preprocessors.h index 1be9b06b8..e1877dd57 100644 --- a/src/VecSim/spaces/computer/preprocessors.h +++ b/src/VecSim/spaces/computer/preprocessors.h @@ -658,3 +658,28 @@ class QuantPreprocessor : public PreprocessorInterface { const size_t storage_bytes_count; const size_t query_bytes_count; }; + +// Query and storage blobs share the SQ8 representation, including FP32 metadata. +template +class QuantizedQueryPreprocessor : public QuantPreprocessor { + using Base = QuantPreprocessor; + +public: + using Base::Base; + + void preprocessQuery(const void *original_blob, void *&blob, size_t &query_blob_size, + unsigned char alignment) const override { + Base::preprocessForStorage(original_blob, blob, query_blob_size, alignment); + } + + void preprocess(const void *original_blob, void *&storage_blob, void *&query_blob, + size_t &storage_blob_size, size_t &query_blob_size, + unsigned char storage_alignment, unsigned char query_alignment) const override { + assert(!storage_blob && !query_blob); + assert(&storage_blob_size != &query_blob_size); + Base::preprocessForStorage(original_blob, storage_blob, storage_blob_size, + spaces::combineAlignments(storage_alignment, query_alignment)); + query_blob = storage_blob; + query_blob_size = storage_blob_size; + } +}; From b5f4a2394609c5ea6e7bd1217d6b20e275898d08 Mon Sep 17 00:00:00 2001 From: Dor Forer Date: Thu, 1 Oct 2026 13:19:36 +0300 Subject: [PATCH 2/5] Test quantized FP16 SQ8 queries through factories and public APIs --- tests/unit/test_components.cpp | 172 +++++++++++++++++++++++++++++++++ tests/unit/test_hnsw_sq8.cpp | 82 ++++++++++++++++ 2 files changed, 254 insertions(+) diff --git a/tests/unit/test_components.cpp b/tests/unit/test_components.cpp index 7ef911762..3c95e4c45 100644 --- a/tests/unit/test_components.cpp +++ b/tests/unit/test_components.cpp @@ -15,6 +15,7 @@ #include "VecSim/spaces/computer/preprocessor_container.h" #include "VecSim/spaces/computer/preprocessors.h" #include "VecSim/spaces/computer/calculator.h" +#include "VecSim/index_factories/components/components_factory.h" #include "VecSim/spaces/IP_space.h" #include "VecSim/spaces/L2_space.h" #include "unit_test_utils.h" @@ -2644,3 +2645,174 @@ TEST(DistanceCalculatorWithNormTest, OddDimension_MatchesBruteForce) { delete calc; } } + +namespace { + +float sq8FactoryMetadata(const void *blob, size_t dim, size_t slot) { + float value; + std::memcpy(&value, static_cast(blob) + dim + slot * sizeof(float), + sizeof(value)); + return value; +} + +template +void verifyFP16SQ8FactoryQuery(size_t dim, bool constant) { + using data_t = vecsim_types::float16; + auto allocator = VecSimAllocator::newVecsimAllocator(); + std::vector mean(dim); + std::vector x(dim), y(dim); + for (size_t d = 0; d < dim; ++d) { + mean[d] = (static_cast(d % 5) - 2) * 0.125f; + const float x_value = constant ? 0.75f : (static_cast(d % 7) - 3) * 0.25f; + const float y_value = constant ? -0.25f : (static_cast((d * 3) % 11) - 5) * 0.125f; + x[d] = vecsim_types::FP32_to_FP16(x_value); + y[d] = vecsim_types::FP32_to_FP16(y_value); + } + const auto original_y = y; + auto components = + CreateSQ8IndexComponents(allocator, dim, WithMean ? mean.data() : nullptr); + const size_t bytes = vecsim_types::sq8::storage_bytes_count(dim); + { + auto x_storage = + components.preprocessors->preprocessForStorage(x.data(), dim * sizeof(data_t)); + auto y_storage = + components.preprocessors->preprocessForStorage(y.data(), dim * sizeof(data_t)); + auto y_query = components.preprocessors->preprocessQuery(y.data(), dim * sizeof(data_t)); + auto paired = components.preprocessors->preprocess(y.data(), dim * sizeof(data_t)); + ASSERT_NE(x_storage.get(), nullptr); + ASSERT_NE(y_storage.get(), nullptr); + ASSERT_NE(y_query.get(), nullptr); + ASSERT_EQ(paired.getStorageBlob(), paired.getQueryBlob()); + EXPECT_EQ(std::memcmp(y_storage.get(), y_query.get(), bytes), 0); + EXPECT_EQ(std::memcmp(y_storage.get(), paired.getQueryBlob(), bytes), 0); + EXPECT_EQ(std::memcmp(y.data(), original_y.data(), dim * sizeof(data_t)), 0); + if (const auto alignment = components.preprocessors->getQueryAlignment()) { + EXPECT_EQ(reinterpret_cast(y_query.get()) % alignment, 0u); + } + if (const auto alignment = components.preprocessors->getStorageAlignment()) { + EXPECT_EQ(reinterpret_cast(paired.getStorageBlob()) % alignment, 0u); + } + + // Reconstruct both SQ8 operands independently of the distance kernel. + const double x_min = sq8FactoryMetadata(x_storage.get(), dim, vecsim_types::sq8::MIN_VAL); + const double x_delta = sq8FactoryMetadata(x_storage.get(), dim, vecsim_types::sq8::DELTA); + const double y_min = sq8FactoryMetadata(y_query.get(), dim, vecsim_types::sq8::MIN_VAL); + const double y_delta = sq8FactoryMetadata(y_query.get(), dim, vecsim_types::sq8::DELTA); + double reference = Metric == VecSimMetric_IP ? 1.0 : 0.0; + const auto *x_bytes = static_cast(x_storage.get()); + const auto *y_bytes = static_cast(y_query.get()); + for (size_t d = 0; d < dim; ++d) { + const double xr = x_min + x_delta * x_bytes[d]; + const double yr = y_min + y_delta * y_bytes[d]; + if constexpr (Metric == VecSimMetric_IP) { + reference -= xr * yr; + } else { + reference += (xr - yr) * (xr - yr); + } + } + if constexpr (Metric == VecSimMetric_IP && WithMean) { + double mean_square = 0.0; + for (float v : mean) + mean_square += double(v) * v; + reference -= sq8FactoryMetadata(x_storage.get(), dim, + vecsim_types::sq8::mean_ip_index()); + reference -= + sq8FactoryMetadata(y_query.get(), dim, vecsim_types::sq8::mean_ip_index()); + reference += mean_square; + } + const float got = + components.indexCalculator->calcDistanceForQuery(x_storage.get(), y_query.get(), dim); + const float stored = + components.indexCalculator->calcDistance(x_storage.get(), y_storage.get(), dim); + const auto query_dispatch = + components.indexCalculator->getDistanceDispatch(DistanceMode::StoredToQuery); + const auto stored_dispatch = + components.indexCalculator->getDistanceDispatch(DistanceMode::StoredToStored); + ASSERT_TRUE(query_dispatch.isValid()); + ASSERT_TRUE(stored_dispatch.isValid()); + EXPECT_NEAR(got, reference, 0.002f); + EXPECT_NEAR(stored, reference, 0.002f); + EXPECT_FLOAT_EQ(query_dispatch(x_storage.get(), y_query.get(), dim), got); + EXPECT_FLOAT_EQ(stored_dispatch(x_storage.get(), y_storage.get(), dim), stored); + } + delete components.preprocessors; + delete components.indexCalculator; +} + +} // namespace + +TEST(SQ8FactoryTest, FP16QueriesShareStorageEncodingAndMatchReconstruction) { + for (size_t dim : {size_t{7}, size_t{64}, size_t{65}}) { + for (bool constant : {false, true}) { + SCOPED_TRACE(::testing::Message() << "dim=" << dim << " constant=" << constant); + verifyFP16SQ8FactoryQuery(dim, constant); + verifyFP16SQ8FactoryQuery(dim, constant); + verifyFP16SQ8FactoryQuery(dim, constant); + verifyFP16SQ8FactoryQuery(dim, constant); + } + } +} + +TEST(SQ8FactoryTest, FP32QueriesRemainAsymmetric) { + auto allocator = VecSimAllocator::newVecsimAllocator(); + constexpr size_t dim = 7; + const float y[dim] = {0.25f, -0.5f, 0.75f, 0.0f, 1.0f, -0.25f, 0.5f}; + auto components = CreateSQ8IndexComponents(allocator, dim, nullptr); + { + auto paired = components.preprocessors->preprocess(y, sizeof(y)); + ASSERT_NE(paired.getStorageBlob(), nullptr); + ASSERT_NE(paired.getQueryBlob(), nullptr); + EXPECT_NE(paired.getStorageBlob(), paired.getQueryBlob()); + EXPECT_EQ(std::memcmp(paired.getQueryBlob(), y, sizeof(y)), 0); + auto query = components.preprocessors->preprocessQuery(y, sizeof(y)); + EXPECT_EQ(std::memcmp(query.get(), y, sizeof(y)), 0); + } + delete components.preprocessors; + delete components.indexCalculator; +} + +TEST(SQ8FactoryTest, FP16CachedDispatchUsesUpdatedMean) { + using data_t = vecsim_types::float16; + constexpr size_t dim = 17; + auto allocator = VecSimAllocator::newVecsimAllocator(); + std::vector zero(dim, 0.0f), mean(dim); + std::vector x(dim), y(dim); + for (size_t d = 0; d < dim; ++d) { + mean[d] = (static_cast(d % 5) - 2) * 0.125f; + x[d] = vecsim_types::FP32_to_FP16((static_cast(d % 7) - 3) * 0.25f); + y[d] = vecsim_types::FP32_to_FP16((static_cast(d % 3) - 1) * 0.5f); + } + auto deferred = CreateSQ8IndexComponents(allocator, dim, zero.data()); + auto direct = CreateSQ8IndexComponents(allocator, dim, mean.data()); + auto cached = deferred.indexCalculator->getDistanceDispatch(DistanceMode::StoredToQuery); + auto *pp = dynamic_cast *>( + static_cast *>(deferred.preprocessors) + ->getPreprocessors()[0]); + auto *calc = + dynamic_cast *>( + deferred.indexCalculator); + ASSERT_NE(pp, nullptr); + ASSERT_NE(calc, nullptr); + pp->setMean(mean); + calc->setMeanSumSquares(mean); + { + auto x_deferred = + deferred.preprocessors->preprocessForStorage(x.data(), dim * sizeof(data_t)); + auto y_deferred = deferred.preprocessors->preprocessQuery(y.data(), dim * sizeof(data_t)); + auto x_direct = direct.preprocessors->preprocessForStorage(x.data(), dim * sizeof(data_t)); + auto y_direct = direct.preprocessors->preprocessQuery(y.data(), dim * sizeof(data_t)); + const size_t bytes = vecsim_types::sq8::storage_bytes_count(dim); + EXPECT_EQ(std::memcmp(x_deferred.get(), x_direct.get(), bytes), 0); + EXPECT_EQ(std::memcmp(y_deferred.get(), y_direct.get(), bytes), 0); + const float expected = + direct.indexCalculator->calcDistanceForQuery(x_direct.get(), y_direct.get(), dim); + EXPECT_FLOAT_EQ(cached(x_deferred.get(), y_deferred.get(), dim), expected); + EXPECT_FLOAT_EQ( + deferred.indexCalculator->calcDistanceForQuery(x_deferred.get(), y_deferred.get(), dim), + expected); + } + delete deferred.preprocessors; + delete deferred.indexCalculator; + delete direct.preprocessors; + delete direct.indexCalculator; +} diff --git a/tests/unit/test_hnsw_sq8.cpp b/tests/unit/test_hnsw_sq8.cpp index d2929d090..cda453573 100644 --- a/tests/unit/test_hnsw_sq8.cpp +++ b/tests/unit/test_hnsw_sq8.cpp @@ -1856,3 +1856,85 @@ TEST(SQ8TieredHNSWTest, MeanCenteredFP16L2BufferedAndMigratedValues) { verify_queries(); } } + +// Query scores for constant vectors are exact after SQ8 reconstruction. Check the public query +// paths against those scores, including the best vector selected for a multi-value label. +TEST(HNSWSQ8QueryTest, FP16QuantizedQueriesAcrossPublicAPIs) { + using data_t = vecsim_types::float16; + constexpr size_t dim = 17; + constexpr size_t count = 3; + const auto make_vector = [=](float value) { + return std::vector(dim, vecsim_types::FP32_to_FP16(value)); + }; + const auto query = make_vector(0.0625f); + const auto original_query = query; + + for (auto metric : {VecSimMetric_L2, VecSimMetric_IP}) { + for (bool with_mean : {false, true}) { + for (bool multi : {false, true}) { + SCOPED_TRACE(::testing::Message() + << "metric=" << metric << " mean=" << with_mean << " multi=" << multi); + std::vector mean(dim, 0.125f); + HNSWParams params = {.type = VecSimType_FLOAT16, + .dim = dim, + .metric = metric, + .multi = multi, + .initialCapacity = count + 1, + .M = 8, + .efConstruction = 20, + .efRuntime = count + 1, + .quantType = VecSimQuant_SQ8, + .quantParams = with_mean ? mean.data() : nullptr}; + VecSimParams vecsim_params = CreateParams(params); + std::unique_ptr index( + VecSimIndex_New(&vecsim_params), VecSimIndex_Free); + ASSERT_NE(index, nullptr); + for (const auto [label, value] : + {std::pair{0, -0.5f}, {1, 0.0f}, {2, 0.5f}}) { + const auto vector = make_vector(value); + ASSERT_EQ(VecSimIndex_AddVector(index.get(), vector.data(), label), 1); + } + if (multi) { + const auto extra = make_vector(0.75f); + ASSERT_EQ(VecSimIndex_AddVector(index.get(), extra.data(), 1), 1); + } + + const auto expected_score = [&](size_t label) { + float value = label == 0 ? -0.5f : (label == 1 ? 0.0f : 0.5f); + if (multi && metric == VecSimMetric_IP && label == 1) + value = 0.75f; + if (metric == VecSimMetric_IP) + return 1.0f - dim * value * 0.0625f; + const float diff = value - 0.0625f; + return dim * diff * diff; + }; + const std::vector order = metric == VecSimMetric_L2 + ? std::vector{1, 2, 0} + : multi ? std::vector{1, 2, 0} + : std::vector{2, 1, 0}; + const auto verify = [&](size_t id, double score, size_t rank) { + EXPECT_EQ(id, order[rank]); + EXPECT_NEAR(score, expected_score(id), 1e-4f); + }; + runTopKSearchTest(index.get(), query.data(), count, verify); + for (size_t label = 0; label < count; ++label) { + EXPECT_NEAR( + VecSimIndex_GetDistanceFrom_Unsafe(index.get(), label, query.data()), + expected_score(label), 1e-4f) + << "label " << label; + } + + const double radius = metric == VecSimMetric_L2 ? 1.0 : 0.75; + const size_t range_count = metric == VecSimMetric_L2 ? 1 : (multi ? 2 : 1); + runRangeQueryTest(index.get(), query.data(), radius, verify, range_count, BY_SCORE); + VecSimBatchIterator *iterator = + VecSimBatchIterator_New(index.get(), query.data(), nullptr); + ASSERT_NE(iterator, nullptr); + runBatchIteratorSearchTest(iterator, count, verify); + VecSimBatchIterator_Free(iterator); + EXPECT_EQ(std::memcmp(query.data(), original_query.data(), dim * sizeof(data_t)), + 0); + } + } + } +} From de341a7d114841e0acaaeb2c6c2fafeb325464c5 Mon Sep 17 00:00:00 2001 From: Dor Forer Date: Thu, 1 Oct 2026 13:35:26 +0300 Subject: [PATCH 3/5] Preserve asymmetric FP16 L2 queries to avoid norm cancellation --- .../components/components_factory.h | 4 ++- tests/unit/test_components.cpp | 36 +++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/VecSim/index_factories/components/components_factory.h b/src/VecSim/index_factories/components/components_factory.h index 0f9fa824b..cfe9a1d6e 100644 --- a/src/VecSim/index_factories/components/components_factory.h +++ b/src/VecSim/index_factories/components/components_factory.h @@ -44,7 +44,9 @@ template IndexComponents CreateSQ8IndexComponents(const std::shared_ptr &allocator, size_t dim, const float *mean_ptr) { - constexpr bool quantize_query = std::is_same_v; + // L2 keeps asymmetric queries to avoid cancellation in the symmetric norm expansion. + constexpr bool quantize_query = + std::is_same_v && Metric == VecSimMetric_IP; using Preprocessor = std::conditional_t, QuantPreprocessor>; diff --git a/tests/unit/test_components.cpp b/tests/unit/test_components.cpp index 3c95e4c45..007f8af25 100644 --- a/tests/unit/test_components.cpp +++ b/tests/unit/test_components.cpp @@ -2745,8 +2745,6 @@ TEST(SQ8FactoryTest, FP16QueriesShareStorageEncodingAndMatchReconstruction) { for (size_t dim : {size_t{7}, size_t{64}, size_t{65}}) { for (bool constant : {false, true}) { SCOPED_TRACE(::testing::Message() << "dim=" << dim << " constant=" << constant); - verifyFP16SQ8FactoryQuery(dim, constant); - verifyFP16SQ8FactoryQuery(dim, constant); verifyFP16SQ8FactoryQuery(dim, constant); verifyFP16SQ8FactoryQuery(dim, constant); } @@ -2816,3 +2814,37 @@ TEST(SQ8FactoryTest, FP16CachedDispatchUsesUpdatedMean) { delete direct.preprocessors; delete direct.indexCalculator; } + +TEST(SQ8FactoryTest, FP16L2KeepsAsymmetricQueriesNearLargeCenteredValues) { + using data_t = vecsim_types::float16; + constexpr size_t dim = 4; + auto allocator = VecSimAllocator::newVecsimAllocator(); + const std::vector mean(dim, 0.4499877989292145f); + const std::vector x(dim, vecsim_types::FP32_to_FP16(24.796875f)); + const std::vector y(dim, vecsim_types::FP32_to_FP16(25.0f)); + for (bool with_mean : {false, true}) { + auto components = CreateSQ8IndexComponents( + allocator, dim, with_mean ? mean.data() : nullptr); + { + auto candidate = + components.preprocessors->preprocessForStorage(x.data(), dim * sizeof(data_t)); + auto query = components.preprocessors->preprocessQuery(y.data(), dim * sizeof(data_t)); + auto paired = components.preprocessors->preprocess(y.data(), dim * sizeof(data_t)); + EXPECT_NE(paired.getStorageBlob(), paired.getQueryBlob()); + if (with_mean) { + const auto *values = static_cast(query.get()); + for (size_t d = 0; d < dim; ++d) + EXPECT_FLOAT_EQ(values[d], 25.0f - mean[d]); + } else { + EXPECT_EQ(std::memcmp(query.get(), y.data(), dim * sizeof(data_t)), 0); + } + constexpr float expected = dim * (25.0f - 24.796875f) * (25.0f - 24.796875f); + const auto dispatch = + components.indexCalculator->getDistanceDispatch(DistanceMode::StoredToQuery); + EXPECT_NEAR(dispatch(candidate.get(), query.get(), dim), expected, + 1e-4f + expected * 0.002f); + } + delete components.preprocessors; + delete components.indexCalculator; + } +} From 0b268f657169d946fd7afeef93b477cd0303c5ee Mon Sep 17 00:00:00 2001 From: Dor Forer Date: Thu, 1 Oct 2026 14:58:56 +0300 Subject: [PATCH 4/5] Remove benchmark artifact upload from FP16 query fix --- .github/workflows/benchmark-runner.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/benchmark-runner.yml b/.github/workflows/benchmark-runner.yml index c4bde4a15..86c5739f9 100644 --- a/.github/workflows/benchmark-runner.yml +++ b/.github/workflows/benchmark-runner.yml @@ -98,13 +98,6 @@ jobs: timeout-minutes: 300 run: | make benchmark BM_FILTER=${{ inputs.setup }} - - name: Upload benchmark results - if: always() - uses: actions/upload-artifact@v7 - with: - name: benchmark-${{ inputs.setup }}-${{ inputs.architecture }} - path: '**/*_results.json' - if-no-files-found: warn - name: Collect results run: | ./tests/benchmark/benchmarks.sh ${{ inputs.setup }} | xargs -P 0 -I {} redisbench-admin export \ From 706c941c56b9c92b5fb765f5d4221ed0a40d7974 Mon Sep 17 00:00:00 2001 From: Dor Forer Date: Sun, 4 Oct 2026 08:53:06 +0300 Subject: [PATCH 5/5] Test finite FP32 SQ8 distances for extreme query ranges --- tests/unit/test_components.cpp | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/unit/test_components.cpp b/tests/unit/test_components.cpp index 007f8af25..e26419eb7 100644 --- a/tests/unit/test_components.cpp +++ b/tests/unit/test_components.cpp @@ -2769,6 +2769,31 @@ TEST(SQ8FactoryTest, FP32QueriesRemainAsymmetric) { delete components.indexCalculator; } +TEST(SQ8FactoryTest, FP32IPPreservesFiniteDistanceWhenQueryRangeOverflows) { + constexpr size_t dim = 2; + const float x[dim] = {1e-37f, 1e-37f}; + const float max = std::numeric_limits::max(); + const float y[dim] = {max, -max}; + auto allocator = VecSimAllocator::newVecsimAllocator(); + auto components = CreateSQ8IndexComponents(allocator, dim, nullptr); + { + auto storage = components.preprocessors->preprocessForStorage(x, sizeof(x)); + auto query = components.preprocessors->preprocessQuery(y, sizeof(y)); + ASSERT_NE(storage.get(), nullptr); + ASSERT_NE(query.get(), nullptr); + EXPECT_EQ(std::memcmp(query.get(), y, sizeof(y)), 0); + const float distance = + components.indexCalculator->calcDistanceForQuery(storage.get(), query.get(), dim); + EXPECT_FLOAT_EQ(distance, 1.0f); + const auto dispatch = + components.indexCalculator->getDistanceDispatch(DistanceMode::StoredToQuery); + ASSERT_TRUE(dispatch.isValid()); + EXPECT_FLOAT_EQ(dispatch(storage.get(), query.get(), dim), 1.0f); + } + delete components.preprocessors; + delete components.indexCalculator; +} + TEST(SQ8FactoryTest, FP16CachedDispatchUsesUpdatedMean) { using data_t = vecsim_types::float16; constexpr size_t dim = 17;