From eeb8a651dcd5fd1f1fc2ccf2a668bac2e83b3659 Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Thu, 21 May 2026 08:38:42 -0700 Subject: [PATCH 1/3] [MMAP] Add MMapAllocator with file-backed memory mapping support Co-authored-by: Ishwar Bhati --- include/svs/concepts/graph.h | 6 +- include/svs/core/allocator.h | 323 ++++++++++++++++++++++++++++++++++- tests/svs/core/allocator.cpp | 253 ++++++++++++++++++++++++++- 3 files changed, 577 insertions(+), 5 deletions(-) diff --git a/include/svs/concepts/graph.h b/include/svs/concepts/graph.h index d3e139eca..51be5b951 100644 --- a/include/svs/concepts/graph.h +++ b/include/svs/concepts/graph.h @@ -1,5 +1,5 @@ /* - * Copyright 2023 Intel Corporation + * Copyright 2023-2026 Intel Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -195,11 +195,11 @@ concept MemoryGraph = requires(T& g, const T& const_g) { /// template bool graphs_equal(const Graph1& x, const Graph2& y) { - if (x.num_nodes() != y.num_nodes()) { + if (x.n_nodes() != y.n_nodes()) { return false; } - for (size_t i = 0, imax = x.num_nodes(); i < imax; ++i) { + for (size_t i = 0, imax = x.n_nodes(); i < imax; ++i) { const auto& xa = x.get_node(i); const auto& ya = y.get_node(i); if (!std::equal(xa.begin(), xa.end(), ya.begin())) { diff --git a/include/svs/core/allocator.h b/include/svs/core/allocator.h index 1e449a7ee..bfc6aad54 100644 --- a/include/svs/core/allocator.h +++ b/include/svs/core/allocator.h @@ -1,5 +1,5 @@ /* - * Copyright 2023 Intel Corporation + * Copyright 2023-2026 Intel Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -691,4 +691,325 @@ AllocatorHandle make_allocator_handle(Alloc alloc) { return AllocatorHandle{std::move(alloc)}; } +namespace detail { +/// +/// @brief Manager for file-backed memory mapped allocations. +/// +/// Tracks active memory-mapped allocations by keeping MMapPtr objects alive +/// in a process-wide registry. All members are static; the class is non- +/// instantiable and acts as a namespaced collection of helper functions over +/// the shared registry. Thread-safe for concurrent allocations. +/// +class MMapAllocationRegistry { + /// The registry is the singleton instance that tracks all active memory-mapped + /// allocations. + private: + MMapAllocationRegistry() = default; + ~MMapAllocationRegistry() = default; + MMapAllocationRegistry(const MMapAllocationRegistry&) = delete; + MMapAllocationRegistry& operator=(const MMapAllocationRegistry&) = delete; + + public: + static MMapAllocationRegistry& instance() { + static MMapAllocationRegistry registry; + return registry; + } + + public: + /// + /// @brief Allocate memory mapped to a freshly created (or extended) file. + /// + /// @param bytes Number of bytes to allocate + /// @param file_path Path to the file for backing storage + /// @return Pointer to the allocated memory + /// + [[nodiscard]] void* allocate(size_t bytes, const std::filesystem::path& file_path) { + MemoryMapper mapper{MemoryMapper::ReadWrite, MemoryMapper::MayCreate}; + auto mmap_ptr = mapper.mmap(file_path, lib::Bytes(bytes)); + + void* ptr = mmap_ptr.data(); + + // Store the MMapPtr to keep the mapping alive + { + std::lock_guard lock{mutex_}; + allocations_.insert({ptr, std::move(mmap_ptr)}); + } + + return ptr; + } + + /// + /// @brief Map an existing file read-only, returning a pointer offset into the mapping. + /// + /// This is used for zero-copy loading: the returned pointer points to + /// `base + offset` within the mmap'd region. The underlying mapping covers + /// the entire file so that munmap and madvise operate on the full range. + /// + /// @param data_bytes Number of bytes of data expected after the offset. + /// @param file_path Path to an existing file. + /// @param offset Byte offset into the file where data starts (e.g., header size). + /// @return Pointer to data at `base + offset`. + /// + [[nodiscard]] void* map_existing_at_offset( + size_t data_bytes, const std::filesystem::path& file_path, size_t offset + ) { + auto file_size = std::filesystem::file_size(file_path); + if (file_size < offset + data_bytes) { + throw ANNEXCEPTION( + "File {} is {} bytes, need at least {} (offset={} + data={})", + file_path, + file_size, + offset + data_bytes, + offset, + data_bytes + ); + } + + MemoryMapper mapper{MemoryMapper::ReadOnly, MemoryMapper::MustUseExisting}; + auto mmap_ptr = mapper.mmap(file_path, lib::Bytes(file_size)); + + void* data_ptr = static_cast(mmap_ptr.data()) + offset; + + { + std::lock_guard lock{mutex_}; + allocations_.insert({data_ptr, std::move(mmap_ptr)}); + } + + return data_ptr; + } + + /// + /// @brief Deallocate a memory-mapped allocation. + /// + /// Removes the MMapPtr, which triggers munmap in its destructor. + /// + /// @param ptr Pointer previously returned by allocate() or + /// map_existing_at_offset(). + /// + void deallocate(void* ptr) { + std::lock_guard lock{mutex_}; + auto itr = allocations_.find(ptr); + if (itr == allocations_.end()) { + throw ANNEXCEPTION("Could not find memory-mapped allocation to deallocate!"); + } + + // Erasing will destroy the MMapPtr, which calls munmap + allocations_.erase(itr); + } + + /// + /// @brief Get count of current allocations (for debugging/testing) + /// + size_t allocation_count() { + std::lock_guard lock{mutex_}; + return allocations_.size(); + } + + /// + /// @brief Evict all mmap'd pages from memory using madvise(MADV_DONTNEED). + /// + /// This tells the kernel to discard the pages backing all active mmap allocations. + /// The pages will be re-faulted from the backing files on next access. + /// Useful for benchmarking to simulate truly cold cache access. + /// + void evict_pages() { +#ifdef __linux__ + std::lock_guard lock{mutex_}; + for (auto& [ptr, mmap_ptr] : allocations_) { + void* base = const_cast(mmap_ptr.base()); + size_t size = mmap_ptr.size(); + if (base != nullptr && size > 0) { + (void)madvise(base, size, MADV_DONTNEED); + } + } +#endif + } + + /// + /// @brief Evict the pages backing a single allocation. + /// + /// Calls madvise(MADV_DONTNEED) on the full underlying mapping for the + /// allocation registered at `ptr`. Useful for selectively dropping just- + /// faulted pages after a one-shot read of an existing file. + /// + void evict_pages_for(void* ptr) { +#ifdef __linux__ + std::lock_guard lock{mutex_}; + auto itr = allocations_.find(ptr); + if (itr == allocations_.end()) { + return; + } + void* base = const_cast(itr->second.base()); + size_t size = itr->second.size(); + if (base != nullptr && size > 0) { + (void)madvise(base, size, MADV_DONTNEED); + } +#else + (void)ptr; +#endif + } + + private: + std::mutex mutex_{}; + tsl::robin_map> allocations_{}; +}; + +} // namespace detail + +/// +/// @brief Access pattern hint for memory-mapped allocations +/// +enum class MMapAccessHint : int { + Normal = MADV_NORMAL, ///< Default access pattern + Sequential = MADV_SEQUENTIAL, ///< Data will be accessed sequentially + Random = MADV_RANDOM ///< Data will be accessed randomly +}; + +namespace detail { + +/// +/// @brief Apply a madvise() access-pattern hint to an mmap'd region. +/// +/// No-op on non-Linux platforms or for null/empty regions. madvise() is a +/// hint, so any error is ignored. +/// +inline void apply_mmap_access_hint(void* ptr, size_t bytes, MMapAccessHint hint) { +#ifdef __linux__ + if (ptr == nullptr || bytes == 0) { + return; + } + (void)madvise(ptr, bytes, static_cast(hint)); +#else + (void)ptr; + (void)bytes; + (void)hint; +#endif +} + +} // namespace detail + +/// +/// @brief File-backed, writable memory-mapped allocator. +/// +/// Each allocate() call creates a fresh temp file under @c base_path_ and +/// returns a writable mmap of that file. Intended for storing data that is +/// produced at runtime (e.g. an index's secondary, full-dimension dataset) +/// in file-backed pages instead of anonymous RAM. +/// +/// For zero-copy loading from a pre-existing file, use +/// @ref MMapFileViewAllocator instead. +/// +/// @tparam T The value type for the allocator. Must be trivially default- +/// constructible: construction is a no-op (storage is either kernel- +/// zeroed for new files, or already-valid bytes for existing files +/// via the read-only sibling allocator). +/// +template class MMapAllocator { + private: + std::filesystem::path base_path_; + MMapAccessHint access_hint_ = MMapAccessHint::Normal; + detail::MMapAllocationRegistry& allocation_resource_; + + public: + // C++ allocator type aliases + using value_type = T; + using propagate_on_container_copy_assignment = std::true_type; + using propagate_on_container_move_assignment = std::true_type; + using propagate_on_container_swap = std::true_type; + using is_always_equal = + std::false_type; // Allocators with different paths are different + + /// + /// @brief Construct a new MMapAllocator + /// + /// @param base_path Directory path for storing memory-mapped files. + /// If empty, will use /tmp with generated names. + /// @param access_hint Hint about how the data will be accessed + /// + explicit MMapAllocator( + std::filesystem::path base_path = std::filesystem::temp_directory_path(), + MMapAccessHint access_hint = MMapAccessHint::Normal, + detail::MMapAllocationRegistry& allocation_resource = + detail::MMapAllocationRegistry::instance() + ) + : base_path_{!base_path.empty() ? std::move(base_path) : std::filesystem::temp_directory_path()} + , access_hint_{access_hint} + , allocation_resource_{allocation_resource} { + std::filesystem::create_directories(base_path_); + } + + // Enable rebinding of allocators + template friend class MMapAllocator; + + template + MMapAllocator(const MMapAllocator& other) + : base_path_{other.base_path_} + , access_hint_{other.access_hint_} + , allocation_resource_{other.allocation_resource_} {} + + /// + /// @brief Compare allocators + /// + /// Two allocators are equal if they use the same base path and access hint + /// + template bool operator==(const MMapAllocator& other) const { + return base_path_ == other.base_path_ && access_hint_ == other.access_hint_; + } + + /// + /// @brief Allocate a writable file-backed mmap of @c n elements. + /// + /// Creates a fresh temp file under base_path_ sized to @c sizeof(T) * n, + /// maps it ReadWrite, and applies the configured madvise() hint. + /// + /// @param n Number of elements to allocate + /// @return Pointer to allocated memory + /// + [[nodiscard]] T* allocate(size_t n) { + size_t bytes = sizeof(T) * n; + auto file_path = generate_file_path(bytes); + void* ptr = allocation_resource_.allocate(bytes, file_path); + detail::apply_mmap_access_hint(ptr, bytes, access_hint_); + return static_cast(ptr); + } + + /// + /// @brief Deallocate memory previously returned by allocate(). + /// + void deallocate(void* ptr, size_t SVS_UNUSED(n)) { + allocation_resource_.deallocate(ptr); + } + + /// @brief Get the base path for allocations. + const std::filesystem::path& get_base_path() const { return base_path_; } + + /// @brief Get the access hint. + MMapAccessHint get_access_hint() const { return access_hint_; } + + /// @brief Set the access hint for future allocations. + void set_access_hint(MMapAccessHint hint) { access_hint_ = hint; } + + /// + /// @brief Evict all mmap'd pages from memory. + /// + /// Calls madvise(MADV_DONTNEED) on all active mmap allocations + /// (including those from MMapFileViewAllocator), forcing pages to be + /// re-faulted from disk on next access. + /// + static void evict_pages() { detail::MMapAllocationRegistry::instance().evict_pages(); } + + private: + /// @brief Generate a unique file path for an allocation. + std::filesystem::path generate_file_path(size_t bytes) { + auto filename = fmt::format( + "mmap_alloc_{}_{}_{}.dat", + std::this_thread::get_id(), + allocation_resource_.allocation_count(), + bytes + ); + + return base_path_ / filename; + } +}; + } // namespace svs diff --git a/tests/svs/core/allocator.cpp b/tests/svs/core/allocator.cpp index 27191454d..6f0b6c63e 100644 --- a/tests/svs/core/allocator.cpp +++ b/tests/svs/core/allocator.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2023 Intel Corporation + * Copyright 2023-2026 Intel Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,18 +15,22 @@ */ // stdlib +#include #include #include #include // svs #include "svs/core/allocator.h" +#include "svs/core/data.h" +#include "svs/core/graph.h" #include "svs/lib/memory.h" // catch2 #include "catch2/catch_test_macros.hpp" // tests +#include "tests/utils/test_dataset.h" #include "tests/utils/utils.h" // Compile-time tests @@ -215,4 +219,251 @@ CATCH_TEST_CASE("Testing Allocator", "[allocators]") { CATCH_STATIC_REQUIRE(std::is_same_v); } } + + CATCH_SECTION("Testing MMapAllocator") { + auto temp_dir = svs_test::prepare_temp_directory_v2(); + + CATCH_SECTION("Basic Behavior") { + using T = float; + constexpr size_t nelements = 256; + const size_t bytes = nelements * sizeof(T); + + auto list_regular_files = [](const std::filesystem::path& dir) { + std::vector paths; + for (const auto& entry : std::filesystem::directory_iterator(dir)) { + if (entry.is_regular_file()) { + paths.push_back(entry.path()); + } + } + return paths; + }; + + auto alloc = svs::MMapAllocator(temp_dir, svs::MMapAccessHint::Sequential); + CATCH_REQUIRE(alloc.get_base_path() == temp_dir); + CATCH_REQUIRE(alloc.get_access_hint() == svs::MMapAccessHint::Sequential); + + alloc.set_access_hint(svs::MMapAccessHint::Random); + CATCH_REQUIRE(alloc.get_access_hint() == svs::MMapAccessHint::Random); + + auto files_before = list_regular_files(temp_dir); + auto count_before = + svs::detail::MMapAllocationRegistry::instance().allocation_count(); + auto* ptr = alloc.allocate(nelements); + CATCH_REQUIRE(ptr != nullptr); + CATCH_REQUIRE( + svs::detail::MMapAllocationRegistry::instance().allocation_count() == + count_before + 1 + ); + + auto files_after = list_regular_files(temp_dir); + CATCH_REQUIRE(files_after.size() == files_before.size() + 1); + + std::optional allocation_file; + for (const auto& candidate : files_after) { + if (std::find(files_before.begin(), files_before.end(), candidate) == + files_before.end()) { + allocation_file = candidate; + break; + } + } + + CATCH_REQUIRE(allocation_file.has_value()); + CATCH_REQUIRE(std::filesystem::exists(*allocation_file)); + CATCH_REQUIRE(std::filesystem::file_size(*allocation_file) == bytes); + + for (size_t i = 0; i < nelements; ++i) { + ptr[i] = static_cast(i); + } + for (size_t i = 0; i < nelements; ++i) { + CATCH_REQUIRE(ptr[i] == static_cast(i)); + } + + alloc.deallocate(ptr, nelements); + CATCH_REQUIRE( + svs::detail::MMapAllocationRegistry::instance().allocation_count() == + count_before + ); + } + + CATCH_SECTION("Rebind and Equality") { + auto int_alloc = svs::MMapAllocator(temp_dir, svs::MMapAccessHint::Normal); + auto float_alloc = svs::MMapAllocator(int_alloc); + + CATCH_REQUIRE(float_alloc.get_base_path() == int_alloc.get_base_path()); + CATCH_REQUIRE(float_alloc.get_access_hint() == int_alloc.get_access_hint()); + CATCH_REQUIRE(float_alloc == svs::MMapAllocator(temp_dir)); + } + + CATCH_SECTION("MMapAllocator with SimpleData (non-blocked)") { + using DataType = + svs::data::SimpleData>; + + // Load reference data + auto original_data = test_dataset::data_f32(); + CATCH_REQUIRE(original_data.size() > 0); + + // Create a SimpleData container with MMapAllocator + auto alloc = + svs::MMapAllocator(temp_dir, svs::MMapAccessHint::Sequential); + auto count_before = + svs::detail::MMapAllocationRegistry::instance().allocation_count(); + + // Construct SimpleData with MMapAllocator as template parameter + auto mmap_data = + DataType(original_data.size(), original_data.dimensions(), alloc); + CATCH_REQUIRE(mmap_data.size() == original_data.size()); + CATCH_REQUIRE(mmap_data.dimensions() == original_data.dimensions()); + CATCH_REQUIRE( + svs::detail::MMapAllocationRegistry::instance().allocation_count() == + count_before + 1 + ); + + // Copy original data to mmap'd data + for (size_t i = 0; i < original_data.size(); ++i) { + mmap_data.set_datum(i, original_data.get_datum(i)); + } + + // Verify data was copied correctly + CATCH_REQUIRE(mmap_data == original_data); + + // Load the data directly from the file + auto mmap_data2 = DataType::load(test_dataset::data_svs_file(), alloc); + // Verify loaded data matches original data + CATCH_REQUIRE(mmap_data2 == original_data); + } + + CATCH_SECTION("MMapAllocator with BlockedData (using Blocked wrapper)") { + using DataType = + svs::data::BlockedData>; + + // Load reference blocked data + auto original_data = test_dataset::data_blocked_f32(); + CATCH_REQUIRE(original_data.size() > 0); + + // Create an allocator + auto alloc = svs::MMapAllocator(temp_dir, svs::MMapAccessHint::Normal); + auto count_before = + svs::detail::MMapAllocationRegistry::instance().allocation_count(); + + // Compute blocking parameters based on original data size and dimensions + // to ensure we allocate 2 blocks for the test dataset. + auto blocking_params = svs::data::BlockingParameters{ + .blocksize_bytes = svs::lib::prevpow2( + sizeof(float) * original_data.dimensions() * original_data.size() - 1 + )}; + + // Construct BlockedData with MMapAllocator via Blocked wrapper + auto blocked_alloc = + svs::data::Blocked>(blocking_params, alloc); + + auto mmap_data = DataType(1, original_data.dimensions(), blocked_alloc); + mmap_data.resize(original_data.size()); + CATCH_REQUIRE(mmap_data.size() == original_data.size()); + CATCH_REQUIRE(mmap_data.dimensions() == original_data.dimensions()); + CATCH_REQUIRE( + svs::detail::MMapAllocationRegistry::instance().allocation_count() == + count_before + 2 + ); + + // Copy original data to mmap'd data + for (size_t i = 0; i < original_data.size(); ++i) { + mmap_data.set_datum(i, original_data.get_datum(i)); + } + + CATCH_REQUIRE(mmap_data == original_data); + + // Load the data directly from the file + auto mmap_data2 = DataType::load(test_dataset::data_svs_file(), blocked_alloc); + // Verify loaded data matches original data + CATCH_REQUIRE(mmap_data2 == original_data); + } + + CATCH_SECTION("MMapAllocator with SimpleGraph") { + using GraphType = + svs::graphs::SimpleGraph>; + + // Load reference graph + auto original_graph = test_dataset::graph(); + CATCH_REQUIRE(original_graph.n_nodes() > 0); + + // Create an allocator for graph nodes (uint32_t) + auto alloc = + svs::MMapAllocator(temp_dir, svs::MMapAccessHint::Random); + auto count_before = + svs::detail::MMapAllocationRegistry::instance().allocation_count(); + + // Construct SimpleGraph with MMapAllocator as template parameter + auto mmap_graph = + GraphType(original_graph.n_nodes(), original_graph.max_degree(), alloc); + CATCH_REQUIRE(mmap_graph.n_nodes() == original_graph.n_nodes()); + CATCH_REQUIRE(mmap_graph.max_degree() == original_graph.max_degree()); + CATCH_REQUIRE( + svs::detail::MMapAllocationRegistry::instance().allocation_count() == + count_before + 1 + ); + + // Copy edges from original to mmap'd graph + for (size_t i = 0; i < original_graph.n_nodes(); ++i) { + mmap_graph.replace_node(i, original_graph.get_node(i)); + } + + // Verify edges were copied correctly + CATCH_REQUIRE(mmap_graph == original_graph); + + // Load the graph directly from the file + auto mmap_graph2 = GraphType::load(test_dataset::graph_file(), alloc); + // Verify loaded graph matches original graph + CATCH_REQUIRE(mmap_graph2 == original_graph); + } + + CATCH_SECTION("MMapAllocator with SimpleBlockedGraph (underlying data)") { + // Note: SimpleBlockedGraph is hardcoded to use HugepageAllocator, but we can + // test that using SimpleGraph with Blocked wrapper + using GraphType = svs::graphs:: + SimpleGraph>>; + + auto original_graph = test_dataset::graph(); + CATCH_REQUIRE(original_graph.n_nodes() > 0); + + // Create an allocator + auto alloc = + svs::MMapAllocator(temp_dir, svs::MMapAccessHint::Random); + auto count_before = + svs::detail::MMapAllocationRegistry::instance().allocation_count(); + + // Compute blocking parameters based on original graph size and dimensions + // to ensure we allocate at least 2 blocks for the test dataset. + auto blocking_params = svs::data::BlockingParameters{ + .blocksize_bytes = svs::lib::prevpow2( + sizeof(uint32_t) * original_graph.n_nodes() * + original_graph.max_degree() - + 1 + )}; + + // Construct Graph with MMapAllocator via Blocked wrapper + auto blocked_alloc = + svs::data::Blocked>(blocking_params, alloc); + auto mmap_graph = GraphType( + original_graph.n_nodes(), original_graph.max_degree(), blocked_alloc + ); + + CATCH_REQUIRE( + svs::detail::MMapAllocationRegistry::instance().allocation_count() >= + count_before + 2 + ); + + // Copy edges from original to mmap'd graph + for (size_t i = 0; i < original_graph.n_nodes(); ++i) { + mmap_graph.replace_node(i, original_graph.get_node(i)); + } + + // Verify edges were copied correctly + CATCH_REQUIRE(mmap_graph == original_graph); + + // Load the graph directly from the file + auto mmap_graph2 = GraphType::load(test_dataset::graph_file(), blocked_alloc); + // Verify loaded graph matches original graph + CATCH_REQUIRE(mmap_graph2 == original_graph); + } + } } From 75aff13d35bef745c24f0bdf8f46755257e27889 Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Thu, 21 May 2026 09:00:46 -0700 Subject: [PATCH 2/3] Revert copyright headers --- include/svs/concepts/graph.h | 2 +- include/svs/core/allocator.h | 2 +- tests/svs/core/allocator.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include/svs/concepts/graph.h b/include/svs/concepts/graph.h index 51be5b951..d82ad1114 100644 --- a/include/svs/concepts/graph.h +++ b/include/svs/concepts/graph.h @@ -1,5 +1,5 @@ /* - * Copyright 2023-2026 Intel Corporation + * Copyright 2023 Intel Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/include/svs/core/allocator.h b/include/svs/core/allocator.h index bfc6aad54..e85274c89 100644 --- a/include/svs/core/allocator.h +++ b/include/svs/core/allocator.h @@ -1,5 +1,5 @@ /* - * Copyright 2023-2026 Intel Corporation + * Copyright 2023 Intel Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/tests/svs/core/allocator.cpp b/tests/svs/core/allocator.cpp index 6f0b6c63e..8298678c4 100644 --- a/tests/svs/core/allocator.cpp +++ b/tests/svs/core/allocator.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2023-2026 Intel Corporation + * Copyright 2023 Intel Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 66b6817b62e9aecc0e7150fb9a7ce43742d937c3 Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Thu, 21 May 2026 09:14:14 -0700 Subject: [PATCH 3/3] Fix MMapAllocationRegistry's description in comments --- include/svs/core/allocator.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/svs/core/allocator.h b/include/svs/core/allocator.h index e85274c89..e4ce6234f 100644 --- a/include/svs/core/allocator.h +++ b/include/svs/core/allocator.h @@ -696,9 +696,9 @@ namespace detail { /// @brief Manager for file-backed memory mapped allocations. /// /// Tracks active memory-mapped allocations by keeping MMapPtr objects alive -/// in a process-wide registry. All members are static; the class is non- -/// instantiable and acts as a namespaced collection of helper functions over -/// the shared registry. Thread-safe for concurrent allocations. +/// in a process-wide registry. The registry is implemented as a singleton with +/// static access through `instance()`, and stores the shared allocation state in +/// that singleton object. Thread-safe for concurrent allocations. /// class MMapAllocationRegistry { /// The registry is the singleton instance that tracks all active memory-mapped