diff --git a/.github/actions/build_cmake/action.yml b/.github/actions/build_cmake/action.yml index 3eddc4920d..110c898cb6 100644 --- a/.github/actions/build_cmake/action.yml +++ b/.github/actions/build_cmake/action.yml @@ -16,6 +16,10 @@ inputs: description: 'Enable ROCm support.' required: false default: OFF + svs: + description: 'Enable SVS support.' + required: false + default: OFF runs: using: composite steps: @@ -130,6 +134,7 @@ runs: -DFAISS_ENABLE_CUVS=${{ inputs.cuvs }} \ -DFAISS_ENABLE_ROCM=${{ inputs.rocm }} \ -DFAISS_OPT_LEVEL=${{ inputs.opt_level }} \ + -DFAISS_ENABLE_SVS=${{ inputs.svs }} \ -DFAISS_ENABLE_C_API=ON \ -DPYTHON_EXECUTABLE=$CONDA/bin/python \ -DCMAKE_BUILD_TYPE=Release \ @@ -189,7 +194,7 @@ runs: if: always() uses: actions/upload-artifact@v4 with: - name: test-results-arch=${{ runner.arch }}-opt=${{ inputs.opt_level }}-gpu=${{ inputs.gpu }}-cuvs=${{ inputs.cuvs }}-rocm=${{ inputs.rocm }} + name: test-results-arch=${{ runner.arch }}-opt=${{ inputs.opt_level }}-gpu=${{ inputs.gpu }}-cuvs=${{ inputs.cuvs }}-rocm=${{ inputs.rocm }}-svs=${{ inputs.svs }} path: test-results - name: Check installed packages channel shell: bash diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml index be699ce780..240876f46d 100644 --- a/.github/workflows/build-pull-request.yml +++ b/.github/workflows/build-pull-request.yml @@ -144,3 +144,14 @@ jobs: fetch-tags: true - name: Build and Package (conda) uses: ./.github/actions/build_conda + linux-x86_64-svs: + name: Linux x86_64 w/ SVS (cmake) + needs: linux-x86_64-cmake + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build and Test (cmake) + uses: ./.github/actions/build_cmake + with: + svs: ON diff --git a/CMakeLists.txt b/CMakeLists.txt index f921302fed..527cf2e468 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,6 +72,9 @@ option(FAISS_ENABLE_PYTHON "Build Python extension." ON) option(FAISS_ENABLE_C_API "Build C API." OFF) option(FAISS_ENABLE_EXTRAS "Build extras like benchmarks and demos" ON) option(FAISS_USE_LTO "Enable Link-Time optimization" OFF) +option(FAISS_ENABLE_SVS "Enable SVS (Intel(R) Scalable Vector Search) integration." OFF) +set(FAISS_SVS_RUNTIME_VERSION "v0" CACHE STRING "Version of the SVS runtime API to use") +set_property(CACHE FAISS_SVS_RUNTIME_VERSION PROPERTY STRINGS "v0") if(FAISS_ENABLE_GPU) if(FAISS_ENABLE_ROCM) diff --git a/INSTALL.md b/INSTALL.md index d7cc637f8b..ee19960b55 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -114,6 +114,12 @@ conda install -c rapidsai -c conda-forge -c nvidia libcuvs=25.10 'cuda-version=1 ``` For more ways to install cuVS 25.10, refer to the [RAPIDS Installation Guide](https://docs.rapids.ai/install). +### Building with Intel(R) SVS + +[Intel(R) Scalable Vector Search (SVS)](https://github.com/intel/svs) is a library for high-performance vector search. Building Faiss with SVS enabled allows using SVS implementations of graph-based indices (e.g., Vamana). + +The SVS library will be automatically fetched and built by CMake if `FAISS_ENABLE_SVS` is set to `ON`. + ## Step 1: invoking CMake ``` shell @@ -137,6 +143,8 @@ Several options can be passed to CMake, among which: are `ON` and `OFF`), - `-DFAISS_ENABLE_C_API=ON` in order to enable building [C API](c_api/INSTALL.md) (possible values are `ON` and `OFF`), + - `-DFAISS_ENABLE_SVS=ON` in order to enable the Intel(R) Scalable Vector Search (SVS) integration (default is `OFF`, possible values are `ON` and `OFF`). + Note: This will download and build the SVS runtime library (`libsvs_runtime.so`). When installing the python package, this library will be copied into the package directory. For C++ usage, ensure this library is in your library path. - optimization-related options: - `-DCMAKE_BUILD_TYPE=Release` in order to enable generic compiler optimization options (enables `-O3` on gcc for instance), diff --git a/conda/faiss-gpu-cuvs/build-lib.sh b/conda/faiss-gpu-cuvs/build-lib.sh index 3fd46428dd..fb593f7dc8 100644 --- a/conda/faiss-gpu-cuvs/build-lib.sh +++ b/conda/faiss-gpu-cuvs/build-lib.sh @@ -25,3 +25,9 @@ make -C _build -j$(nproc) faiss faiss_avx2 faiss_avx512 faiss_c faiss_c_avx2 fai cmake --install _build --prefix $PREFIX cmake --install _build --prefix _libfaiss_stage/ + +# Copy SVS runtime if it exists +if [ -d "_build/_deps/svs-src/lib" ]; then + cp -P _build/_deps/svs-src/lib/libsvs_runtime.so* $PREFIX/lib/ || true + cp -P _build/_deps/svs-src/lib/libsvs_runtime.so* _libfaiss_stage/lib/ || true +fi diff --git a/conda/faiss-gpu/build-lib.sh b/conda/faiss-gpu/build-lib.sh index 71c77e3ca1..5f50b1b143 100755 --- a/conda/faiss-gpu/build-lib.sh +++ b/conda/faiss-gpu/build-lib.sh @@ -31,3 +31,9 @@ make -C _build -j$(nproc) faiss faiss_avx2 faiss_avx512 faiss_c faiss_c_avx2 fai cmake --install _build --prefix $PREFIX cmake --install _build --prefix _libfaiss_stage/ + +# Copy SVS runtime if it exists +if [ -d "_build/_deps/svs-src/lib" ]; then + cp -P _build/_deps/svs-src/lib/libsvs_runtime.so* $PREFIX/lib/ || true + cp -P _build/_deps/svs-src/lib/libsvs_runtime.so* _libfaiss_stage/lib/ || true +fi diff --git a/conda/faiss/build-lib.sh b/conda/faiss/build-lib.sh index 2db92e890d..03ee5003fa 100755 --- a/conda/faiss/build-lib.sh +++ b/conda/faiss/build-lib.sh @@ -23,3 +23,9 @@ make -C _build -j$(nproc) faiss faiss_avx2 faiss_avx512 faiss_c faiss_c_avx2 fai cmake --install _build --prefix $PREFIX cmake --install _build --prefix _libfaiss_stage/ + +# Copy SVS runtime if it exists +if [ -d "_build/_deps/svs-src/lib" ]; then + cp -P _build/_deps/svs-src/lib/libsvs_runtime.so* $PREFIX/lib/ || true + cp -P _build/_deps/svs-src/lib/libsvs_runtime.so* _libfaiss_stage/lib/ || true +fi diff --git a/faiss/CMakeLists.txt b/faiss/CMakeLists.txt index f0abd9e07d..cf2bae229d 100644 --- a/faiss/CMakeLists.txt +++ b/faiss/CMakeLists.txt @@ -107,6 +107,16 @@ set(FAISS_SRC utils/distances_fused/simdlib_based.cpp ) +if(FAISS_ENABLE_SVS) + list(APPEND FAISS_SRC + svs/IndexSVSFlat.cpp + svs/IndexSVSVamana.cpp + svs/IndexSVSVamanaLVQ.cpp + svs/IndexSVSVamanaLeanVec.cpp + impl/svs_io.cpp + ) +endif() + set(FAISS_HEADERS AutoTune.h Clustering.h @@ -256,6 +266,15 @@ set(FAISS_HEADERS utils/hamming_distance/avx512-inl.h ) +if(FAISS_ENABLE_SVS) + list(APPEND FAISS_HEADERS + svs/IndexSVSFlat.h + svs/IndexSVSVamana.h + svs/IndexSVSVamanaLVQ.h + svs/IndexSVSVamanaLeanVec.h + ) +endif() + if(NOT WIN32) list(APPEND FAISS_SRC invlists/OnDiskInvertedLists.cpp) list(APPEND FAISS_HEADERS invlists/OnDiskInvertedLists.h) @@ -335,6 +354,27 @@ if(NOT WIN32) endif() endif() +if(FAISS_ENABLE_SVS) + include(FetchContent) + set(SVS_URL "https://github.com/intel/ScalableVectorSearch/releases/download/v1.0.0-dev/svs-cpp-runtime-bindings-1.0.0-NIGHTLY-20251120-808-private.tar.gz") + FetchContent_Declare( + svs + URL "${SVS_URL}" + ) + FetchContent_MakeAvailable(svs) + list(APPEND CMAKE_PREFIX_PATH "${svs_SOURCE_DIR}") + find_package(svs_runtime REQUIRED) + #target_compile_options(svs::svs INTERFACE "-DSVS_ENABLE_OMP=1") + + target_link_libraries(faiss PUBLIC svs::svs_runtime) + target_link_libraries(faiss_avx2 PUBLIC svs::svs_runtime) + target_link_libraries(faiss_avx512 PUBLIC svs::svs_runtime) + target_link_libraries(faiss_avx512_spr PUBLIC svs::svs_runtime) + target_link_libraries(faiss_sve PUBLIC svs::svs_runtime) + + install(DIRECTORY ${svs_SOURCE_DIR}/lib/ DESTINATION ${CMAKE_INSTALL_LIBDIR} FILES_MATCHING PATTERN "libsvs_runtime.so*") +endif() + # Handle `#include `. target_include_directories(faiss PUBLIC $) @@ -364,6 +404,14 @@ if(WIN32) target_compile_definitions(faiss_sve PRIVATE FAISS_MAIN_LIB) endif() +if(FAISS_ENABLE_SVS) + target_compile_definitions(faiss PRIVATE FAISS_ENABLE_SVS FAISS_SVS_RUNTIME_VERSION=${FAISS_SVS_RUNTIME_VERSION}) + target_compile_definitions(faiss_avx2 PRIVATE FAISS_ENABLE_SVS FAISS_SVS_RUNTIME_VERSION=${FAISS_SVS_RUNTIME_VERSION}) + target_compile_definitions(faiss_avx512 PRIVATE FAISS_ENABLE_SVS FAISS_SVS_RUNTIME_VERSION=${FAISS_SVS_RUNTIME_VERSION}) + target_compile_definitions(faiss_avx512_spr PRIVATE FAISS_ENABLE_SVS FAISS_SVS_RUNTIME_VERSION=${FAISS_SVS_RUNTIME_VERSION}) + target_compile_definitions(faiss_sve PRIVATE FAISS_ENABLE_SVS FAISS_SVS_RUNTIME_VERSION=${FAISS_SVS_RUNTIME_VERSION}) +endif() + if(WIN32) set_target_properties(faiss PROPERTIES LINK_FLAGS "-Wl,--export-all-symbols") endif() diff --git a/faiss/impl/index_read.cpp b/faiss/impl/index_read.cpp index a4abe1330b..f92ac2588c 100644 --- a/faiss/impl/index_read.cpp +++ b/faiss/impl/index_read.cpp @@ -48,6 +48,13 @@ #include #include #include +#ifdef FAISS_ENABLE_SVS +#include +#include +#include +#include +#include +#endif #include #include #include @@ -1359,7 +1366,66 @@ Index* read_index(IOReader* f, int io_flags) { ivrq->code_size = ivrq->rabitq.code_size; read_InvertedLists(ivrq, f, io_flags); idx = ivrq; - } else if (h == fourcc("Iwrf")) { + } +#ifdef FAISS_ENABLE_SVS + else if ( + h == fourcc("ILVQ") || h == fourcc("ISVL") || h == fourcc("ISVD")) { + IndexSVSVamana* svs; + if (h == fourcc("ILVQ")) { + svs = new IndexSVSVamanaLVQ(); + } else if (h == fourcc("ISVL")) { + svs = new IndexSVSVamanaLeanVec(); + } else if (h == fourcc("ISVD")) { + svs = new IndexSVSVamana(); + } + + read_index_header(svs, f); + READ1(svs->graph_max_degree); + READ1(svs->alpha); + READ1(svs->search_window_size); + READ1(svs->search_buffer_capacity); + READ1(svs->construction_window_size); + READ1(svs->max_candidate_pool_size); + READ1(svs->prune_to); + READ1(svs->use_full_search_history); + READ1(svs->storage_kind); + if (h == fourcc("ISVL")) { + READ1(dynamic_cast(svs)->leanvec_d); + } + + bool initialized; + READ1(initialized); + if (initialized) { + faiss::svs_io::ReaderStreambuf rbuf(f); + std::istream is(&rbuf); + svs->deserialize_impl(is); + } + if (h == fourcc("ISVL")) { + bool trained; + READ1(trained); + if (trained) { + faiss::svs_io::ReaderStreambuf rbuf(f); + std::istream is(&rbuf); + dynamic_cast(svs) + ->deserialize_training_data(is); + } + } + idx = svs; + } else if (h == fourcc("ISVF")) { + IndexSVSFlat* svs = new IndexSVSFlat(); + read_index_header(svs, f); + + bool initialized; + READ1(initialized); + if (initialized) { + faiss::svs_io::ReaderStreambuf rbuf(f); + std::istream is(&rbuf); + svs->deserialize_impl(is); + idx = svs; + } + } +#endif // FAISS_ENABLE_SVS + else if (h == fourcc("Iwrf")) { IndexIVFRaBitQFastScan* ivrqfs = new IndexIVFRaBitQFastScan(); read_ivf_header(ivrqfs, f); read_RaBitQuantizer(&ivrqfs->rabitq, f, false); diff --git a/faiss/impl/index_write.cpp b/faiss/impl/index_write.cpp index e98d32d257..fec6d81685 100644 --- a/faiss/impl/index_write.cpp +++ b/faiss/impl/index_write.cpp @@ -46,6 +46,13 @@ #include #include #include +#ifdef FAISS_ENABLE_SVS +#include +#include +#include +#include +#include +#endif #include #include #include @@ -969,7 +976,79 @@ void write_index(const Index* idx, IOWriter* f, int io_flags) { WRITE1(ivrq->by_residual); WRITE1(ivrq->qb); write_InvertedLists(ivrq->invlists, f); + } +#ifdef FAISS_ENABLE_SVS + else if ( + const IndexSVSVamana* svs = + dynamic_cast(idx)) { + uint32_t h; + auto* lvq = dynamic_cast(svs); + auto* lean = dynamic_cast(svs); + if (lvq != nullptr) { + h = fourcc("ILVQ"); // LVQ + } else if (lean != nullptr) { + h = fourcc("ISVL"); // LeanVec + } else { + h = fourcc("ISVD"); // uncompressed + } + + WRITE1(h); + write_index_header(svs, f); + WRITE1(svs->graph_max_degree); + WRITE1(svs->alpha); + WRITE1(svs->search_window_size); + WRITE1(svs->search_buffer_capacity); + WRITE1(svs->construction_window_size); + WRITE1(svs->max_candidate_pool_size); + WRITE1(svs->prune_to); + WRITE1(svs->use_full_search_history); + WRITE1(svs->storage_kind); + + if (lean != nullptr) { + WRITE1(lean->leanvec_d); + } + + bool initialized = (svs->impl != nullptr); + WRITE1(initialized); + if (initialized) { + faiss::BufferedIOWriter bwr(f); + faiss::svs_io::WriterStreambuf wbuf(&bwr); + std::ostream os(&wbuf); + svs->serialize_impl(os); + os.flush(); + } + + if (lean != nullptr) { + // Store training data info + bool trained = (lean->training_data != nullptr); + WRITE1(trained); + if (trained) { + faiss::BufferedIOWriter bwr(f); + faiss::svs_io::WriterStreambuf wbuf(&bwr); + std::ostream os(&wbuf); + lean->serialize_training_data(os); + os.flush(); + } + } } else if ( + const IndexSVSFlat* svs = dynamic_cast(idx)) { + uint32_t h = fourcc("ISVF"); + WRITE1(h); + write_index_header(idx, f); + + bool initialized = (svs->impl != nullptr); + WRITE1(initialized); + if (initialized) { + // Wrap SVS I/O and stream to IOWriter + faiss::BufferedIOWriter bwr(f); + faiss::svs_io::WriterStreambuf wbuf(&bwr); + std::ostream os(&wbuf); + svs->serialize_impl(os); + os.flush(); + } + } +#endif // FAISS_ENABLE_SVS + else if ( const IndexIVFRaBitQFastScan* ivrqfs = dynamic_cast(idx)) { uint32_t h = fourcc("Iwrf"); diff --git a/faiss/impl/svs_io.cpp b/faiss/impl/svs_io.cpp new file mode 100644 index 0000000000..6c97fe5127 --- /dev/null +++ b/faiss/impl/svs_io.cpp @@ -0,0 +1,85 @@ +/* + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/* + * Portions 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 + +#include + +namespace faiss { +namespace svs_io { + +WriterStreambuf::WriterStreambuf(IOWriter* w_) : w(w_) {} + +WriterStreambuf::~WriterStreambuf() = default; + +std::streamsize WriterStreambuf::xsputn(const char* s, std::streamsize n) { + if (n <= 0) + return 0; + size_t wrote = (*w)(s, 1, static_cast(n)); + return static_cast(wrote); +} + +int WriterStreambuf::overflow(int ch) { + if (ch == traits_type::eof()) + return 0; + char c = static_cast(ch); + size_t wrote = (*w)(&c, 1, 1); + return wrote == 1 ? ch : traits_type::eof(); +} + +ReaderStreambuf::ReaderStreambuf(IOReader* rr) : r(rr), single_char_buffer(0) { + // Initialize with empty get area + setg(nullptr, nullptr, nullptr); +} + +ReaderStreambuf::~ReaderStreambuf() = default; + +std::streambuf::int_type ReaderStreambuf::underflow() { + // Called by std::istream for single-character operations (get, peek, etc.) + // when the get area is exhausted. Reads one byte from IOReader. + size_t got = (*r)(&single_char_buffer, 1, 1); + if (got == 0) { + return traits_type::eof(); + } + + // Configure get area to expose the single buffered character + setg(&single_char_buffer, &single_char_buffer, &single_char_buffer + 1); + return traits_type::to_int_type(single_char_buffer); +} + +std::streamsize ReaderStreambuf::xsgetn(char* s, std::streamsize n) { + // Called by std::istream for bulk reads (read, readsome, etc.). + // Forwards directly to IOReader without intermediate buffering to avoid + // advancing IOReader beyond what the stream consumer requested. + if (n <= 0) { + return 0; + } + + size_t got = (*r)(s, 1, n); + return static_cast(got); +} + +} // namespace svs_io +} // namespace faiss diff --git a/faiss/impl/svs_io.h b/faiss/impl/svs_io.h new file mode 100644 index 0000000000..fbf7e8f020 --- /dev/null +++ b/faiss/impl/svs_io.h @@ -0,0 +1,66 @@ +/* + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/* + * Portions 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 faiss { +namespace svs_io { + +// Bridges IOWriter to std::ostream for streaming serialization. +// No buffering concerns since consumer is expected to write everything +// he receives. +struct WriterStreambuf : std::streambuf { + IOWriter* w; + explicit WriterStreambuf(IOWriter* w_); + ~WriterStreambuf() override; + + protected: + std::streamsize xsputn(const char* s, std::streamsize n) override; + int overflow(int ch) override; +}; + +// Bridges IOReader to std::istream for streaming deserialization. +// Uses minimal buffering (single byte) to avoid over-reading from IOReader, +// which would advance its position beyond what the stream consumer actually +// read. This ensures subsequent direct reads from IOReader continue at the +// correct position. Bulk reads via xsgetn() forward directly to IOReader +// without intermediate buffering. +struct ReaderStreambuf : std::streambuf { + IOReader* r; + char single_char_buffer; // Single-byte buffer for underflow() operations + + explicit ReaderStreambuf(IOReader* rr); + ~ReaderStreambuf() override; + + protected: + int_type underflow() override; + std::streamsize xsgetn(char* s, std::streamsize n) override; +}; + +} // namespace svs_io +} // namespace faiss diff --git a/faiss/index_factory.cpp b/faiss/index_factory.cpp index 7a8d440a14..add0c29278 100644 --- a/faiss/index_factory.cpp +++ b/faiss/index_factory.cpp @@ -52,6 +52,13 @@ #include #include #include + +#ifdef FAISS_ENABLE_SVS +#include +#include +#include +#include +#endif #include #include #include @@ -552,6 +559,109 @@ IndexNSG* parse_IndexNSG( return nullptr; } +#ifdef FAISS_ENABLE_SVS +/*************************************************************** + * Parse IndexSVS + */ + +SVSStorageKind parse_lvq(const std::string& lvq_string) { + if (lvq_string == "LVQ4x0") { + return SVSStorageKind::SVS_LVQ4x0; + } + if (lvq_string == "LVQ4x4") { + return SVSStorageKind::SVS_LVQ4x4; + } + if (lvq_string == "LVQ4x8") { + return SVSStorageKind::SVS_LVQ4x8; + } + FAISS_ASSERT(!"not supported SVS LVQ level"); +} + +SVSStorageKind parse_leanvec(const std::string& leanvec_string) { + if (leanvec_string == "LeanVec4x4") { + return SVSStorageKind::SVS_LeanVec4x4; + } + if (leanvec_string == "LeanVec4x8") { + return SVSStorageKind::SVS_LeanVec4x8; + } + if (leanvec_string == "LeanVec8x8") { + return SVSStorageKind::SVS_LeanVec8x8; + } + FAISS_ASSERT(!"not supported SVS Leanvec level"); +} + +Index* parse_svs_datatype( + const std::string& index_type, + const std::string& arg_string, + const std::string& datatype_string, + int d, + MetricType mt) { + std::smatch sm; + + if (datatype_string.empty()) { + if (index_type == "Vamana") + return new IndexSVSVamana(d, std::stoul(arg_string), mt); + if (index_type == "Flat") + return new IndexSVSFlat(d, mt); + FAISS_ASSERT(!"Unspported SVS index type"); + } + if (re_match(datatype_string, "FP16", sm)) { + if (index_type == "Vamana") + return new IndexSVSVamana( + d, std::stoul(arg_string), mt, SVSStorageKind::SVS_FP16); + FAISS_ASSERT(!"Unspported SVS index type for Float16"); + } + if (re_match(datatype_string, "SQI8", sm)) { + if (index_type == "Vamana") + return new IndexSVSVamana( + d, std::stoul(arg_string), mt, SVSStorageKind::SVS_SQI8); + FAISS_ASSERT(!"Unspported SVS index type for SQI8"); + } + if (re_match(datatype_string, "(LVQ[0-9]+x[0-9]+)", sm)) { + if (index_type == "Vamana") + return new IndexSVSVamanaLVQ( + d, std::stoul(arg_string), mt, parse_lvq(sm[0].str())); + FAISS_ASSERT(!"Unspported SVS index type for LVQ"); + } + if (re_match(datatype_string, "(LeanVec[0-9]+x[0-9]+)(_[0-9]+)?", sm)) { + std::string leanvec_d_string = + sm[2].length() > 0 ? sm[2].str().substr(1) : "0"; + int leanvec_d = std::stoul(leanvec_d_string); + + if (index_type == "Vamana") + return new IndexSVSVamanaLeanVec( + d, + std::stoul(arg_string), + mt, + leanvec_d, + parse_leanvec(sm[1].str())); + FAISS_ASSERT(!"Unspported SVS index type for LeanVec"); + } + return nullptr; +} + +Index* parse_IndexSVS(const std::string& code_string, int d, MetricType mt) { + std::smatch sm; + if (re_match(code_string, "Flat(,.+)?", sm)) { + std::string datatype_string = + sm[1].length() > 0 ? sm[1].str().substr(1) : ""; + return parse_svs_datatype("Flat", "", datatype_string, d, mt); + } + if (re_match(code_string, "Vamana([0-9]+)(,.+)?", sm)) { + Index* index{nullptr}; + std::string degree_string = sm[1].str(); + std::string datatype_string = + sm[2].length() > 0 ? sm[2].str().substr(1) : ""; + return parse_svs_datatype( + "Vamana", degree_string, datatype_string, d, mt); + } + if (re_match(code_string, "IVF([0-9]+)(,.+)?", sm)) { + FAISS_ASSERT(!"Unspported SVS index type"); + } + return nullptr; +} +#endif // FAISS_ENABLE_SVS + /*************************************************************** * Parse basic indexes */ @@ -857,6 +967,25 @@ std::unique_ptr index_factory_sub( return std::unique_ptr(index); } +#ifdef FAISS_ENABLE_SVS + if (re_match(description, "SVS((?:Flat|Vamana|IVF).*)", sm)) { + std::string code_string = sm[1].str(); + if (verbose) { + printf("parsing SVS string %s code_string=%s", + description.c_str(), + code_string.c_str()); + } + + Index* index = parse_IndexSVS(code_string, d, metric); + FAISS_THROW_IF_NOT_FMT( + index, + "could not parse SVS code description %s in %s", + code_string.c_str(), + description.c_str()); + return std::unique_ptr(index); + } +#endif // FAISS_ENABLE_SVS + // NSG variants (it was unclear in the old version that the separator was a // "," so we support both "_" and ",") if (re_match(description, "NSG([0-9]*)([,_].*)?", sm)) { diff --git a/faiss/python/CMakeLists.txt b/faiss/python/CMakeLists.txt index ce74c0c6cd..ad431a3e0f 100644 --- a/faiss/python/CMakeLists.txt +++ b/faiss/python/CMakeLists.txt @@ -37,6 +37,11 @@ macro(configure_swigfaiss source) SWIG_FLAGS -DSWIGWIN ) endif() + if(FAISS_ENABLE_SVS) + set_property(SOURCE ${source} APPEND PROPERTY + SWIG_FLAGS -DFAISS_ENABLE_SVS -DFAISS_SVS_RUNTIME_VERSION=${FAISS_SVS_RUNTIME_VERSION} + ) + endif() if(FAISS_ENABLE_GPU) set_source_files_properties(${source} PROPERTIES COMPILE_DEFINITIONS GPU_WRAPPER @@ -189,6 +194,15 @@ else() target_compile_options(faiss_example_external_module PRIVATE /bigobj) endif() +if(FAISS_ENABLE_SVS) + target_compile_definitions(swigfaiss PRIVATE FAISS_ENABLE_SVS FAISS_SVS_RUNTIME_VERSION=${FAISS_SVS_RUNTIME_VERSION}) + target_compile_definitions(swigfaiss_avx2 PRIVATE FAISS_ENABLE_SVS FAISS_SVS_RUNTIME_VERSION=${FAISS_SVS_RUNTIME_VERSION}) + target_compile_definitions(swigfaiss_avx512 PRIVATE FAISS_ENABLE_SVS FAISS_SVS_RUNTIME_VERSION=${FAISS_SVS_RUNTIME_VERSION}) + target_compile_definitions(swigfaiss_avx512_spr PRIVATE FAISS_ENABLE_SVS FAISS_SVS_RUNTIME_VERSION=${FAISS_SVS_RUNTIME_VERSION}) + target_compile_definitions(swigfaiss_sve PRIVATE FAISS_ENABLE_SVS FAISS_SVS_RUNTIME_VERSION=${FAISS_SVS_RUNTIME_VERSION}) + target_compile_definitions(faiss_example_external_module PRIVATE FAISS_ENABLE_SVS FAISS_SVS_RUNTIME_VERSION=${FAISS_SVS_RUNTIME_VERSION}) +endif() + if(FAISS_ENABLE_GPU) if(FAISS_ENABLE_ROCM) target_link_libraries(swigfaiss PRIVATE hip::host) diff --git a/faiss/python/__init__.py b/faiss/python/__init__.py index 20bdeb9323..51292212d1 100644 --- a/faiss/python/__init__.py +++ b/faiss/python/__init__.py @@ -222,6 +222,12 @@ def replacement_function(*args): add_ref_in_constructor(IndexIVFRaBitQ, 0) add_ref_in_constructor(IndexIVFRaBitQFastScan, 0) +if "SVS" in get_compile_options(): + add_ref_in_constructor(IndexSVSVamana, 0) + add_ref_in_constructor(IndexSVSVamanaLVQ, 0) + add_ref_in_constructor(IndexSVSVamanaLeanVec, 0) + add_ref_in_constructor(IndexSVSFlat, 0) + # seems really marginal... # remove_ref_from_method(IndexReplicas, 'removeIndex', 0) diff --git a/faiss/python/swigfaiss.swig b/faiss/python/swigfaiss.swig index 2ec52266ba..7ad49d8353 100644 --- a/faiss/python/swigfaiss.swig +++ b/faiss/python/swigfaiss.swig @@ -201,6 +201,13 @@ typedef uint64_t size_t; #include #include +#ifdef FAISS_ENABLE_SVS +#include +#include +#include +#include +#endif + %} /******************************************************** @@ -649,6 +656,15 @@ struct faiss::simd16uint16 {}; %include +#ifdef FAISS_ENABLE_SVS +%ignore faiss::to_svs_storage_kind; + +%include +%include +%include +%include +#endif // FAISS_ENABLE_SVS + #ifdef GPU_WRAPPER // quiet SWIG warnings @@ -779,6 +795,12 @@ struct faiss::simd16uint16 {}; DOWNCAST ( IndexRandom ) DOWNCAST ( IndexRowwiseMinMax ) DOWNCAST ( IndexRowwiseMinMaxFP16 ) +#ifdef FAISS_ENABLE_SVS + DOWNCAST ( IndexSVSFlat ) + DOWNCAST ( IndexSVSVamanaLeanVec ) + DOWNCAST ( IndexSVSVamanaLVQ ) + DOWNCAST ( IndexSVSVamana ) +#endif // FAISS_ENABLE_SVS #ifdef GPU_WRAPPER #ifdef FAISS_ENABLE_CUVS DOWNCAST_GPU ( GpuIndexCagra ) diff --git a/faiss/svs/IndexSVSFaissUtils.h b/faiss/svs/IndexSVSFaissUtils.h new file mode 100644 index 0000000000..ff93684117 --- /dev/null +++ b/faiss/svs/IndexSVSFaissUtils.h @@ -0,0 +1,260 @@ +/* + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/* + * Portions 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 + +#include +#include +#include +#include +#include +#include + +// validate FAISS_SVS_RUNTIME_VERSION is set +#ifndef FAISS_SVS_RUNTIME_VERSION +#error "FAISS_SVS_RUNTIME_VERSION is not defined" +#endif +// create svs_runtime as alias for svs::runtime::FAISS_SVS_RUNTIME_VERSION +SVS_RUNTIME_CREATE_API_ALIAS(svs_runtime, FAISS_SVS_RUNTIME_VERSION); + +// SVS forward declarations +namespace svs { +namespace runtime { +inline namespace v0 { +struct FlatIndex; +struct VamanaIndex; +struct DynamicVamanaIndex; +struct LeanVecTrainingData; +} // namespace v0 +} // namespace runtime +} // namespace svs + +namespace faiss { + +inline svs_runtime::MetricType to_svs_metric(faiss::MetricType metric) { + switch (metric) { + case METRIC_INNER_PRODUCT: + return svs_runtime::MetricType::INNER_PRODUCT; + case METRIC_L2: + return svs_runtime::MetricType::L2; + default: + FAISS_ASSERT(!"not supported SVS distance"); + } +} + +struct FaissIDFilter : public svs_runtime::IDFilter { + FaissIDFilter(const faiss::IDSelector& sel) : selector(sel) {} + + bool is_member(size_t id) const override { + return selector.is_member(static_cast(id)); + } + + private: + const faiss::IDSelector& selector; +}; + +inline std::unique_ptr make_faiss_id_filter( + const SearchParameters* params = nullptr) { + if (params && params->sel) { + return std::make_unique(*params->sel); + } + return nullptr; +} + +template +struct InputBufferConverter { + InputBufferConverter(std::span data = {}) : buffer(data.size()) { + FAISS_ASSERT( + !"InputBufferConverter: there is no suitable user code for this type conversion"); + std::transform( + data.begin(), data.end(), buffer.begin(), [](const U& val) { + return static_cast(val); + }); + } + + operator T*() { + return buffer.data(); + } + operator const T*() const { + return buffer.data(); + } + + operator std::span() { + return buffer; + } + operator std::span() const { + return buffer; + } + + private: + std::vector buffer; +}; + +// Specialization for reinterpret cast when types are integral and have the same +// size +template +struct InputBufferConverter< + T, + U, + std::enable_if_t< + std::is_same_v || + (std::is_integral_v && std::is_integral_v && + sizeof(T) == sizeof(U))>> { + InputBufferConverter(std::span data = {}) : data_span(data) {} + operator T*() { + return reinterpret_cast(data_span.data()); + } + operator const T*() const { + return reinterpret_cast(data_span.data()); + } + operator std::span() { + return std::span( + reinterpret_cast(data_span.data()), data_span.size()); + } + operator std::span() const { + return std::span( + reinterpret_cast(data_span.data()), data_span.size()); + } + + private: + std::span data_span; +}; + +template +struct OutputBufferConverter { + OutputBufferConverter(std::span data = {}) + : data_span(data), buffer(data.size()) { + FAISS_ASSERT( + !"OutputBufferConverter: there is no suitable user code for this type conversion"); + } + + ~OutputBufferConverter() { + std::transform( + buffer.begin(), + buffer.end(), + data_span.begin(), + [](const T& val) { return static_cast(val); }); + } + + operator T*() { + return buffer.data(); + } + operator std::span() { + return buffer; + } + + private: + std::span data_span; + std::vector buffer; +}; + +// Specialization for reinterpret cast when types are integral and have the same +// size +template +struct OutputBufferConverter< + T, + U, + std::enable_if_t< + std::is_same_v || + (std::is_integral_v && std::is_integral_v && + sizeof(T) == sizeof(U))>> { + OutputBufferConverter(std::span data = {}) : data_span(data) {} + operator T*() { + return reinterpret_cast(data_span.data()); + } + operator std::span() { + return std::span( + reinterpret_cast(data_span.data()), data_span.size()); + } + + private: + std::span data_span; +}; + +template +auto convert_input_buffer(std::span data) { + // Create temporary buffer and convert input data + // to target type T in the temporary buffer + // The temporary buffer will be destroyed + // when going out of scope + return InputBufferConverter(data); +} + +template +auto convert_input_buffer(const U* data, size_t size) { + return convert_input_buffer(std::span(data, size)); +} + +// Output buffer conversion +template +auto convert_output_buffer(std::span data) { + // Create temporary buffer for output data + // The temporary buffer will be destroyed + // when going out of scope, copying back + // the converted data to original buffer + return OutputBufferConverter(data); +} + +template +auto convert_output_buffer(U* data, size_t size) { + return convert_output_buffer(std::span(data, size)); +} + +struct FaissResultsAllocator : public svs_runtime::ResultsAllocator { + FaissResultsAllocator(faiss::RangeSearchResult* result) : result(result) { + FAISS_ASSERT(result != nullptr); + } + + svs_runtime::SearchResultsStorage allocate( + std::span result_counts) const override { + FAISS_ASSERT(result != nullptr); + FAISS_ASSERT(result_counts.size() == result->nq); + + // RangeSearchResult .ctor() allows unallocated lims + if (result->lims == nullptr) { + result->lims = new size_t[result->nq + 1]; + } + + std::copy(result_counts.begin(), result_counts.end(), result->lims); + result->do_allocation(); + this->labels_converter = LabelsConverter{ + std::span(result->labels, result->lims[result_counts.size()])}; + return svs_runtime::SearchResultsStorage{ + labels_converter, + std::span( + result->distances, result->lims[result_counts.size()])}; + } + + private: + faiss::RangeSearchResult* result; + using LabelsConverter = OutputBufferConverter; + mutable LabelsConverter labels_converter; +}; +} // namespace faiss diff --git a/faiss/svs/IndexSVSFlat.cpp b/faiss/svs/IndexSVSFlat.cpp new file mode 100644 index 0000000000..4ccc77a5a7 --- /dev/null +++ b/faiss/svs/IndexSVSFlat.cpp @@ -0,0 +1,116 @@ +/* + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/* + * Portions 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 + +#include + +#include + +namespace faiss { + +IndexSVSFlat::IndexSVSFlat(idx_t d, MetricType metric) : Index(d, metric) {} + +IndexSVSFlat::~IndexSVSFlat() { + if (impl) { + auto status = svs_runtime::FlatIndex::destroy(impl); + FAISS_ASSERT(status.ok()); + impl = nullptr; + } +} + +void IndexSVSFlat::add(idx_t n, const float* x) { + if (!impl) { + create_impl(); + } + + auto status = impl->add(n, x); + if (!status.ok()) { + FAISS_THROW_MSG(status.message()); + } + ntotal += n; +} + +void IndexSVSFlat::reset() { + if (impl) { + auto status = impl->reset(); + if (!status.ok()) { + FAISS_THROW_MSG(status.message()); + } + } + ntotal = 0; +} + +void IndexSVSFlat::search( + idx_t n, + const float* x, + idx_t k, + float* distances, + idx_t* labels, + const SearchParameters* params) const { + FAISS_THROW_IF_NOT(impl); + auto status = impl->search( + n, + x, + static_cast(k), + distances, + reinterpret_cast(labels)); + if (!status.ok()) { + FAISS_THROW_MSG(status.message()); + } +} + +/* Initializes the implementation*/ +void IndexSVSFlat::create_impl() { + FAISS_ASSERT(impl == nullptr); + auto svs_metric = to_svs_metric(metric_type); + auto status = svs_runtime::FlatIndex::build(&impl, d, svs_metric); + if (!status.ok()) { + FAISS_THROW_MSG(status.message()); + } + FAISS_THROW_IF_NOT(impl); +} + +/* Serialization */ +void IndexSVSFlat::serialize_impl(std::ostream& out) const { + FAISS_THROW_IF_NOT_MSG( + impl, "Cannot serialize: SVS index not initialized."); + + auto status = impl->save(out); + if (!status.ok()) { + FAISS_THROW_MSG(status.message()); + } +} + +void IndexSVSFlat::deserialize_impl(std::istream& in) { + FAISS_THROW_IF_MSG(impl, "Cannot deserialize: SVS index already loaded."); + auto metric = to_svs_metric(metric_type); + auto status = impl->load(&impl, in, metric); + if (!status.ok()) { + FAISS_THROW_MSG(status.message()); + } + FAISS_THROW_IF_NOT(impl); +} + +} // namespace faiss diff --git a/faiss/svs/IndexSVSFlat.h b/faiss/svs/IndexSVSFlat.h new file mode 100644 index 0000000000..bc560b33da --- /dev/null +++ b/faiss/svs/IndexSVSFlat.h @@ -0,0 +1,65 @@ +/* + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/* + * Portions 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 + +namespace faiss { + +struct IndexSVSFlat : Index { + // sequential labels + size_t nlabels{0}; + + IndexSVSFlat() = default; + IndexSVSFlat(idx_t d, MetricType metric = METRIC_L2); + + ~IndexSVSFlat() override; + + void add(idx_t n, const float* x) override; + + void search( + idx_t n, + const float* x, + idx_t k, + float* distances, + idx_t* labels, + const SearchParameters* params = nullptr) const override; + + void reset() override; + + /* The actual SVS implementation */ + svs_runtime::FlatIndex* impl{nullptr}; + + /* Serialization */ + void serialize_impl(std::ostream& out) const; + void deserialize_impl(std::istream& in); + + protected: + /* Initializes the implementation*/ + virtual void create_impl(); +}; + +} // namespace faiss diff --git a/faiss/svs/IndexSVSVamana.cpp b/faiss/svs/IndexSVSVamana.cpp new file mode 100644 index 0000000000..9412479a67 --- /dev/null +++ b/faiss/svs/IndexSVSVamana.cpp @@ -0,0 +1,244 @@ +/* + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/* + * Portions 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 + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace faiss { +namespace { +svs_runtime::VamanaIndex::SearchParams make_search_parameters( + const IndexSVSVamana& index, + const SearchParameters* params) { + FAISS_THROW_IF_NOT(index.impl); + + auto search_window_size = index.search_window_size; + auto search_buffer_capacity = index.search_buffer_capacity; + + if (auto svs_params = + dynamic_cast(params)) { + if (svs_params->search_window_size > 0) + search_window_size = svs_params->search_window_size; + if (svs_params->search_buffer_capacity > 0) + search_buffer_capacity = svs_params->search_buffer_capacity; + } + + return {search_window_size, search_buffer_capacity}; +} +} // namespace + +IndexSVSVamana::IndexSVSVamana() = default; + +IndexSVSVamana::IndexSVSVamana( + idx_t d, + size_t degree, + MetricType metric, + SVSStorageKind storage) + : Index(d, metric), graph_max_degree{degree}, storage_kind{storage} { + prune_to = graph_max_degree < 4 ? graph_max_degree : graph_max_degree - 4; + alpha = metric == METRIC_L2 ? 1.2f : 0.95f; + + // Validate the requested storage kind is available in current runtime. + // NB: LVQ/LeanVec are only available on Intel(R) hardware AND when using + // a build based on LVQ/LeanVec-enabled SVS. + auto svs_storage = to_svs_storage_kind(storage_kind); + auto status = + svs_runtime::DynamicVamanaIndex::check_storage_kind(svs_storage); + if (!status.ok()) { + FAISS_THROW_MSG(status.message()); + } +} + +bool IndexSVSVamana::is_lvq_leanvec_enabled() { + auto lvq = to_svs_storage_kind(SVS_LVQ4x0); + auto status = svs_runtime::DynamicVamanaIndex::check_storage_kind(lvq); + if (!status.ok()) { + return false; + } + auto leanvec = to_svs_storage_kind(SVS_LeanVec4x4); + status = svs_runtime::DynamicVamanaIndex::check_storage_kind(leanvec); + if (!status.ok()) { + return false; + } + return true; +} + +IndexSVSVamana::~IndexSVSVamana() { + if (impl) { + auto status = svs_runtime::DynamicVamanaIndex::destroy(impl); + FAISS_ASSERT(status.ok()); + impl = nullptr; + } +} + +void IndexSVSVamana::add(idx_t n, const float* x) { + if (!impl) { + create_impl(); + } + + std::vector labels(n); + std::iota(labels.begin(), labels.end(), ntotal); + + auto status = impl->add(n, labels.data(), x); + if (!status.ok()) { + FAISS_THROW_MSG(status.message()); + } + ntotal += n; +} + +void IndexSVSVamana::reset() { + if (impl) { + impl->reset(); + } + is_trained = false; + ntotal = 0; +} + +void IndexSVSVamana::search( + idx_t n, + const float* x, + idx_t k, + float* distances, + idx_t* labels, + const SearchParameters* params) const { + if (!impl) { + for (idx_t i = 0; i < n; ++i) { + distances[i] = std::numeric_limits::infinity(); + labels[i] = -1; + } + return; + } + FAISS_THROW_IF_NOT(k > 0); + FAISS_THROW_IF_NOT(is_trained); + + auto sp = make_search_parameters(*this, params); + auto id_filter = make_faiss_id_filter(params); + auto status = impl->search( + static_cast(n), + x, + static_cast(k), + distances, + convert_output_buffer(labels, static_cast(n * k)), + &sp, + id_filter.get()); + + if (!status.ok()) { + FAISS_THROW_MSG(status.message()); + } +} + +void IndexSVSVamana::range_search( + idx_t n, + const float* x, + float radius, + RangeSearchResult* result, + const SearchParameters* params) const { + FAISS_THROW_IF_NOT(impl); + FAISS_THROW_IF_NOT(radius > 0); + FAISS_THROW_IF_NOT(is_trained); + FAISS_THROW_IF_NOT(result->nq == static_cast(n)); + + auto sp = make_search_parameters(*this, params); + auto id_filter = make_faiss_id_filter(params); + auto status = impl->range_search( + static_cast(n), + x, + radius, + FaissResultsAllocator{result}, + &sp, + id_filter.get()); + if (!status.ok()) { + FAISS_THROW_MSG(status.message()); + } +} + +size_t IndexSVSVamana::remove_ids(const IDSelector& sel) { + FAISS_THROW_IF_NOT(impl); + auto id_filter = FaissIDFilter{sel}; + size_t removed = 0; + auto Status = impl->remove_selected(&removed, id_filter); + ntotal -= removed; + return removed; +} + +void IndexSVSVamana::create_impl() { + FAISS_THROW_IF_NOT(!impl); + ntotal = 0; + auto svs_metric = to_svs_metric(metric_type); + auto svs_storage_kind = to_svs_storage_kind(storage_kind); + auto build_params = svs_runtime::VamanaIndex::BuildParams{ + .graph_max_degree = graph_max_degree, + .prune_to = prune_to, + .alpha = alpha, + .construction_window_size = construction_window_size, + .max_candidate_pool_size = max_candidate_pool_size, + .use_full_search_history = use_full_search_history, + }; + auto search_params = svs_runtime::VamanaIndex::SearchParams{ + .search_window_size = search_window_size, + .search_buffer_capacity = search_buffer_capacity, + }; + auto Status = svs_runtime::DynamicVamanaIndex::build( + &impl, + d, + svs_metric, + svs_storage_kind, + build_params, + search_params); + if (!Status.ok()) { + FAISS_THROW_MSG(Status.message()); + } + FAISS_THROW_IF_NOT(impl); +} + +void IndexSVSVamana::serialize_impl(std::ostream& out) const { + FAISS_THROW_IF_NOT_MSG( + impl, "Cannot serialize: SVS index not initialized."); + + auto status = impl->save(out); + if (!status.ok()) { + FAISS_THROW_MSG(status.message()); + } +} + +void IndexSVSVamana::deserialize_impl(std::istream& in) { + FAISS_THROW_IF_MSG(impl, "Cannot deserialize: SVS index already loaded."); + auto svs_metric = to_svs_metric(metric_type); + auto svs_storage_kind = to_svs_storage_kind(storage_kind); + auto status = impl->load(&impl, in, svs_metric, svs_storage_kind); + if (!status.ok()) { + FAISS_THROW_MSG(status.message()); + } +} + +} // namespace faiss diff --git a/faiss/svs/IndexSVSVamana.h b/faiss/svs/IndexSVSVamana.h new file mode 100644 index 0000000000..18567f0e23 --- /dev/null +++ b/faiss/svs/IndexSVSVamana.h @@ -0,0 +1,136 @@ +/* + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/* + * Portions 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 faiss { + +struct SearchParametersSVSVamana : public SearchParameters { + size_t search_window_size = 0; + size_t search_buffer_capacity = 0; +}; + +// redefinition for swig export +enum SVSStorageKind { + SVS_FP32, + SVS_FP16, + SVS_SQI8, + SVS_LVQ4x0, + SVS_LVQ4x4, + SVS_LVQ4x8, + SVS_LeanVec4x4, + SVS_LeanVec4x8, + SVS_LeanVec8x8, +}; + +inline svs_runtime::StorageKind to_svs_storage_kind(SVSStorageKind kind) { + switch (kind) { + case SVS_FP32: + return svs_runtime::StorageKind::FP32; + case SVS_FP16: + return svs_runtime::StorageKind::FP16; + case SVS_SQI8: + return svs_runtime::StorageKind::SQI8; + case SVS_LVQ4x0: + return svs_runtime::StorageKind::LVQ4x0; + case SVS_LVQ4x4: + return svs_runtime::StorageKind::LVQ4x4; + case SVS_LVQ4x8: + return svs_runtime::StorageKind::LVQ4x8; + case SVS_LeanVec4x4: + return svs_runtime::StorageKind::LeanVec4x4; + case SVS_LeanVec4x8: + return svs_runtime::StorageKind::LeanVec4x8; + case SVS_LeanVec8x8: + return svs_runtime::StorageKind::LeanVec8x8; + default: + FAISS_ASSERT(!"not supported SVS storage kind"); + } +} + +struct IndexSVSVamana : Index { + size_t graph_max_degree; + size_t prune_to; + float alpha = 1.2; + size_t search_window_size = 10; + size_t search_buffer_capacity = 10; + size_t construction_window_size = 40; + size_t max_candidate_pool_size = 200; + bool use_full_search_history = true; + + SVSStorageKind storage_kind; + + IndexSVSVamana(); + + IndexSVSVamana( + idx_t d, + size_t degree, + MetricType metric = METRIC_L2, + SVSStorageKind storage = SVSStorageKind::SVS_FP32); + + ~IndexSVSVamana() override; + + // static member that exposes whether or not LVQ/LeanVec are enabled for + // this build and runtime. + static bool is_lvq_leanvec_enabled(); + + void add(idx_t n, const float* x) override; + + void search( + idx_t n, + const float* x, + idx_t k, + float* distances, + idx_t* labels, + const SearchParameters* params = nullptr) const override; + + void range_search( + idx_t n, + const float* x, + float radius, + RangeSearchResult* result, + const SearchParameters* params = nullptr) const override; + + size_t remove_ids(const IDSelector& sel) override; + + void reset() override; + + /* Serialization and deserialization helpers */ + void serialize_impl(std::ostream& out) const; + virtual void deserialize_impl(std::istream& in); + + /* The actual SVS implementation */ + svs_runtime::DynamicVamanaIndex* impl{nullptr}; + + protected: + /* Initializes the implementation*/ + virtual void create_impl(); +}; + +} // namespace faiss diff --git a/faiss/svs/IndexSVSVamanaLVQ.cpp b/faiss/svs/IndexSVSVamanaLVQ.cpp new file mode 100644 index 0000000000..3e882b49f8 --- /dev/null +++ b/faiss/svs/IndexSVSVamanaLVQ.cpp @@ -0,0 +1,38 @@ +/* + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/* + * Portions 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 faiss { + +IndexSVSVamanaLVQ::IndexSVSVamanaLVQ() : IndexSVSVamana() { + storage_kind = SVSStorageKind::SVS_LVQ4x0; +} + +IndexSVSVamanaLVQ::IndexSVSVamanaLVQ( + idx_t d, + size_t degree, + MetricType metric, + SVSStorageKind storage) + : IndexSVSVamana(d, degree, metric, storage) {} + +} // namespace faiss diff --git a/faiss/svs/IndexSVSVamanaLVQ.h b/faiss/svs/IndexSVSVamanaLVQ.h new file mode 100644 index 0000000000..9b2ad80bb4 --- /dev/null +++ b/faiss/svs/IndexSVSVamanaLVQ.h @@ -0,0 +1,41 @@ +/* + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/* + * Portions 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 + +namespace faiss { + +struct IndexSVSVamanaLVQ : IndexSVSVamana { + IndexSVSVamanaLVQ(); + + IndexSVSVamanaLVQ( + idx_t d, + size_t degree, + MetricType metric = METRIC_L2, + SVSStorageKind storage = SVSStorageKind::SVS_LVQ4x4); + + ~IndexSVSVamanaLVQ() override = default; +}; + +} // namespace faiss diff --git a/faiss/svs/IndexSVSVamanaLeanVec.cpp b/faiss/svs/IndexSVSVamanaLeanVec.cpp new file mode 100644 index 0000000000..0e9f631511 --- /dev/null +++ b/faiss/svs/IndexSVSVamanaLeanVec.cpp @@ -0,0 +1,148 @@ +/* + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/* + * Portions 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 +#include +#include + +#include +#include +#include "faiss/svs/IndexSVSVamana.h" + +namespace faiss { + +IndexSVSVamanaLeanVec::IndexSVSVamanaLeanVec() : IndexSVSVamana() { + is_trained = false; + storage_kind = SVSStorageKind::SVS_LeanVec4x4; +} + +IndexSVSVamanaLeanVec::IndexSVSVamanaLeanVec( + idx_t d, + size_t degree, + MetricType metric, + size_t leanvec_dims, + SVSStorageKind storage_kind) + : IndexSVSVamana(d, degree, metric, storage_kind) { + is_trained = false; + leanvec_d = leanvec_dims == 0 ? d / 2 : leanvec_dims; +} + +IndexSVSVamanaLeanVec::~IndexSVSVamanaLeanVec() { + if (training_data) { + auto status = svs_runtime::LeanVecTrainingData::destroy(training_data); + FAISS_ASSERT(status.ok()); + training_data = nullptr; + } + IndexSVSVamana::~IndexSVSVamana(); +} + +void IndexSVSVamanaLeanVec::add(idx_t n, const float* x) { + FAISS_THROW_IF_MSG( + !is_trained, "Index not trained: call train() before add()."); + IndexSVSVamana::add(n, x); +} + +void IndexSVSVamanaLeanVec::train(idx_t n, const float* x) { + FAISS_THROW_IF_MSG( + training_data || impl, "Index already trained or contains data."); + + FAISS_THROW_IF_NOT_MSG( + IndexSVSVamana::is_lvq_leanvec_enabled(), + "LVQ/LeanVec support not available on this platform or build"); + + auto status = svs_runtime::LeanVecTrainingData::build( + &training_data, d, n, x, leanvec_d); + if (!status.ok()) { + FAISS_THROW_MSG(status.message()); + } + FAISS_THROW_IF_NOT_MSG( + training_data, "Failed to build leanvec training info."); + is_trained = true; +} + +void IndexSVSVamanaLeanVec::serialize_training_data(std::ostream& out) const { + FAISS_THROW_IF_NOT_MSG( + training_data, "Cannot serialize: Training data not initialized."); + + auto status = training_data->save(out); + if (!status.ok()) { + FAISS_THROW_MSG(status.message()); + } +} + +void IndexSVSVamanaLeanVec::deserialize_training_data(std::istream& in) { + svs_runtime::LeanVecTrainingData* tdata = nullptr; + auto status = svs_runtime::LeanVecTrainingData::load(&tdata, in); + if (!status.ok()) { + FAISS_THROW_MSG(status.message()); + } + FAISS_THROW_IF_NOT_MSG(tdata, "Failed to load leanvec training data."); + training_data = tdata; +} + +void IndexSVSVamanaLeanVec::create_impl() { + ntotal = 0; + auto svs_metric = to_svs_metric(metric_type); + auto svs_storage_kind = to_svs_storage_kind(storage_kind); + auto build_params = svs_runtime::VamanaIndex::BuildParams{ + .graph_max_degree = graph_max_degree, + .prune_to = prune_to, + .alpha = alpha, + .construction_window_size = construction_window_size, + .max_candidate_pool_size = max_candidate_pool_size, + .use_full_search_history = use_full_search_history, + }; + auto search_params = svs_runtime::VamanaIndex::SearchParams{ + .search_window_size = search_window_size, + .search_buffer_capacity = search_buffer_capacity, + }; + auto status = svs_runtime::Status_Ok; + if (training_data) { + status = svs_runtime::DynamicVamanaIndexLeanVec::build( + &impl, + d, + svs_metric, + svs_storage_kind, + training_data, + build_params, + search_params); + } else { + status = svs_runtime::DynamicVamanaIndexLeanVec::build( + &impl, + d, + svs_metric, + svs_storage_kind, + leanvec_d, + build_params, + search_params); + } + + if (!status.ok()) { + FAISS_THROW_MSG(status.message()); + } + FAISS_THROW_IF_NOT(impl); +} + +} // namespace faiss diff --git a/faiss/svs/IndexSVSVamanaLeanVec.h b/faiss/svs/IndexSVSVamanaLeanVec.h new file mode 100644 index 0000000000..01617cbc70 --- /dev/null +++ b/faiss/svs/IndexSVSVamanaLeanVec.h @@ -0,0 +1,57 @@ +/* + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/* + * Portions 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 + +namespace faiss { + +struct IndexSVSVamanaLeanVec : IndexSVSVamana { + IndexSVSVamanaLeanVec(); + + IndexSVSVamanaLeanVec( + idx_t d, + size_t degree, + MetricType metric = METRIC_L2, + size_t leanvec_dims = 0, + SVSStorageKind storage = SVSStorageKind::SVS_LeanVec4x4); + + ~IndexSVSVamanaLeanVec() override; + + void add(idx_t n, const float* x) override; + + void train(idx_t n, const float* x) override; + + void serialize_training_data(std::ostream& out) const; + void deserialize_training_data(std::istream& in); + + size_t leanvec_d; + + /* Training information */ + svs_runtime::LeanVecTrainingData* training_data{nullptr}; + + protected: + void create_impl() override; +}; + +} // namespace faiss diff --git a/faiss/utils/utils.cpp b/faiss/utils/utils.cpp index c13c9c8f5f..f4dddf51f1 100644 --- a/faiss/utils/utils.cpp +++ b/faiss/utils/utils.cpp @@ -127,6 +127,10 @@ std::string get_compile_options() { options += "GENERIC "; #endif +#ifdef FAISS_ENABLE_SVS + options += "SVS "; +#endif + options += ref_gpu_compile_options(); return options; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 285b9090ed..b7dabf130f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -40,8 +40,16 @@ set(FAISS_TEST_SRC test_zerocopy.cpp ) +if(FAISS_ENABLE_SVS) + list(APPEND FAISS_TEST_SRC test_svs.cpp) +endif() + add_executable(faiss_test ${FAISS_TEST_SRC}) +if(FAISS_ENABLE_SVS) + target_compile_definitions(faiss_test PRIVATE FAISS_ENABLE_SVS FAISS_SVS_RUNTIME_VERSION=${FAISS_SVS_RUNTIME_VERSION}) +endif() + include(../cmake/link_to_faiss_lib.cmake) link_to_faiss_lib(faiss_test) diff --git a/tests/test_svs.cpp b/tests/test_svs.cpp new file mode 100644 index 0000000000..561e8f1b83 --- /dev/null +++ b/tests/test_svs.cpp @@ -0,0 +1,490 @@ +/* + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/* + * Portions 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test_util.h" + +namespace { +pthread_mutex_t temp_file_mutex = PTHREAD_MUTEX_INITIALIZER; + +// Test fixture class to manage shared test data +class SVS : public ::testing::Test { + protected: + static void SetUpTestSuite() { + // Generate test data once for all tests + constexpr size_t d = 64; + constexpr size_t n = 100; + test_data.resize(d * n); + + std::mt19937 gen(123); + std::uniform_real_distribution dis(0.0f, 1.0f); + for (size_t i = 0; i < test_data.size(); ++i) { + test_data[i] = dis(gen); + } + } + + static std::vector test_data; + static constexpr size_t d = 64; + static constexpr size_t n = 100; +}; + +// LVQ/LeanVec tests are only executed if LVQ/LeanVec support is available +class SVS_LL : public SVS { + protected: + void SetUp() override { + if (!faiss::IndexSVSVamana::is_lvq_leanvec_enabled()) { + GTEST_SKIP() << "LVQ/LeanVec support not available on this " + "platform or build configuration"; + } + } +}; + +// Consistency checks for behavior if LVQ/LeanVec are not available +// Only runs if LVQ/LeanVec is NOT available +class SVS_NoLL : public SVS { + protected: + void SetUp() override { + if (faiss::IndexSVSVamana::is_lvq_leanvec_enabled()) { + GTEST_SKIP() << "LVQ/LeanVec support is available; skipping " + "NoLL tests"; + } + } +}; + +// Define static members +std::vector SVS::test_data; +} // namespace + +template +void write_and_read_index(T& index, const std::vector& xb, size_t n) { + index.train(n, xb.data()); + index.add(n, xb.data()); + + std::string temp_filename_template = "/tmp/faiss_svs_test_XXXXXX"; + Tempfilename filename(&temp_file_mutex, temp_filename_template); + + // Serialize + ASSERT_NO_THROW({ faiss::write_index(&index, filename.c_str()); }); + + // Deserialize + T* loaded = nullptr; + ASSERT_NO_THROW({ + loaded = dynamic_cast(faiss::read_index(filename.c_str())); + }); + + // Basic checks + ASSERT_NE(loaded, nullptr); + ASSERT_NE(loaded->impl, nullptr); + EXPECT_EQ(loaded->d, index.d); + EXPECT_EQ(loaded->metric_type, index.metric_type); + EXPECT_EQ(loaded->graph_max_degree, index.graph_max_degree); + EXPECT_EQ(loaded->alpha, index.alpha); + EXPECT_EQ(loaded->search_window_size, index.search_window_size); + EXPECT_EQ(loaded->search_buffer_capacity, index.search_buffer_capacity); + EXPECT_EQ(loaded->construction_window_size, index.construction_window_size); + EXPECT_EQ(loaded->max_candidate_pool_size, index.max_candidate_pool_size); + EXPECT_EQ(loaded->prune_to, index.prune_to); + EXPECT_EQ(loaded->use_full_search_history, index.use_full_search_history); + if constexpr (std::is_same_v< + std::decay_t, + faiss::IndexSVSVamanaLeanVec>) { + auto* leanvec_loaded = + dynamic_cast(loaded); + ASSERT_NE(leanvec_loaded, nullptr); + EXPECT_EQ(leanvec_loaded->leanvec_d, index.leanvec_d); + + EXPECT_NE(leanvec_loaded->training_data, nullptr); + } + + delete loaded; +} + +template +void train_save_load_and_add_index( + T& index, + const std::vector& xb, + size_t n) { + index.train(n, xb.data()); + + std::string temp_filename_template = "/tmp/faiss_svs_test_XXXXXX"; + Tempfilename filename(&temp_file_mutex, temp_filename_template); + + ASSERT_NO_THROW({ faiss::write_index(&index, filename.c_str()); }); + + T* loaded = nullptr; + ASSERT_NO_THROW({ + loaded = dynamic_cast(faiss::read_index(filename.c_str())); + }); + + ASSERT_NE(loaded, nullptr); + ASSERT_NO_THROW({ loaded->add(n, xb.data()); }); + + delete loaded; +} + +template +void save_and_load_index() { + T index; + + std::string temp_filename_template = "/tmp/faiss_svs_test_XXXXXX"; + Tempfilename filename(&temp_file_mutex, temp_filename_template); + + faiss::write_index(&index, filename.c_str()); + T* loaded = nullptr; + ASSERT_NO_THROW({ + loaded = dynamic_cast(faiss::read_index(filename.c_str())); + }); + delete loaded; +} + +TEST_F(SVS, WriteAndReadIndexSVS) { + faiss::IndexSVSVamana index{d, 64ul}; + write_and_read_index(index, test_data, n); +} + +TEST_F(SVS, WriteAndReadIndexSVSFP16) { + faiss::IndexSVSVamana index{ + d, 64ul, faiss::METRIC_L2, faiss::SVSStorageKind::SVS_FP16}; + write_and_read_index(index, test_data, n); +} + +TEST_F(SVS, WriteAndReadIndexSVSSQI8) { + faiss::IndexSVSVamana index{ + d, 64ul, faiss::METRIC_L2, faiss::SVSStorageKind::SVS_SQI8}; + write_and_read_index(index, test_data, n); +} + +TEST_F(SVS_LL, WriteAndReadIndexSVSLVQ4x0) { + faiss::IndexSVSVamanaLVQ index{d, 64ul}; + index.storage_kind = faiss::SVSStorageKind::SVS_LVQ4x0; + write_and_read_index(index, test_data, n); +} + +TEST_F(SVS_LL, WriteAndReadIndexSVSLVQ4x4) { + faiss::IndexSVSVamanaLVQ index{d, 64ul}; + index.storage_kind = faiss::SVSStorageKind::SVS_LVQ4x4; + write_and_read_index(index, test_data, n); +} + +TEST_F(SVS_LL, WriteAndReadIndexSVSLVQ4x8) { + faiss::IndexSVSVamanaLVQ index{d, 64ul}; + index.storage_kind = faiss::SVSStorageKind::SVS_LVQ4x8; + write_and_read_index(index, test_data, n); +} + +TEST_F(SVS_LL, WriteAndReadIndexSVSVamanaLeanVec4x4) { + faiss::IndexSVSVamanaLeanVec index{ + d, + 64ul, + faiss::METRIC_L2, + 0, + faiss::SVSStorageKind::SVS_LeanVec4x4}; + write_and_read_index(index, test_data, n); +} + +TEST_F(SVS_LL, WriteAndReadIndexSVSVamanaLeanVec4x8) { + faiss::IndexSVSVamanaLeanVec index{ + d, + 64ul, + faiss::METRIC_L2, + 0, + faiss::SVSStorageKind::SVS_LeanVec4x8}; + write_and_read_index(index, test_data, n); +} + +TEST_F(SVS_LL, WriteAndReadIndexSVSVamanaLeanVec8x8) { + faiss::IndexSVSVamanaLeanVec index{ + d, + 64ul, + faiss::METRIC_L2, + 0, + faiss::SVSStorageKind::SVS_LeanVec8x8}; + write_and_read_index(index, test_data, n); +} + +TEST_F(SVS_LL, LeanVecThrowsWithoutTraining) { + faiss::IndexSVSVamanaLeanVec index{ + 64, + 64ul, + faiss::METRIC_L2, + 0, + faiss::SVSStorageKind::SVS_LeanVec4x4}; + ASSERT_THROW(index.add(100, test_data.data()), faiss::FaissException); +} + +TEST_F(SVS, VamanaTrainSaveLoadAndAdd) { + faiss::IndexSVSVamana index{d, 64ul}; + train_save_load_and_add_index(index, test_data, n); +} + +TEST_F(SVS, VamanaFP16TrainSaveLoadAndAdd) { + faiss::IndexSVSVamana index{ + d, 64ul, faiss::METRIC_L2, faiss::SVSStorageKind::SVS_FP16}; + train_save_load_and_add_index(index, test_data, n); +} + +TEST_F(SVS, VamanaSQI8TrainSaveLoadAndAdd) { + faiss::IndexSVSVamana index{ + d, 64ul, faiss::METRIC_L2, faiss::SVSStorageKind::SVS_SQI8}; + train_save_load_and_add_index(index, test_data, n); +} + +TEST_F(SVS_LL, LVQ4x0TrainSaveLoadAndAdd) { + faiss::IndexSVSVamanaLVQ index{d, 64ul}; + index.storage_kind = faiss::SVSStorageKind::SVS_LVQ4x0; + train_save_load_and_add_index(index, test_data, n); +} + +TEST_F(SVS_LL, LVQ4x4TrainSaveLoadAndAdd) { + faiss::IndexSVSVamanaLVQ index{d, 64ul}; + index.storage_kind = faiss::SVSStorageKind::SVS_LVQ4x4; + train_save_load_and_add_index(index, test_data, n); +} + +TEST_F(SVS_LL, LVQ4x8TrainSaveLoadAndAdd) { + faiss::IndexSVSVamanaLVQ index{d, 64ul}; + index.storage_kind = faiss::SVSStorageKind::SVS_LVQ4x8; + train_save_load_and_add_index(index, test_data, n); +} + +TEST_F(SVS_LL, LeanVec4x4TrainSaveLoadAndAdd) { + faiss::IndexSVSVamanaLeanVec index{ + d, + 64ul, + faiss::METRIC_L2, + 0, + faiss::SVSStorageKind::SVS_LeanVec4x4}; + train_save_load_and_add_index(index, test_data, n); +} + +TEST_F(SVS_LL, LeanVec4x8TrainSaveLoadAndAdd) { + faiss::IndexSVSVamanaLeanVec index{ + d, + 64ul, + faiss::METRIC_L2, + 0, + faiss::SVSStorageKind::SVS_LeanVec4x8}; + train_save_load_and_add_index(index, test_data, n); +} + +TEST_F(SVS_LL, LeanVec8x8TrainSaveLoadAndAdd) { + faiss::IndexSVSVamanaLeanVec index{ + d, + 64ul, + faiss::METRIC_L2, + 0, + faiss::SVSStorageKind::SVS_LeanVec8x8}; + train_save_load_and_add_index(index, test_data, n); +} + +TEST_F(SVS, SaveAndLoadIndexSVSFlat) { + save_and_load_index(); +} + +TEST_F(SVS, SaveAndLoadIndexSVSVamana) { + save_and_load_index(); +} + +TEST_F(SVS_LL, SaveAndLoadIndexSVSVamanaLVQ) { + save_and_load_index(); +} + +TEST_F(SVS_LL, SaveAndLoadIndexSVSVamanaLeanVec) { + save_and_load_index(); +} + +TEST_F(SVS, WriteAndReadIndexSVSFlat) { + faiss::IndexSVSFlat index{d}; + index.add(n, test_data.data()); + + std::string temp_filename_template = "/tmp/faiss_svs_test_XXXXXX"; + Tempfilename filename(&temp_file_mutex, temp_filename_template); + + // Serialize + ASSERT_NO_THROW({ faiss::write_index(&index, filename.c_str()); }); + + // Deserialize + faiss::IndexSVSFlat* loaded = nullptr; + ASSERT_NO_THROW({ + loaded = dynamic_cast( + faiss::read_index(filename.c_str())); + }); + + ASSERT_NE(loaded, nullptr); + ASSERT_NE(loaded->impl, nullptr); + EXPECT_EQ(loaded->d, index.d); + EXPECT_EQ(loaded->nlabels, index.nlabels); + EXPECT_EQ(loaded->metric_type, index.metric_type); + + delete loaded; +} + +// Test search with IDSelector filtering +TEST_F(SVS, SearchWithIDSelector) { + faiss::IndexSVSVamana index{d, 64ul}; + index.add(n, test_data.data()); + + const int nq = 8; // number of queries + const float* xq = test_data.data(); // reuse first nq vectors as queries + const int k = 10; + + size_t min_id = n / 5; // inclusive + size_t max_id = n * 4 / 5; // exclusive + faiss::IDSelectorRange selector(min_id, max_id); + + faiss::SearchParameters params; // generic search parameters with selector + params.sel = &selector; + + std::vector distances(nq * k); + std::vector labels(nq * k); + + ASSERT_NO_THROW( + index.search(nq, xq, k, distances.data(), labels.data(), ¶ms)); + + // All returned labels must fall inside the selected range + for (int i = 0; i < nq * k; ++i) { + EXPECT_GE(labels[i], (faiss::idx_t)min_id); + EXPECT_LT(labels[i], (faiss::idx_t)max_id); + } +} + +// Basic functional test for range_search and parameter override helper +TEST_F(SVS, RangeSearchFunctional) { + faiss::IndexSVSVamana index{d, 64ul}; + index.add(n, test_data.data()); + const int nq = 5; + const float* xq = test_data.data(); + + // Small radius + faiss::RangeSearchResult res_small(nq); + ASSERT_NO_THROW(index.range_search(nq, xq, 0.05f, &res_small)); + + // Larger radius to exercise loop continuation + faiss::RangeSearchResult res_big(nq); + ASSERT_NO_THROW(index.range_search(nq, xq, 5.0f, &res_big)); + EXPECT_GE(res_big.lims[nq], res_small.lims[nq]); + + // Provide custom params to ensure branch coverage (non-null params) + faiss::SearchParametersSVSVamana params; + params.search_window_size = 15; + params.search_buffer_capacity = 20; + + // Provide IDSelector to ensure branch coverage (non-null params->sel) + size_t min_id = n / 5; // inclusive + size_t max_id = n * 4 / 5; // exclusive + faiss::IDSelectorRange selector(min_id, max_id); + params.sel = &selector; + + faiss::RangeSearchResult res_params(nq); + ASSERT_NO_THROW(index.range_search(nq, xq, 1.0f, &res_params, ¶ms)); + + // All returned labels must fall inside the selected range + for (size_t i = 0; i < res_params.lims[nq]; ++i) { + EXPECT_GE(res_params.labels[i], (faiss::idx_t)min_id); + EXPECT_LT(res_params.labels[i], (faiss::idx_t)max_id); + } +} + +TEST_F(SVS_LL, LVQAndLeanVecDoNotThrowWhenEnabled) { + // explicit constructor with LVQ dataset + ASSERT_NO_THROW({ + faiss::IndexSVSVamanaLVQ index( + d, 64ul, faiss::METRIC_L2, faiss::SVSStorageKind::SVS_LVQ4x4); + }); + + // default constructor, will initialize dataset on first add() + ASSERT_NO_THROW({ + faiss::IndexSVSVamanaLVQ index; + index.add(n, test_data.data()); + }); + + // explicit constructor with LeanVec dataset + ASSERT_NO_THROW({ + faiss::IndexSVSVamanaLeanVec index( + d, + 64ul, + faiss::METRIC_L2, + faiss::SVSStorageKind::SVS_LeanVec4x4); + }); + + // default constructor, will initialize dataset on first add() + ASSERT_NO_THROW({ + faiss::IndexSVSVamanaLeanVec index; + index.d = 64; + index.leanvec_d = 32; + index.train(n, test_data.data()); + index.add(n, test_data.data()); + }); +} + +TEST_F(SVS_NoLL, LVQAndLeanVecThrowWhenNotEnabled) { + // explicit constructor with LVQ dataset + ASSERT_THROW( + { + faiss::IndexSVSVamanaLVQ index( + d, + 64ul, + faiss::METRIC_L2, + faiss::SVSStorageKind::SVS_LVQ4x4); + }, + faiss::FaissException); + + // default constructor, will initialize dataset on first add() + ASSERT_THROW( + { + faiss::IndexSVSVamanaLVQ index; + index.add(n, test_data.data()); + }, + faiss::FaissException); + + // explicit constructor with LeanVec dataset + ASSERT_THROW( + { + faiss::IndexSVSVamanaLeanVec index( + d, + 64ul, + faiss::METRIC_L2, + faiss::SVSStorageKind::SVS_LeanVec4x4); + }, + faiss::FaissException); + + // default constructor, will initialize dataset on first add() + ASSERT_THROW( + { + faiss::IndexSVSVamanaLeanVec index; + index.d = 64; + index.leanvec_d = 32; + index.train(n, test_data.data()); + }, + faiss::FaissException); +} diff --git a/tests/test_svs.py b/tests/test_svs.py new file mode 100644 index 0000000000..6199375a1f --- /dev/null +++ b/tests/test_svs.py @@ -0,0 +1,563 @@ +# Portions Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. +# +# Portions 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. +# + +import numpy as np +import unittest +import faiss + +_SKIP_SVS = "SVS" not in faiss.get_compile_options().split() +_SKIP_REASON = "SVS support not compiled in" + +# Check if LVQ/LeanVec support is available +_SKIP_SVS_LL = _SKIP_SVS or not faiss.IndexSVSVamana.is_lvq_leanvec_enabled() +_SKIP_SVS_LL_REASON = "LVQ/LeanVec support not available on this platform or build configuration" + + +@unittest.skipIf(_SKIP_SVS, _SKIP_REASON) +class TestSVSAdapter(unittest.TestCase): + """Test the FAISS-SVS adapter layer integration""" + + target_class = None # set in setUpClass + + def _create_instance(self) -> "faiss.IndexSVSVamana | faiss.IndexSVSFlat": + """Create an instance of the SVS index""" + self.assertIsNotNone(self.target_class, "target_class must be configured in setUpClass()") + return self.target_class(self.d, 64) + + @classmethod + def setUpClass(cls): + # need to configure target_class here to avoid issues when SVS support is not compiled in + cls.target_class = faiss.IndexSVSVamana + + def setUp(self): + self.d = 32 + self.nb = 1000 + self.nq = 100 + np.random.seed(1234) + self.xb = np.random.random((self.nb, self.d)).astype('float32') + self.xq = np.random.random((self.nq, self.d)).astype('float32') + + def test_svs_construction(self): + """Test construction and basic properties""" + # Test default construction + index = self._create_instance() + self.assertEqual(index.d, self.d) + self.assertTrue(index.is_trained) + self.assertEqual(index.ntotal, 0) + self.assertEqual(index.metric_type, faiss.METRIC_L2) + + index_ip = self._create_instance() + index_ip.metric_type = faiss.METRIC_INNER_PRODUCT + self.assertEqual(index_ip.metric_type, faiss.METRIC_INNER_PRODUCT) + + def test_svs_add_search_remove_interface(self): + """Test FAISS add/search/remove_ids interface compatibility""" + index = self._create_instance() + + # Test add interface + index.add(self.xb) + self.assertEqual(index.ntotal, self.nb) + + # Test search interface + k = 4 + D, I = index.search(self.xq, k) + self.assertEqual(D.shape, (self.nq, k)) + self.assertEqual(I.shape, (self.nq, k)) + self.assertTrue(np.all(I >= 0)) + self.assertTrue(np.all(I < self.nb)) + + # Test remove + ids = np.arange(index.ntotal) + toremove = np.ascontiguousarray(ids[0:200:3]) + sel = faiss.IDSelectorArray(50, faiss.swig_ptr(toremove[:50])) + nremove = index.remove_ids(sel) + nremove += index.remove_ids(toremove[50:]) + + self.assertEqual(nremove, len(toremove)) + + # remove more to trigger cleanup + toremove = np.ascontiguousarray(ids[200:800]) + nremove = index.remove_ids(toremove) + self.assertEqual(nremove, len(toremove)) + + # Test reset + index.reset() + self.assertEqual(index.ntotal, 0) + + def test_svs_search_selected(self): + """Test FAISS search with IDSelector interface compatibility""" + index = self._create_instance() + + # Test add interface + index.add(self.xb) + self.assertEqual(index.ntotal, self.nb) + + # Create selector to select a subset of ids + min = self.nb // 5 + max = self.nb * 4 // 5 + sel = faiss.IDSelectorRange(min, max) # select ids in [100, 200) + params = faiss.SearchParameters(sel=sel) + + # Test search interface + k = 10 + D, I = index.search(self.xq, k, params=params) + self.assertEqual(D.shape, (self.nq, k)) + self.assertEqual(I.shape, (self.nq, k)) + self.assertTrue(np.all(I >= min)) + self.assertTrue(np.all(I < max)) + + def test_svs_range_search(self): + """Test FAISS range_search interface compatibility""" + index = self._create_instance() + + # Test add interface + index.add(self.xb) + self.assertEqual(index.ntotal, self.nb) + + # Test search interface + range = 0.1 + lims, D, I = index.range_search(self.xq, range) + self.assertEqual(D.shape, I.shape) + self.assertTrue(np.all(D <= range)) + self.assertTrue(np.all(I >= 0)) + self.assertTrue(np.all(I < self.nb)) + + def test_svs_range_search_ip(self): + """Test FAISS range_search interface compatibility""" + index = self._create_instance() + index.metric_type = faiss.METRIC_INNER_PRODUCT + index.alpha = 0.95 + + # Test add interface + index.add(self.xb) + self.assertEqual(index.ntotal, self.nb) + + # Test search interface + range = 10 + lims, D, I = index.range_search(self.xq, range) + self.assertEqual(D.shape, I.shape) + self.assertTrue(np.all(D >= range)) + self.assertTrue(np.all(I >= 0)) + self.assertTrue(np.all(I < self.nb)) + + def test_svs_range_search_selected(self): + """Test FAISS add/search/remove_ids interface compatibility""" + index = self._create_instance() + + # Test add interface + index.add(self.xb) + self.assertEqual(index.ntotal, self.nb) + + # Create selector to select a subset of ids + min = self.nb // 5 + max = self.nb * 4 // 5 + sel = faiss.IDSelectorRange(min, max) # select ids in [100, 200) + params = faiss.SearchParameters(sel=sel) + + # Test search interface + radius = 0.1 + lims, D, I = index.range_search(self.xq, radius, params=params) + self.assertEqual(D.shape, I.shape) + self.assertTrue(np.all(D <= radius)) + self.assertTrue(np.all(I >= min)) + self.assertTrue(np.all(I < max)) + + # Test reset + index.reset() + self.assertEqual(index.ntotal, 0) + + def test_svs_metric_types(self): + """Test different metric types are handled correctly""" + # L2 metric + index_l2 = self._create_instance() + index_l2.metric_type = faiss.METRIC_L2 + index_l2.add(self.xb) + D_l2, _ = index_l2.search(self.xq[:10], 4) + + index_ip = self._create_instance() + index_ip.metric_type = faiss.METRIC_INNER_PRODUCT + index_ip.alpha = 0.95 + index_ip.add(self.xb) + D_ip, _ = index_ip.search(self.xq[:10], 4) + + # Results should be different (testing adapter forwards metric correctly) + self.assertFalse(np.array_equal(D_l2, D_ip)) + + def test_svs_serialization(self): + """Test FAISS serialization system works with SVS indices""" + index = self._create_instance() + + index.add(self.xb) + D_before, I_before = index.search(self.xq, 4) + + loaded = faiss.deserialize_index(faiss.serialize_index(index)) + # Verify adapter layer preserves type and parameters + self.assertIsInstance(loaded, self.target_class) + self.assertEqual(loaded.d, self.d) + self.assertEqual(loaded.ntotal, self.nb) + self.assertEqual(loaded.metric_type, index.metric_type) + + # Verify functionality is preserved + D_after, I_after = loaded.search(self.xq, 4) + np.testing.assert_array_equal(I_before, I_after) + np.testing.assert_allclose(D_before, D_after, rtol=1e-6) + + def test_svs_error_handling(self): + """Test that FAISS error handling works with SVS indices""" + index = self._create_instance() + + # Test wrong dimension + wrong_dim_data = np.random.random((100, self.d + 1)).astype('float32') + with self.assertRaises(AssertionError): + index.add(wrong_dim_data) + + def test_svs_fourcc_handling(self): + """Test that FAISS I/O system handles SVS fourccs correctly""" + # Create and populate index + index = self._create_instance() + index.add(self.xb[:100]) # Smaller dataset for speed + + # Test round-trip serialization preserves exact type + loaded = faiss.deserialize_index(faiss.serialize_index(index)) + + # Verify exact type preservation (fourcc working correctly) + self.assertEqual(type(loaded), self.target_class) + + def test_svs_batch_operations(self): + """Test that batch operations work correctly through adapter""" + index = self._create_instance() + + # Add in multiple batches + batch_size = 250 + for i in range(0, self.nb, batch_size): + end_idx = min(i + batch_size, self.nb) + index.add(self.xb[i:end_idx]) + + self.assertEqual(index.ntotal, self.nb) + + # Verify search still works after batch operations + D, _ = index.search(self.xq, 4) + self.assertEqual(D.shape, (self.nq, 4)) + +@unittest.skipIf(_SKIP_SVS, _SKIP_REASON) +class TestSVSFactory(unittest.TestCase): + """Test that SVS factory works correctly""" + + def test_svs_factory_flat(self): + index = faiss.index_factory(32, "SVSFlat") + self.assertEqual(index.d, 32) + + def test_svs_factory_vamana(self): + index = faiss.index_factory(32, "SVSVamana64") + self.assertEqual(index.d, 32) + self.assertEqual(index.graph_max_degree, 64) + self.assertEqual(index.metric_type, faiss.METRIC_L2) + self.assertEqual(index.storage_kind, faiss.SVS_FP32) + + def test_svs_factory_fp16(self): + index = faiss.index_factory(256, "SVSVamana16,FP16") + self.assertEqual(index.d, 256) + self.assertEqual(index.graph_max_degree, 16) + self.assertEqual(index.storage_kind, faiss.SVS_FP16) + + def test_svs_factory_sqi8(self): + index = faiss.index_factory(64, "SVSVamana24,SQI8") + self.assertEqual(index.d, 64) + self.assertEqual(index.graph_max_degree, 24) + self.assertEqual(index.storage_kind, faiss.SVS_SQI8) + +@unittest.skipIf(_SKIP_SVS_LL, _SKIP_SVS_LL_REASON) +class TestSVSFactoryLVQLeanVec(unittest.TestCase): + """Test that SVS factory works correctly for LVQ and LeanVec""" + + def test_svs_factory_lvq(self): + index = faiss.index_factory(16, "SVSVamana32,LVQ4x8") + self.assertEqual(index.d, 16) + self.assertEqual(index.graph_max_degree, 32) + self.assertEqual(index.storage_kind, faiss.SVS_LVQ4x8) + + def test_svs_factory_leanvec(self): + index = faiss.index_factory(128, "SVSVamana48,LeanVec4x4_64") + self.assertEqual(index.d, 128) + self.assertEqual(index.graph_max_degree, 48) + self.assertEqual(index.storage_kind, faiss.SVS_LeanVec4x4) + self.assertEqual(index.leanvec_d, 64) + + +@unittest.skipIf(_SKIP_SVS, _SKIP_REASON) +class TestSVSAdapterFP16(TestSVSAdapter): + """Repeat all tests for SVS Float16 variant""" + def _create_instance(self): + idx = self.target_class(self.d, 64) + idx.storage_kind = faiss.SVS_FP16 + return idx + +@unittest.skipIf(_SKIP_SVS, _SKIP_REASON) +class TestSVSAdapterSQI8(TestSVSAdapter): + """Repeat all tests for SVS SQ int8 variant""" + def _create_instance(self): + idx = self.target_class(self.d, 64) + idx.storage_kind = faiss.SVS_SQI8 + return idx + +@unittest.skipIf(_SKIP_SVS_LL, _SKIP_SVS_LL_REASON) +class TestSVSAdapterLVQ4x0(TestSVSAdapter): + """Repeat all tests for SVSLVQ4x0 variant""" + + @classmethod + def setUpClass(cls): + cls.target_class = faiss.IndexSVSVamanaLVQ + + def _create_instance(self): + idx = self.target_class(self.d, 64) + idx.storage_kind = faiss.SVS_LVQ4x0 + return idx + +@unittest.skipIf(_SKIP_SVS_LL, _SKIP_SVS_LL_REASON) +class TestSVSAdapterLVQ4x4(TestSVSAdapter): + """Repeat all tests for SVSLVQ4x4 variant""" + + @classmethod + def setUpClass(cls): + cls.target_class = faiss.IndexSVSVamanaLVQ + + def _create_instance(self): + idx = self.target_class(self.d, 64) + idx.storage_kind = faiss.SVS_LVQ4x4 + return idx + +@unittest.skipIf(_SKIP_SVS_LL, _SKIP_SVS_LL_REASON) +class TestSVSAdapterLVQ4x8(TestSVSAdapter): + """Repeat all tests for SVSLVQ4x8 variant""" + + @classmethod + def setUpClass(cls): + cls.target_class = faiss.IndexSVSVamanaLVQ + + def _create_instance(self): + idx = self.target_class(self.d, 64) + idx.storage_kind = faiss.SVS_LVQ4x8 + return idx + +class TestSVSAdapterFlat(TestSVSAdapter): + """Repeat all tests for SVSFlat variant""" + + @classmethod + def setUpClass(cls): + cls.target_class = faiss.IndexSVSFlat + + def _create_instance(self): + return self.target_class(self.d) + + def test_svs_metric_types(self): + """Test different metric types are handled correctly""" + # L2 metric + index_l2 = self._create_instance() + index_l2.metric_type = faiss.METRIC_L2 + index_l2.add(self.xb) + D_l2, _ = index_l2.search(self.xq[:10], 4) + + index_ip = self._create_instance() + index_ip.metric_type = faiss.METRIC_INNER_PRODUCT + index_ip.add(self.xb) + D_ip, _ = index_ip.search(self.xq[:10], 4) + + # Results should be different (testing adapter forwards metric correctly) + self.assertFalse(np.array_equal(D_l2, D_ip)) + + # The fowlloing tests are expected to fail for IndexSVSFlat as it doesn't support yet + @unittest.expectedFailure + def test_svs_search_selected(self): + return super().test_svs_search_selected() + + @unittest.expectedFailure + def test_svs_range_search(self): + return super().test_svs_range_search() + + @unittest.expectedFailure + def test_svs_range_search_ip(self): + return super().test_svs_range_search_ip() + + @unittest.expectedFailure + def test_svs_range_search_selected(self): + return super().test_svs_range_search_selected() + + @unittest.expectedFailure + def test_svs_add_search_remove_interface(self): + super().test_svs_add_search_remove_interface() + + @unittest.expectedFailure + def test_svs_batch_operations(self): + super().test_svs_batch_operations() + + +@unittest.skipIf(_SKIP_SVS, _SKIP_REASON) +class TestSVSVamanaParameters(unittest.TestCase): + """Test Vamana-specific parameter forwarding and persistence for SVS Vamana variants""" + + @classmethod + def setUpClass(cls): + cls.target_class = faiss.IndexSVSVamana + + def _create_instance(self): + """Create an instance of the SVS Vamana index""" + return self.target_class(self.d ,64) + + def setUp(self): + self.d = 32 + self.nb = 500 # Smaller dataset for parameter tests + self.nq = 50 + np.random.seed(1234) + self.xb = np.random.random((self.nb, self.d)).astype('float32') + self.xq = np.random.random((self.nq, self.d)).astype('float32') + + def test_vamana_parameter_setting(self): + """Test that all Vamana parameters can be set and retrieved""" + index = self._create_instance() + + # Set non-default values for all parameters + index.graph_max_degree = 32 + index.alpha = 1.5 + index.search_window_size = 20 + index.search_buffer_capacity = 25 + index.construction_window_size = 80 + index.max_candidate_pool_size = 150 + index.prune_to = 30 + index.use_full_search_history = False + + # Verify all parameters are set correctly + self.assertEqual(index.graph_max_degree, 32) + self.assertAlmostEqual(index.alpha, 1.5, places=6) + self.assertEqual(index.search_window_size, 20) + self.assertEqual(index.search_buffer_capacity, 25) + self.assertEqual(index.construction_window_size, 80) + self.assertEqual(index.max_candidate_pool_size, 150) + self.assertEqual(index.prune_to, 30) + self.assertEqual(index.use_full_search_history, False) + + def test_vamana_parameter_defaults(self): + """Test that Vamana parameters have correct default values""" + index = self._create_instance() + + # Verify default values match C++ header + self.assertEqual(index.graph_max_degree, 64) + self.assertAlmostEqual(index.alpha, 1.2, places=6) + self.assertEqual(index.search_window_size, 10) + self.assertEqual(index.search_buffer_capacity, 10) + self.assertEqual(index.construction_window_size, 40) + self.assertEqual(index.max_candidate_pool_size, 200) + self.assertEqual(index.prune_to, 60) + self.assertEqual(index.use_full_search_history, True) + + def test_vamana_parameter_serialization(self): + """Test that all Vamana parameters are preserved through serialization""" + index = self._create_instance() + + # Set distinctive non-default values + index.graph_max_degree = 48 + index.alpha = 1.8 + index.search_window_size = 15 + index.search_buffer_capacity = 18 + index.construction_window_size = 60 + index.max_candidate_pool_size = 180 + index.prune_to = 45 + index.use_full_search_history = False + + # Add data and train + index.add(self.xb) + + # Serialize and deserialize + loaded = faiss.deserialize_index(faiss.serialize_index(index)) + + # Verify all parameters are preserved + self.assertIsInstance(loaded, self.target_class) + self.assertEqual(loaded.graph_max_degree, 48) + self.assertAlmostEqual(loaded.alpha, 1.8, places=6) + self.assertEqual(loaded.search_window_size, 15) + self.assertEqual(loaded.search_buffer_capacity, 18) + self.assertEqual(loaded.construction_window_size, 60) + self.assertEqual(loaded.max_candidate_pool_size, 180) + self.assertEqual(loaded.prune_to, 45) + self.assertEqual(loaded.use_full_search_history, False) + + # Verify results are unaffected + D_before, I_before = index.search(self.xq, 4) + D_after, I_after = loaded.search(self.xq, 4) + np.testing.assert_array_equal(I_before, I_after) + np.testing.assert_allclose(D_before, D_after, rtol=1e-6) + + +@unittest.skipIf(_SKIP_SVS, _SKIP_REASON) +class TestSVSVamanaParametersFP16(TestSVSVamanaParameters): + """Repeat Vamana parameter tests for SVS Float16 variant""" + def _create_instance(self): + idx = self.target_class(self.d, 64) + idx.storage_kind = faiss.SVS_FP16 + return idx + +@unittest.skipIf(_SKIP_SVS, _SKIP_REASON) +class TestSVSVamanaParametersSQI8(TestSVSVamanaParameters): + """Repeat Vamana parameter tests for SVS SQ int8 variant""" + def _create_instance(self): + idx = self.target_class(self.d, 64) + idx.storage_kind = faiss.SVS_SQI8 + return idx + +@unittest.skipIf(_SKIP_SVS_LL, _SKIP_SVS_LL_REASON) +class TestSVSVamanaParametersLVQ4x0(TestSVSVamanaParameters): + """Repeat Vamana parameter tests for SVSLVQ4x0 variant""" + + @classmethod + def setUpClass(cls): + cls.target_class = faiss.IndexSVSVamanaLVQ + + def _create_instance(self): + idx = self.target_class(self.d, 64) + idx.storage_kind = faiss.SVS_LVQ4x0 + return idx + +@unittest.skipIf(_SKIP_SVS_LL, _SKIP_SVS_LL_REASON) +class TestSVSVamanaParametersLVQ4x4(TestSVSVamanaParameters): + """Repeat Vamana parameter tests for SVSLVQ4x4 variant""" + + @classmethod + def setUpClass(cls): + cls.target_class = faiss.IndexSVSVamanaLVQ + + def _create_instance(self): + idx = self.target_class(self.d, 64) + idx.storage_kind = faiss.SVS_LVQ4x4 + return idx + +@unittest.skipIf(_SKIP_SVS_LL, _SKIP_SVS_LL_REASON) +class TestSVSVamanaParametersLVQ4x8(TestSVSVamanaParameters): + """Repeat Vamana parameter tests for SVSLVQ4x8 variant""" + + @classmethod + def setUpClass(cls): + cls.target_class = faiss.IndexSVSVamanaLVQ + + def _create_instance(self): + idx = self.target_class(self.d, 64) + idx.storage_kind = faiss.SVS_LVQ4x8 + return idx + +if __name__ == '__main__': + unittest.main() diff --git a/tutorial/cpp/10-SVS-Vamana-LVQ.cpp b/tutorial/cpp/10-SVS-Vamana-LVQ.cpp new file mode 100644 index 0000000000..c74778ba74 --- /dev/null +++ b/tutorial/cpp/10-SVS-Vamana-LVQ.cpp @@ -0,0 +1,88 @@ +/* + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/* + * Portions 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 +#include + +#include + +using idx_t = faiss::idx_t; + +int main() { + int d = 64; // dimension + int nb = 100000; // database size + int nq = 10000; // nb of queries + + std::mt19937 rng; + std::uniform_real_distribution<> distrib; + + float* xb = new float[d * nb]; + float* xq = new float[d * nq]; + + for (int i = 0; i < nb; i++) { + for (int j = 0; j < d; j++) + xb[d * i + j] = distrib(rng); + xb[d * i] += i / 1000.; + } + + for (int i = 0; i < nq; i++) { + for (int j = 0; j < d; j++) + xq[d * i + j] = distrib(rng); + xq[d * i] += i / 1000.; + } + + int k = 4; + + faiss::IndexSVSVamanaLVQ index(d, 64); + index.add(nb, xb); + + { // search xq + idx_t* I = new idx_t[k * nq]; + float* D = new float[k * nq]; + + index.search(nq, xq, k, D, I); + + printf("I=\n"); + for (int i = nq - 5; i < nq; i++) { + for (int j = 0; j < k; j++) + printf("%5zd ", I[i * k + j]); + printf("\n"); + } + + printf("D=\n"); + for (int i = nq - 5; i < nq; i++) { + for (int j = 0; j < k; j++) + printf("%5f ", D[i * k + j]); + printf("\n"); + } + + delete[] I; + delete[] D; + } + + delete[] xb; + delete[] xq; + + return 0; +} diff --git a/tutorial/cpp/11-SVS-Vamana-LeanVec.cpp b/tutorial/cpp/11-SVS-Vamana-LeanVec.cpp new file mode 100644 index 0000000000..cfd020bb47 --- /dev/null +++ b/tutorial/cpp/11-SVS-Vamana-LeanVec.cpp @@ -0,0 +1,89 @@ +/* + * Portions Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/* + * Portions 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 +#include + +#include + +using idx_t = faiss::idx_t; + +int main() { + int d = 64; // dimension + int nb = 100000; // database size + int nq = 10000; // nb of queries + + std::mt19937 rng; + std::uniform_real_distribution<> distrib; + + float* xb = new float[d * nb]; + float* xq = new float[d * nq]; + + for (int i = 0; i < nb; i++) { + for (int j = 0; j < d; j++) + xb[d * i + j] = distrib(rng); + xb[d * i] += i / 1000.; + } + + for (int i = 0; i < nq; i++) { + for (int j = 0; j < d; j++) + xq[d * i + j] = distrib(rng); + xq[d * i] += i / 1000.; + } + + int k = 4; + + faiss::IndexSVSVamanaLeanVec index(d, 32); + index.train(nb, xb); + index.add(nb, xb); + + { // search xq + idx_t* I = new idx_t[k * nq]; + float* D = new float[k * nq]; + + index.search(nq, xq, k, D, I); + + printf("I=\n"); + for (int i = nq - 5; i < nq; i++) { + for (int j = 0; j < k; j++) + printf("%5zd ", I[i * k + j]); + printf("\n"); + } + + printf("D=\n"); + for (int i = nq - 5; i < nq; i++) { + for (int j = 0; j < k; j++) + printf("%5f ", D[i * k + j]); + printf("\n"); + } + + delete[] I; + delete[] D; + } + + delete[] xb; + delete[] xq; + + return 0; +} diff --git a/tutorial/cpp/CMakeLists.txt b/tutorial/cpp/CMakeLists.txt index 045c1bb092..68a1118444 100644 --- a/tutorial/cpp/CMakeLists.txt +++ b/tutorial/cpp/CMakeLists.txt @@ -29,3 +29,14 @@ target_link_libraries(8-PQFastScanRefine PRIVATE faiss) add_executable(9-RefineComparison EXCLUDE_FROM_ALL 9-RefineComparison.cpp) target_link_libraries(9-RefineComparison PRIVATE faiss) + +if(FAISS_ENABLE_SVS) + + add_executable(10-SVS-Vamana-LVQ EXCLUDE_FROM_ALL 10-SVS-Vamana-LVQ.cpp) + target_link_libraries(10-SVS-Vamana-LVQ PRIVATE faiss) + target_compile_definitions(10-SVS-Vamana-LVQ PRIVATE FAISS_ENABLE_SVS FAISS_SVS_RUNTIME_VERSION=${FAISS_SVS_RUNTIME_VERSION}) + + add_executable(11-SVS-Vamana-LeanVec EXCLUDE_FROM_ALL 11-SVS-Vamana-LeanVec.cpp) + target_link_libraries(11-SVS-Vamana-LeanVec PRIVATE faiss) + target_compile_definitions(11-SVS-Vamana-LeanVec PRIVATE FAISS_ENABLE_SVS FAISS_SVS_RUNTIME_VERSION=${FAISS_SVS_RUNTIME_VERSION}) +endif() diff --git a/tutorial/python/11-SVS.py b/tutorial/python/11-SVS.py new file mode 100644 index 0000000000..d58a185861 --- /dev/null +++ b/tutorial/python/11-SVS.py @@ -0,0 +1,83 @@ +# Portions Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. +# +# Portions 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. +# + +import numpy as np + +d = 64 # dimension +nb = 100000 # database size +nq = 10000 # nb of queries +np.random.seed(1234) # make reproducible +xb = np.random.random((nb, d)).astype('float32') +xb[:, 0] += np.arange(nb) / 1000. +xq = np.random.random((nq, d)).astype('float32') +xq[:, 0] += np.arange(nq) / 1000. + +import faiss # make faiss available +index = faiss.IndexSVSVamana(d, 64) # build the index (DynamicVamana, float32) + +print(index.is_trained) +index.add(xb) # add vectors to the index +print(index.ntotal) + +k = 4 # we want to see 4 nearest neighbors + +print(f"{k} nearest neighbors of the first 5 vectors") +D, I = index.search(xb[:5], k) # sanity check +print(I) +print(D) +D, I = index.search(xq, k) # actual search +print(f"{k} nearest neighbors of the 5 first query vectors") +print(I[:5]) # neighbors of the 5 first queries +print(f"{k} nearest neighbors of the 5 last query vectors") +print(I[-5:]) # neighbors of the 5 last queries + +faiss.write_index(index, "index.faiss") +reloaded = faiss.read_index("index.faiss") + +D, I = reloaded.search(xq, k) # search with the reloaded +print(f"{k} nearest neighbors of the 5 first query vectors (after reloading)") +print(I[:5]) # neighbors of the 5 first queries +print(f"{k} nearest neighbors of the 5 last query vectors (after reloading)") +print(I[-5:]) # neighbors of the 5 last queries + +flat_idx_fac = faiss.index_factory(d, 'SVSFlat', faiss.METRIC_L2) # example of using factory for SVS Flat +flat_idx_fac.add(xb) +flat_idx_fac.search(xq, k) + +uncompressed_idx_fac = faiss.index_factory(d, 'SVSVamana64', faiss.METRIC_L2) # example of using factory for SVS Vamana uncompressed +uncompressed_idx_fac.add(xb) +uncompressed_idx_fac.search(xq, k) + +lvq_idx = faiss.IndexSVSVamanaLVQ(d, faiss.METRIC_L2, faiss.LVQ4x8) # example of using SVS Vamana LVQ +lvq_idx_fac_2 = faiss.index_factory(d, 'SVSVamana32,LVQ4x4', faiss.METRIC_L2) # example of using factory for SVS Vamana LVQ +lvq_idx_fac_2.add(xb) +lvq_idx_fac_2.search(xq, k) + + +leanvec_idx = faiss.IndexSVSVamanaLeanVec(d, faiss.METRIC_L2, 0, faiss.LeanVec4x4) # example of using SVS Vamana LeanVec +leanvec_idx_fac = faiss.index_factory(d, 'SVSVamana32,LeanVec4x4', faiss.METRIC_L2) # example of using factory for SVS Vamana LeanVec +leanvec_idx_fac.train(xb) +leanvec_idx_fac.add(xb) +leanvec_idx_fac.search(xq, k) + +leanvec_idx_fac2 = faiss.index_factory(d, 'SVSVamana64,LeanVec4x4_16', faiss.METRIC_L2) # example of using factory for SVS Vamana LeanVec. leanvec_dim is 16 +leanvec_idx_fac2.train(xb) +leanvec_idx_fac2.add(xb) +leanvec_idx_fac2.search(xq, k)