feat(vector): tiering M1 — one memory-accounting spine every ceiling reads - #253
Conversation
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
📝 WalkthroughWalkthroughThis PR adds resident-byte accounting for IVF and DiskANN cold-tier vector segments, incorporates vector memory into maxmemory eviction and elastic-budget calculations, changes warm-mmap-budget semantics to an instance-total cap divided per shard, updates INFO reclamation reporting with live counters, and documents changes in CHANGELOG.md. ChangesVector tiering accounting spine
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Timers as Shard::run_eviction
participant ShardDb as ShardDatabases
participant VectorStore
participant Eviction as Eviction helper
Timers->>VectorStore: read published vector bytes (once)
Timers->>ShardDb: sum estimated KV memory across dbs
Timers->>Timers: compute aggregate remaining budget
loop for each db over aggregate budget
Timers->>Eviction: try_evict_if_needed_with_spill_and_total_budget(remaining)
Eviction-->>Timers: freed memory
Timers->>Timers: remaining -= freed
end
Timers->>ShardDb: publish_memory (KV estimate)
Timers->>ShardDb: recompute_elastic_budget()
ShardDb->>ShardDb: used = KV memory + vector resident bytes per shard
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
d78dc6c to
f258407
Compare
The `reclamation_mmap_warm_bytes` INFO field read a module-local `RECL_MMAP_WARM_BYTES` static in info_reclamation.rs that is never incremented anywhere (its own doc comment was a standing `TODO(P10->Wave2): wire from WarmSearchSegment on map/unmap`), so the field reported a permanent 0 regardless of how much WARM-tier heap the `--vec-warm-mmap-budget` LRU cap was actually holding or evicting. Meanwhile the REAL counter — `admin::recl_atomics::RECL_MMAP_WARM_BYTES`, add/sub'd by `MmapBudget::register_segment`/`enforce_budget` on every WARM register and eviction — was write-only: read by no INFO field, no Prometheus gauge, no MEMORY DOCTOR line. Dead-end telemetry on the one tier that actually has byte accounting. - Re-point the INFO emit at `recl_atomics::warm_resident_bytes()`. - Also surface `reclamation_mmap_budget_evictions_total` from the live `budget_evictions_total()` counter (previously unexposed). - Delete the dead local static. Part of the vector-tiering accounting-spine work (Theme A, finding 3): give every memory ceiling and dashboard a truthful view of WARM-tier RAM. Test (red/green): info_reclamation_mmap_warm_bytes_reflects_live_counter holds a known delta on the live counter across the emit and asserts the field reflects it (>= delta) — robust to parallel MmapBudget tests that share the process-global counter. Fails on the dead static (emits 0). author: Tin Dang
…guation Accounting-spine A1 (tiering-v2 architecture, tmp/TIERING-ARCHITECTURE.md): every tier that pins heap must report it, or the memory-pressure trigger, MEMORY DOCTOR, and Prometheus stay blind to that tier. - IvfSegment::resident_bytes(): centroids + FWHT sign flips + per-posting- list interleaved codes/ids/norms + struct overhead. Was a hardcoded 0 in the SegmentHolder roll-up. - DiskAnnSegment::resident_bytes(): PQ codes (num_vectors * m, RAM-resident) + trained codebook via new ProductQuantizer::resident_bytes(). The Vamana graph lives on NVMe and is excluded; Linux io_uring read buffers are small/bounded and excluded. Was untracked resident memory. - SegmentHolder::resident_bytes() now sums both tiers (doc updated — the "contribute 0" caveat is gone). D9 quarantine (keep DiskANN inert, decide productionize-vs-delete at the M3 exit-review on real EWMA telemetry): config.rs cold-tier section now disambiguates COLD-stub (`unloaded`, exact reload-on-touch default valve) vs COLD-ann (`cold`, DiskANN serve-from-disk); the three dead knobs are marked [reserved: M3] (segment-cold-min-qps -> frequency-classifier boundary) and [reserved: M5] (diskann beam width / cache levels). Tests (red/green): test_ivf_resident_bytes_accounts_heap, test_diskann_resident_bytes_accounts_pq (floor assertions on known heap), test_resident_bytes_includes_ivf_and_cold_tiers (holder roll-up sums both tiers; DiskANN constructed against a dummy on-disk vamana file). All were E0599-red before the accessors existed; ivf 19 / diskann 22 / holder 15 green after. author: Tin Dang
Accounting-spine A3 (tiering-v2 D3, owner decision: vectors ALWAYS count): --maxmemory was 100% blind to vector memory — the background eviction check compared only KV `db.estimated_memory()` against the per-shard budget, so a pure-vector workload (the documented primary disk-offload use case) could drive RSS to OOM while eviction reported "under budget" and never freed a byte. `timers::run_eviction` now adds the shard's published vector resident bytes (`ShardStoreMemory.vector`, stored by the same 100ms tick before this runs; lag <= 1 tick) to the used total passed into the existing `try_evict_if_needed_with_spill_and_total_budget`. One Relaxed atomic load per tick — no recompute on the eviction path. Semantics when the un-evictable vector term alone exceeds the budget: the bounded eviction loop drains KV then returns OOM (vector data legitimately owns that memory; evicting the backing hashes also shrinks the indexes via the unindex path). The pressure cascade — which CAN shrink vector memory via offload to COLD — fires earlier at --disk-offload-threshold (0.85 x budget), so with disk-offload enabled vectors shed before KV pays. The A4 follow-up (vector-aware elastic budget) widens a vector-heavy shard's budget as the counterweight. NOTE on the "auto maxmemory from RAM" half of this workstream: it already ships as the G1 guardrail (`apply_memory_guardrail`, cgroup-aware v2+v1 with host-min, macOS sysctl) at 80% — kept at 80% rather than churning existing deployments to the proposed 75%; the intent (auto, container-aware, explicit flag wins) is already satisfied. Test (red/green): test_run_eviction_counts_vector_memory — 100 keys far under a 1 MiB budget stay put; publishing 2 MiB of vector bytes must trigger KV eviction. Failed (100 vs 100) before the fix. Eviction suite 26/26, persistence_tick 7/7 green. author: Tin Dang
Accounting-spine A5 (tiering-v2 D3): each shard's MmapBudget was constructed with the FULL configured value, so an N-shard instance silently allowed N x the configured WARM memory (default "2gb" on 4 shards = 8 GiB aggregate) — unlike --maxmemory, which divides via maxmemory_per_shard(). Operators sizing containers off the flag were over-allocating by the shard count. - New ServerConfig::vec_warm_mmap_budget_bytes_per_shard(): total / shards, with two guarded edges — "0" stays 0 (enforcement disabled), and a nonzero total floors at 1 byte/shard (integer division to 0 would flip semantics to "unlimited", the unsafe direction). - event_loop constructs the per-shard enforcer from the new accessor (single construction site, shared by both runtime arms). - Flag doc rewritten: instance-total, divided evenly, matching --maxmemory semantics; eviction demotes to reloadable COLD stubs. BEHAVIOR CHANGE for multi-shard deployments: the effective per-shard WARM cap tightens from `budget` to `budget / shards`. Deployments that relied on the old per-shard meaning should multiply their flag value by the shard count. (Deferred: min(2gb, 25% of maxmemory) auto-default — needs unset-detection plumbing through ArgMatches; roadmap M1 note.) Test (red/green; red = E0599, accessor absent): test_vec_warm_budget_divided_per_shard — 4 shards x "2gb" = 512 MiB, 1 shard unchanged, "0" disables at any shard count, 100 bytes / 128 shards floors at 1 not 0. mmap_budget suite 7/7 green. author: Tin Dang
Accounting-spine A4 (tiering-v2 D3): `recompute_elastic_budget` built its per-shard `used` snapshot from `memory_per_shard` alone (KV-only), so a vector-heavy/KV-light shard was misclassified as an idle donor — it lent its per-shard maxmemory headroom to siblings while its true resident footprint (KV + vector) was already over base. Worse, the pressure cascade then compared a vector-INCLUSIVE used-term against a budget inflated by that donation: the two mechanisms sharing the elastic_budget atomic disagreed about what "used" means. The snapshot now sums each shard's published KV bytes + published vector resident bytes (ShardStoreMemory.vector, same 100ms tick; two Relaxed loads per shard, SmallVec stack path unchanged). A vector-heavy shard is now classified HOT: it stops donating and instead shares the true donors' surplus — the counterweight to A3 counting vector bytes in the eviction used-term (budget and used-term now move together). Test (red/green): recompute_elastic_budget_vector_heavy_shard_not_donor — 4 shards, base 100; shard 1 has KV=10 + vector=200. KV-blind math gave the hot shard 370 (shard 1 donated 90); vector-aware gives 190 (surplus 180 from the two true donors, split across the two hot shards), shard 1 itself gets 190, true idles keep base. First run caught my own wrong expectation (280) — the pool splits among hot shards, asserting the real algorithm. Suite 8/8, persistence_tick 7/7, eviction 26/26 green. author: Tin Dang
CHANGELOG [Unreleased] entry for the five accounting-spine commits: maxmemory counts vectors, vector-aware elastic budget, instance-total WARM budget, IVF/DiskANN resident_bytes, live INFO warm counter, D9 COLD disambiguation. Notes the A5 behavior change for multi-shard deployments. author: Tin Dang
…r-db Adversarial perf + security reviews of the accounting-spine branch independently flagged the same CRITICAL in A3's initial formulation: the shard-wide vector term was re-added to EVERY logical db's independent eviction check. Two failure modes from one root cause: 1. Under-detection: with KV spread across dbs (default 16) such that no single `db_i + vector` crossed the budget, NOTHING evicted while the true aggregate (sum KV + vector) overran RSS — the very bug A3 was written to close. 2. Over-eviction: a vector term over budget made every db independently drain to empty each 100ms tick (16x amplified multi-tenant blast radius), instead of stopping when the shard was back under budget. `run_eviction` now computes the aggregate ONCE (sum all dbs' estimated_memory + published vector bytes) and walks the dbs with a running remainder: each db's eviction reduces the remainder by what it actually freed, and once the remainder is under budget the remaining dbs see an under-budget total and return without evicting. Semantics when the un-evictable vector term alone exceeds budget are unchanged and now documented: KV drains then errors OOM (shared-budget, same as one tenant's KV growth evicting siblings under allkeys-lru); per-db quotas (WS5b) remain the tenant-isolation mechanism. Also from the reviews: - MEDIUM (documented, not changed): pressure-cascade step 3 stays KV-only DELIBERATELY — the vector term already fired the trigger and step 2 sheds vector memory directly; adding it to step 3 would evict KV to pay for memory step 2 reclaims more cheaply. Comment added. - LOW (fixed): store-memory publish reordered ABOVE the KV publish + elastic recompute so the vector-aware donor/hot classification reads THIS tick's own-shard vector figure, not last tick's. - HIGH (documented follow-up): the on-write eviction gate remains KV-only — under noeviction a vector-heavy shard over budget does not yet reject writes (bounded by pressure cascade + RSS watchdog). CHANGELOG callout + roadmap entry; fix tracked as M1 follow-up. Test (red/green): test_run_eviction_gates_on_aggregate_not_per_db — two dbs (~256 KiB each) + 700 KiB vector vs 1 MiB budget: no single db crosses budget (old code evicted nothing — RED), aggregate does, and eviction must stop at the aggregate target leaving db 1 untouched. Suites green both runtimes: timers 2, persistence_tick 7, shared_databases 8, eviction 26; clippy -D warnings clean. author: Tin Dang
2da34b9 to
db4f7a9
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
src/vector/segment/holder.rs (1)
1-1950: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoffFile exceeds the 1500-line guideline.
This file is now well past 1500 lines (test module alone runs to ~1950). This PR adds more tests on top of an already-oversized module rather than splitting it into submodules.
As per coding guidelines, "No single
.rsfile should exceed 1500 lines; split larger modules into submodules."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/vector/segment/holder.rs` around lines 1 - 1950, The holder module is over the 1500-line limit because the in-file tests are keeping `src/vector/segment/holder.rs` oversized. Split the `#[cfg(test)] mod tests` block out into one or more test submodules/files (for example around `SegmentHolder`, `SearchSnapshot`, and the reload helpers) and keep `SegmentList`, `SegmentHolder`, `SearchSnapshot`, and `MvccContext` in the main module while preserving the existing test coverage and imports.Source: Coding guidelines
src/command/info_reclamation.rs (1)
358-402: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueOptional: assert the newly emitted
reclamation_mmap_budget_evictions_totalfield.The new INFO key is emitted at Line 278 but isn't covered by the required-fields presence check, so a future regression that drops it wouldn't be caught.
💚 Add the key to the required list
"reclamation_mmap_warm_bytes:", + "reclamation_mmap_budget_evictions_total:", "reclamation_mvcc_committed:",🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/command/info_reclamation.rs` around lines 358 - 402, The reclamation section presence test is missing coverage for the newly emitted reclamation_mmap_budget_evictions_total key, so a regression could remove it unnoticed. Update info_reclamation_contains_all_required_fields to include reclamation_mmap_budget_evictions_total in the required_fields list, using write_reclamation_section as the source of truth for the emitted INFO keys.src/config.rs (1)
2466-2499: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTest doesn't cover the
--shards 0auto-detect case.The new test only exercises explicit nonzero
config.shardsvalues. Given the concern above aboutself.shardspossibly being0at auto-detect time, a case asserting behavior whenshards == 0would help pin down intended semantics.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/config.rs` around lines 2466 - 2499, The new `test_vec_warm_budget_divided_per_shard` does not cover the auto-detect path where `ServerConfig.shards` is 0. Add an assertion in this test (or a nearby focused test) that exercises `vec_warm_mmap_budget_bytes_per_shard()` with `shards == 0` and verifies the intended fallback behavior, using the existing `ServerConfig::parse_from` setup and the `vec_warm_mmap_budget_bytes_per_shard` accessor to pin down the semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/config.rs`:
- Around line 1045-1060: The vec_warm_mmap_budget_bytes_per_shard helper is
using ServerConfig.shards directly, which can be 0 before auto-shard resolution
and causes the per-shard budget to be wrong. Update the call path so this logic
uses the resolved shard count (the same source used by
per_shard_pagecache_budget), either by passing num_shards into
vec_warm_mmap_budget_bytes_per_shard or by resolving shards before cloning
ServerConfig. Keep the 0-total and minimum-1-byte behavior intact, but base the
division on the resolved shard count.
In `@src/vector/diskann/segment.rs`:
- Around line 492-500: The resident_bytes estimate for DiskAnnSegment is
double-counting the inline ProductQuantizer struct overhead because
size_of::<Self>() already includes the pq field’s layout and pq.resident_bytes()
also starts with size_of::<ProductQuantizer>(). Update
DiskAnnSegment::resident_bytes to only add the truly extra heap-resident pieces
from pq (such as its trained codebook/owned buffers) or make
ProductQuantizer::resident_bytes exclude its own inline struct size, so the
accounting is not counted twice.
---
Nitpick comments:
In `@src/command/info_reclamation.rs`:
- Around line 358-402: The reclamation section presence test is missing coverage
for the newly emitted reclamation_mmap_budget_evictions_total key, so a
regression could remove it unnoticed. Update
info_reclamation_contains_all_required_fields to include
reclamation_mmap_budget_evictions_total in the required_fields list, using
write_reclamation_section as the source of truth for the emitted INFO keys.
In `@src/config.rs`:
- Around line 2466-2499: The new `test_vec_warm_budget_divided_per_shard` does
not cover the auto-detect path where `ServerConfig.shards` is 0. Add an
assertion in this test (or a nearby focused test) that exercises
`vec_warm_mmap_budget_bytes_per_shard()` with `shards == 0` and verifies the
intended fallback behavior, using the existing `ServerConfig::parse_from` setup
and the `vec_warm_mmap_budget_bytes_per_shard` accessor to pin down the
semantics.
In `@src/vector/segment/holder.rs`:
- Around line 1-1950: The holder module is over the 1500-line limit because the
in-file tests are keeping `src/vector/segment/holder.rs` oversized. Split the
`#[cfg(test)] mod tests` block out into one or more test submodules/files (for
example around `SegmentHolder`, `SearchSnapshot`, and the reload helpers) and
keep `SegmentList`, `SegmentHolder`, `SearchSnapshot`, and `MvccContext` in the
main module while preserving the existing test coverage and imports.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e7fc32ba-3119-4a63-9431-e931b8bc219a
📒 Files selected for processing (11)
CHANGELOG.mdsrc/command/info_reclamation.rssrc/config.rssrc/shard/event_loop.rssrc/shard/persistence_tick.rssrc/shard/shared_databases.rssrc/shard/timers.rssrc/vector/diskann/pq.rssrc/vector/diskann/segment.rssrc/vector/segment/holder.rssrc/vector/segment/ivf.rs
| /// Per-shard share of `--vec-warm-mmap-budget` (accounting-spine A5, | ||
| /// tiering-v2 D3): the flag is an INSTANCE-TOTAL cap divided across | ||
| /// shards, matching `maxmemory_per_shard` semantics. Previously each | ||
| /// shard applied the full value — an N-shard instance silently allowed | ||
| /// N× the configured WARM memory. `0` still disables enforcement; a | ||
| /// nonzero total floors at 1 byte per shard (0 would flip semantics to | ||
| /// "unlimited", the unsafe direction). Division floor is fine otherwise: | ||
| /// under-allocating a soft budget is the safe direction. | ||
| pub fn vec_warm_mmap_budget_bytes_per_shard(&self) -> u64 { | ||
| let total = self.vec_warm_mmap_budget_bytes(); | ||
| if total == 0 { | ||
| return 0; | ||
| } | ||
| (total / self.shards.max(1) as u64).max(1) | ||
| } | ||
|
|
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check whether ServerConfig.shards gets mutated/resolved from 0 to the
# actual auto-detected count before shards are spawned, and how event_loop's
# Shard.num_shards is derived relative to it.
rg -n 'shards\s*==\s*0|shards\s*=\s*0' -A5 -B5 src/main.rs src/embedded.rs 2>/dev/null
rg -n '\.shards\s*=' -g '!*/config.rs' -g '*.rs' src
rg -n 'num_shards' -A3 -B3 src/shard/mod.rs 2>/dev/nullRepository: pilotspace/moon
Length of output: 4388
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the method and its call sites, plus the nearby per-shard pagecache helper
# that the original comment compares against.
rg -n 'vec_warm_mmap_budget_bytes_per_shard|per_shard_pagecache_budget|pagecache_budget' src/config.rs src/main.rs src/server/embedded.rs src -A4 -B4
# Show the relevant config methods around the target lines.
sed -n '1010,1085p' src/config.rs
# Show how config.shards is handled in the startup path.
sed -n '430,490p' src/main.rs
sed -n '110,145p' src/server/embedded.rsRepository: pilotspace/moon
Length of output: 15977
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Trace RuntimeConfig construction and where `shards` is copied into it.
rg -n 'RuntimeConfig|runtime_config|shards:\s*config\.shards|shards:\s*num_shards|num_shards' src/main.rs src/server/embedded.rs src/shard -A6 -B6
# Inspect the shard spawn path and event-loop setup.
sed -n '490,760p' src/main.rs
sed -n '140,260p' src/server/embedded.rs
sed -n '440,690p' src/shard/event_loop.rsRepository: pilotspace/moon
Length of output: 50372
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find where RuntimeConfig is built or cloned into shard state with shard counts.
rg -n 'RuntimeConfig::default\(\)|runtime_config|num_shards\s*:|shards\s*:' src/main.rs src/server/embedded.rs src/shard -A4 -B4
# Show the exact startup block that creates shard state in main.
sed -n '490,720p' src/main.rs
# Show the embedded startup path around the config mutation and shard spawn.
sed -n '120,260p' src/server/embedded.rsRepository: pilotspace/moon
Length of output: 50372
Use the resolved shard count here
ServerConfig.shards can still be 0 when auto-shard resolution is enabled, so this helper can hand every shard the full instance budget. Pass the resolved num_shards in here (like per_shard_pagecache_budget) or resolve it before cloning the config.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/config.rs` around lines 1045 - 1060, The
vec_warm_mmap_budget_bytes_per_shard helper is using ServerConfig.shards
directly, which can be 0 before auto-shard resolution and causes the per-shard
budget to be wrong. Update the call path so this logic uses the resolved shard
count (the same source used by per_shard_pagecache_budget), either by passing
num_shards into vec_warm_mmap_budget_bytes_per_shard or by resolving shards
before cloning ServerConfig. Keep the 0-total and minimum-1-byte behavior
intact, but base the division on the resolved shard count.
| /// Estimated resident heap bytes of this cold segment (accounting-spine | ||
| /// A1): PQ codes (`num_vectors * m` bytes, kept in RAM) + the trained | ||
| /// codebook, plus fixed struct overhead. The Vamana graph lives on NVMe | ||
| /// and is deliberately excluded; the Linux io_uring read buffers are | ||
| /// small, bounded, and also excluded. Previously this tier reported 0 — | ||
| /// untracked resident memory (D9 quarantine). | ||
| pub fn resident_bytes(&self) -> usize { | ||
| std::mem::size_of::<Self>() + self.pq_codes.len() + self.pq.resident_bytes() | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Double-counts ProductQuantizer's struct overhead.
pq is an inline field of DiskAnnSegment, so size_of::<Self>() already includes ProductQuantizer's stack layout. Adding pq.resident_bytes() (which itself starts from size_of::<ProductQuantizer>()) re-adds that same fixed overhead a second time. Magnitude is small (tens of bytes) but the composition pattern is incorrect.
🔧 Proposed fix
pub fn resident_bytes(&self) -> usize {
- std::mem::size_of::<Self>() + self.pq_codes.len() + self.pq.resident_bytes()
+ std::mem::size_of::<Self>() + self.pq_codes.len()
+ + (self.pq.resident_bytes()
+ - std::mem::size_of::<crate::vector::diskann::pq::ProductQuantizer>())
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /// Estimated resident heap bytes of this cold segment (accounting-spine | |
| /// A1): PQ codes (`num_vectors * m` bytes, kept in RAM) + the trained | |
| /// codebook, plus fixed struct overhead. The Vamana graph lives on NVMe | |
| /// and is deliberately excluded; the Linux io_uring read buffers are | |
| /// small, bounded, and also excluded. Previously this tier reported 0 — | |
| /// untracked resident memory (D9 quarantine). | |
| pub fn resident_bytes(&self) -> usize { | |
| std::mem::size_of::<Self>() + self.pq_codes.len() + self.pq.resident_bytes() | |
| } | |
| pub fn resident_bytes(&self) -> usize { | |
| std::mem::size_of::<Self>() + self.pq_codes.len() | |
| (self.pq.resident_bytes() | |
| - std::mem::size_of::<crate::vector::diskann::pq::ProductQuantizer>()) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/vector/diskann/segment.rs` around lines 492 - 500, The resident_bytes
estimate for DiskAnnSegment is double-counting the inline ProductQuantizer
struct overhead because size_of::<Self>() already includes the pq field’s layout
and pq.resident_bytes() also starts with size_of::<ProductQuantizer>(). Update
DiskAnnSegment::resident_bytes to only add the truly extra heap-resident pieces
from pq (such as its trained codebook/owned buffers) or make
ProductQuantizer::resident_bytes exclude its own inline struct size, so the
accounting is not counted twice.
…sed] + resolve PR #TBD placeholders (#330) The v0.6.0 tag (355f68d, 2026-07-10) absorbed several PRs that merged after the v0.6.0 release PR itself but before the tag was cut. Their changelog entries were left under [Unreleased], understating what shipped in the tagged v0.6.0 binary. Audit (task #65, v0.7.0 roll-up prep) identified 23 sections whose PR merge commits are verified ancestors of v0.6.0 via `git merge-base --is-ancestor`: PR #248 (FastScan SIMD, SQ8 default, TQ ADC L2 fix, EF_RUNTIME FT.CONFIG, RERANK_MULT+EXACT_BEAM, CLI/moon.conf tuning defaults), PR #250 (13 production-hardening sections), PR #251 (COLD-segment reload off event loop), PR #254 (roadmap docs suite), PR #255 (CI macOS 30m), PR #256 (v0.6.0 ledger closure). Moved verbatim to a new subsection at the top of [0.6.0], with a note explaining the absorption. Sections from PR #252, #253, and #257-#263 were verified NOT ancestors of v0.6.0 and correctly remain in [Unreleased]. Also resolves the "PR #TBD" placeholders left by those sections (now #248, #257, #261) now that the real PR numbers are known. Section-count invariant holds (306 before, 306 after); content unchanged except for the PR-number substitutions. author: Tin Dang Co-authored-by: Tin Dang <tindang.ht97@gmail.com>
Vector tiering M1: one memory-accounting spine every ceiling reads
Stacked on #252 (base =
feat/vector-memory-aware-offload) — shows only this delta. First milestone of the tiering-v2 architecture (decision record:tmp/TIERING-ARCHITECTURE.md, D3 accounting spine + D9 DiskANN quarantine).Problem
A three-agent architecture review of the HOT/WARM/COLD tier found the memory ceilings don't share an accounting spine:
--maxmemorywas 100% blind to vector memory — a pure-vector workload (the documented primary disk-offload case) could drive RSS to OOM while eviction reported "under budget" and never freed a byte.--vec-warm-mmap-budgetapplied per-shard, so an N-shard instance silently allowed N× the configured WARM memory (unlike--maxmemory, which divides).INFO reclamation_mmap_warm_byteswas a permanent 0 (read a never-incremented twin static) while the realMmapBudgetcounter was write-only, read by nothing.Fix (five commits, red/green TDD each)
INFO reclamation_mmap_warm_bytesre-pointed at the liverecl_atomicscounter; newreclamation_mmap_budget_evictions_total; dead static deleted.resident_bytes()forIvfSegment(centroids + posting lists),DiskAnnSegment(PQ codes + codebook; NVMe graph excluded by design),ProductQuantizer; summed into the holder roll-up. D9 quarantine: config docs disambiguate COLD-stub vs COLD-ann; dead knobs marked[reserved: M3/M5].run_evictiongates on the shard aggregate (Σ all dbs' KV + published vector bytes, one Relaxed load per 100ms tick) and evicts across dbs only until the aggregate is back under budget.--vec-warm-mmap-budgetis now an instance-total divided across shards (floors at 1 byte/shard; "0" still disables). Behavior change for multi-shard deployments (CHANGELOG callout).Adversarial review outcome (perf-reviewer + security-reviewer)
2da34b92): A3's initial per-db formulation charged the shard-wide vector term to every logical db — under-detecting with KV spread across dbs (nothing evicted while aggregate RSS overran) and over-evicting (16× multi-tenant drain). Now aggregate-gated with a pinning test (eviction stops at the aggregate target; sibling db untouched).noevictiona vector-heavy shard over budget does not yet reject writes; RSS is bounded by the pressure cascade + RSS watchdog. CHANGELOG limitation + tracked M1 follow-up.Note on "auto maxmemory"
Already shipped as the G1 guardrail (
apply_memory_guardrail, cgroup-aware v2+v1 with host-min, macOS sysctl) at 80% of detected RAM — kept at 80% rather than churning existing deployments; explicit--maxmemoryalways wins.Verification
Dual-runtime: clippy
-D warningsclean (monoio + tokio), fmt clean. New tests:info_reclamation_mmap_warm_bytes_reflects_live_counter,test_ivf_resident_bytes_accounts_heap,test_diskann_resident_bytes_accounts_pq,test_resident_bytes_includes_ivf_and_cold_tiers,test_run_eviction_counts_vector_memory,test_run_eviction_gates_on_aggregate_not_per_db,test_vec_warm_budget_divided_per_shard,recompute_elastic_budget_vector_heavy_shard_not_donor. Suites green both runtimes: timers 2, persistence_tick 7, shared_databases 8, eviction 26, holder 15, ivf 19, diskann 22, info_reclamation 9 (tokio batch 108 + 43 after fixes).Summary by CodeRabbit
New Features
Bug Fixes