diff --git a/.github/workflows/build-cpp-runtime-bindings.yml b/.github/workflows/build-cpp-runtime-bindings.yml new file mode 100644 index 00000000..4e9b4712 --- /dev/null +++ b/.github/workflows/build-cpp-runtime-bindings.yml @@ -0,0 +1,92 @@ +# Copyright 2025 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Build C++ Runtime Bindings +run-name: ${{ github.event.inputs.run_name || github.event.pull_request.title }} + +on: + workflow_dispatch: + inputs: + run_name: + description: "Custom workflow name" + required: false + submodule_url: + description: "Submodule url (e.g. https://github.com/intel/ScalableVectorSearch.git)" + required: false + submodule_sha: + description: "Submodule sha/branch" + required: false + pull_request: + +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' + cancel-in-progress: true + +jobs: + build-cpp-runtime-bindings: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v5 + + - name: Build Docker image + run: | + docker build -t svs-manylinux228:latest -f docker/x86_64/manylinux228/Dockerfile . + + - name: Build libraries in Docker container + run: | + docker run --rm \ + -v ${{ github.workspace }}:/workspace \ + -w /workspace \ + svs-manylinux228:latest \ + /bin/bash -c "chmod +x docker/x86_64/build-cpp-runtime-bindings.sh && ./docker/x86_64/build-cpp-runtime-bindings.sh" + + - name: Upload cpp runtime bindings artifacts + uses: actions/upload-artifact@v4 + with: + name: svs-cpp-runtime-bindings + path: svs-cpp-runtime-bindings.tar.gz + retention-days: 7 # Reduce retention due to size + + test: + needs: build-cpp-runtime-bindings + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v5 + + - name: Build Docker image + run: | + docker build -t svs-manylinux228:latest -f docker/x86_64/manylinux228/Dockerfile . + + # Need to download for a new job + - name: Download shared libraries + uses: actions/download-artifact@v4 + with: + name: svs-cpp-runtime-bindings + path: runtime_lib + + - name: List available artifacts + run: | + ls -la runtime_lib/ + + - name: Test in Docker container + run: | + docker run --rm \ + -v ${{ github.workspace }}:/workspace \ + -v ${{ github.workspace }}/runtime_lib:/runtime_lib \ + -w /workspace \ + svs-manylinux228:latest \ + /bin/bash -c "chmod +x docker/x86_64/test-cpp-runtime-bindings.sh && ./docker/x86_64/test-cpp-runtime-bindings.sh" diff --git a/.gitignore b/.gitignore index c7456765..e0c76cb9 100644 --- a/.gitignore +++ b/.gitignore @@ -19,5 +19,8 @@ __pycache__/ /bindings/python/_skbuild/ /bindings/python/dist/ +# CPP bindings build files +/bindings/cpp/build/ + # Example generated files. example_data_*/ diff --git a/bindings/cpp/CMakeLists.txt b/bindings/cpp/CMakeLists.txt new file mode 100644 index 00000000..197405b9 --- /dev/null +++ b/bindings/cpp/CMakeLists.txt @@ -0,0 +1,194 @@ +# Copyright 2025 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required(VERSION 3.21) +project(svs_runtime VERSION 0.0.10 LANGUAGES CXX) +set(TARGET_NAME svs_runtime) + +set(SVS_RUNTIME_HEADERS + include/svs/runtime/version.h + include/svs/runtime/api_defs.h + include/svs/runtime/training.h + include/svs/runtime/vamana_index.h + include/svs/runtime/dynamic_vamana_index.h + include/svs/runtime/flat_index.h +) + +set(SVS_RUNTIME_SOURCES + src/svs_runtime_utils.h + src/dynamic_vamana_index_impl.h + src/flat_index_impl.h + src/api_defs.cpp + src/training.cpp + src/vamana_index.cpp + src/dynamic_vamana_index.cpp + src/flat_index.cpp +) + +option(SVS_RUNTIME_ENABLE_LVQ_LEANVEC "Enable compilation of SVS runtime with LVQ and LeanVec support" ON) +if (SVS_RUNTIME_ENABLE_LVQ_LEANVEC) + message(STATUS "SVS runtime will be built with LVQ support") +else() + message(STATUS "SVS runtime will be built without LVQ or LeanVec support") +endif() + +add_library(${TARGET_NAME} SHARED + ${SVS_RUNTIME_HEADERS} + ${SVS_RUNTIME_SOURCES} +) + +target_include_directories(${TARGET_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include +) + +find_package(OpenMP REQUIRED) +target_link_libraries(${TARGET_NAME} PUBLIC OpenMP::OpenMP_CXX) + +target_compile_options(${TARGET_NAME} PRIVATE + -DSVS_ENABLE_OMP=1 + -fvisibility=hidden +) + +if(UNIX AND NOT APPLE) + # Don't export 3rd-party symbols from the lib + target_link_options(${TARGET_NAME} PRIVATE "SHELL:-Wl,--exclude-libs,ALL") +endif() + +target_compile_features(${TARGET_NAME} INTERFACE cxx_std_20) +set_target_properties(${TARGET_NAME} PROPERTIES PUBLIC_HEADER "${SVS_RUNTIME_HEADERS}") +set_target_properties(${TARGET_NAME} PROPERTIES CXX_STANDARD 20) +set_target_properties(${TARGET_NAME} PROPERTIES CXX_STANDARD_REQUIRED ON) +set_target_properties(${TARGET_NAME} PROPERTIES CXX_EXTENSIONS OFF) +set_target_properties(${TARGET_NAME} PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR} ) + +if(DEFINED SVS_LVQ_HEADER AND DEFINED SVS_LEANVEC_HEADER) + # expected that pre-defined headers are implementation headers + message(STATUS "Using pre-defined LVQ header: ${SVS_LVQ_HEADER}") + message(STATUS "Using pre-defined LeanVec header: ${SVS_LEANVEC_HEADER}") +else() + set(SVS_LVQ_HEADER "svs/extensions/vamana/lvq.h") + set(SVS_LEANVEC_HEADER "svs/extensions/vamana/leanvec.h") +endif() + +if ((SVS_RUNTIME_ENABLE_LVQ_LEANVEC)) + if(RUNTIME_BINDINGS_PRIVATE_SOURCE_BUILD) + message(STATUS "Building directly from private sources") + target_link_libraries(${TARGET_NAME} PRIVATE + svs::svs + ) + link_mkl_static(${TARGET_NAME}) + elseif(TARGET svs_static_library) + # Links to SVS static library built as part of the main SVS build + target_link_libraries(${TARGET_NAME} PRIVATE + svs_devel + svs_static_library + svs_compile_options + svs_x86_options_base + ) + elseif(TARGET svs::svs) + message(FATAL_ERROR + "Pre-built LVQ/LeanVec SVS library cannot be used in SVS main build. " + "Please build SVS Runtime using bindins/cpp directory as CMake source root." + ) + else() + # Links to LTO-enabled static library, requires GCC/G++ 11.2 + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "11.2" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS "11.3") + set(SVS_URL "https://github.com/intel/ScalableVectorSearch/releases/download/v1.0.0-dev/svs-shared-library-1.0.0-NIGHTLY-20251107-773.tar.gz" + CACHE STRING "URL to download SVS shared library") + else() + message(WARNING + "Pre-built LVQ/LeanVec SVS library requires GCC/G++ v.11.2 to apply LTO optimizations." + "Current compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}" + ) + set(SVS_URL "https://github.com/intel/ScalableVectorSearch/releases/download/v1.0.0-dev/svs-shared-library-1.0.0-NIGHTLY-20251017-faiss.tar.gz") + endif() + include(FetchContent) + FetchContent_Declare( + svs + URL ${SVS_URL} + ) + FetchContent_MakeAvailable(svs) + list(APPEND CMAKE_PREFIX_PATH "${svs_SOURCE_DIR}") + find_package(svs REQUIRED) + target_link_libraries(${TARGET_NAME} PRIVATE + svs::svs + svs::svs_compile_options + svs::svs_static_library + ) + endif() + target_compile_definitions(${TARGET_NAME} PRIVATE + PUBLIC "SVS_LVQ_HEADER=\"${SVS_LVQ_HEADER}\"" + PUBLIC "SVS_LEANVEC_HEADER=\"${SVS_LEANVEC_HEADER}\"" + ) +else() + # Include the SVS library directly if needed. + if (NOT TARGET svs::svs) + add_subdirectory("../.." "${CMAKE_CURRENT_BINARY_DIR}/svs") + endif() + target_link_libraries(${TARGET_NAME} PRIVATE + svs::svs + svs_compile_options + svs_x86_options_base + ) +endif() + +# installing +include(GNUInstallDirs) + +set(SVS_RUNTIME_EXPORT_NAME ${TARGET_NAME}) +set(VERSION_CONFIG "${CMAKE_CURRENT_BINARY_DIR}/${SVS_RUNTIME_EXPORT_NAME}ConfigVersion.cmake") +set(SVS_RUNTIME_CONFIG_INSTALL_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/svs_runtime) +set(SVS_RUNTIME_COMPONENT_NAME "Runtime") + +install(TARGETS ${TARGET_NAME} + EXPORT ${SVS_RUNTIME_EXPORT_NAME} + COMPONENT ${SVS_RUNTIME_COMPONENT_NAME} + LIBRARY DESTINATION lib + PUBLIC_HEADER DESTINATION include/svs/runtime + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +) + +install(DIRECTORY include/svs/runtime + COMPONENT ${SVS_RUNTIME_COMPONENT_NAME} + DESTINATION include/svs + FILES_MATCHING PATTERN "*.h" +) + +install(EXPORT ${SVS_RUNTIME_EXPORT_NAME} + COMPONENT ${SVS_RUNTIME_COMPONENT_NAME} + NAMESPACE svs:: + DESTINATION ${SVS_RUNTIME_CONFIG_INSTALL_DIR} +) + +include(CMakePackageConfigHelpers) +configure_package_config_file( + "${CMAKE_CURRENT_LIST_DIR}/runtimeConfig.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/${SVS_RUNTIME_EXPORT_NAME}Config.cmake" + INSTALL_DESTINATION "${SVS_RUNTIME_CONFIG_INSTALL_DIR}" +) + +# Don't make compatibility guarantees until we reach a compatibility milestone. +write_basic_package_version_file( + ${VERSION_CONFIG} + VERSION ${PROJECT_VERSION} + COMPATIBILITY ExactVersion +) + +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/${SVS_RUNTIME_EXPORT_NAME}Config.cmake" + "${VERSION_CONFIG}" + COMPONENT ${SVS_RUNTIME_COMPONENT_NAME} + DESTINATION "${SVS_RUNTIME_CONFIG_INSTALL_DIR}" +) + diff --git a/bindings/cpp/include/svs/runtime/api_defs.h b/bindings/cpp/include/svs/runtime/api_defs.h new file mode 100644 index 00000000..466615c3 --- /dev/null +++ b/bindings/cpp/include/svs/runtime/api_defs.h @@ -0,0 +1,111 @@ +/* + * Copyright 2025 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include + +#ifdef svs_runtime_EXPORTS +#define SVS_RUNTIME_API __attribute__((visibility("default"))) +#define SVS_RUNTIME_API_INTERFACE // reserved for future use +#else +#define SVS_RUNTIME_API +#define SVS_RUNTIME_API_INTERFACE // reserved for future use +#endif + +namespace svs { +namespace runtime { +namespace v0 { + +enum class MetricType { L2, INNER_PRODUCT }; + +enum class StorageKind { + FP32, + FP16, + SQI8, + LVQ4x0, + LVQ4x4, + LVQ4x8, + LeanVec4x4, + LeanVec4x8, + LeanVec8x8, +}; + +enum class ErrorCode { + SUCCESS = 0, + UNKNOWN_ERROR = 1, + INVALID_ARGUMENT = 2, + NOT_IMPLEMENTED = 3, + NOT_INITIALIZED = 4, + RUNTIME_ERROR = 5 +}; + +struct SVS_RUNTIME_API Status { + constexpr Status(ErrorCode c = ErrorCode::SUCCESS, const char* msg = nullptr) + : code(c) + , message_storage_(nullptr) { + if (msg != nullptr) { + store_message(msg); + } + } + + constexpr ~Status() noexcept { + if (message_storage_ != nullptr) { + destroy_message(); + } + } + + ErrorCode code = ErrorCode::SUCCESS; + const char* message() const { return message_storage_ ? message_storage_ : ""; }; + constexpr bool ok() const { return code == ErrorCode::SUCCESS; } + + private: + void store_message(const char* msg) noexcept; + void destroy_message() noexcept; + char* message_storage_ = nullptr; +}; + +constexpr Status Status_Ok{}; + +struct SVS_RUNTIME_API_INTERFACE IDFilter { + virtual bool is_member(size_t id) const = 0; + virtual ~IDFilter() = default; + + // Helper method to allow using IDFilter instances as callable objects + bool operator()(size_t id) const { return this->is_member(id); } +}; + +struct SearchResultsStorage { + std::span labels; // faiss::idx_t is int64_t + std::span distances; +}; + +struct SVS_RUNTIME_API_INTERFACE ResultsAllocator { + virtual SearchResultsStorage allocate(std::span result_counts) const = 0; + virtual ~ResultsAllocator() = default; + + // Helper method to allow using ResultsAllocator instances as callable objects + SearchResultsStorage operator()(std::span result_counts) const { + return this->allocate(result_counts); + } +}; + +} // namespace v0 +} // namespace runtime +} // namespace svs \ No newline at end of file diff --git a/bindings/cpp/include/svs/runtime/dynamic_vamana_index.h b/bindings/cpp/include/svs/runtime/dynamic_vamana_index.h new file mode 100644 index 00000000..d978347d --- /dev/null +++ b/bindings/cpp/include/svs/runtime/dynamic_vamana_index.h @@ -0,0 +1,86 @@ +/* + * Copyright 2025 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include + +#include +#include +#include + +namespace svs { +namespace runtime { +namespace v0 { + +// Abstract interface for Dynamic Vamana-based indexes. +struct SVS_RUNTIME_API DynamicVamanaIndex : public VamanaIndex { + virtual Status add(size_t n, const size_t* labels, const float* x) noexcept = 0; + virtual Status + remove_selected(size_t* num_removed, const IDFilter& selector) noexcept = 0; + virtual Status remove(size_t n, const size_t* labels) noexcept = 0; + + virtual Status reset() noexcept = 0; + + // Static constructors and destructors + static Status build( + DynamicVamanaIndex** index, + size_t dim, + MetricType metric, + StorageKind storage_kind, + const VamanaIndex::BuildParams& params, + const VamanaIndex::SearchParams& default_search_params = {10, 10, 0, 0} + ) noexcept; + + static Status destroy(DynamicVamanaIndex* index) noexcept; + + virtual Status save(std::ostream& out) const noexcept = 0; + static Status load( + DynamicVamanaIndex** index, + std::istream& in, + MetricType metric, + StorageKind storage_kind + ) noexcept; +}; + +struct SVS_RUNTIME_API DynamicVamanaIndexLeanVec : public DynamicVamanaIndex { + // Specialization to build LeanVec-based Vamana index with specified leanvec dims + static Status build( + DynamicVamanaIndex** index, + size_t dim, + MetricType metric, + StorageKind storage_kind, + size_t leanvec_dims, + const VamanaIndex::BuildParams& params, + const VamanaIndex::SearchParams& default_search_params = {10, 10, 0, 0} + ) noexcept; + + // Specialization to build LeanVec-based Vamana index with provided training data + static Status build( + DynamicVamanaIndex** index, + size_t dim, + MetricType metric, + StorageKind storage_kind, + const LeanVecTrainingData* training_data, + const VamanaIndex::BuildParams& params, + const VamanaIndex::SearchParams& default_search_params = {10, 10, 0, 0} + ) noexcept; +}; + +} // namespace v0 +} // namespace runtime +} // namespace svs diff --git a/bindings/cpp/include/svs/runtime/flat_index.h b/bindings/cpp/include/svs/runtime/flat_index.h new file mode 100644 index 00000000..7d98462c --- /dev/null +++ b/bindings/cpp/include/svs/runtime/flat_index.h @@ -0,0 +1,48 @@ +/* + * Copyright 2025 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include + +#include +#include +#include + +namespace svs { +namespace runtime { +namespace v0 { + +// Abstract interface for Flat indices. +struct SVS_RUNTIME_API FlatIndex { + // Static constructors and destructors + static Status build(FlatIndex** index, size_t dim, MetricType metric) noexcept; + static Status destroy(FlatIndex* index) noexcept; + virtual ~FlatIndex(); + + virtual Status search( + size_t n, const float* x, size_t k, float* distances, size_t* labels + ) const noexcept = 0; + + virtual Status add(size_t n, const float* x) noexcept = 0; + virtual Status reset() noexcept = 0; + + virtual Status save(std::ostream& out) const noexcept = 0; + static Status load(FlatIndex** index, std::istream& in, MetricType metric) noexcept; +}; + +} // namespace v0 +} // namespace runtime +} // namespace svs diff --git a/bindings/cpp/include/svs/runtime/training.h b/bindings/cpp/include/svs/runtime/training.h new file mode 100644 index 00000000..08b2e36d --- /dev/null +++ b/bindings/cpp/include/svs/runtime/training.h @@ -0,0 +1,46 @@ +/* + * Copyright 2025 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include + +#include +#include +#include + +namespace svs { +namespace runtime { +namespace v0 { + +struct SVS_RUNTIME_API LeanVecTrainingData { + virtual ~LeanVecTrainingData(); + static Status build( + LeanVecTrainingData** training_data, + size_t dim, + size_t n, + const float* x, + size_t leanvec_dims + ) noexcept; + + static Status destroy(LeanVecTrainingData* training_data) noexcept; + + virtual Status save(std::ostream& out) const noexcept = 0; + static Status load(LeanVecTrainingData** training_data, std::istream& in) noexcept; +}; + +} // namespace v0 +} // namespace runtime +} // namespace svs diff --git a/bindings/cpp/include/svs/runtime/vamana_index.h b/bindings/cpp/include/svs/runtime/vamana_index.h new file mode 100644 index 00000000..8c20a042 --- /dev/null +++ b/bindings/cpp/include/svs/runtime/vamana_index.h @@ -0,0 +1,69 @@ +/* + * Copyright 2025 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include + +#include + +namespace svs { +namespace runtime { +namespace v0 { + +// Abstract interface for Vamana-based indices. +// NOTE VamanaIndex is not implemented directly, only DynamicVamanaIndex is implemented. +struct SVS_RUNTIME_API VamanaIndex { + virtual ~VamanaIndex(); + + struct BuildParams { + size_t graph_max_degree; + size_t prune_to = 0; + float alpha = 0; + size_t construction_window_size = 40; + size_t max_candidate_pool_size = 200; + bool use_full_search_history = true; + }; + + struct SearchParams { + size_t search_window_size = 10; + size_t search_buffer_capacity = 10; + size_t prefetch_lookahead = 0; + size_t prefetch_step = 0; + }; + + virtual Status search( + size_t n, + const float* x, + size_t k, + float* distances, + size_t* labels, + const SearchParams* params = nullptr, + IDFilter* filter = nullptr + ) const noexcept = 0; + + virtual Status range_search( + size_t n, + const float* x, + float radius, + const ResultsAllocator& results, + const SearchParams* params = nullptr, + IDFilter* filter = nullptr + ) const noexcept = 0; +}; + +} // namespace v0 +} // namespace runtime +} // namespace svs diff --git a/bindings/cpp/include/svs/runtime/version.h b/bindings/cpp/include/svs/runtime/version.h new file mode 100644 index 00000000..b46558e5 --- /dev/null +++ b/bindings/cpp/include/svs/runtime/version.h @@ -0,0 +1,113 @@ +/* + * Copyright 2025 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +/// +/// @brief Version information and API versioning for SVS Runtime +/// +/// This header defines the SVS Runtime API versioning scheme similar to oneDAL: +/// 1. Versioned namespaces (e.g., v0, v1) for API stability +/// 2. Using declarations to bring current version to parent namespace +/// 3. Clean integration points for external libraries +/// +/// Usage: +/// - Users can access APIs via svs::runtime::FlatIndex (maps to current version) +/// - External integrators can use namespace aliases (e.g., namespace svs_api = +/// svs::runtime::v0) +/// - Specific versions can be accessed via svs::runtime::v0::FlatIndex +/// + +///// Version Numbers + +#ifndef SVS_RUNTIME_VERSION_MAJOR +/// Major version number - incremented for breaking API changes +/// When this changes, a new version namespace (e.g., v0 -> v1) is created +#define SVS_RUNTIME_VERSION_MAJOR 0 +#endif + +#ifndef SVS_RUNTIME_VERSION_MINOR +/// Minor version number - incremented for backward-compatible feature additions +#define SVS_RUNTIME_VERSION_MINOR 1 +#endif + +#ifndef SVS_RUNTIME_VERSION_PATCH +/// Patch version number - incremented for backward-compatible bug fixes +#define SVS_RUNTIME_VERSION_PATCH 0 +#endif + +#ifndef SVS_RUNTIME_VERSION_STRING +/// Complete version string +#define SVS_RUNTIME_VERSION_STRING "0.1.0" +#endif + +#ifndef SVS_RUNTIME_API_VERSION +/// Default to current major version if not specified by client +#define SVS_RUNTIME_API_VERSION SVS_RUNTIME_VERSION_MAJOR +#endif + +#if (SVS_RUNTIME_API_VERSION == 0) +/// Use v0 API +/// Current API version namespace +#define SVS_RUNTIME_CURRENT_API_NAMESPACE v0 +namespace svs { +namespace runtime { +/// Current API version namespace (v0) +/// All public runtime APIs live here and are accessible as svs::runtime::FunctionName +/// due to inline namespace +inline namespace v0 { +// Public runtime APIs will be defined in their respective headers +// IMPORTANT: include this header before other runtime headers to ensure proper versioning +} +} // namespace runtime +} // namespace svs +#else +#error "Unsupported SVS Runtime major version" +#endif + +///// Integration Support + +/// Helper macro to create namespace aliases for external integrators +/// Example: SVS_RUNTIME_CREATE_API_ALIAS(svs_runtime_api, v0) +/// creates: namespace svs_runtime_api = svs::runtime::v0; +#define SVS_RUNTIME_CREATE_API_ALIAS(alias_name, version_ns) \ + namespace alias_name = svs::runtime::version_ns + +/// +/// @brief Version information structure for runtime queries +/// +namespace svs::runtime::v0 { + +struct VersionInfo { + static constexpr int major = SVS_RUNTIME_VERSION_MAJOR; + static constexpr int minor = SVS_RUNTIME_VERSION_MINOR; + static constexpr int patch = SVS_RUNTIME_VERSION_PATCH; + static constexpr const char* version_string = SVS_RUNTIME_VERSION_STRING; + static constexpr const char* api_namespace = "v0"; + + /// Get the complete version as a string + static const char* get_version() { return version_string; } + + /// Get the API namespace identifier + static const char* get_api_namespace() { return api_namespace; } + + /// Check if this version is compatible with a requested major version + static bool is_compatible_with_major(int requested_major) { + return major == requested_major; + } +}; + +} // namespace svs::runtime::v0 diff --git a/bindings/cpp/runtimeConfig.cmake.in b/bindings/cpp/runtimeConfig.cmake.in new file mode 100644 index 00000000..5c869b85 --- /dev/null +++ b/bindings/cpp/runtimeConfig.cmake.in @@ -0,0 +1,18 @@ +# Copyright 2025 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@PACKAGE_INIT@ + +include("${CMAKE_CURRENT_LIST_DIR}/svs_runtime.cmake") +check_required_components(svs_runtime) diff --git a/bindings/cpp/src/api_defs.cpp b/bindings/cpp/src/api_defs.cpp new file mode 100644 index 00000000..110bca85 --- /dev/null +++ b/bindings/cpp/src/api_defs.cpp @@ -0,0 +1,46 @@ +/* + * Copyright 2025 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +namespace svs { +namespace runtime { +void Status::store_message(const char* msg) noexcept { + assert(msg != nullptr); + try { + auto len = std::strlen(msg); + message_storage_ = new char[len + 1]; + std::strcpy(message_storage_, msg); + } catch (...) { + // In case of any error, leave message_storage_ as nullptr + if (message_storage_) { + delete[] message_storage_; + message_storage_ = nullptr; + } + return; + } +} + +void Status::destroy_message() noexcept { + assert(message_storage_ != nullptr); + delete[] message_storage_; + message_storage_ = nullptr; +} +} // namespace runtime +} // namespace svs \ No newline at end of file diff --git a/bindings/cpp/src/dynamic_vamana_index.cpp b/bindings/cpp/src/dynamic_vamana_index.cpp new file mode 100644 index 00000000..b02e03b0 --- /dev/null +++ b/bindings/cpp/src/dynamic_vamana_index.cpp @@ -0,0 +1,203 @@ +/* + * Copyright 2025 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "dynamic_vamana_index_impl.h" +#include "svs_runtime_utils.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include SVS_LVQ_HEADER +#include SVS_LEANVEC_HEADER + +namespace svs { +namespace runtime { + +namespace { +template +struct DynamicVamanaIndexManagerBase : public DynamicVamanaIndex { + std::unique_ptr impl_; + + DynamicVamanaIndexManagerBase(std::unique_ptr impl) + : impl_{std::move(impl)} { + assert(impl_ != nullptr); + } + + DynamicVamanaIndexManagerBase(const DynamicVamanaIndexManagerBase&) = delete; + DynamicVamanaIndexManagerBase& operator=(const DynamicVamanaIndexManagerBase&) = delete; + DynamicVamanaIndexManagerBase(DynamicVamanaIndexManagerBase&&) = default; + DynamicVamanaIndexManagerBase& operator=(DynamicVamanaIndexManagerBase&&) = default; + ~DynamicVamanaIndexManagerBase() override = default; + + Status add(size_t n, const size_t* labels, const float* x) noexcept override { + return runtime_error_wrapper([&] { + svs::data::ConstSimpleDataView data{x, n, impl_->dimensions()}; + std::span lbls(labels, n); + impl_->add(data, lbls); + }); + } + + Status + remove_selected(size_t* num_removed, const IDFilter& selector) noexcept override { + return runtime_error_wrapper([&] { + *num_removed = impl_->remove_selected(selector); + }); + } + + Status remove(size_t n, const size_t* labels) noexcept override { + return runtime_error_wrapper([&] { + std::span lbls(labels, n); + impl_->remove(lbls); + }); + } + + Status search( + size_t n, + const float* x, + size_t k, + float* distances, + size_t* labels, + const SearchParams* params = nullptr, + IDFilter* filter = nullptr + ) const noexcept override { + return runtime_error_wrapper([&] { + // TODO wrap arguments into proper data structures in DynamicVamanaIndexImpl and + // here + impl_->search(n, x, k, distances, labels, params, filter); + }); + } + + Status range_search( + size_t n, + const float* x, + float radius, + const ResultsAllocator& results, + const SearchParams* params = nullptr, + IDFilter* filter = nullptr + ) const noexcept override { + return runtime_error_wrapper([&] { + // TODO wrap arguments into proper data structures in DynamicVamanaIndexImpl and + // here + impl_->range_search(n, x, radius, results, params, filter); + }); + } + + Status reset() noexcept override { + return runtime_error_wrapper([&] { impl_->reset(); }); + } + + Status save(std::ostream& out) const noexcept override { + return runtime_error_wrapper([&] { impl_->save(out); }); + } +}; + +using DynamicVamanaIndexManager = DynamicVamanaIndexManagerBase; +using DynamicVamanaIndexLeanVecImplManager = + DynamicVamanaIndexManagerBase; + +} // namespace + +// DynamicVamanaIndex interface implementation +Status DynamicVamanaIndex::build( + DynamicVamanaIndex** index, + size_t dim, + MetricType metric, + StorageKind storage_kind, + const DynamicVamanaIndex::BuildParams& params, + const DynamicVamanaIndex::SearchParams& default_search_params +) noexcept { + *index = nullptr; + return runtime_error_wrapper([&] { + auto impl = std::make_unique( + dim, metric, storage_kind, params, default_search_params + ); + *index = new DynamicVamanaIndexManager{std::move(impl)}; + }); +} + +Status DynamicVamanaIndex::destroy(DynamicVamanaIndex* index) noexcept { + return runtime_error_wrapper([&] { delete index; }); +} + +Status DynamicVamanaIndex::load( + DynamicVamanaIndex** index, + std::istream& in, + MetricType metric, + StorageKind storage_kind +) noexcept { + *index = nullptr; + return runtime_error_wrapper([&] { + std::unique_ptr impl{ + DynamicVamanaIndexImpl::load(in, metric, storage_kind)}; + *index = new DynamicVamanaIndexManager{std::move(impl)}; + }); +} + +// Specialization to build LeanVec-based Vamana index with specified leanvec dims +Status DynamicVamanaIndexLeanVec::build( + DynamicVamanaIndex** index, + size_t dim, + MetricType metric, + StorageKind storage_kind, + size_t leanvec_dims, + const DynamicVamanaIndex::BuildParams& params, + const DynamicVamanaIndex::SearchParams& default_search_params +) noexcept { + *index = nullptr; + return runtime_error_wrapper([&] { + auto impl = std::make_unique( + dim, metric, storage_kind, leanvec_dims, params, default_search_params + ); + *index = new DynamicVamanaIndexLeanVecImplManager{std::move(impl)}; + }); +} + +// Specialization to build LeanVec-based Vamana index with provided training data +Status DynamicVamanaIndexLeanVec::build( + DynamicVamanaIndex** index, + size_t dim, + MetricType metric, + StorageKind storage_kind, + const LeanVecTrainingData* training_data, + const DynamicVamanaIndex::BuildParams& params, + const DynamicVamanaIndex::SearchParams& default_search_params +) noexcept { + *index = nullptr; + return runtime_error_wrapper([&] { + auto training_data_impl = + static_cast(training_data)->impl_; + auto impl = std::make_unique( + dim, metric, storage_kind, training_data_impl, params, default_search_params + ); + *index = new DynamicVamanaIndexLeanVecImplManager{std::move(impl)}; + }); +} + +} // namespace runtime +} // namespace svs diff --git a/bindings/cpp/src/dynamic_vamana_index_impl.h b/bindings/cpp/src/dynamic_vamana_index_impl.h new file mode 100644 index 00000000..01de5c22 --- /dev/null +++ b/bindings/cpp/src/dynamic_vamana_index_impl.h @@ -0,0 +1,592 @@ +/* + * Copyright 2025 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "svs_runtime_utils.h" +#include "training_impl.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include SVS_LVQ_HEADER +#include SVS_LEANVEC_HEADER + +namespace svs { +namespace runtime { + +// Vamana index implementation +class DynamicVamanaIndexImpl { + public: + DynamicVamanaIndexImpl( + size_t dim, + MetricType metric, + StorageKind storage_kind, + const VamanaIndex::BuildParams& params, + const VamanaIndex::SearchParams& default_search_params + ) + : dim_{dim} + , metric_type_{metric} + , storage_kind_{storage_kind} + , build_params_{params} + , default_search_params_{default_search_params} { + if (build_params_.prune_to == 0) { + build_params_.prune_to = build_params_.graph_max_degree < 4 + ? build_params_.graph_max_degree + : build_params_.graph_max_degree - 4; + } + if (build_params_.alpha == 0) { + build_params_.alpha = metric == MetricType::L2 ? 1.2f : 0.95f; + } + } + + size_t size() const { return impl_ ? impl_->size() : 0; } + + size_t dimensions() const { return dim_; } + + MetricType metric_type() const { return metric_type_; } + + StorageKind get_storage_kind() const { return storage_kind_; } + + void add(data::ConstSimpleDataView data, std::span labels) { + if (!impl_) { + return init_impl(data, labels); + } + + impl_->add_points(data, labels); + } + + void search( + size_t n, + const float* x, + size_t k, + float* distances, + size_t* labels, + const VamanaIndex::SearchParams* params = nullptr, + IDFilter* filter = nullptr + ) const { + if (!impl_) { + for (size_t i = 0; i < n; ++i) { + distances[i] = std::numeric_limits::infinity(); + labels[i] = -1; + } + throw StatusException{ErrorCode::NOT_INITIALIZED, "Index not initialized"}; + } + + if (k == 0) { + throw StatusException{ErrorCode::INVALID_ARGUMENT, "k must be greater than 0"}; + } + + auto sp = make_search_parameters(params); + + // Simple search + if (filter == nullptr) { + auto queries = svs::data::ConstSimpleDataView(x, n, dim_); + + // TODO: faiss use int64_t as label whereas SVS uses size_t? + auto results = svs::QueryResultView{ + svs::MatrixView{ + svs::make_dims(n, k), static_cast(static_cast(labels))}, + svs::MatrixView{svs::make_dims(n, k), distances}}; + impl_->search(results, queries, sp); + return; + } + + // Selective search with IDSelector + auto old_sp = impl_->get_search_parameters(); + impl_->set_search_parameters(sp); + + auto search_closure = [&](const auto& range, uint64_t SVS_UNUSED(tid)) { + for (auto i : range) { + // For every query + auto query = std::span(x + i * dim_, dim_); + auto curr_distances = std::span(distances + i * k, k); + auto curr_labels = std::span(labels + i * k, k); + + auto iterator = impl_->batch_iterator(query); + size_t found = 0; + do { + iterator.next(k); + for (auto& neighbor : iterator.results()) { + if (filter->is_member(neighbor.id())) { + curr_distances[found] = neighbor.distance(); + curr_labels[found] = neighbor.id(); + found++; + if (found == k) { + break; + } + } + } + } while (found < k && !iterator.done()); + // Pad with -1s + for (; found < k; ++found) { + curr_distances[found] = -1; + curr_labels[found] = -1; + } + } + }; + + auto threadpool = default_threadpool(); + + svs::threads::parallel_for( + threadpool, svs::threads::StaticPartition{n}, search_closure + ); + + impl_->set_search_parameters(old_sp); + } + + void range_search( + size_t n, + const float* x, + float radius, + const ResultsAllocator& results, + const VamanaIndex::SearchParams* params = nullptr, + IDFilter* filter = nullptr + ) const { + if (!impl_) { + throw StatusException{ErrorCode::NOT_INITIALIZED, "Index not initialized"}; + } + if (radius <= 0) { + throw StatusException{ + ErrorCode::INVALID_ARGUMENT, "radius must be greater than 0"}; + } + + auto sp = make_search_parameters(params); + auto old_sp = impl_->get_search_parameters(); + impl_->set_search_parameters(sp); + + // Using ResultHandler makes no sense due to it's complexity, overhead and + // missed features; e.g. add_result() does not indicate whether result added + // or not - we have to manually manage threshold comparison and id + // selection. + + // Prepare output buffers + std::vector>> all_results(n); + // Reserve space for allocation to avoid multiple reallocations + // Use search_buffer_capacity as a heuristic + const auto result_capacity = sp.buffer_config_.get_total_capacity(); + for (auto& res : all_results) { + res.reserve(result_capacity); + } + + svs::DistanceDispatcher distance_dispatcher(to_svs_distance(metric_type_)); + + std::function compare = distance_dispatcher([](auto&& dist) { + return std::function{svs::distance::comparator(dist)}; + }); + + std::function select = [](size_t) { return true; }; + if (filter != nullptr) { + select = [&](size_t id) { return filter->is_member(id); }; + } + + // Set iterator batch size to search window size + auto batch_size = sp.buffer_config_.get_search_window_size(); + + auto range_search_closure = [&](const auto& range, uint64_t SVS_UNUSED(tid)) { + for (auto i : range) { + // For every query + auto query = std::span(x + i * dim_, dim_); + + auto iterator = impl_->batch_iterator(query); + bool in_range = true; + + do { + iterator.next(batch_size); + for (auto& neighbor : iterator.results()) { + // SVS comparator functor returns true if the first distance + // is 'closer' than the second one + in_range = compare(neighbor.distance(), radius); + if (in_range) { + // Selective search with IDSelector + if (select(neighbor.id())) { + all_results[i].push_back(neighbor); + } + } else { + // Since iterator.results() are ordered by distance, we + // can stop processing + break; + } + } + } while (in_range && !iterator.done()); + } + }; + + auto threadpool = default_threadpool(); + + svs::threads::parallel_for( + threadpool, svs::threads::StaticPartition{n}, range_search_closure + ); + + // Allocate output + std::vector result_counts(n); + std::transform( + all_results.begin(), + all_results.end(), + result_counts.begin(), + [](const auto& res) { return res.size(); } + ); + auto results_storage = results(result_counts); + + // Fill in results + for (size_t q = 0, ofs = 0; q < n; ++q) { + for (const auto& [id, distance] : all_results[q]) { + results_storage.labels[ofs] = id; + results_storage.distances[ofs] = distance; + ofs++; + } + } + + impl_->set_search_parameters(old_sp); + } + + size_t remove(std::span labels) { + if (!impl_) { + throw StatusException{ErrorCode::NOT_INITIALIZED, "Index not initialized"}; + } + + // SVS deletion is a soft deletion, meaning the corresponding vectors are + // marked as deleted but still present in both the dataset and the graph, + // and will be navigated through during search. + // Actual cleanup happens once a large enough number of soft deleted vectors + // are collected. + impl_->delete_points(labels); + ntotal_soft_deleted += labels.size(); + + auto ntotal = impl_->size(); + const float cleanup_threshold = .5f; + if (ntotal == 0 || (float)ntotal_soft_deleted / ntotal > cleanup_threshold) { + impl_->consolidate(); + impl_->compact(); + ntotal_soft_deleted = 0; + } + return labels.size(); + } + + size_t remove_selected(const IDFilter& selector) { + if (!impl_) { + throw StatusException{ErrorCode::NOT_INITIALIZED, "Index not initialized"}; + } + + auto ids = impl_->all_ids(); + std::vector ids_to_delete; + std::copy_if( + ids.begin(), + ids.end(), + std::back_inserter(ids_to_delete), + [&](size_t id) { return selector(id); } + ); + + return remove(ids_to_delete); + } + + void reset() { + impl_.reset(); + ntotal_soft_deleted = 0; + } + + void save(std::ostream& out) const { + if (!impl_) { + throw StatusException{ + ErrorCode::NOT_INITIALIZED, "Cannot serialize: SVS index not initialized."}; + } + + impl_->save(out); + } + + protected: + // Utility functions + svs::index::vamana::VamanaBuildParameters vamana_build_parameters() const { + return svs::index::vamana::VamanaBuildParameters{ + build_params_.alpha, + build_params_.graph_max_degree, + build_params_.construction_window_size, + build_params_.max_candidate_pool_size, + build_params_.prune_to, + build_params_.use_full_search_history}; + } + + svs::index::vamana::VamanaSearchParameters + make_search_parameters(const VamanaIndex::SearchParams* params) const { + if (!impl_) { + throw StatusException{ErrorCode::NOT_INITIALIZED, "Index not initialized"}; + } + + auto sp = impl_->get_search_parameters(); + + auto search_window_size = default_search_params_.search_window_size; + auto search_buffer_capacity = default_search_params_.search_buffer_capacity; + if (default_search_params_.prefetch_lookahead > 0) { + sp = sp.prefetch_lookahead(default_search_params_.prefetch_lookahead); + } + if (default_search_params_.prefetch_step > 0) { + sp = sp.prefetch_step(default_search_params_.prefetch_step); + } + + if (params != nullptr) { + if (params->search_window_size > 0) + search_window_size = params->search_window_size; + if (params->search_buffer_capacity > 0) + search_buffer_capacity = params->search_buffer_capacity; + if (params->prefetch_lookahead > 0) { + sp = sp.prefetch_lookahead(params->prefetch_lookahead); + } + if (params->prefetch_step > 0) { + sp = sp.prefetch_step(params->prefetch_step); + } + } + + return impl_->get_search_parameters().buffer_config( + {search_window_size, search_buffer_capacity} + ); + } + + template + static svs::DynamicVamana* build_impl( + Tag&& tag, + MetricType metric, + const index::vamana::VamanaBuildParameters& parameters, + const svs::data::ConstSimpleDataView& data, + std::span labels, + StorageArgs&&... storage_args + ) { + auto threadpool = default_threadpool(); + + auto storage = make_storage( + std::forward(tag), + data, + threadpool, + std::forward(storage_args)... + ); + + svs::DistanceDispatcher distance_dispatcher(to_svs_distance(metric)); + return distance_dispatcher([&](auto&& distance) { + return new svs::DynamicVamana(svs::DynamicVamana::build( + parameters, + std::move(storage), + std::move(labels), + std::forward(distance), + std::move(threadpool) + )); + }); + } + + virtual void + init_impl(data::ConstSimpleDataView data, std::span labels) { + impl_.reset(storage::dispatch_storage_kind( + get_storage_kind(), + [this]( + auto&& tag, + data::ConstSimpleDataView data, + std::span labels + ) { + using Tag = std::decay_t; + return build_impl( + std::forward(tag), + this->metric_type_, + this->vamana_build_parameters(), + data, + labels + ); + }, + data, + labels + )); + } + + // Constructor used during loading + DynamicVamanaIndexImpl( + std::unique_ptr&& impl, + MetricType metric, + StorageKind storage_kind + ) + : impl_{std::move(impl)} { + dim_ = impl_->dimensions(); + const auto& buffer_config = impl_->get_search_parameters().buffer_config_; + default_search_params_ = { + buffer_config.get_search_window_size(), buffer_config.get_total_capacity()}; + metric_type_ = metric; + storage_kind_ = storage_kind; + build_params_ = { + impl_->get_graph_max_degree(), + impl_->get_prune_to(), + impl_->get_alpha(), + impl_->get_construction_window_size(), + impl_->get_max_candidates(), + impl_->get_full_search_history()}; + } + + template + static svs::DynamicVamana* + load_impl_t(Tag&& SVS_UNUSED(tag), std::istream& stream, MetricType metric) { + auto threadpool = default_threadpool(); + + svs::DistanceDispatcher distance_dispatcher(to_svs_distance(metric)); + return distance_dispatcher([&](auto&& distance) { + return new svs::DynamicVamana( + svs::DynamicVamana::assemble>( + stream, + std::forward(distance), + std::move(threadpool) + ) + ); + }); + } + + public: + static DynamicVamanaIndexImpl* + load(std::istream& stream, MetricType metric, StorageKind storage_kind) { + return storage::dispatch_storage_kind( + storage_kind, + [&](auto&& tag, std::istream& stream, MetricType metric) { + using Tag = std::decay_t; + std::unique_ptr impl{ + load_impl_t(std::forward(tag), stream, metric)}; + + return new DynamicVamanaIndexImpl(std::move(impl), metric, storage_kind); + }, + stream, + metric + ); + } + + // Data members + protected: + size_t dim_; + MetricType metric_type_; + StorageKind storage_kind_; + VamanaIndex::BuildParams build_params_; + VamanaIndex::SearchParams default_search_params_; + std::unique_ptr impl_; + size_t ntotal_soft_deleted{0}; +}; + +struct DynamicVamanaIndexLeanVecImpl : public DynamicVamanaIndexImpl { + DynamicVamanaIndexLeanVecImpl( + std::unique_ptr&& impl, + MetricType metric, + StorageKind storage_kind + ) + : DynamicVamanaIndexImpl{std::move(impl), metric, storage_kind} + , leanvec_dims_{0} + , leanvec_matrices_{std::nullopt} { + check_storage_kind(storage_kind); + } + + DynamicVamanaIndexLeanVecImpl( + size_t dim, + MetricType metric, + StorageKind storage_kind, + const LeanVecTrainingDataImpl& training_data, + const VamanaIndex::BuildParams& params, + const VamanaIndex::SearchParams& default_search_params = {10, 10} + ) + : DynamicVamanaIndexImpl{dim, metric, storage_kind, params, default_search_params} + , leanvec_dims_{training_data.get_leanvec_dims()} + , leanvec_matrices_{training_data.get_leanvec_matrices()} { + check_storage_kind(storage_kind); + } + + DynamicVamanaIndexLeanVecImpl( + size_t dim, + MetricType metric, + StorageKind storage_kind, + size_t leanvec_dims, + const VamanaIndex::BuildParams& params, + const VamanaIndex::SearchParams& default_search_params = {10, 10} + ) + : DynamicVamanaIndexImpl{dim, metric, storage_kind, params, default_search_params} + , leanvec_dims_{leanvec_dims} + , leanvec_matrices_{std::nullopt} { + check_storage_kind(storage_kind); + } + + template + static auto dispatch_leanvec_storage_kind(StorageKind kind, F&& f, Args&&... args) { + switch (kind) { + case StorageKind::LeanVec4x4: + return f(storage::LeanVec4x4Tag{}, std::forward(args)...); + case StorageKind::LeanVec4x8: + return f(storage::LeanVec4x8Tag{}, std::forward(args)...); + case StorageKind::LeanVec8x8: + return f(storage::LeanVec8x8Tag{}, std::forward(args)...); + default: + throw StatusException{ + ErrorCode::INVALID_ARGUMENT, "SVS LeanVec storage kind required"}; + } + } + + void init_impl(data::ConstSimpleDataView data, std::span labels) + override { + assert(storage::is_leanvec_storage(this->storage_kind_)); + impl_.reset(dispatch_leanvec_storage_kind( + this->storage_kind_, + [this]( + auto&& tag, + data::ConstSimpleDataView data, + std::span labels + ) { + using Tag = std::decay_t; + return DynamicVamanaIndexImpl::build_impl( + std::forward(tag), + this->metric_type_, + this->vamana_build_parameters(), + data, + labels, + this->leanvec_dims_, + this->leanvec_matrices_ + ); + }, + data, + labels + )); + } + + protected: + size_t leanvec_dims_; + std::optional leanvec_matrices_; + + StorageKind check_storage_kind(StorageKind kind) { + if (!storage::is_leanvec_storage(kind)) { + throw StatusException( + ErrorCode::INVALID_ARGUMENT, "SVS LeanVec storage kind required" + ); + } + if (!svs::detail::lvq_leanvec_enabled()) { + throw StatusException( + ErrorCode::NOT_IMPLEMENTED, + "LeanVec storage kind requested but not supported by CPU" + ); + } + return kind; + } +}; + +} // namespace runtime +} // namespace svs diff --git a/bindings/cpp/src/flat_index.cpp b/bindings/cpp/src/flat_index.cpp new file mode 100644 index 00000000..111f324c --- /dev/null +++ b/bindings/cpp/src/flat_index.cpp @@ -0,0 +1,109 @@ +/* + * Copyright 2025 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "flat_index_impl.h" +#include "svs_runtime_utils.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace svs { +namespace runtime { + +namespace { +struct FlatIndexManager : public FlatIndex { + std::unique_ptr impl_; + + FlatIndexManager(std::unique_ptr impl) + : impl_{std::move(impl)} { + assert(impl_ != nullptr); + } + + ~FlatIndexManager() override = default; + + Status add(size_t n, const float* x) noexcept override { + return runtime_error_wrapper([&] { + svs::data::ConstSimpleDataView data{x, n, impl_->dimensions()}; + impl_->add(data); + return Status_Ok; + }); + } + + Status search(size_t n, const float* x, size_t k, float* distances, size_t* labels) + const noexcept override { + return runtime_error_wrapper([&] { + // TODO wrap arguments into proper data structures in FlatIndexImpl and + // here + impl_->search(n, x, k, distances, labels); + return Status_Ok; + }); + } + + Status reset() noexcept override { + return runtime_error_wrapper([&] { + impl_->reset(); + return Status_Ok; + }); + } + + Status save(std::ostream& out) const noexcept override { + return runtime_error_wrapper([&] { + impl_->save(out); + return Status_Ok; + }); + } +}; +} // namespace + +// FlatIndex interface implementation +FlatIndex::~FlatIndex() = default; + +Status FlatIndex::build(FlatIndex** index, size_t dim, MetricType metric) noexcept { + *index = nullptr; + return runtime_error_wrapper([&] { + auto impl = std::make_unique(dim, metric); + *index = new FlatIndexManager{std::move(impl)}; + return Status_Ok; + }); +} + +Status FlatIndex::destroy(FlatIndex* index) noexcept { + return runtime_error_wrapper([&] { + delete index; + return Status_Ok; + }); +} + +Status FlatIndex::load(FlatIndex** index, std::istream& in, MetricType metric) noexcept { + *index = nullptr; + return runtime_error_wrapper([&] { + std::unique_ptr impl{FlatIndexImpl::load(in, metric)}; + *index = new FlatIndexManager{std::move(impl)}; + return Status_Ok; + }); +} +} // namespace runtime +} // namespace svs diff --git a/bindings/cpp/src/flat_index_impl.h b/bindings/cpp/src/flat_index_impl.h new file mode 100644 index 00000000..021a022a --- /dev/null +++ b/bindings/cpp/src/flat_index_impl.h @@ -0,0 +1,150 @@ +/* + * Copyright 2025 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "svs_runtime_utils.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace svs { +namespace runtime { + +// Vamana index implementation +class FlatIndexImpl { + public: + FlatIndexImpl(size_t dim, MetricType metric) + : dim_{dim} + , metric_type_{metric} {} + + size_t size() const { return impl_ ? impl_->size() : 0; } + + size_t dimensions() const { return dim_; } + + MetricType metric_type() const { return metric_type_; } + + void add(data::ConstSimpleDataView data) { + if (!impl_) { + return init_impl(data); + } + + throw StatusException{ + ErrorCode::NOT_IMPLEMENTED, + "Flat index does not support adding points after initialization"}; + } + + void search( + size_t n, + const float* x, + size_t k, + float* distances, + size_t* labels, + IDFilter* filter = nullptr + ) const { + if (!impl_) { + for (size_t i = 0; i < n; ++i) { + distances[i] = std::numeric_limits::infinity(); + labels[i] = -1; + } + throw StatusException{ErrorCode::NOT_INITIALIZED, "Index not initialized"}; + } + + if (k == 0) { + throw StatusException{ErrorCode::INVALID_ARGUMENT, "k must be greater than 0"}; + } + + // Simple search + if (filter == nullptr) { + auto queries = svs::data::ConstSimpleDataView(x, n, dim_); + + // TODO: faiss use int64_t as label whereas SVS uses size_t? + auto results = svs::QueryResultView{ + svs::MatrixView{ + svs::make_dims(n, k), static_cast(static_cast(labels))}, + svs::MatrixView{svs::make_dims(n, k), distances}}; + impl_->search(results, queries, {}); + } else { + throw StatusException{ + ErrorCode::NOT_IMPLEMENTED, "Filtered search not implemented yet"}; + } + } + + void reset() { impl_.reset(); } + + void save(std::ostream& out) const { + if (!impl_) { + throw StatusException{ + ErrorCode::NOT_INITIALIZED, "Cannot serialize: SVS index not initialized."}; + } + + impl_->save(out); + } + + static FlatIndexImpl* load(std::istream& in, MetricType metric) { + auto threadpool = default_threadpool(); + using storage_type = svs::runtime::storage::StorageType_t; + + svs::DistanceDispatcher distance_dispatcher(to_svs_distance(metric)); + return distance_dispatcher([&](auto&& distance) { + auto impl = new svs::Flat{svs::Flat::assemble( + in, std::forward(distance), std::move(threadpool) + )}; + + return new FlatIndexImpl(std::unique_ptr{impl}, metric); + }); + } + + protected: + // Constructor used during loading + FlatIndexImpl(std::unique_ptr&& impl, MetricType metric) + : dim_{impl->dimensions()} + , metric_type_{metric} + , impl_{std::move(impl)} {} + + void init_impl(data::ConstSimpleDataView data) { + auto threadpool = default_threadpool(); + + auto storage = svs::runtime::storage::make_storage( + svs::runtime::storage::FP32Tag{}, data, threadpool + ); + + svs::DistanceDispatcher distance_dispatcher(to_svs_distance(metric_type_)); + impl_.reset(distance_dispatcher([&](auto&& distance) { + return new svs::Flat(svs::Flat::assemble( + std::move(storage), + std::forward(distance), + std::move(threadpool) + )); + })); + } + + // Data members + size_t dim_; + MetricType metric_type_; + std::unique_ptr impl_; +}; +} // namespace runtime +} // namespace svs diff --git a/bindings/cpp/src/svs_runtime_utils.h b/bindings/cpp/src/svs_runtime_utils.h new file mode 100644 index 00000000..7929dd89 --- /dev/null +++ b/bindings/cpp/src/svs_runtime_utils.h @@ -0,0 +1,281 @@ +/* + * Copyright 2025 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +// TODO remove unused includes +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef SVS_LVQ_HEADER +#define SVS_LVQ_HEADER "svs/quantization/lvq/lvq.h" +#endif + +#ifndef SVS_LEANVEC_HEADER +#define SVS_LEANVEC_HEADER "svs/leanvec/leanvec.h" +#endif + +#include SVS_LVQ_HEADER +#include SVS_LEANVEC_HEADER + +namespace svs::runtime { + +inline svs::DistanceType to_svs_distance(MetricType metric) { + switch (metric) { + case MetricType::L2: + return svs::DistanceType::L2; + case MetricType::INNER_PRODUCT: + return svs::DistanceType::MIP; + } + throw ANNEXCEPTION("unreachable reached"); // Make GCC happy +} + +class StatusException : public svs::lib::ANNException { + public: + StatusException(const svs::runtime::ErrorCode& code, const std::string& message) + : svs::lib::ANNException(message) + , errcode_{code} {} + + svs::runtime::ErrorCode code() const { return errcode_; } + + private: + svs::runtime::ErrorCode errcode_; +}; + +template +inline auto runtime_error_wrapper(Callable&& func) noexcept -> Status { + try { + func(); + return Status_Ok; + } catch (const svs::runtime::StatusException& ex) { + return Status(ex.code(), ex.what()); + } catch (const std::invalid_argument& ex) { + return Status(ErrorCode::INVALID_ARGUMENT, ex.what()); + } catch (const std::runtime_error& ex) { + return Status(ErrorCode::RUNTIME_ERROR, ex.what()); + } catch (const std::exception& ex) { + return Status(ErrorCode::UNKNOWN_ERROR, ex.what()); + } catch (...) { + return Status(ErrorCode::UNKNOWN_ERROR, "An unknown error has occurred."); + } +} + +using LeanVecMatricesType = svs::leanvec::LeanVecMatrices; + +namespace storage { + +// Simplified trait checking +template inline constexpr bool is_simple_dataset = false; +template +inline constexpr bool is_simple_dataset> = + true; + +template +concept IsSimpleDataset = is_simple_dataset; + +// Consolidated storage kind checks using constexpr functions +inline constexpr bool is_lvq_storage(StorageKind kind) { + return kind == StorageKind::LVQ4x0 || kind == StorageKind::LVQ4x4 || + kind == StorageKind::LVQ4x8; +} + +inline constexpr bool is_leanvec_storage(StorageKind kind) { + return kind == StorageKind::LeanVec4x4 || kind == StorageKind::LeanVec4x8 || + kind == StorageKind::LeanVec8x8; +} + +// Storage kind processing +// Most kinds map to std::byte storage, but some have specific element types. +// Storage kind tag types for function argument deduction +template struct StorageKindTag { + static constexpr StorageKind value = K; +}; + +#define SVS_DEFINE_STORAGE_KIND_TAG(Kind) \ + using Kind##Tag = StorageKindTag + +SVS_DEFINE_STORAGE_KIND_TAG(FP32); +SVS_DEFINE_STORAGE_KIND_TAG(FP16); +SVS_DEFINE_STORAGE_KIND_TAG(SQI8); +SVS_DEFINE_STORAGE_KIND_TAG(LVQ4x0); +SVS_DEFINE_STORAGE_KIND_TAG(LVQ4x4); +SVS_DEFINE_STORAGE_KIND_TAG(LVQ4x8); +SVS_DEFINE_STORAGE_KIND_TAG(LeanVec4x4); +SVS_DEFINE_STORAGE_KIND_TAG(LeanVec4x8); +SVS_DEFINE_STORAGE_KIND_TAG(LeanVec8x8); + +#undef SVS_DEFINE_STORAGE_KIND_TAG + +template inline constexpr bool is_storage_tag = false; +template inline constexpr bool is_storage_tag> = true; + +template +concept StorageTag = is_storage_tag; + +// Storage types +template +using SimpleDatasetType = + svs::data::SimpleData>>; + +template +using SQDatasetType = svs::quantization::scalar:: + SQDataset>>; + +template +using LVQDatasetType = svs::quantization::lvq::LVQDataset< + Primary, + Residual, + svs::Dynamic, + svs::quantization::lvq::Turbo<16, 8>, + svs::data::Blocked>>; + +template +using LeanDatasetType = svs::leanvec::LeanDataset< + svs::leanvec::UsingLVQ, + svs::leanvec::UsingLVQ, + svs::Dynamic, + svs::Dynamic, + svs::data::Blocked>>; + +// Storage type mapping - use macro to reduce repetition +template struct StorageType; +template using StorageType_t = typename StorageType::type; + +#define DEFINE_STORAGE_TYPE(Kind, ...) \ + template <> struct StorageType { \ + using type = __VA_ARGS__; \ + } + +DEFINE_STORAGE_TYPE(FP32, SimpleDatasetType); +DEFINE_STORAGE_TYPE(FP16, SimpleDatasetType); +DEFINE_STORAGE_TYPE(SQI8, SQDatasetType); +DEFINE_STORAGE_TYPE(LVQ4x0, LVQDatasetType<4, 0>); +DEFINE_STORAGE_TYPE(LVQ4x4, LVQDatasetType<4, 4>); +DEFINE_STORAGE_TYPE(LVQ4x8, LVQDatasetType<4, 8>); +DEFINE_STORAGE_TYPE(LeanVec4x4, LeanDatasetType<4, 4>); +DEFINE_STORAGE_TYPE(LeanVec4x8, LeanDatasetType<4, 8>); +DEFINE_STORAGE_TYPE(LeanVec8x8, LeanDatasetType<8, 8>); + +#undef DEFINE_STORAGE_TYPE + +// Storage factory functions +template +StorageType make_storage(const svs::data::ConstSimpleDataView& data, Pool& pool) { + StorageType result(data.size(), data.dimensions()); + svs::threads::parallel_for( + pool, + svs::threads::StaticPartition(result.size()), + [&](auto is, auto SVS_UNUSED(tid)) { + for (auto i : is) { + result.set_datum(i, data.get_datum(i)); + } + } + ); + return result; +} + +template +SQStorageType make_storage(const svs::data::ConstSimpleDataView& data, Pool& pool) { + return SQStorageType::compress(data, pool); +} + +template < + svs::quantization::lvq::IsLVQDataset LVQStorageType, + svs::threads::ThreadPool Pool> +LVQStorageType make_storage(const svs::data::ConstSimpleDataView& data, Pool& pool) { + return LVQStorageType::compress(data, pool, 0); +} + +template +LeanVecStorageType make_storage( + const svs::data::ConstSimpleDataView& data, + Pool& pool, + size_t leanvec_d = 0, + std::optional matrices = std::nullopt +) { + if (leanvec_d == 0) { + leanvec_d = (data.dimensions() + 1) / 2; + } + return LeanVecStorageType::reduce( + data, std::move(matrices), pool, 0, svs::lib::MaybeStatic{leanvec_d} + ); +} + +template +auto make_storage(Tag&& SVS_UNUSED(tag), Args&&... args) { + return make_storage>(std::forward(args)...); +} + +inline StorageKind to_supported_storage_kind(StorageKind kind) { + if (svs::detail::lvq_leanvec_enabled()) { + return kind; + } else if (is_lvq_storage(kind) || is_leanvec_storage(kind)) { + return StorageKind::SQI8; + } + return kind; +} + +template +auto dispatch_storage_kind(StorageKind kind, F&& f, Args&&... args) { + switch (to_supported_storage_kind(kind)) { + case StorageKind::FP32: + return f(FP32Tag{}, std::forward(args)...); + case StorageKind::FP16: + return f(FP16Tag{}, std::forward(args)...); + case StorageKind::SQI8: + return f(SQI8Tag{}, std::forward(args)...); + case StorageKind::LVQ4x0: + return f(LVQ4x0Tag{}, std::forward(args)...); + case StorageKind::LVQ4x4: + return f(LVQ4x4Tag{}, std::forward(args)...); + case StorageKind::LVQ4x8: + return f(LVQ4x8Tag{}, std::forward(args)...); + case StorageKind::LeanVec4x4: + return f(LeanVec4x4Tag{}, std::forward(args)...); + case StorageKind::LeanVec4x8: + return f(LeanVec4x8Tag{}, std::forward(args)...); + case StorageKind::LeanVec8x8: + return f(LeanVec8x8Tag{}, std::forward(args)...); + default: + throw ANNEXCEPTION("not supported SVS storage kind"); + } +} +} // namespace storage + +inline svs::threads::ThreadPoolHandle default_threadpool() { + return svs::threads::ThreadPoolHandle(svs::threads::OMPThreadPool(omp_get_max_threads()) + ); +} +} // namespace svs::runtime diff --git a/bindings/cpp/src/training.cpp b/bindings/cpp/src/training.cpp new file mode 100644 index 00000000..aa974338 --- /dev/null +++ b/bindings/cpp/src/training.cpp @@ -0,0 +1,52 @@ +/* + * Copyright 2025 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "svs_runtime_utils.h" +#include "training_impl.h" + +namespace svs { +namespace runtime { + +LeanVecTrainingData::~LeanVecTrainingData() = default; + +Status LeanVecTrainingData::build( + LeanVecTrainingData** training_data, + size_t dim, + size_t n, + const float* x, + size_t leanvec_dims +) noexcept { + return runtime_error_wrapper([&] { + const auto data = svs::data::ConstSimpleDataView(x, n, dim); + *training_data = + new LeanVecTrainingDataManager{LeanVecTrainingDataImpl{data, leanvec_dims}}; + }); +} + +Status LeanVecTrainingData::destroy(LeanVecTrainingData* training_data) noexcept { + return runtime_error_wrapper([&] { delete training_data; }); +} + +Status +LeanVecTrainingData::load(LeanVecTrainingData** training_data, std::istream& in) noexcept { + return runtime_error_wrapper([&] { + *training_data = new LeanVecTrainingDataManager{LeanVecTrainingDataImpl::load(in)}; + }); +} +} // namespace runtime +} // namespace svs diff --git a/bindings/cpp/src/training_impl.h b/bindings/cpp/src/training_impl.h new file mode 100644 index 00000000..ddfdc370 --- /dev/null +++ b/bindings/cpp/src/training_impl.h @@ -0,0 +1,96 @@ +/* + * Copyright 2025 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "svs_runtime_utils.h" + +#include + +#include +#include + +#include +#include +#include +#include + +#include SVS_LVQ_HEADER +#include SVS_LEANVEC_HEADER + +namespace svs { +namespace runtime { + +struct LeanVecTrainingDataImpl { + LeanVecTrainingDataImpl(LeanVecMatricesType&& matrices) + : leanvec_dims_{matrices.view_data_matrix().dimensions()} + , leanvec_matrices_{std::move(matrices)} {} + + LeanVecTrainingDataImpl( + const svs::data::ConstSimpleDataView& data, size_t leanvec_dims + ) + : leanvec_dims_{leanvec_dims} + , leanvec_matrices_{compute_leanvec_matrices(data, leanvec_dims)} {} + + size_t get_leanvec_dims() const { return leanvec_dims_; } + const LeanVecMatricesType& get_leanvec_matrices() const { return leanvec_matrices_; } + + void save(std::ostream& out) const { + lib::UniqueTempDirectory tempdir{"svs_leanvec_matrix_save"}; + svs::lib::save_to_disk(leanvec_matrices_, tempdir); + lib::DirectoryArchiver::pack(tempdir, out); + } + + static LeanVecTrainingDataImpl load(std::istream& in) { + lib::UniqueTempDirectory tempdir{"svs_leanvec_matrix_load"}; + lib::DirectoryArchiver::unpack(in, tempdir); + return LeanVecTrainingDataImpl{ + svs::lib::load_from_disk(tempdir)}; + } + + private: + size_t leanvec_dims_; + LeanVecMatricesType leanvec_matrices_; + + static LeanVecMatricesType compute_leanvec_matrices( + const svs::data::ConstSimpleDataView& data, size_t leanvec_dims + ) { + auto threadpool = default_threadpool(); + + auto means = svs::utils::compute_medioid(data, threadpool); + auto matrix = svs::leanvec::compute_leanvec_matrix( + data, means, threadpool, svs::lib::MaybeStatic{leanvec_dims} + ); + // Create a copy of the matrix for the query matrix to avoid double free. + // LeanVecMatrices expects two separate matrix objects. + auto matrix_copy = matrix; + return LeanVecMatricesType{std::move(matrix), std::move(matrix_copy)}; + } +}; + +struct LeanVecTrainingDataManager : public svs::runtime::LeanVecTrainingData { + LeanVecTrainingDataManager(LeanVecTrainingDataImpl impl) + : impl_{std::move(impl)} {} + + Status save(std::ostream& out) const noexcept override { + return runtime_error_wrapper([&] { impl_.save(out); }); + } + + LeanVecTrainingDataImpl impl_; +}; + +} // namespace runtime +} // namespace svs diff --git a/bindings/cpp/src/vamana_index.cpp b/bindings/cpp/src/vamana_index.cpp new file mode 100644 index 00000000..8b31b260 --- /dev/null +++ b/bindings/cpp/src/vamana_index.cpp @@ -0,0 +1,26 @@ +/* + * Copyright 2025 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +namespace svs { +namespace runtime { + +// VamanaIndex interface implementation +VamanaIndex::~VamanaIndex() = default; + +} // namespace runtime +} // namespace svs \ No newline at end of file diff --git a/cmake/mkl.cmake b/cmake/mkl.cmake index 088cfbea..46194421 100644 --- a/cmake/mkl.cmake +++ b/cmake/mkl.cmake @@ -95,4 +95,21 @@ elseif(NOT SVS_EXPERIMENTAL_LINK_STATIC_MKL) ${SVS_LIB} INTERFACE $ ) target_link_libraries(${SVS_LIB} INTERFACE $) +else() # if static link and not custom mkl + message(STATUS "Statically linking ${target} to Intel(R) MKL") + function(link_mkl_static target) + target_link_libraries(${target} PRIVATE + -Wl,--start-group + ${MKL_ROOT}/lib/intel64/libmkl_intel_lp64.a + ${MKL_ROOT}/lib/intel64/libmkl_sequential.a + ${MKL_ROOT}/lib/intel64/libmkl_core.a + -Wl,--end-group -lpthread -lm -ldl + ) + if(UNIX AND NOT APPLE) + target_link_options(${target} PRIVATE "SHELL:-Wl,--exclude-libs,ALL") + endif() + target_include_directories( + ${target} PRIVATE $ + ) + endfunction() endif() diff --git a/docker/x86_64/build-cpp-runtime-bindings.sh b/docker/x86_64/build-cpp-runtime-bindings.sh new file mode 100644 index 00000000..529ceffe --- /dev/null +++ b/docker/x86_64/build-cpp-runtime-bindings.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Copyright 2025 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e # Exit on error + +# Source environment setup (for compiler and MKL) +source /etc/bashrc || true + +# Create build+install directories for cpp runtime bindings +rm -rf /workspace/bindings/cpp/build_cpp_bindings /workspace/install_cpp_bindings +mkdir -p /workspace/bindings/cpp/build_cpp_bindings /workspace/install_cpp_bindings + +# Build and install runtime bindings library +cd /workspace/bindings/cpp/build_cpp_bindings +CC=gcc CXX=g++ cmake .. -DCMAKE_INSTALL_PREFIX=/workspace/install_cpp_bindings -DCMAKE_INSTALL_LIBDIR=lib +cmake --build . -j +cmake --install . + +# Create tarball with symlink for compatibility +cd /workspace/install_cpp_bindings && \ +ln -s lib lib64 && \ +tar -czvf /workspace/svs-cpp-runtime-bindings.tar.gz . diff --git a/docker/x86_64/manylinux228/Dockerfile b/docker/x86_64/manylinux228/Dockerfile new file mode 100644 index 00000000..491cbf03 --- /dev/null +++ b/docker/x86_64/manylinux228/Dockerfile @@ -0,0 +1,34 @@ +# Copyright 2025 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM quay.io/pypa/manylinux_2_28_x86_64:latest + +# Install CMake3 +RUN yum install -y cmake3 \ + && (if [ -x /usr/local/bin/cmake ]; then rm /usr/local/bin/cmake; fi) \ + && ln -sf /usr/bin/cmake3 /usr/local/bin/cmake + +# Install gcc-11 +RUN yum install -y gcc-toolset-11 wget + +# Configure environment setup properly without recursion +RUN echo '# Configure gcc-11' > /etc/profile.d/01-gcc.sh && \ + echo 'source scl_source enable gcc-toolset-11' >> /etc/profile.d/01-gcc.sh && \ + chmod +x /etc/profile.d/01-gcc.sh + +# Download and install Miniforge +RUN wget https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh -O /tmp/miniforge.sh && \ + bash /tmp/miniforge.sh -b -p /opt/conda && \ + rm /tmp/miniforge.sh +ENV PATH="/opt/conda/bin:$PATH" diff --git a/docker/x86_64/test-cpp-runtime-bindings.sh b/docker/x86_64/test-cpp-runtime-bindings.sh new file mode 100644 index 00000000..f283af78 --- /dev/null +++ b/docker/x86_64/test-cpp-runtime-bindings.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# Copyright 2025 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e + +# Source environment setup (for compiler and MKL) +source /etc/bashrc || true + +# chmod +x docker/x86_64/list-dependencies.sh +# ./docker/x86_64/list-dependencies.sh + +# FAISS validation scope for now +# Create conda env matching https://github.com/facebookresearch/faiss/blob/main/.github/actions/build_cmake/action.yml +conda create -y -n svsenv python=3.11 +source /opt/conda/etc/profile.d/conda.sh +conda activate svsenv +conda config --set solver libmamba +conda install -y -c conda-forge cmake=3.30.4 make=4.2 swig=4.0 "numpy>=2.0,<3.0" scipy=1.16 pytest=7.4 gflags=2.2 +conda install -y -c conda-forge gxx_linux-64=14.2 sysroot_linux-64=2.17 +conda install -y mkl=2022.2.1 mkl-devel=2022.2.1 + +# TODO: point to root repo eventually +git clone -b svs-io https://github.com/ahuber21/faiss.git +cd faiss +sed -i "s|set(SVS_URL .*|set(SVS_URL \"file:///runtime_lib/svs-cpp-runtime-bindings${PLATFORM_NAME}.tar.gz\" CACHE STRING \"SVS URL\")|" faiss/CMakeLists.txt + +echo "================================================" +echo " Runnning validation of library against FAISS CI" +echo "------------------------------------------------" +echo " FAISS Build: " +mkdir build && cd build +# TODO: create conda env +cmake -DBUILD_TESTING=ON -DFAISS_ENABLE_SVS=ON -DFAISS_ENABLE_GPU=OFF .. +make -j swigfaiss faiss_test +echo "------------------------------------------------" +echo " FAISS C++ tests: " +./tests/faiss_test --gtest_filter=SVS.* +echo "------------------------------------------------" +echo " FAISS python bindings: " +cd faiss/python/ +python setup.py build +echo "------------------------------------------------" +echo " FAISS python tests: " +cd ../../../tests/ +PYTHONPATH=../build/faiss/python/build/lib/ OMP_NUM_THREADS=8 python -m unittest test_svs.py + +# TODO: C++ tests diff --git a/tools/clang-format.sh b/tools/clang-format.sh index e92dd4a7..7646ae5a 100755 --- a/tools/clang-format.sh +++ b/tools/clang-format.sh @@ -16,7 +16,7 @@ # Allow users to supply a custom path to `clang-format` CLANGFORMAT="${1:-clang-format}" -DIRECTORIES=( "bindings/python/src" "bindings/python/include" "include" "benchmark" "tests" "utils" "examples/cpp" ) +DIRECTORIES=( "bindings/python/src" "bindings/python/include" "bindings/cpp" "include" "benchmark" "tests" "utils" "examples/cpp" ) for i in "${DIRECTORIES[@]}" do