Skip to content

[MMAP] Add in-memory (mmap) stream index mapping.#310

Open
rfsaliev wants to merge 13 commits intomainfrom
rfsaliev/mmap-loading
Open

[MMAP] Add in-memory (mmap) stream index mapping.#310
rfsaliev wants to merge 13 commits intomainfrom
rfsaliev/mmap-loading

Conversation

@rfsaliev
Copy link
Copy Markdown
Member

@rfsaliev rfsaliev commented Apr 3, 2026

This pull request adds to CPP Runtime API support for loading vector search indices (FlatIndex and VamanaIndex) 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:

  • Added map_to_file and map_to_memory static methods to both FlatIndex and VamanaIndex classes, allowing indices to be loaded directly from memory-mapped files or memory buffers, in addition to streams. [1] [2]
  • Implemented the corresponding logic in the source files, including resource management to keep memory-mapped streams alive as long as the index exists. [1] [2]

Internal implementation changes:

  • Added and updated constructors and helper methods in FlatIndexImpl and VamanaIndexImpl to support mapping from streams, and to manage the lifetime of mapped streams via unique_ptr members. [1] [2] [3] [4] [5]
  • Included necessary headers for memory stream support in relevant files. [1] [2] [3] [4]
  • Updated dataset loading logic to enforce that view allocators are only used with memory streams, improving safety and correctness.

Testing improvements:

  • Added a generic test helper (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]

@rfsaliev rfsaliev requested a review from razdoburdin April 3, 2026 13:25
@rfsaliev rfsaliev force-pushed the rfsaliev/mmap-loading branch 2 times, most recently from 1b35ca7 to 9e5588f Compare April 14, 2026 13:32
Comment on lines +98 to +113

// 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;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexanderguzhva, is this API suitable?

Base automatically changed from dev/razdoburdin_streaming to main April 16, 2026 09:55
…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.
@rfsaliev rfsaliev force-pushed the rfsaliev/mmap-loading branch from 9e5588f to a5ed391 Compare April 16, 2026 14:25
@rfsaliev rfsaliev marked this pull request as ready for review April 20, 2026 09:54
@rfsaliev rfsaliev requested a review from Copilot April 20, 2026 10:00
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 a MemoryStreamAllocator to 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_memory for 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

Comment thread include/svs/core/io/memstream.h Outdated
Comment thread include/svs/core/io/memstream.h Outdated
Comment thread include/svs/core/io/memstream.h Outdated
Comment thread include/svs/core/io/memstream.h
Comment thread include/svs/core/data/simple.h Outdated
Comment thread bindings/cpp/src/vamana_index.cpp
Comment thread bindings/cpp/tests/runtime_test.cpp Outdated
Comment thread tests/svs/index/flat/flat.cpp Outdated
@rfsaliev rfsaliev force-pushed the rfsaliev/mmap-loading branch from 748365f to f04b1c2 Compare April 22, 2026 12:43
@rfsaliev rfsaliev requested a review from Copilot April 22, 2026 12:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 8 comments.

Comment on lines +385 to +401
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();
}
};
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +418 to +435
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);
}
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +75 to +78
basic_mmstreambuf* open(
const std::filesystem::path& path,
std::ios_base::openmode mode = std::ios_base::in | std::ios_base::out
) {
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +91 to +92
auto perm =
(mode & std::ios_base::out) ? MemoryMapper::ReadWrite : MemoryMapper::ReadOnly;
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +527 to +539
[[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;
}
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +31
#include <array>
#include <cstddef>
#include <filesystem>
#include <fstream>
#include <span>
#include <sstream>
#include <string>
#include <system_error>
#include <utility>
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>.

Copilot uses AI. Check for mistakes.
CATCH_REQUIRE(base_ptr != nullptr);
CATCH_REQUIRE(*base_ptr == 'H');

for (std::size_t i = 0; i < std::strlen(text); ++i) {
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>.

Copilot uses AI. Check for mistakes.
Comment on lines +194 to +199
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);
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
rfsaliev and others added 2 commits April 22, 2026 17:24
Copy link
Copy Markdown
Member

@ibhati ibhati left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great to me!

@ethanglaser
Copy link
Copy Markdown
Member

Please address CI failure before merging

@ethanglaser
Copy link
Copy Markdown
Member

ethanglaser commented Apr 23, 2026

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)

rfsaliev and others added 2 commits April 24, 2026 10:17
Co-authored-by: Copilot <copilot@github.com>
…ed allocator handling

Co-authored-by: Copilot <copilot@github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants