Skip to content

MOD-15578 Track shared SVS thread pool memory & expose it through public API#972

Open
dor-forer wants to merge 10 commits into
mainfrom
dor-forer-MOD-15578-track-svs-thpool-memory
Open

MOD-15578 Track shared SVS thread pool memory & expose it through public API#972
dor-forer wants to merge 10 commits into
mainfrom
dor-forer-MOD-15578-track-svs-thpool-memory

Conversation

@dor-forer
Copy link
Copy Markdown
Collaborator

@dor-forer dor-forer commented May 26, 2026

The shared VecSimSVSThreadPool singleton was previously created via raw new with the default allocator, so its slot vector and per-slot ThreadSlot objects bypassed VecSimAllocator and were invisible to any memory accounting downstream (FT.INFO, INFO MODULES, etc.).

This PR:

  1. Routes the shared SVS thread pool's allocations through a dedicated VecSimAllocator (using VecsimSTLAllocator for the slot vector and std::allocate_shared for thread objects).
  2. Adds a new public API size_t VecSim_GetGlobalMemory(void) returning the total bytes of process-wide VecSim allocations not tied to any single index — today equal to the shared SVS thread pool's tracked allocation size.
  3. Surfaces these bytes in VECSIM_INFO via two new fields:
    • GLOBAL_MEMORY — appended at the top level of every algorithm's debug response by the C API wrapper VecSimIndex_DebugInfoIterator. Always present (value may be 0).
    • SHARED_SVS_THREADPOOL_MEMORY — appended at the end of any SVS algorithm section by SVSIndex::debugInfoIterator(). Present at the top level of a non-tiered SVS response, or inside BACKEND_INDEX of a tiered SVS response.

Public API change

Before

// (no API to query process-wide VecSim memory)

After

// Returns total bytes of process-wide VecSim allocations not tied to any single
// index (today: the shared SVS thread pool singleton). Returns 0 when no such
// global allocations exist.
size_t VecSim_GetGlobalMemory(void);

Callers (e.g. RediSearch) can fold this into per-spec or process-wide memory metrics without depending on which algorithm contributes.


VECSIM_INFO (FT.DEBUG) output change

Common header (every algorithm, unchanged)

ALGORITHM, TYPE, DIMENSION, METRIC, IS_MULTI_VALUE, IS_DISK,
INDEX_SIZE, INDEX_LABEL_COUNT, MEMORY, LAST_SEARCH_MODE

FLAT — 11 → 12 fields

  <common header × 10>
  BLOCK_SIZE
+ GLOBAL_MEMORY

HNSW — 18 → 19 fields

  <common header × 10>
  BLOCK_SIZE, M, EF_CONSTRUCTION, EF_RUNTIME, MAX_LEVEL, ENTRYPOINT,
  EPSILON, NUMBER_OF_MARKED_DELETED
+ GLOBAL_MEMORY

SVS (non-tiered) — 25 → 27 fields

  <common header × 10>
  BLOCK_SIZE, QUANT_BITS, ALPHA, GRAPH_MAX_DEGREE, CONSTRUCTION_WINDOW_SIZE,
  MAX_CANDIDATE_POOL_SIZE, PRUNE_TO, USE_SEARCH_HISTORY, NUM_THREADS,
  LAST_RESERVED_NUM_THREADS, NUMBER_OF_MARKED_DELETED, SEARCH_WINDOW_SIZE,
  SEARCH_BUFFER_CAPACITY, LEANVEC_DIMENSION, EPSILON
+ SHARED_SVS_THREADPOOL_MEMORY
+ GLOBAL_MEMORY

Tiered HNSW — 16 → 17 fields

  <common header × 10>           (ALGORITHM = "TIERED")
  MANAGEMENT_LAYER_MEMORY, BACKGROUND_INDEXING, TIERED_BUFFER_LIMIT
  FRONTEND_INDEX = nested [<FLAT fields, 11>]      (no GLOBAL_MEMORY in nested)
  BACKEND_INDEX  = nested [<HNSW fields, 18>]      (no GLOBAL_MEMORY in nested)
  TIERED_HNSW_SWAP_JOBS_THRESHOLD
+ GLOBAL_MEMORY

Tiered SVS — 18 → 19 fields

  <common header × 10>           (ALGORITHM = "TIERED")
  MANAGEMENT_LAYER_MEMORY, BACKGROUND_INDEXING, TIERED_BUFFER_LIMIT
  FRONTEND_INDEX = nested [<FLAT fields, 11>]
- BACKEND_INDEX  = nested [<SVS fields, 25>]
+ BACKEND_INDEX  = nested [<SVS fields, 26 — incl. SHARED_SVS_THREADPOOL_MEMORY>]
  TIERED_SVS_TRAINING_THRESHOLD, TIERED_SVS_UPDATE_THRESHOLD,
  TIERED_SVS_THREADS_RESERVE_TIMEOUT
+ GLOBAL_MEMORY

Two emission rules

  1. GLOBAL_MEMORY is appended exactly once, at the outermost iterator level, by the C API wrapper. Never appears inside a nested FRONTEND_INDEX/BACKEND_INDEX.
  2. SHARED_SVS_THREADPOOL_MEMORY is appended at the end of any SVS algorithm section by SVSIndex::debugInfoIterator(). So it shows up at the top level of a non-tiered SVS response, or inside BACKEND_INDEX of a tiered SVS response — never duplicated.

Stats / API output change

VecSim_GetGlobalMemory()

Before: API did not exist. The pool's slot vector and per-slot ThreadSlot objects went through the default allocator and were not tracked anywhere.

After:

size_t bytes = VecSim_GetGlobalMemory();
// == VecSimSVSThreadPool::getSharedAllocationSize()
//    (slot vector + per-slot ThreadSlot objects, allocated through the
//     dedicated VecSimAllocator).
// == 0 before any SVS index has been constructed and the worker pool size set.

Per-index getAllocationSize() (SVS only)

Before: Did not include any per-index portion of the parallelism slot, since the pool was untracked entirely.

After: Each SVS index's per-index allocator now tracks its own parallelism slot (a small fixed-size structure inside the index, allocated through the index's VecSimAllocator). The shared pool itself remains process-wide and reported via VecSim_GetGlobalMemory().

Cross-field invariant

Since the SVS thread pool is currently the only contributor to global memory:

VecSim_GetGlobalMemory() == VecSimSVSThreadPool::getSharedAllocationSize()
                         == <SHARED_SVS_THREADPOOL_MEMORY value in any SVS section>
                         == <GLOBAL_MEMORY value in any top-level VECSIM_INFO response>

This invariant is enforced by the new gtest SVSTest.debugInfoGlobalMemoryEqualsSharedSVSThreadPoolMemory. If a future contributor is added to VecSim_GetGlobalMemory() without updating breakdowns, the test will catch the drift.


Tests

  • Added SVSTest.debugInfoGlobalMemoryEqualsSharedSVSThreadPoolMemory (typed test, runs per SVS data type) — asserts both new fields exist exactly once in a non-tiered SVS response and report the same bytes as VecSim_GetGlobalMemory().
  • Updated compareFlatIndexInfoToIterator, compareHNSWIndexInfoToIterator, compareSVSIndexInfoToIterator to take an expect_global_memory parameter (default true) — needed because these comparators are called both with the C API iterator (top level, has GLOBAL_MEMORY) and as nested-backend comparators inside compareTieredIndexInfoToIterator (no GLOBAL_MEMORY).
  • Bumped expected field counts: SVS 25 → 26. TIERED_SVS constant is unchanged — the new SHARED_SVS_THREADPOOL_MEMORY field lives inside the nested SVS BACKEND_INDEX iterator (already counted as one entry at the tiered level), and GLOBAL_MEMORY is added via the comparator's +1 when called with the C API iterator. FLAT/HNSW/TIERED_HNSW field-count constants represent the C++ method count (no GLOBAL_MEMORY); the comparators add +1 when called with the C API iterator.
  • getFlatFields(), getHNSWFields(), getTieredHNSWFields(), getSVSFields(), getTieredSVSFields() updated to append GLOBAL_MEMORY (and SHARED_SVS_THREADPOOL_MEMORY for SVS) at the new field positions.

Compatibility

  • Wire-compatible — adds new fields at well-defined positions in VECSIM_INFO. Existing consumers parsing by field name are unaffected; consumers indexing by position must shift their expectations accordingly (covered above).
  • Source-compatibleVecSim_GetGlobalMemory() is purely additive.
  • Behavioral parity — for callers that don't read the new fields/API, total tracked memory is now strictly larger by exactly the bytes the shared SVS thread pool was already consuming but not reporting.

Mark if applicable

  • This PR introduces API changes
  • This PR introduces serialization changes

Supersedes #967 — ownership transferred to @dor-forer. Original branch authored by @meiravgri; identical commits cherry-pushed to dor-forer-MOD-15578-track-svs-thpool-memory so a non-self reviewer can be assigned.


Note

Medium Risk
Adds public API and VECSIM_INFO fields that downstream metrics must interpret; memory totals change for SVS workloads but behavior is otherwise unchanged.

Overview
This PR makes shared SVS thread pool memory visible to operators and embedders instead of leaving it untracked on the default allocator.

The shared VecSimSVSThreadPoolImpl singleton now allocates its slot vector and ThreadSlot objects through a dedicated VecSimAllocator (VecsimSTLAllocator / allocate_shared). Per-index VecSimSVSThreadPool wrappers take the index allocator and track the small parallelism_ control block there; initial size estimation includes that cost.

VecSim_GetGlobalMemory() reports process-wide bytes not attributed to a single index (today: the shared pool). VecSimIndex_DebugInfoIterator always appends GLOBAL_MEMORY at the outermost level; SVS adds SHARED_SVS_THREADPOOL_MEMORY in debugInfoIterator(). VecSimIndexStatsInfo docs clarify per-index memory excludes global pool bytes to avoid double-counting when aggregating indexes.

An isInitialized flag lets global memory queries return 0 without lazy-constructing the pool. Tests and debug-info comparators were updated for the new fields and C API vs nested iterator behavior.

Reviewed by Cursor Bugbot for commit fda6a43. Bugbot is set up for automated code reviews on this repo. Configure here.

@jit-ci
Copy link
Copy Markdown

jit-ci Bot commented May 26, 2026

🛡️ Jit Security Scan Results

CRITICAL HIGH MEDIUM

✅ No security findings were detected in this PR


Security scan by Jit

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

This PR improves VecSim memory accounting by routing the shared SVS thread pool singleton allocations through a tracked allocator and exposing that process-wide (non-index) memory via a new public C API and additional VECSIM_INFO fields.

Changes:

  • Added VecSim_GetGlobalMemory() and appended GLOBAL_MEMORY to the top-level debug info iterator returned by VecSimIndex_DebugInfoIterator.
  • Tracked shared SVS thread pool allocations and exposed them via SHARED_SVS_THREADPOOL_MEMORY in SVS debug info.
  • Updated unit-test comparators/field-order expectations and added a test to enforce the invariant between GLOBAL_MEMORY, SHARED_SVS_THREADPOOL_MEMORY, and VecSim_GetGlobalMemory().

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/unit/unit_test_utils.h Extends debug-info comparator signatures to optionally expect GLOBAL_MEMORY.
tests/unit/unit_test_utils.cpp Updates debug-info comparators and expected field orders to include GLOBAL_MEMORY / SHARED_SVS_THREADPOOL_MEMORY.
tests/unit/test_svs.cpp Adds a typed test validating global-memory vs shared-threadpool-memory invariants.
tests/unit/test_svs_tiered.cpp Updates SVS threadpool wrapper construction to pass a VecSimAllocator.
tests/unit/test_svs_threadpool.cpp Updates threadpool wrapper tests to pass an allocator and resets shared pool state in setup.
src/VecSim/vec_sim.h Adds the new public API declaration VecSim_GetGlobalMemory().
src/VecSim/vec_sim.cpp Implements VecSim_GetGlobalMemory() and appends GLOBAL_MEMORY in the C API debug iterator wrapper.
src/VecSim/vec_sim_common.h Clarifies that per-index memory excludes process-wide/global allocations.
src/VecSim/utils/vec_utils.h Adds new debug field name constants for GLOBAL_MEMORY / SHARED_SVS_THREADPOOL_MEMORY.
src/VecSim/utils/vec_utils.cpp Defines the new debug field name strings.
src/VecSim/index_factories/svs_factory.cpp Accounts for per-index threadpool wrapper allocation in SVS initial size estimation.
src/VecSim/algorithms/svs/svs.h Constructs the per-index SVS threadpool wrapper with the index allocator and adds SHARED_SVS_THREADPOOL_MEMORY to debug info.
src/VecSim/algorithms/svs/svs_utils.h Introduces a tracked allocator for the shared pool, tracks allocations, and changes wrapper ctor to allocate parallelism via index allocator.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/VecSim/index_factories/svs_factory.cpp Outdated
Comment thread src/VecSim/utils/vec_utils.h Outdated
Comment thread tests/unit/unit_test_utils.cpp Outdated
Comment thread tests/unit/unit_test_utils.cpp
@dor-forer dor-forer force-pushed the dor-forer-MOD-15578-track-svs-thpool-memory branch from 31c0e0a to 0adca87 Compare May 26, 2026 14:51
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0adca87. Configure here.

Comment thread src/VecSim/algorithms/svs/svs_utils.h Outdated
The doc string claimed the field is exposed only in SVS tiered indexes,
but `SVSIndex::debugInfoIterator()` emits it for flat SVS indexes too
(and inside the BACKEND_INDEX section of tiered SVS).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 26, 2026

Codecov Report

❌ Patch coverage is 95.65217% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.97%. Comparing base (bbe9dfd) to head (fda6a43).

Files with missing lines Patch % Lines
src/VecSim/algorithms/svs/svs_utils.h 92.30% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #972      +/-   ##
==========================================
- Coverage   96.99%   96.97%   -0.03%     
==========================================
  Files         130      130              
  Lines        7793     7829      +36     
==========================================
+ Hits         7559     7592      +33     
- Misses        234      237       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

* Drop the unconditional `#include "VecSim/algorithms/svs/svs_utils.h"`
  at the top of `unit_test_utils.cpp` (added in 3c2023a). The header
  pulls in `<svs/core/distance.h>`, `<svs/index/vamana/dynamic_index.h>`,
  etc. — none of which are available when `USE_SVS=OFF`. The
  HAVE_SVS-guarded include lower in the file already provides the symbols
  `validateSVSIndexAttributesInfo` needs.
* Wrap `compareSVSIndexInfoToIterator` (definition + declaration in the
  header) and its call site inside `chooseCompareIndexInfoToIterator`
  with `#if HAVE_SVS`. The function references `VecSimSVSThreadPool`,
  which is undefined when SVS is disabled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dor-forer dor-forer force-pushed the dor-forer-MOD-15578-track-svs-thpool-memory branch from 0adca87 to 9d082f0 Compare May 26, 2026 15:23
- svs_factory.cpp: mark unused 'p' variable with [[maybe_unused]] to
  silence -Wunused-variable under -Werror (Copilot review).
- vec_utils.h: rewrite SHARED_SVS_THREADPOOL_MEMORY_STRING doc comment
  to match actual emission behavior — emitted by SVSIndex::debugInfoIterator()
  for both non-tiered (top-level) and tiered (BACKEND_INDEX) SVS responses
  (Copilot review).
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 13 out of 13 changed files in this pull request and generated no new comments.

Comment thread src/VecSim/vec_sim.h
* (e.g. the shared SVS thread pool singleton). 0 if no such allocations
* have been made.
*/
size_t VecSim_GetGlobalMemory(void);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Problematic name, implies that it also includes the per-index memory

// Capacity hint for the iterator. Must equal the number of addInfoField()
// calls below (1 for ALGORITHM + 9 from addCommonInfoToIterator + 16 SVS-specific).
// Update this number when fields are added or removed.
size_t numberOfInfoFields = 26;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Only one pair was added no? How come it increased by 3?

Comment thread tests/unit/test_svs.cpp
Comment on lines +3416 to +3418
VecSimSVSThreadPool::resize(1);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is there a validation that the tracked memory actually increased/decreased as expected (rather than just that both APIs return the same value) ?

std::shared_ptr<VecSimAllocator> allocator_; // pool's own allocator for memory tracking
mutable std::mutex pool_mutex_;
std::vector<std::shared_ptr<ThreadSlot>> slots_;
std::vector<SlotPtr, SlotVecAllocator> slots_;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Wouldn't using vecsim_stl::vector simplify the whole thing?

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