selene_graph/vector_index/memory.rs
1//! Memory and structural diagnostics for vector indexes.
2
3const BASIS_POINTS_DENOMINATOR: usize = 10_000;
4
5/// Minimum pending IVF entries before a rebuild recommendation can fire.
6///
7/// The first retrain-policy bench showed that very small novel clusters can
8/// be too little mass for retrained width-2 partitions even when the ratio is
9/// non-zero. This floor keeps the diagnostic from recommending tiny retrains.
10pub const IVF_REBUILD_MIN_PENDING_RETRAIN_ENTRIES: usize = 100;
11
12/// Minimum pending IVF retrain ratio, scaled by 10,000, for rebuild advice.
13pub const IVF_REBUILD_PENDING_RETRAIN_BASIS_POINTS: usize = 100;
14
15/// Estimated resident memory and cardinality details for one vector index.
16///
17/// This is intentionally an estimate rather than allocator-exact accounting.
18/// `estimated_index_bytes` counts index-owned structures and excludes primary
19/// graph vector component allocations that ANN indexes may share through `Arc`
20/// handles. `estimated_reachable_bytes` adds the component bytes referenced by
21/// derived entries and centroids as an upper-bound view; deleted ANN entries can
22/// retain old component storage until the derived index is rebuilt.
23#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
24pub struct VectorIndexMemoryUsage {
25 /// Number of live rows currently admitted to the index.
26 pub indexed_rows: u64,
27 /// Estimated heap bytes owned by the row bitmap.
28 pub row_bitmap_bytes: usize,
29 /// Roaring serialized size for the row bitmap.
30 pub row_bitmap_serialized_bytes: usize,
31 /// Estimated heap bytes owned by the HNSW derived index, excluding vector components.
32 pub hnsw_index_bytes: usize,
33 /// Component bytes reachable through HNSW vector handles.
34 pub hnsw_referenced_vector_bytes: usize,
35 /// Total HNSW entries, including stale deleted row versions.
36 pub hnsw_entries: usize,
37 /// Live HNSW entries reachable from row membership.
38 pub hnsw_live_entries: usize,
39 /// Stale HNSW entries retained for traversability after update/delete.
40 pub hnsw_deleted_entries: usize,
41 /// Stored directed HNSW links across all layers.
42 pub hnsw_link_count: usize,
43 /// Stored directed HNSW links in the level-0 layer.
44 pub hnsw_level_zero_link_count: usize,
45 /// Stored directed HNSW links above the level-0 layer.
46 pub hnsw_upper_layer_link_count: usize,
47 /// Maximum HNSW layer count attached to any indexed entry.
48 pub hnsw_max_layer_count: usize,
49 /// Maximum directed HNSW links stored in a single entry layer.
50 pub hnsw_max_links_per_layer: usize,
51 /// Average directed HNSW links per entry, scaled by 10,000.
52 pub hnsw_average_links_per_entry_basis_points: usize,
53 /// Estimated heap bytes owned by the IVF derived index, excluding vector components.
54 pub ivf_index_bytes: usize,
55 /// Component bytes reachable through IVF vector handles.
56 pub ivf_referenced_vector_bytes: usize,
57 /// Total IVF entries, including stale deleted row versions.
58 pub ivf_entries: usize,
59 /// Live IVF entries reachable from row membership.
60 pub ivf_live_entries: usize,
61 /// Stale IVF entries retained until the derived index is rebuilt.
62 pub ivf_deleted_entries: usize,
63 /// Number of trained IVF centroids.
64 pub ivf_centroids: usize,
65 /// Number of IVF inverted lists.
66 pub ivf_list_count: usize,
67 /// Number of IVF inverted lists with at least one assigned live entry.
68 pub ivf_non_empty_list_count: usize,
69 /// Maximum assigned live entries in one IVF inverted list.
70 pub ivf_max_list_len: usize,
71 /// Average assigned live entries per IVF inverted list, scaled by 10,000.
72 pub ivf_average_list_len_basis_points: usize,
73 /// Non-stale IVF entries assigned to inverted lists.
74 pub ivf_assigned_entries: usize,
75 /// Live IVF entries whose current vector was inserted or replaced after centroid training.
76 pub ivf_pending_retrain_entries: usize,
77 /// Estimated heap bytes owned by the TurboQuant derived index, excluding vector components.
78 pub turbo_quant_index_bytes: usize,
79 /// Component bytes reachable through TurboQuant-owned full-vector handles.
80 pub turbo_quant_referenced_vector_bytes: usize,
81 /// Total TurboQuant compressed entries.
82 pub turbo_quant_entries: usize,
83 /// Live TurboQuant row entries.
84 pub turbo_quant_live_entries: usize,
85 /// Stale TurboQuant entries retained by the derived index.
86 ///
87 /// TurboQuant compacts deletes and replacements immediately, so this should
88 /// normally remain zero.
89 pub turbo_quant_deleted_entries: usize,
90 /// Packed TurboQuant coordinate-code bytes.
91 pub turbo_quant_code_bytes: usize,
92 /// TurboQuant scalar codebook bytes.
93 pub turbo_quant_codebook_bytes: usize,
94 /// TurboQuant per-dimension calibration bytes.
95 pub turbo_quant_calibration_bytes: usize,
96 /// Estimated bytes for index-owned structures, excluding referenced vector components.
97 pub estimated_index_bytes: usize,
98 /// Estimated upper-bound bytes reachable from the index including ANN vector components.
99 pub estimated_reachable_bytes: usize,
100}
101
102impl VectorIndexMemoryUsage {
103 /// Return pending IVF retrain entries divided by live IVF entries, scaled by 10,000.
104 #[must_use]
105 pub fn ivf_pending_retrain_basis_points(&self) -> usize {
106 self.ivf_pending_retrain_entries
107 .saturating_mul(BASIS_POINTS_DENOMINATOR)
108 .checked_div(self.ivf_live_entries)
109 .unwrap_or_default()
110 }
111
112 /// Return true when the IVF index should be rebuilt by maintenance soon.
113 ///
114 /// The recommendation is deliberately diagnostic only: reads never rebuild
115 /// indexes, and callers still decide when to run `selene.rebuild_vector_indexes`.
116 /// Deleted IVF entries are not part of this first trigger because delete
117 /// maintenance already unlinks them from inverted lists; existing reclaimed
118 /// counters expose that memory-only pressure separately.
119 #[must_use]
120 pub fn ivf_rebuild_recommended(&self) -> bool {
121 self.ivf_pending_retrain_entries >= IVF_REBUILD_MIN_PENDING_RETRAIN_ENTRIES
122 && self.ivf_pending_retrain_basis_points() >= IVF_REBUILD_PENDING_RETRAIN_BASIS_POINTS
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::{
129 IVF_REBUILD_MIN_PENDING_RETRAIN_ENTRIES, IVF_REBUILD_PENDING_RETRAIN_BASIS_POINTS,
130 VectorIndexMemoryUsage,
131 };
132
133 #[test]
134 fn ivf_pending_retrain_ratio_uses_live_entries() {
135 let usage = VectorIndexMemoryUsage {
136 ivf_live_entries: 10_000,
137 ivf_pending_retrain_entries: 100,
138 ..VectorIndexMemoryUsage::default()
139 };
140
141 assert_eq!(
142 usage.ivf_pending_retrain_basis_points(),
143 IVF_REBUILD_PENDING_RETRAIN_BASIS_POINTS
144 );
145 }
146
147 #[test]
148 fn ivf_rebuild_recommendation_requires_ratio_and_floor() {
149 let below_floor = VectorIndexMemoryUsage {
150 ivf_live_entries: 1_000,
151 ivf_pending_retrain_entries: IVF_REBUILD_MIN_PENDING_RETRAIN_ENTRIES - 1,
152 ..VectorIndexMemoryUsage::default()
153 };
154 let below_ratio = VectorIndexMemoryUsage {
155 ivf_live_entries: 20_000,
156 ivf_pending_retrain_entries: IVF_REBUILD_MIN_PENDING_RETRAIN_ENTRIES,
157 ..VectorIndexMemoryUsage::default()
158 };
159 let recommended = VectorIndexMemoryUsage {
160 ivf_live_entries: 10_000,
161 ivf_pending_retrain_entries: IVF_REBUILD_MIN_PENDING_RETRAIN_ENTRIES,
162 ..VectorIndexMemoryUsage::default()
163 };
164
165 assert!(!below_floor.ivf_rebuild_recommended());
166 assert!(!below_ratio.ivf_rebuild_recommended());
167 assert!(recommended.ivf_rebuild_recommended());
168 }
169}