From d1cdfac1fcad501701a4b4ca06a7185ee36200f8 Mon Sep 17 00:00:00 2001 From: Dor Forer Date: Tue, 14 Jul 2026 17:59:06 +0800 Subject: [PATCH 1/4] [MOD-14953] Add calcDistanceForQuery to IndexCalculatorInterface (#1) * IndexCalculatorInterface change Add calcDistanceForQuery() API and use it in HNSW index * Improve tests (cherry picked from commit ed1df68a4f1585747068a0560720fd76fb92a6b9) --- src/VecSim/algorithms/hnsw/hnsw.h | 21 ++++++----- .../algorithms/hnsw/hnsw_batch_iterator.h | 13 ++++--- src/VecSim/algorithms/hnsw/hnsw_multi.h | 3 +- src/VecSim/algorithms/hnsw/hnsw_single.h | 3 +- src/VecSim/spaces/computer/calculator.h | 31 +++++++++++++--- src/VecSim/vec_sim_index.h | 22 ++++++++++- tests/unit/test_components.cpp | 37 +++++++++++++++---- tests/unit/test_hnsw_tiered.cpp | 7 +++- 8 files changed, 104 insertions(+), 33 deletions(-) diff --git a/src/VecSim/algorithms/hnsw/hnsw.h b/src/VecSim/algorithms/hnsw/hnsw.h index 2510ddbfb..ffccf32f7 100644 --- a/src/VecSim/algorithms/hnsw/hnsw.h +++ b/src/VecSim/algorithms/hnsw/hnsw.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 @@ -544,7 +545,7 @@ void HNSWIndex::processCandidate( elements_tags[candidate_id] = visited_tag; - DistType cur_dist = this->calcDistance(query_data, cur_data); + DistType cur_dist = this->calcDistanceForQuery(cur_data, query_data); if (lowerBound > cur_dist || top_candidates.size() < ef) { candidate_set.emplace(-cur_dist, candidate_id); @@ -572,7 +573,7 @@ void HNSWIndex::processCandidate( elements_tags[candidate_id] = visited_tag; - DistType cur_dist = this->calcDistance(query_data, cur_data); + DistType cur_dist = this->calcDistanceForQuery(cur_data, query_data); if (lowerBound > cur_dist || top_candidates.size() < ef) { candidate_set.emplace(-cur_dist, candidate_id); @@ -629,7 +630,7 @@ void HNSWIndex::processCandidate_RangeSearch( elements_tags[candidate_id] = visited_tag; - DistType cur_dist = this->calcDistance(query_data, cur_data); + DistType cur_dist = this->calcDistanceForQuery(cur_data, query_data); if (cur_dist < dyn_range) { candidate_set.emplace(-cur_dist, candidate_id); @@ -647,7 +648,7 @@ void HNSWIndex::processCandidate_RangeSearch( elements_tags[candidate_id] = visited_tag; - DistType cur_dist = this->calcDistance(query_data, cur_data); + DistType cur_dist = this->calcDistanceForQuery(cur_data, query_data); if (cur_dist < dyn_range) { candidate_set.emplace(-cur_dist, candidate_id); @@ -674,7 +675,7 @@ HNSWIndex::searchLayer(idType ep_id, const void *data_point, DistType lowerBound; if (!isMarkedDeleted(ep_id)) { - DistType dist = this->calcDistance(data_point, getDataByInternalId(ep_id)); + DistType dist = this->calcDistanceForQuery(getDataByInternalId(ep_id), data_point); lowerBound = dist; top_candidates.emplace(dist, ep_id); candidate_set.emplace(-dist, ep_id); @@ -1219,7 +1220,7 @@ void HNSWIndex::greedySearchLevel(const void *vector_data, s if (isInProcess(candidate)) { continue; } - DistType d = this->calcDistance(vector_data, getDataByInternalId(candidate)); + DistType d = this->calcDistanceForQuery(getDataByInternalId(candidate), vector_data); if (d < curDist) { curDist = d; bestCand = candidate; @@ -1557,7 +1558,7 @@ void HNSWIndex::insertElementToGraph(idType element_id, size_t max_common_level; if (element_max_level < global_max_level) { max_common_level = element_max_level; - cur_dist = this->calcDistance(vector_data, getDataByInternalId(curr_element)); + cur_dist = this->calcDistanceForQuery(getDataByInternalId(curr_element), vector_data); for (auto level = static_cast(global_max_level); level > static_cast(element_max_level); level--) { // this is done for the levels which are above the max level @@ -1878,7 +1879,7 @@ idType HNSWIndex::searchBottomLayerEP(const void *query_data if (curr_element == INVALID_ID) return curr_element; // index is empty. - DistType cur_dist = this->calcDistance(query_data, getDataByInternalId(curr_element)); + DistType cur_dist = this->calcDistanceForQuery(getDataByInternalId(curr_element), query_data); for (size_t level = max_level; level > 0 && curr_element != INVALID_ID; --level) { greedySearchLevel(query_data, level, curr_element, cur_dist, timeoutCtx, rc); } @@ -1901,7 +1902,7 @@ HNSWIndex::searchBottomLayer_WithTimeout(idType ep_id, const if (!isMarkedDeleted(ep_id)) { // If ep is not marked as deleted, get its distance and set lower bound and heaps // accordingly - DistType dist = this->calcDistance(data_point, getDataByInternalId(ep_id)); + DistType dist = this->calcDistanceForQuery(getDataByInternalId(ep_id), data_point); lowerBound = dist; top_candidates->emplace(dist, getExternalLabel(ep_id)); candidate_set.emplace(-dist, ep_id); @@ -2009,7 +2010,7 @@ VecSimQueryResultContainer HNSWIndex::searchRangeBottomLayer dynamic_range_search_boundaries = dynamic_range = ep_dist; } else { // If ep is not marked as deleted, get its distance and set ranges accordingly - ep_dist = this->calcDistance(data_point, getDataByInternalId(ep_id)); + ep_dist = this->calcDistanceForQuery(getDataByInternalId(ep_id), data_point); dynamic_range = ep_dist; if (ep_dist <= radius) { // Entry-point is within the radius - add it to the results. diff --git a/src/VecSim/algorithms/hnsw/hnsw_batch_iterator.h b/src/VecSim/algorithms/hnsw/hnsw_batch_iterator.h index 77e44738d..6be783446 100644 --- a/src/VecSim/algorithms/hnsw/hnsw_batch_iterator.h +++ b/src/VecSim/algorithms/hnsw/hnsw_batch_iterator.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 @@ -138,8 +139,8 @@ VecSimQueryReply_Code HNSW_BatchIterator::scanGraphInternal( this->visitNode(candidate_id); const char *candidate_data = this->index->getDataByInternalId(candidate_id); - DistType candidate_dist = - this->index->calcDistance(this->getQueryBlob(), (const void *)candidate_data); + DistType candidate_dist = this->index->calcDistanceForQuery( + (const void *)candidate_data, this->getQueryBlob()); candidates.emplace(candidate_dist, candidate_id); } @@ -150,8 +151,8 @@ VecSimQueryReply_Code HNSW_BatchIterator::scanGraphInternal( this->visitNode(candidate_id); const char *candidate_data = this->index->getDataByInternalId(candidate_id); - DistType candidate_dist = - this->index->calcDistance(this->getQueryBlob(), (const void *)candidate_data); + DistType candidate_dist = this->index->calcDistanceForQuery( + (const void *)candidate_data, this->getQueryBlob()); candidates.emplace(candidate_dist, candidate_id); } @@ -175,8 +176,8 @@ HNSW_BatchIterator::scanGraph(VecSimQueryReply_Code *rc) { if (this->getResultsCount() == 0 && this->top_candidates_extras.empty() && this->candidates.empty()) { if (!index->isMarkedDeleted(this->entry_point)) { - this->lower_bound = this->index->calcDistance( - this->getQueryBlob(), this->index->getDataByInternalId(this->entry_point)); + this->lower_bound = this->index->calcDistanceForQuery( + this->index->getDataByInternalId(this->entry_point), this->getQueryBlob()); } else { this->lower_bound = std::numeric_limits::max(); } diff --git a/src/VecSim/algorithms/hnsw/hnsw_multi.h b/src/VecSim/algorithms/hnsw/hnsw_multi.h index 50ff1a37d..348aadfc5 100644 --- a/src/VecSim/algorithms/hnsw/hnsw_multi.h +++ b/src/VecSim/algorithms/hnsw/hnsw_multi.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 @@ -153,7 +154,7 @@ double HNSWIndex_Multi::getDistanceFromInternal(labelType la // Iterate over the ids and find the minimum distance. for (auto id : IDs) { - DistType d = this->calcDistance(this->getDataByInternalId(id), vector_data); + DistType d = this->calcDistanceForQuery(this->getDataByInternalId(id), vector_data); dist = std::fmin(dist, d); } diff --git a/src/VecSim/algorithms/hnsw/hnsw_single.h b/src/VecSim/algorithms/hnsw/hnsw_single.h index 61899a142..8955ec179 100644 --- a/src/VecSim/algorithms/hnsw/hnsw_single.h +++ b/src/VecSim/algorithms/hnsw/hnsw_single.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 @@ -129,7 +130,7 @@ HNSWIndex_Single::getDistanceFromInternal(labelType label, } idType id = it->second; - return this->calcDistance(vector_data, this->getDataByInternalId(id)); + return this->calcDistanceForQuery(this->getDataByInternalId(id), vector_data); } template diff --git a/src/VecSim/spaces/computer/calculator.h b/src/VecSim/spaces/computer/calculator.h index 539325d8e..3dc66aa1a 100644 --- a/src/VecSim/spaces/computer/calculator.h +++ b/src/VecSim/spaces/computer/calculator.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 @@ -24,8 +25,13 @@ class IndexCalculatorInterface : public VecsimBaseObject { virtual DistType calcDistance(const void *v1, const void *v2, size_t dim) const = 0; + virtual DistType calcDistanceForQuery(const void *candidate_vector, const void *query_vector, + size_t dim) const = 0; + // Raw distance function; cached by the index to skip the vtable on the hot path. virtual spaces::dist_func_t getDistFunc() const = 0; + + virtual spaces::dist_func_t getDistFuncForQuery() const = 0; }; /** @@ -39,12 +45,17 @@ class IndexCalculatorInterface : public VecsimBaseObject { template class DistanceCalculatorInterface : public IndexCalculatorInterface { public: - DistanceCalculatorInterface(std::shared_ptr allocator, DistFuncType dist_func) - : IndexCalculatorInterface(allocator), dist_func(dist_func) {} + DistanceCalculatorInterface(std::shared_ptr allocator, DistFuncType dist_func, + DistFuncType query_dist_func = nullptr) + : IndexCalculatorInterface(allocator), dist_func(dist_func), + query_dist_func(query_dist_func ? query_dist_func : dist_func) {} virtual DistType calcDistance(const void *v1, const void *v2, size_t dim) const = 0; + virtual DistType calcDistanceForQuery(const void *candidate_vector, const void *query_vector, + size_t dim) const = 0; protected: DistFuncType dist_func; + DistFuncType query_dist_func; }; template @@ -52,13 +63,23 @@ class DistanceCalculatorCommon : public DistanceCalculatorInterface> { public: DistanceCalculatorCommon(std::shared_ptr allocator, - spaces::dist_func_t dist_func) - : DistanceCalculatorInterface>(allocator, - dist_func) {} + spaces::dist_func_t dist_func, + spaces::dist_func_t query_dist_func = nullptr) + : DistanceCalculatorInterface>(allocator, dist_func, + query_dist_func) {} DistType calcDistance(const void *v1, const void *v2, size_t dim) const override { return this->dist_func(v1, v2, dim); } + DistType calcDistanceForQuery(const void *candidate_vector, const void *query_vector, + size_t dim) const override { + return this->query_dist_func(candidate_vector, query_vector, dim); + } + spaces::dist_func_t getDistFunc() const override { return this->dist_func; } + + spaces::dist_func_t getDistFuncForQuery() const override { + return this->query_dist_func; + } }; diff --git a/src/VecSim/vec_sim_index.h b/src/VecSim/vec_sim_index.h index 38d47a2ca..049f6ca4b 100644 --- a/src/VecSim/vec_sim_index.h +++ b/src/VecSim/vec_sim_index.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 @@ -87,7 +88,9 @@ struct VecSimIndexAbstract : public VecSimIndexInterface { private: IndexCalculatorInterface *indexCalculator; // Distance calculator. spaces::dist_func_t cachedDistFunc; // Cached dist func, used on the hot path. - PreprocessorsContainerAbstract *preprocessors; // Storage and query preprocessors. + spaces::dist_func_t + cachedDistFuncForQuery; // Cached dist func for query, used on the hot path. + PreprocessorsContainerAbstract *preprocessors; // Storage and query preprocessors. size_t inputBlobSize; // The size of input vectors/queries blob in bytes. May differ from dim * // sizeof(vecType) when vectors have been externally preprocessed (e.g., @@ -128,6 +131,9 @@ struct VecSimIndexAbstract : public VecSimIndexInterface { indexCalculator(components.indexCalculator), cachedDistFunc(components.indexCalculator ? components.indexCalculator->getDistFunc() : nullptr), + cachedDistFuncForQuery(components.indexCalculator + ? components.indexCalculator->getDistFuncForQuery() + : nullptr), preprocessors(components.preprocessors), inputBlobSize(params.inputBlobSize), storedDataSize(params.storedDataSize) { assert(VecSimType_sizeof(vecType)); @@ -168,6 +174,20 @@ struct VecSimIndexAbstract : public VecSimIndexInterface { return cachedDistFunc(vector_data1, vector_data2, this->dim); } + /** + * @brief Calculate the distance between a stored candidate vector and a query vector. + * Allows asymmetric distance computation (e.g., for quantized stored vectors). + * + * @note Precondition: @c cachedDistFuncForQuery must be non-null. Subclasses that construct + * this index with a null @c indexCalculator (e.g. SVS, which uses its own + * internal distance kernels) must not call this method. + * + * @return the distance between the candidate and the query. + */ + DistType calcDistanceForQuery(const void *candidate_vector, const void *query_vector) const { + return cachedDistFuncForQuery(candidate_vector, query_vector, this->dim); + } + /** * @brief Preprocess a blob for both storage and query. * diff --git a/tests/unit/test_components.cpp b/tests/unit/test_components.cpp index eb465fa92..718a0c16a 100644 --- a/tests/unit/test_components.cpp +++ b/tests/unit/test_components.cpp @@ -20,35 +20,58 @@ class IndexCalculatorTest : public ::testing::Test {}; namespace dummyCalcultor { using DummyType = int; -using dummy_dist_func_t = DummyType (*)(int); +using dummy_dist_func_t = DummyType (*)(int, int); -int dummyDistFunc(int value) { return value; } +int dummyDistFunc(int v1, int v2) { return v1 + v2; } +int dummyQueryDistFunc(int candidate, int query) { return candidate - query; } template class DistanceCalculatorDummy : public DistanceCalculatorInterface { public: - DistanceCalculatorDummy(std::shared_ptr allocator, dummy_dist_func_t dist_func) - : DistanceCalculatorInterface(allocator, dist_func) {} + DistanceCalculatorDummy(std::shared_ptr allocator, dummy_dist_func_t dist_func, + dummy_dist_func_t query_dist_func = nullptr) + : DistanceCalculatorInterface(allocator, dist_func, + query_dist_func) {} virtual DistType calcDistance(const void *v1, const void *v2, size_t dim) const { - return this->dist_func(7); + int v1_int = *static_cast(v1); + int v2_int = *static_cast(v2); + return this->dist_func(v1_int, v2_int); + } + + virtual DistType calcDistanceForQuery(const void *candidate_vector, const void *query_vector, + size_t dim) const { + int candidate_int = *static_cast(candidate_vector); + int query_int = *static_cast(query_vector); + return this->query_dist_func(candidate_int, query_int); } // Dummy uses a non-standard dist func signature, so the standard slot is unavailable. spaces::dist_func_t getDistFunc() const override { return nullptr; } + spaces::dist_func_t getDistFuncForQuery() const override { return nullptr; } }; } // namespace dummyCalcultor TEST(IndexCalculatorTest, TestIndexCalculator) { + int v1 = 20, v2 = 10; std::shared_ptr allocator = VecSimAllocator::newVecsimAllocator(); // Test computer with a distance function signature different from dim(v1, v2, dim()). using namespace dummyCalcultor; auto distance_calculator = DistanceCalculatorDummy(allocator, dummyDistFunc); - - ASSERT_EQ(distance_calculator.calcDistance(nullptr, nullptr, 0), 7); + ASSERT_EQ(distance_calculator.calcDistance(&v1, &v2, 0), 30); + ASSERT_EQ(distance_calculator.calcDistance(&v2, &v1, 0), 30); + ASSERT_EQ(distance_calculator.calcDistanceForQuery(&v1, &v2, 0), 30); + ASSERT_EQ(distance_calculator.calcDistanceForQuery(&v2, &v1, 0), 30); + + auto asymmetric_distance_calculator = + DistanceCalculatorDummy(allocator, dummyDistFunc, dummyQueryDistFunc); + ASSERT_EQ(asymmetric_distance_calculator.calcDistance(&v1, &v2, 0), 30); + ASSERT_EQ(asymmetric_distance_calculator.calcDistance(&v2, &v1, 0), 30); + ASSERT_EQ(asymmetric_distance_calculator.calcDistanceForQuery(&v1, &v2, 0), 10); + ASSERT_EQ(asymmetric_distance_calculator.calcDistanceForQuery(&v2, &v1, 0), -10); } class PreprocessorsTest : public ::testing::Test {}; diff --git a/tests/unit/test_hnsw_tiered.cpp b/tests/unit/test_hnsw_tiered.cpp index b64885b97..3bbb2f141 100644 --- a/tests/unit/test_hnsw_tiered.cpp +++ b/tests/unit/test_hnsw_tiered.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 @@ -4382,7 +4383,8 @@ TYPED_TEST(HNSWTieredIndexTestBasic, HNSWWithPreprocessor) { expected_score = frontend_index->calcDistance(normalized_query, normalized_vec); } if (label == 1) { // the result in the hnsw index - expected_score = hnsw_index->calcDistance(norm_double_query, norm_double_vec); + expected_score = + hnsw_index->calcDistanceForQuery(norm_double_vec, norm_double_query); } ASSERT_EQ(score, expected_score) << "label: " << label; }; @@ -4408,7 +4410,8 @@ TYPED_TEST(HNSWTieredIndexTestBasic, HNSWWithPreprocessor) { // Both vector were processed by the hnsw preprocessor, as well as the query. // The score should be the distance from the doubled query to the doubled vector. auto verify_res = [&](size_t label, double score, size_t result_rank) { - double expected_score = hnsw_index->calcDistance(norm_double_query, norm_double_vec); + double expected_score = + hnsw_index->calcDistanceForQuery(norm_double_vec, norm_double_query); ASSERT_EQ(score, expected_score) << "label: " << label; }; From 2fe184d2fcea8a10d38f68b08c4ac2d4b1993435 Mon Sep 17 00:00:00 2001 From: Dor Forer Date: Wed, 22 Jul 2026 11:54:30 +0300 Subject: [PATCH 2/4] Format ARM copyright headers --- src/VecSim/algorithms/hnsw/hnsw.h | 3 ++- src/VecSim/algorithms/hnsw/hnsw_batch_iterator.h | 3 ++- src/VecSim/algorithms/hnsw/hnsw_multi.h | 3 ++- src/VecSim/algorithms/hnsw/hnsw_single.h | 3 ++- src/VecSim/spaces/computer/calculator.h | 3 ++- src/VecSim/vec_sim_index.h | 3 ++- tests/unit/test_hnsw_tiered.cpp | 3 ++- 7 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/VecSim/algorithms/hnsw/hnsw.h b/src/VecSim/algorithms/hnsw/hnsw.h index ffccf32f7..3d5b4dfb1 100644 --- a/src/VecSim/algorithms/hnsw/hnsw.h +++ b/src/VecSim/algorithms/hnsw/hnsw.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/src/VecSim/algorithms/hnsw/hnsw_batch_iterator.h b/src/VecSim/algorithms/hnsw/hnsw_batch_iterator.h index 6be783446..3e18847e6 100644 --- a/src/VecSim/algorithms/hnsw/hnsw_batch_iterator.h +++ b/src/VecSim/algorithms/hnsw/hnsw_batch_iterator.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/src/VecSim/algorithms/hnsw/hnsw_multi.h b/src/VecSim/algorithms/hnsw/hnsw_multi.h index 348aadfc5..ca3e9313a 100644 --- a/src/VecSim/algorithms/hnsw/hnsw_multi.h +++ b/src/VecSim/algorithms/hnsw/hnsw_multi.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/src/VecSim/algorithms/hnsw/hnsw_single.h b/src/VecSim/algorithms/hnsw/hnsw_single.h index 8955ec179..455d340ce 100644 --- a/src/VecSim/algorithms/hnsw/hnsw_single.h +++ b/src/VecSim/algorithms/hnsw/hnsw_single.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/src/VecSim/spaces/computer/calculator.h b/src/VecSim/spaces/computer/calculator.h index 3dc66aa1a..d55814cea 100644 --- a/src/VecSim/spaces/computer/calculator.h +++ b/src/VecSim/spaces/computer/calculator.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/src/VecSim/vec_sim_index.h b/src/VecSim/vec_sim_index.h index 049f6ca4b..4182f2940 100644 --- a/src/VecSim/vec_sim_index.h +++ b/src/VecSim/vec_sim_index.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_hnsw_tiered.cpp b/tests/unit/test_hnsw_tiered.cpp index 3bbb2f141..3d8009d5b 100644 --- a/tests/unit/test_hnsw_tiered.cpp +++ b/tests/unit/test_hnsw_tiered.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 8bccfaba2eb7ff0d766b92dabd6b8b4f6d29a4e2 Mon Sep 17 00:00:00 2001 From: Dor Forer Date: Wed, 22 Jul 2026 18:20:12 +0300 Subject: [PATCH 3/4] Remove ARM copyright headers from modified files --- src/VecSim/algorithms/hnsw/hnsw.h | 2 -- src/VecSim/algorithms/hnsw/hnsw_batch_iterator.h | 2 -- src/VecSim/algorithms/hnsw/hnsw_multi.h | 2 -- src/VecSim/algorithms/hnsw/hnsw_single.h | 2 -- src/VecSim/spaces/computer/calculator.h | 2 -- src/VecSim/vec_sim_index.h | 2 -- tests/unit/test_hnsw_tiered.cpp | 2 -- 7 files changed, 14 deletions(-) diff --git a/src/VecSim/algorithms/hnsw/hnsw.h b/src/VecSim/algorithms/hnsw/hnsw.h index 3d5b4dfb1..2c1fae87f 100644 --- a/src/VecSim/algorithms/hnsw/hnsw.h +++ b/src/VecSim/algorithms/hnsw/hnsw.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/algorithms/hnsw/hnsw_batch_iterator.h b/src/VecSim/algorithms/hnsw/hnsw_batch_iterator.h index 3e18847e6..16d8323d8 100644 --- a/src/VecSim/algorithms/hnsw/hnsw_batch_iterator.h +++ b/src/VecSim/algorithms/hnsw/hnsw_batch_iterator.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/algorithms/hnsw/hnsw_multi.h b/src/VecSim/algorithms/hnsw/hnsw_multi.h index ca3e9313a..db978d28c 100644 --- a/src/VecSim/algorithms/hnsw/hnsw_multi.h +++ b/src/VecSim/algorithms/hnsw/hnsw_multi.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/algorithms/hnsw/hnsw_single.h b/src/VecSim/algorithms/hnsw/hnsw_single.h index 455d340ce..e6892dcab 100644 --- a/src/VecSim/algorithms/hnsw/hnsw_single.h +++ b/src/VecSim/algorithms/hnsw/hnsw_single.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/spaces/computer/calculator.h b/src/VecSim/spaces/computer/calculator.h index d55814cea..6df7f7448 100644 --- a/src/VecSim/spaces/computer/calculator.h +++ b/src/VecSim/spaces/computer/calculator.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/vec_sim_index.h b/src/VecSim/vec_sim_index.h index 4182f2940..37a6f8402 100644 --- a/src/VecSim/vec_sim_index.h +++ b/src/VecSim/vec_sim_index.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_hnsw_tiered.cpp b/tests/unit/test_hnsw_tiered.cpp index 3d8009d5b..21f504177 100644 --- a/tests/unit/test_hnsw_tiered.cpp +++ b/tests/unit/test_hnsw_tiered.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 82f246277ae0a47f8f84dc48999d83ccf7999c3f Mon Sep 17 00:00:00 2001 From: Dor Forer Date: Thu, 23 Jul 2026 14:34:54 +0300 Subject: [PATCH 4/4] Add context-aware distance dispatch --- src/VecSim/spaces/computer/calculator.h | 54 ++++++++++++++++++---- src/VecSim/vec_sim_index.h | 36 ++++++++------- tests/unit/test_components.cpp | 59 +++++++++++++++++++++++-- 3 files changed, 121 insertions(+), 28 deletions(-) diff --git a/src/VecSim/spaces/computer/calculator.h b/src/VecSim/spaces/computer/calculator.h index 6df7f7448..c12699946 100644 --- a/src/VecSim/spaces/computer/calculator.h +++ b/src/VecSim/spaces/computer/calculator.h @@ -12,6 +12,46 @@ #include "VecSim/memory/vecsim_base.h" #include "VecSim/spaces/spaces.h" +enum class DistanceMode { + StoredToStored, + StoredToQuery, +}; + +/** + * A distance callback selected once when an index is constructed. + * + * Stateless calculators populate `stateless_func`, preserving the existing hot path of one + * indirect call to the selected distance kernel. Stateful calculators populate `stateful_func` + * and `context`; this avoids a vtable lookup in the hot path without forcing stateless + * calculators through an adapter. + */ +template +struct DistanceDispatch { + using stateful_dist_func_t = DistType (*)(const void *context, const void *lhs, const void *rhs, + size_t dim); + + spaces::dist_func_t stateless_func = nullptr; + stateful_dist_func_t stateful_func = nullptr; + const void *context = nullptr; + + static DistanceDispatch stateless(spaces::dist_func_t func) { + return {.stateless_func = func}; + } + + static DistanceDispatch stateful(const void *context, stateful_dist_func_t func) { + return {.stateful_func = func, .context = context}; + } + + bool isValid() const { return (stateless_func != nullptr) != (stateful_func != nullptr); } + + DistType operator()(const void *lhs, const void *rhs, size_t dim) const { + if (stateless_func) { + return stateless_func(lhs, rhs, dim); + } + return stateful_func(context, lhs, rhs, dim); + } +}; + // We need this "wrapper" class to hold the DistanceCalculatorInterface in the index, that is not // templated according to the distance function signature. template @@ -27,10 +67,9 @@ class IndexCalculatorInterface : public VecsimBaseObject { virtual DistType calcDistanceForQuery(const void *candidate_vector, const void *query_vector, size_t dim) const = 0; - // Raw distance function; cached by the index to skip the vtable on the hot path. - virtual spaces::dist_func_t getDistFunc() const = 0; - - virtual spaces::dist_func_t getDistFuncForQuery() const = 0; + // Called once per mode when constructing the index. The returned dispatch is cached so + // distance calculations in the hot path do not perform virtual calls. + virtual DistanceDispatch getDistanceDispatch(DistanceMode mode) const = 0; }; /** @@ -76,9 +115,8 @@ class DistanceCalculatorCommon return this->query_dist_func(candidate_vector, query_vector, dim); } - spaces::dist_func_t getDistFunc() const override { return this->dist_func; } - - spaces::dist_func_t getDistFuncForQuery() const override { - return this->query_dist_func; + DistanceDispatch getDistanceDispatch(DistanceMode mode) const override { + auto func = mode == DistanceMode::StoredToStored ? this->dist_func : this->query_dist_func; + return DistanceDispatch::stateless(func); } }; diff --git a/src/VecSim/vec_sim_index.h b/src/VecSim/vec_sim_index.h index 37a6f8402..dcf2ac30d 100644 --- a/src/VecSim/vec_sim_index.h +++ b/src/VecSim/vec_sim_index.h @@ -86,9 +86,8 @@ struct VecSimIndexAbstract : public VecSimIndexInterface { RawDataContainer *vectors; // The raw vectors data container. private: IndexCalculatorInterface *indexCalculator; // Distance calculator. - spaces::dist_func_t cachedDistFunc; // Cached dist func, used on the hot path. - spaces::dist_func_t - cachedDistFuncForQuery; // Cached dist func for query, used on the hot path. + DistanceDispatch storedDistanceDispatch; + DistanceDispatch queryDistanceDispatch; PreprocessorsContainerAbstract *preprocessors; // Storage and query preprocessors. size_t inputBlobSize; // The size of input vectors/queries blob in bytes. May differ from dim * @@ -128,16 +127,21 @@ struct VecSimIndexAbstract : public VecSimIndexInterface { blockSize(params.blockSize ? params.blockSize : DEFAULT_BLOCK_SIZE), lastMode(EMPTY_MODE), isMulti(params.multi), isDisk(params.isDisk), logCallbackCtx(params.logCtx), indexCalculator(components.indexCalculator), - cachedDistFunc(components.indexCalculator ? components.indexCalculator->getDistFunc() - : nullptr), - cachedDistFuncForQuery(components.indexCalculator - ? components.indexCalculator->getDistFuncForQuery() - : nullptr), + storedDistanceDispatch( + components.indexCalculator + ? components.indexCalculator->getDistanceDispatch(DistanceMode::StoredToStored) + : DistanceDispatch{}), + queryDistanceDispatch( + components.indexCalculator + ? components.indexCalculator->getDistanceDispatch(DistanceMode::StoredToQuery) + : DistanceDispatch{}), preprocessors(components.preprocessors), inputBlobSize(params.inputBlobSize), storedDataSize(params.storedDataSize) { assert(VecSimType_sizeof(vecType)); assert(storedDataSize); assert(inputBlobSize); + assert(indexCalculator == nullptr || storedDistanceDispatch.isValid()); + assert(indexCalculator == nullptr || queryDistanceDispatch.isValid()); // DataBlocksContainer holds the persistent storage vectors, so it must honor the storage // alignment hint (not the query alignment). Note: this only aligns the base address of // each block; vectors inside a block are packed back-to-back at stride `storedDataSize`, @@ -161,30 +165,28 @@ struct VecSimIndexAbstract : public VecSimIndexInterface { /** * @brief Calculate the distance between two vectors based on index parameters. * - * Uses the cached dist func to avoid the indexCalculator vtable on the hot path. + * Uses the cached dispatch to avoid the indexCalculator vtable on the hot path. * - * @note Precondition: @c cachedDistFunc must be non-null. Subclasses that construct - * this index with a null @c indexCalculator (e.g. SVS, which uses its own - * internal distance kernels) must not call this method. + * @note Subclasses that construct this index with a null @c indexCalculator (e.g. SVS, which + * uses its own internal distance kernels) must not call this method. * * @return the distance between the vectors. */ DistType calcDistance(const void *vector_data1, const void *vector_data2) const { - return cachedDistFunc(vector_data1, vector_data2, this->dim); + return storedDistanceDispatch(vector_data1, vector_data2, this->dim); } /** * @brief Calculate the distance between a stored candidate vector and a query vector. * Allows asymmetric distance computation (e.g., for quantized stored vectors). * - * @note Precondition: @c cachedDistFuncForQuery must be non-null. Subclasses that construct - * this index with a null @c indexCalculator (e.g. SVS, which uses its own - * internal distance kernels) must not call this method. + * @note Subclasses that construct this index with a null @c indexCalculator (e.g. SVS, which + * uses its own internal distance kernels) must not call this method. * * @return the distance between the candidate and the query. */ DistType calcDistanceForQuery(const void *candidate_vector, const void *query_vector) const { - return cachedDistFuncForQuery(candidate_vector, query_vector, this->dim); + return queryDistanceDispatch(candidate_vector, query_vector, this->dim); } /** diff --git a/tests/unit/test_components.cpp b/tests/unit/test_components.cpp index 718a0c16a..c40d37e86 100644 --- a/tests/unit/test_components.cpp +++ b/tests/unit/test_components.cpp @@ -25,6 +25,25 @@ using dummy_dist_func_t = DummyType (*)(int, int); int dummyDistFunc(int v1, int v2) { return v1 + v2; } int dummyQueryDistFunc(int candidate, int query) { return candidate - query; } +float commonDistFunc(const void *v1, const void *v2, size_t dim) { + return *static_cast(v1) + *static_cast(v2) + + static_cast(dim); +} + +float commonQueryDistFunc(const void *candidate, const void *query, size_t dim) { + return *static_cast(candidate) - *static_cast(query) + + static_cast(dim); +} + +struct StatefulDistanceContext { + float offset; +}; + +float statefulDistFunc(const void *context, const void *v1, const void *v2, size_t dim) { + const auto *state = static_cast(context); + return commonDistFunc(v1, v2, dim) + state->offset; +} + template class DistanceCalculatorDummy : public DistanceCalculatorInterface { public: @@ -46,9 +65,8 @@ class DistanceCalculatorDummy : public DistanceCalculatorInterfacequery_dist_func(candidate_int, query_int); } - // Dummy uses a non-standard dist func signature, so the standard slot is unavailable. - spaces::dist_func_t getDistFunc() const override { return nullptr; } - spaces::dist_func_t getDistFuncForQuery() const override { return nullptr; } + // Dummy uses a non-standard dist func signature and is not installed in an index. + DistanceDispatch getDistanceDispatch(DistanceMode mode) const override { return {}; } }; } // namespace dummyCalcultor @@ -74,6 +92,41 @@ TEST(IndexCalculatorTest, TestIndexCalculator) { ASSERT_EQ(asymmetric_distance_calculator.calcDistanceForQuery(&v2, &v1, 0), -10); } +TEST(IndexCalculatorTest, CommonCalculatorExportsStatelessDispatches) { + auto allocator = VecSimAllocator::newVecsimAllocator(); + float candidate = 20.0f; + float query = 10.0f; + constexpr size_t dim = 3; + + DistanceCalculatorCommon symmetric_calculator(allocator, dummyCalcultor::commonDistFunc); + auto stored_dispatch = symmetric_calculator.getDistanceDispatch(DistanceMode::StoredToStored); + auto query_dispatch = symmetric_calculator.getDistanceDispatch(DistanceMode::StoredToQuery); + ASSERT_TRUE(stored_dispatch.isValid()); + ASSERT_EQ(stored_dispatch.stateless_func, dummyCalcultor::commonDistFunc); + ASSERT_EQ(query_dispatch.stateless_func, dummyCalcultor::commonDistFunc); + ASSERT_EQ(query_dispatch(&candidate, &query, dim), 33.0f); + + DistanceCalculatorCommon asymmetric_calculator(allocator, dummyCalcultor::commonDistFunc, + dummyCalcultor::commonQueryDistFunc); + stored_dispatch = asymmetric_calculator.getDistanceDispatch(DistanceMode::StoredToStored); + query_dispatch = asymmetric_calculator.getDistanceDispatch(DistanceMode::StoredToQuery); + ASSERT_EQ(stored_dispatch.stateless_func, dummyCalcultor::commonDistFunc); + ASSERT_EQ(query_dispatch.stateless_func, dummyCalcultor::commonQueryDistFunc); + ASSERT_EQ(query_dispatch(&candidate, &query, dim), 13.0f); +} + +TEST(IndexCalculatorTest, StatefulDispatchUsesCachedContext) { + dummyCalcultor::StatefulDistanceContext context{.offset = 7.0f}; + auto dispatch = DistanceDispatch::stateful(&context, dummyCalcultor::statefulDistFunc); + float v1 = 20.0f; + float v2 = 10.0f; + + ASSERT_TRUE(dispatch.isValid()); + ASSERT_EQ(dispatch.stateless_func, nullptr); + ASSERT_EQ(dispatch.context, &context); + ASSERT_EQ(dispatch(&v1, &v2, 3), 40.0f); +} + class PreprocessorsTest : public ::testing::Test {}; namespace dummyPreprocessors {