[MMAP] Add in-memory (mmap) stream index mapping.#310
[MMAP] Add in-memory (mmap) stream index mapping.#310
Conversation
1b35ca7 to
9e5588f
Compare
|
|
||
| // Load from a memory-mapped file. | ||
| // The file is expected to be in the format produced by save(). | ||
| static Status map_to_file( | ||
| VamanaIndex** index, const char* path, MetricType metric, StorageKind storage_kind | ||
| ) noexcept; | ||
|
|
||
| // Load from a memory buffer. | ||
| // The buffer is expected to be in the format produced by save(). | ||
| static Status map_to_memory( | ||
| VamanaIndex** index, | ||
| const void* data, | ||
| size_t size, | ||
| MetricType metric, | ||
| StorageKind storage_kind | ||
| ) noexcept; |
…ests - Introduced `memstream.h` and `memstream.cpp` for memory-mapped stream functionality. - Updated `io.h` and `simple.h` to include memory stream support. - Enhanced `simple.cpp` and `flat.cpp` tests to validate loading from memory streams.
…emory and memory-mapped stream support
9e5588f to
a5ed391
Compare
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds mmap/in-memory stream support for loading indices and datasets (zero-copy “view” loading) across the core library and C++ runtime API, plus tests to validate behavior.
Changes:
- Introduces
mmstream/mmstreambuf,is_memory_stream, and pointer helpers (current_ptr/begin_ptr/end_ptr) plus aMemoryStreamAllocatorto enable view-based, zero-copy loading. - Updates loading paths (e.g.,
SimpleData,SimpleGraph, Vamana graph selection) to support view allocators and to enforce memory-stream requirements. - Extends C++ runtime API with
map_to_file/map_to_memoryfor Flat and Vamana indices and adds new coverage.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| include/svs/core/io/memstream.h | Adds mmapped streambuf/istream + memory-stream utilities and allocator for zero-copy loads |
| tests/svs/core/io/memstream.cpp | Adds unit tests for mmstream, pointer helpers, and memory-stream detection |
| include/svs/core/data/simple.h | Adds view-only load path from ContextFreeLoadTable + memory-stream enforcement |
| include/svs/core/data/io.h | Skips populate() for view allocators; enforces memory-stream constraint |
| include/svs/core/graph/graph.h | Allows forwarding allocator args during stream load to support view allocators |
| include/svs/orchestrators/vamana.h | Adjusts graph type selection for view-allocated data (mmapped/index views) |
| include/svs/lib/array.h | Generalizes “view allocator” DenseArray specialization to support MemoryStreamAllocator |
| include/svs/quantization/scalar/scalar.h | Fixes min/max init and adjusts SQDataset load APIs + adds mutable get_datum |
| bindings/cpp/include/svs/runtime/flat_index.h | Adds map_to_file / map_to_memory for FlatIndex |
| bindings/cpp/src/flat_index.cpp | Implements FlatIndex mapping APIs |
| bindings/cpp/src/flat_index_impl.h | Holds mapped stream lifetime and adds map_to_stream implementation |
| bindings/cpp/include/svs/runtime/vamana_index.h | Adds map_to_file / map_to_memory for VamanaIndex |
| bindings/cpp/src/vamana_index.cpp | Implements VamanaIndex mapping APIs |
| bindings/cpp/src/vamana_index_impl.h | Holds mapped stream lifetime and adds map_to_stream implementation |
| bindings/cpp/tests/runtime_test.cpp | Adds runtime tests for mapping APIs across storage kinds |
| tests/svs/core/data/simple.cpp | Adds tests for loading SimpleDataView from stringstream + error-path test |
| tests/svs/quantization/scalar/scalar.cpp | Adds SQDataset view-load test from stringstream |
| tests/svs/index/flat/flat.cpp | Adds FlatIndex view-load tests from stringstream and mmapped file |
| tests/svs/index/vamana/index.cpp | Adds Vamana view-load tests from stringstream/mmapped file + SQ view-load test |
| tests/CMakeLists.txt | Registers new memstream test source |
748365f to
f04b1c2
Compare
| template <typename CharT, typename Traits> | ||
| struct StreambufAccessor : std::basic_streambuf<CharT, Traits> { | ||
| static CharT* get(std::basic_streambuf<CharT, Traits>* buf) noexcept { | ||
| // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) | ||
| return static_cast<StreambufAccessor*>(buf)->gptr(); | ||
| } | ||
|
|
||
| static CharT* begin(std::basic_streambuf<CharT, Traits>* buf) noexcept { | ||
| // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) | ||
| return static_cast<StreambufAccessor*>(buf)->eback(); | ||
| } | ||
|
|
||
| static CharT* end(std::basic_streambuf<CharT, Traits>* buf) noexcept { | ||
| // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) | ||
| return static_cast<StreambufAccessor*>(buf)->egptr(); | ||
| } | ||
| }; |
There was a problem hiding this comment.
The static_cast<StreambufAccessor*>(buf) downcast is undefined behavior because buf does not actually point to a StreambufAccessor object. This can break across standard library implementations/RTTI modes and is a memory-safety risk. Prefer a defined mechanism: either (a) only support current_ptr/begin_ptr/end_ptr for SVS-owned streambuf types that expose gptr/eback/egptr via a public virtual interface, or (b) explicitly dynamic_cast to known concrete types (basic_mmstreambuf, basic_spanbuf) and return nullptr for std::basic_stringbuf (or require callers to use ispanstream/mmstream for zero-copy).
| template <typename T, typename CharT, typename Traits = std::char_traits<CharT>> | ||
| [[nodiscard]] T* current_ptr(std::basic_istream<CharT, Traits>& stream) noexcept { | ||
| static_assert(sizeof(CharT) == 1, "current_ptr requires a 1-byte character type."); | ||
| if (!is_memory_stream(stream)) { | ||
| return nullptr; | ||
| } | ||
|
|
||
| auto* buf = stream.rdbuf(); | ||
| auto begin = detail::StreambufAccessor<CharT, Traits>::begin(buf); | ||
| auto end = detail::StreambufAccessor<CharT, Traits>::end(buf); | ||
| if (begin == end) { | ||
| return nullptr; | ||
| } | ||
| auto raw = detail::StreambufAccessor<CharT, Traits>::get(buf); | ||
|
|
||
| // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) | ||
| return reinterpret_cast<T*>(raw); | ||
| } |
There was a problem hiding this comment.
reinterpret_cast<T*>(raw) can produce misaligned T* when the stream position is not aligned for T (common when reading past headers), which is undefined behavior if the returned pointer is dereferenced. Add an alignment check (e.g., reinterpret_cast<std::uintptr_t>(raw) % alignof(T) == 0) and return nullptr/throw when misaligned, and consider also validating that the remaining buffer size is at least sizeof(T) (or documenting that callers must ensure alignment/size).
| basic_mmstreambuf* open( | ||
| const std::filesystem::path& path, | ||
| std::ios_base::openmode mode = std::ios_base::in | std::ios_base::out | ||
| ) { |
There was a problem hiding this comment.
basic_mmstreambuf::open defaults to in | out, which selects ReadWrite mappings by default even though the streambuf explicitly disallows writing (setp(&empty_, &empty_)). This can cause unexpected failures when mapping read-only files and requests stronger permissions than necessary. Consider defaulting to std::ios_base::in (read-only) and only enabling ReadWrite when the implementation actually supports writes for the put area.
| auto perm = | ||
| (mode & std::ios_base::out) ? MemoryMapper::ReadWrite : MemoryMapper::ReadOnly; |
There was a problem hiding this comment.
basic_mmstreambuf::open defaults to in | out, which selects ReadWrite mappings by default even though the streambuf explicitly disallows writing (setp(&empty_, &empty_)). This can cause unexpected failures when mapping read-only files and requests stronger permissions than necessary. Consider defaulting to std::ios_base::in (read-only) and only enabling ReadWrite when the implementation actually supports writes for the put area.
| [[nodiscard]] pointer allocate(size_type n) { | ||
| T* current = current_ptr<T>(stream_); | ||
| if (current == nullptr) { | ||
| throw std::runtime_error("Failed to obtain current pointer from memory stream." | ||
| ); | ||
| } | ||
| pointer result = current; | ||
| stream_.seekg(n * sizeof(T), std::ios_base::cur); | ||
| if (!stream_) { | ||
| throw std::runtime_error("Failed to advance memory stream after allocation."); | ||
| } | ||
| return result; | ||
| } |
There was a problem hiding this comment.
n * sizeof(T) can overflow size_t (especially for n derived from serialized sizes), leading to an incorrect/negative seek and potentially returning a pointer to an undersized region. Add an overflow check before multiplication and ensure the seek distance fits into std::streamoff/std::streamsize on the target platform.
| #include <array> | ||
| #include <cstddef> | ||
| #include <filesystem> | ||
| #include <fstream> | ||
| #include <span> | ||
| #include <sstream> | ||
| #include <string> | ||
| #include <system_error> | ||
| #include <utility> |
There was a problem hiding this comment.
std::strlen is used but <cstring> is not included in this test file, which can cause compilation failures depending on standard library headers included transitively. Add #include <cstring>.
| CATCH_REQUIRE(base_ptr != nullptr); | ||
| CATCH_REQUIRE(*base_ptr == 'H'); | ||
|
|
||
| for (std::size_t i = 0; i < std::strlen(text); ++i) { |
There was a problem hiding this comment.
std::strlen is used but <cstring> is not included in this test file, which can cause compilation failures depending on standard library headers included transitively. Add #include <cstring>.
| svs_test::prepare_temp_directory(); | ||
| auto temp_dir = svs_test::temp_directory(); | ||
| auto filename = temp_dir / "index_test.bin"; | ||
|
|
||
| // Serialize | ||
| std::ofstream out(filename, std::ios::binary); |
There was a problem hiding this comment.
The mapped-index tests always write to the fixed filename index_test.bin, which risks collisions if tests are ever executed concurrently (e.g., parallel CTest) or re-entered. Prefer a unique filename per test invocation (e.g., incorporate PID, timestamp, or a monotonically increasing counter / use UniqueTempDirectory like other tests).
…stream&) to accept custom accessors
…views Co-authored-by: Copilot <copilot@github.com>
|
Please address CI failure before merging |
|
And if it is expected to require updates from private source to work correctly then we'll need to update https://github.com/intel/ScalableVectorSearch/blob/main/bindings/cpp/CMakeLists.txt#L126 with LTO shared lib build from the private PR (can publish as nightly here) |
Co-authored-by: Copilot <copilot@github.com>
…ed allocator handling Co-authored-by: Copilot <copilot@github.com>
This pull request adds to CPP Runtime API support for loading vector search indices (
FlatIndexandVamanaIndex) from memory-mapped files and memory buffers, in addition to existing stream-based loading. It introduces new API methods, updates internal implementations to manage mapped resources, and adds comprehensive tests to ensure correct behavior for all supported storage types.Major new features:
map_to_fileandmap_to_memorystatic methods to bothFlatIndexandVamanaIndexclasses, allowing indices to be loaded directly from memory-mapped files or memory buffers, in addition to streams. [1] [2]Internal implementation changes:
FlatIndexImplandVamanaIndexImplto support mapping from streams, and to manage the lifetime of mapped streams viaunique_ptrmembers. [1] [2] [3] [4] [5]Testing improvements:
write_and_map_index) and comprehensive test cases to verify correct loading and querying of indices from memory-mapped files for all supported storage kinds and index types. [1] [2] [3] [4] [5] [6] [7]