@harperfast/hnsw 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +229 -0
- package/Cargo.toml +30 -0
- package/DESIGN.md +319 -0
- package/LICENSE +202 -0
- package/README.md +88 -0
- package/build.mjs +46 -0
- package/build.rs +6 -0
- package/index.d.ts +104 -0
- package/index.js +57 -0
- package/package.json +44 -0
- package/prebuilds/darwin-arm64/hnsw-plane.node +0 -0
- package/prebuilds/linux-arm64/hnsw-plane.node +0 -0
- package/prebuilds/linux-x64/hnsw-plane.node +0 -0
- package/prebuilds/win32-x64/hnsw-plane.node +0 -0
- package/src/bin/bench.rs +246 -0
- package/src/distance.rs +138 -0
- package/src/format.rs +556 -0
- package/src/graph.rs +650 -0
- package/src/insert.rs +292 -0
- package/src/lib.rs +15 -0
- package/src/napi.rs +486 -0
- package/src/search.rs +477 -0
- package/src/seqlock.rs +217 -0
package/src/graph.rs
ADDED
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
//! Slot-level node access over the plane file, mediated by per-slot seqlocks. Hot-path
|
|
2
|
+
//! reads (distance, neighbor ids) are zero-copy against the mmap; full-copy read_node
|
|
3
|
+
//! exists for construction paths. Upper-layer adjacency lives in a fixed-entry region of
|
|
4
|
+
//! the same file (per-entry seqlocks), so the hierarchy persists with the graph and
|
|
5
|
+
//! concurrent searches share nothing mutable.
|
|
6
|
+
|
|
7
|
+
use crate::distance::{cosine_i8_i8_raw, cosine_int8_raw, Query};
|
|
8
|
+
use crate::format::{
|
|
9
|
+
PlaneFile, FLAG_DELETED, FLAG_VALID, MAX_UPPER_LEVELS, NO_UPPER, S_DEGREE, S_FLAGS, S_INV_MAG, S_LEVEL, S_SCALE,
|
|
10
|
+
S_UPPER_IDX, S_VECTOR, UPPER_CAP, UPPER_LEVEL_STRIDE, U_LEVELS, U_LISTS,
|
|
11
|
+
};
|
|
12
|
+
use crate::seqlock;
|
|
13
|
+
use crate::seqlock::Wedged;
|
|
14
|
+
|
|
15
|
+
pub struct Graph {
|
|
16
|
+
pub file: PlaneFile,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// A consistent full copy of one node (construction paths only; search uses zero-copy).
|
|
20
|
+
pub struct NodeRead {
|
|
21
|
+
pub level: u8,
|
|
22
|
+
pub scale: f32,
|
|
23
|
+
pub inv_mag: f32,
|
|
24
|
+
pub vector: Vec<i8>,
|
|
25
|
+
pub neighbors: Vec<u32>,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
impl Graph {
|
|
29
|
+
pub fn new(file: PlaneFile) -> Self {
|
|
30
|
+
Graph { file }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#[inline]
|
|
34
|
+
fn in_range(&self, id: u32) -> bool {
|
|
35
|
+
(id as u64) < self.file.id_high_water()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Sanitizer for a slot lock taken over from a dead writer: the payload is half-written,
|
|
39
|
+
/// so the slot must read as deleted until something rewrites it (heal-on-touch contract;
|
|
40
|
+
/// FLAG_DELETED rather than 0 so hosts can still free/reuse the id).
|
|
41
|
+
fn slot_sanitizer(&self, id: u32) -> impl Fn() + '_ {
|
|
42
|
+
move || unsafe {
|
|
43
|
+
let p = self.file.slot_ptr_mut(id);
|
|
44
|
+
// a dead writer's slot may hold a garbage (or zero-initialized) upper index; a
|
|
45
|
+
// later raw rewrite would reuse it and clobber another node's hierarchy
|
|
46
|
+
(p.add(S_UPPER_IDX) as *mut u32).write_unaligned(NO_UPPER);
|
|
47
|
+
*p.add(S_FLAGS) = FLAG_DELETED;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn owner_dead(&self) -> impl Fn(u32) -> bool + '_ {
|
|
52
|
+
move |tag| self.file.tag_is_dead(tag)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Sanitizer for an upper-entry lock taken over from a dead writer.
|
|
56
|
+
fn upper_sanitizer(&self, idx: u32) -> impl Fn() + '_ {
|
|
57
|
+
move || unsafe { *self.file.upper_ptr_mut(idx).add(U_LEVELS) = 0 }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Zero-copy distance from `query` to the stored vector of `id`. None for absent/deleted.
|
|
61
|
+
#[inline]
|
|
62
|
+
pub fn distance_to(&self, id: u32, query: &Query) -> Option<f32> {
|
|
63
|
+
if !self.in_range(id) {
|
|
64
|
+
return None;
|
|
65
|
+
}
|
|
66
|
+
let seq = self.file.seq_atomic(id);
|
|
67
|
+
seqlock::read_consistent(seq, self.file.self_tag, || {
|
|
68
|
+
let p = self.file.slot_ptr(id);
|
|
69
|
+
unsafe {
|
|
70
|
+
let flags = *p.add(S_FLAGS);
|
|
71
|
+
if flags & FLAG_VALID == 0 || flags & FLAG_DELETED != 0 {
|
|
72
|
+
return None;
|
|
73
|
+
}
|
|
74
|
+
let scale = (p.add(S_SCALE) as *const f32).read_unaligned();
|
|
75
|
+
let inv_mag = (p.add(S_INV_MAG) as *const f32).read_unaligned();
|
|
76
|
+
Some(cosine_int8_raw(query, p.add(S_VECTOR) as *const i8, scale, inv_mag))
|
|
77
|
+
}
|
|
78
|
+
}, self.slot_sanitizer(id), || None, self.owner_dead())
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Symmetric stored-to-stored distance (construction-time neighbor↔neighbor checks).
|
|
82
|
+
/// Plain unlocked reads: a torn read only perturbs a construction heuristic.
|
|
83
|
+
pub fn distance_between(&self, a: u32, b: u32) -> Option<f32> {
|
|
84
|
+
if !self.in_range(a) || !self.in_range(b) {
|
|
85
|
+
return None;
|
|
86
|
+
}
|
|
87
|
+
let dims = self.file.dims;
|
|
88
|
+
let pa = self.file.slot_ptr(a);
|
|
89
|
+
let pb = self.file.slot_ptr(b);
|
|
90
|
+
unsafe {
|
|
91
|
+
let fa = *pa.add(S_FLAGS);
|
|
92
|
+
let fb = *pb.add(S_FLAGS);
|
|
93
|
+
if fa & FLAG_VALID == 0 || fa & FLAG_DELETED != 0 || fb & FLAG_VALID == 0 || fb & FLAG_DELETED != 0 {
|
|
94
|
+
return None;
|
|
95
|
+
}
|
|
96
|
+
let scale_a = (pa.add(S_SCALE) as *const f32).read_unaligned();
|
|
97
|
+
let inv_a = (pa.add(S_INV_MAG) as *const f32).read_unaligned();
|
|
98
|
+
let scale_b = (pb.add(S_SCALE) as *const f32).read_unaligned();
|
|
99
|
+
let inv_b = (pb.add(S_INV_MAG) as *const f32).read_unaligned();
|
|
100
|
+
Some(cosine_i8_i8_raw(
|
|
101
|
+
pa.add(S_VECTOR) as *const i8,
|
|
102
|
+
scale_a,
|
|
103
|
+
inv_a,
|
|
104
|
+
pb.add(S_VECTOR) as *const i8,
|
|
105
|
+
scale_b,
|
|
106
|
+
inv_b,
|
|
107
|
+
dims,
|
|
108
|
+
))
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// Copy layer-0 neighbor ids into `out` (cleared first). Returns the node's level,
|
|
113
|
+
/// or None for absent/deleted.
|
|
114
|
+
#[inline]
|
|
115
|
+
pub fn neighbors_into(&self, id: u32, out: &mut Vec<u32>) -> Option<u8> {
|
|
116
|
+
out.clear();
|
|
117
|
+
if !self.in_range(id) {
|
|
118
|
+
return None;
|
|
119
|
+
}
|
|
120
|
+
let seq = self.file.seq_atomic(id);
|
|
121
|
+
let cap = self.file.layer0_cap;
|
|
122
|
+
let dims = self.file.dims;
|
|
123
|
+
seqlock::read_consistent(seq, self.file.self_tag, || {
|
|
124
|
+
out.clear();
|
|
125
|
+
let p = self.file.slot_ptr(id);
|
|
126
|
+
unsafe {
|
|
127
|
+
let flags = *p.add(S_FLAGS);
|
|
128
|
+
if flags & FLAG_VALID == 0 || flags & FLAG_DELETED != 0 {
|
|
129
|
+
return None;
|
|
130
|
+
}
|
|
131
|
+
let level = *p.add(S_LEVEL);
|
|
132
|
+
let degree = u16::from_le((p.add(S_DEGREE) as *const u16).read_unaligned()) as usize;
|
|
133
|
+
let base = p.add(S_VECTOR + dims) as *const u32;
|
|
134
|
+
for i in 0..degree.min(cap) {
|
|
135
|
+
out.push(u32::from_le(base.add(i).read_unaligned()));
|
|
136
|
+
}
|
|
137
|
+
Some(level)
|
|
138
|
+
}
|
|
139
|
+
}, self.slot_sanitizer(id), || None, self.owner_dead())
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// The node's upper-region entry index, or NO_UPPER.
|
|
143
|
+
#[inline]
|
|
144
|
+
fn upper_idx_of(&self, id: u32) -> u32 {
|
|
145
|
+
if !self.in_range(id) {
|
|
146
|
+
return NO_UPPER;
|
|
147
|
+
}
|
|
148
|
+
let seq = self.file.seq_atomic(id);
|
|
149
|
+
seqlock::read_consistent(seq, self.file.self_tag, || {
|
|
150
|
+
let p = self.file.slot_ptr(id);
|
|
151
|
+
unsafe {
|
|
152
|
+
let flags = *p.add(S_FLAGS);
|
|
153
|
+
if flags & FLAG_VALID == 0 || flags & FLAG_DELETED != 0 {
|
|
154
|
+
return NO_UPPER;
|
|
155
|
+
}
|
|
156
|
+
(p.add(S_UPPER_IDX) as *const u32).read_unaligned()
|
|
157
|
+
}
|
|
158
|
+
}, self.slot_sanitizer(id), || NO_UPPER, self.owner_dead())
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// Copy `id`'s neighbor ids at upper `level` (1-based) into `out`. False when the node
|
|
162
|
+
/// has no upper entry or no such level.
|
|
163
|
+
pub fn upper_neighbors_into(&self, id: u32, level: u8, out: &mut Vec<u32>) -> bool {
|
|
164
|
+
out.clear();
|
|
165
|
+
debug_assert!(level >= 1);
|
|
166
|
+
let idx = self.upper_idx_of(id);
|
|
167
|
+
if idx == NO_UPPER || (idx as u64) >= self.file.upper_capacity || level as usize > MAX_UPPER_LEVELS {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
let seq = self.file.upper_seq_atomic(idx);
|
|
171
|
+
seqlock::read_consistent(seq, self.file.self_tag, || {
|
|
172
|
+
out.clear();
|
|
173
|
+
let p = self.file.upper_ptr(idx);
|
|
174
|
+
unsafe {
|
|
175
|
+
let levels = *p.add(U_LEVELS);
|
|
176
|
+
if level > levels {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
let lp = p.add(U_LISTS + (level as usize - 1) * UPPER_LEVEL_STRIDE);
|
|
180
|
+
let degree = u16::from_le((lp as *const u16).read_unaligned()) as usize;
|
|
181
|
+
let base = lp.add(2) as *const u32;
|
|
182
|
+
for i in 0..degree.min(UPPER_CAP) {
|
|
183
|
+
out.push(u32::from_le(base.add(i).read_unaligned()));
|
|
184
|
+
}
|
|
185
|
+
true
|
|
186
|
+
}
|
|
187
|
+
}, self.upper_sanitizer(idx), || false, self.owner_dead())
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Write a node's full upper adjacency into a fresh region entry; returns the entry
|
|
191
|
+
/// index to store in the slot (NO_UPPER when the region is exhausted or levels is empty).
|
|
192
|
+
pub fn write_upper(&self, levels: &[Vec<u32>]) -> Result<u32, Wedged> {
|
|
193
|
+
if levels.is_empty() {
|
|
194
|
+
return Ok(NO_UPPER);
|
|
195
|
+
}
|
|
196
|
+
let idx = self.file.allocate_upper();
|
|
197
|
+
if idx == NO_UPPER {
|
|
198
|
+
return Ok(NO_UPPER);
|
|
199
|
+
}
|
|
200
|
+
let seq = self.file.upper_seq_atomic(idx);
|
|
201
|
+
let _guard = seqlock::write_lock(seq, self.file.self_tag, self.upper_sanitizer(idx), self.owner_dead())?;
|
|
202
|
+
let p = self.file.upper_ptr_mut(idx);
|
|
203
|
+
unsafe {
|
|
204
|
+
let n = levels.len().min(MAX_UPPER_LEVELS);
|
|
205
|
+
*p.add(U_LEVELS) = n as u8;
|
|
206
|
+
for (l, list) in levels.iter().take(n).enumerate() {
|
|
207
|
+
let lp = p.add(U_LISTS + l * UPPER_LEVEL_STRIDE);
|
|
208
|
+
let deg = list.len().min(UPPER_CAP);
|
|
209
|
+
(lp as *mut u16).write_unaligned((deg as u16).to_le());
|
|
210
|
+
let base = lp.add(2) as *mut u32;
|
|
211
|
+
for (i, id) in list.iter().take(deg).enumerate() {
|
|
212
|
+
base.add(i).write_unaligned(id.to_le());
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
Ok(idx)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// Rewrite an existing upper entry in place (full state). Used by the raw mirroring
|
|
220
|
+
/// path so repeated updates to a high-level node reuse its entry instead of leaking one
|
|
221
|
+
/// per rewrite.
|
|
222
|
+
pub fn rewrite_upper(&self, idx: u32, levels: &[Vec<u32>]) -> Result<(), Wedged> {
|
|
223
|
+
let seq = self.file.upper_seq_atomic(idx);
|
|
224
|
+
let _guard = seqlock::write_lock(seq, self.file.self_tag, self.upper_sanitizer(idx), self.owner_dead())?;
|
|
225
|
+
let p = self.file.upper_ptr_mut(idx);
|
|
226
|
+
unsafe {
|
|
227
|
+
let n = levels.len().min(MAX_UPPER_LEVELS);
|
|
228
|
+
*p.add(U_LEVELS) = n as u8;
|
|
229
|
+
for (l, list) in levels.iter().take(n).enumerate() {
|
|
230
|
+
let lp = p.add(U_LISTS + l * UPPER_LEVEL_STRIDE);
|
|
231
|
+
let deg = list.len().min(UPPER_CAP);
|
|
232
|
+
(lp as *mut u16).write_unaligned((deg as u16).to_le());
|
|
233
|
+
let base = lp.add(2) as *mut u32;
|
|
234
|
+
for (i, id) in list.iter().take(deg).enumerate() {
|
|
235
|
+
base.add(i).write_unaligned(id.to_le());
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
Ok(())
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/// Whether a slot has ever been written (valid or deleted) — the builder scan's
|
|
243
|
+
/// skip-if-touched check.
|
|
244
|
+
pub fn node_touched(&self, id: u32) -> bool {
|
|
245
|
+
if !self.in_range(id) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
let seq = self.file.seq_atomic(id);
|
|
249
|
+
seqlock::read_consistent(seq, self.file.self_tag, || unsafe { *self.file.slot_ptr(id).add(S_FLAGS) != 0 }, self.slot_sanitizer(id), || true, self.owner_dead())
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// The slot's stored upper idx regardless of valid/deleted flags — the raw mirroring
|
|
253
|
+
/// path reuses a cleared node's entry when the host rewrites the same id.
|
|
254
|
+
fn upper_idx_raw(&self, id: u32) -> u32 {
|
|
255
|
+
if !self.in_range(id) {
|
|
256
|
+
return NO_UPPER;
|
|
257
|
+
}
|
|
258
|
+
let seq = self.file.seq_atomic(id);
|
|
259
|
+
seqlock::read_consistent(seq, self.file.self_tag, || {
|
|
260
|
+
let p = self.file.slot_ptr(id);
|
|
261
|
+
unsafe {
|
|
262
|
+
if *p.add(S_FLAGS) == 0 {
|
|
263
|
+
return NO_UPPER; // never written
|
|
264
|
+
}
|
|
265
|
+
(p.add(S_UPPER_IDX) as *const u32).read_unaligned()
|
|
266
|
+
}
|
|
267
|
+
}, self.slot_sanitizer(id), || NO_UPPER, self.owner_dead())
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/// Mirror a host-maintained node into the plane: full state per call, host-allocated id
|
|
271
|
+
/// (high-water is raised, the plane allocator is bypassed), upper entry reused in place
|
|
272
|
+
/// when present. This is the dual-write phase-1 write path.
|
|
273
|
+
pub fn write_node_raw(
|
|
274
|
+
&self,
|
|
275
|
+
id: u32,
|
|
276
|
+
level: u8,
|
|
277
|
+
vector: &[i8],
|
|
278
|
+
scale: f32,
|
|
279
|
+
inv_mag: f32,
|
|
280
|
+
neighbors: &[u32],
|
|
281
|
+
upper_levels: &[Vec<u32>],
|
|
282
|
+
) -> Result<(), Wedged> {
|
|
283
|
+
self.file.ensure_high_water(id);
|
|
284
|
+
let existing = match self.upper_idx_raw(id) {
|
|
285
|
+
idx if idx != NO_UPPER && (idx as u64) >= self.file.upper_capacity => NO_UPPER, // corrupt stored index
|
|
286
|
+
idx => idx,
|
|
287
|
+
};
|
|
288
|
+
let upper_idx = if upper_levels.is_empty() {
|
|
289
|
+
existing // keep an existing entry bound (level never shrinks in practice)
|
|
290
|
+
} else if existing != NO_UPPER {
|
|
291
|
+
self.rewrite_upper(existing, upper_levels)?;
|
|
292
|
+
existing
|
|
293
|
+
} else {
|
|
294
|
+
self.write_upper(upper_levels)?
|
|
295
|
+
};
|
|
296
|
+
let mut l0 = neighbors.to_vec();
|
|
297
|
+
l0.truncate(self.file.layer0_cap);
|
|
298
|
+
self.write_node(id, level, vector, scale, inv_mag, &l0, upper_idx)?;
|
|
299
|
+
Ok(())
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/// Mark deleted WITHOUT returning the id to the plane freelist — dual-write mode, where
|
|
303
|
+
/// the host owns id allocation and may re-mint or reuse ids on its own schedule.
|
|
304
|
+
pub fn clear_node(&self, id: u32) -> Result<(), Wedged> {
|
|
305
|
+
if (id as u64) >= self.file.max_nodes {
|
|
306
|
+
return Ok(());
|
|
307
|
+
}
|
|
308
|
+
// extend the high-water rather than skipping: a delete mirrored while a backfill
|
|
309
|
+
// scan runs must leave a touched (deleted) slot behind, or the scan's older
|
|
310
|
+
// snapshot would resurrect the node when its cursor reaches this id
|
|
311
|
+
self.file.ensure_high_water(id);
|
|
312
|
+
let seq = self.file.seq_atomic(id);
|
|
313
|
+
let _guard = seqlock::write_lock(seq, self.file.self_tag, self.slot_sanitizer(id), self.owner_dead())?;
|
|
314
|
+
let p = self.file.slot_ptr_mut(id);
|
|
315
|
+
unsafe {
|
|
316
|
+
if *p.add(S_FLAGS) == 0 {
|
|
317
|
+
// tombstoning a never-written slot: its zero-initialized upper_idx would
|
|
318
|
+
// otherwise read as the VALID index 0, and a later raw rewrite of this id
|
|
319
|
+
// would clobber upper entry 0 — another node's hierarchy
|
|
320
|
+
(p.add(S_UPPER_IDX) as *mut u32).write_unaligned(NO_UPPER);
|
|
321
|
+
}
|
|
322
|
+
*p.add(S_FLAGS) = FLAG_DELETED;
|
|
323
|
+
}
|
|
324
|
+
Ok(())
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/// Atomic read-modify-write of `id`'s upper adjacency at `level` (1-based). Returns
|
|
328
|
+
/// false when the node has no entry or level. `f` may read other slots.
|
|
329
|
+
pub fn update_upper_level<F: FnOnce(&mut Vec<u32>)>(&self, id: u32, level: u8, f: F) -> Result<bool, Wedged> {
|
|
330
|
+
let idx = self.upper_idx_of(id);
|
|
331
|
+
if idx == NO_UPPER || (idx as u64) >= self.file.upper_capacity || level as usize > MAX_UPPER_LEVELS {
|
|
332
|
+
return Ok(false);
|
|
333
|
+
}
|
|
334
|
+
let seq = self.file.upper_seq_atomic(idx);
|
|
335
|
+
let _guard = seqlock::write_lock(seq, self.file.self_tag, self.upper_sanitizer(idx), self.owner_dead())?;
|
|
336
|
+
let p = self.file.upper_ptr_mut(idx);
|
|
337
|
+
unsafe {
|
|
338
|
+
let levels = *p.add(U_LEVELS);
|
|
339
|
+
if level > levels {
|
|
340
|
+
return Ok(false);
|
|
341
|
+
}
|
|
342
|
+
let lp = p.add(U_LISTS + (level as usize - 1) * UPPER_LEVEL_STRIDE);
|
|
343
|
+
let degree = u16::from_le((lp as *const u16).read_unaligned()) as usize;
|
|
344
|
+
let base = lp.add(2) as *mut u32;
|
|
345
|
+
let mut list: Vec<u32> = (0..degree.min(UPPER_CAP)).map(|i| u32::from_le(base.add(i).read_unaligned())).collect();
|
|
346
|
+
f(&mut list);
|
|
347
|
+
list.truncate(UPPER_CAP);
|
|
348
|
+
(lp as *mut u16).write_unaligned((list.len() as u16).to_le());
|
|
349
|
+
for (i, id) in list.iter().enumerate() {
|
|
350
|
+
base.add(i).write_unaligned(id.to_le());
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
Ok(true)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/// Seqlock-consistent full copy (construction paths).
|
|
357
|
+
pub fn read_node(&self, id: u32) -> Option<NodeRead> {
|
|
358
|
+
if !self.in_range(id) {
|
|
359
|
+
return None;
|
|
360
|
+
}
|
|
361
|
+
let seq = self.file.seq_atomic(id);
|
|
362
|
+
let dims = self.file.dims;
|
|
363
|
+
let cap = self.file.layer0_cap;
|
|
364
|
+
seqlock::read_consistent(seq, self.file.self_tag, || {
|
|
365
|
+
let p = self.file.slot_ptr(id);
|
|
366
|
+
unsafe {
|
|
367
|
+
let flags = *p.add(S_FLAGS);
|
|
368
|
+
if flags & FLAG_VALID == 0 || flags & FLAG_DELETED != 0 {
|
|
369
|
+
return None;
|
|
370
|
+
}
|
|
371
|
+
let level = *p.add(S_LEVEL);
|
|
372
|
+
let degree = u16::from_le((p.add(S_DEGREE) as *const u16).read_unaligned()) as usize;
|
|
373
|
+
let scale = (p.add(S_SCALE) as *const f32).read_unaligned();
|
|
374
|
+
let inv_mag = (p.add(S_INV_MAG) as *const f32).read_unaligned();
|
|
375
|
+
let vector = std::slice::from_raw_parts(p.add(S_VECTOR) as *const i8, dims).to_vec();
|
|
376
|
+
let nbase = p.add(S_VECTOR + dims) as *const u32;
|
|
377
|
+
let neighbors = (0..degree.min(cap)).map(|i| u32::from_le(nbase.add(i).read_unaligned())).collect();
|
|
378
|
+
Some(NodeRead { level, scale, inv_mag, vector, neighbors })
|
|
379
|
+
}
|
|
380
|
+
}, self.slot_sanitizer(id), || None, self.owner_dead())
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/// Write a full slot under its seqlock. `neighbors` is pruned to layer0_cap by the
|
|
384
|
+
/// caller; `upper_idx` is a write_upper() result (NO_UPPER for level-0 nodes).
|
|
385
|
+
pub fn write_node(&self, id: u32, level: u8, vector: &[i8], scale: f32, inv_mag: f32, neighbors: &[u32], upper_idx: u32) -> Result<(), Wedged> {
|
|
386
|
+
debug_assert!(neighbors.len() <= self.file.layer0_cap);
|
|
387
|
+
debug_assert_eq!(vector.len(), self.file.dims);
|
|
388
|
+
let seq = self.file.seq_atomic(id);
|
|
389
|
+
let _guard = seqlock::write_lock(seq, self.file.self_tag, self.slot_sanitizer(id), self.owner_dead())?;
|
|
390
|
+
let p = self.file.slot_ptr_mut(id);
|
|
391
|
+
let dims = self.file.dims;
|
|
392
|
+
unsafe {
|
|
393
|
+
*p.add(S_LEVEL) = level;
|
|
394
|
+
(p.add(S_DEGREE) as *mut u16).write_unaligned((neighbors.len() as u16).to_le());
|
|
395
|
+
(p.add(S_SCALE) as *mut f32).write_unaligned(scale);
|
|
396
|
+
(p.add(S_INV_MAG) as *mut f32).write_unaligned(inv_mag);
|
|
397
|
+
(p.add(S_UPPER_IDX) as *mut u32).write_unaligned(upper_idx);
|
|
398
|
+
std::ptr::copy_nonoverlapping(vector.as_ptr() as *const u8, p.add(S_VECTOR), dims);
|
|
399
|
+
for (i, n) in neighbors.iter().enumerate() {
|
|
400
|
+
(p.add(S_VECTOR + dims + i * 4) as *mut u32).write_unaligned(n.to_le());
|
|
401
|
+
}
|
|
402
|
+
// valid last within the locked section; the seqlock release publishes it
|
|
403
|
+
*p.add(S_FLAGS) = FLAG_VALID;
|
|
404
|
+
}
|
|
405
|
+
Ok(())
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/// Atomic read-modify-write of a node's layer-0 neighbor list under its seqlock.
|
|
409
|
+
/// `f` may read OTHER slots (e.g. distance_between for pruning) — those are plain
|
|
410
|
+
/// unlocked reads, so no lock ordering issue — but must not lock this graph's slots.
|
|
411
|
+
/// Returns false for absent/deleted nodes.
|
|
412
|
+
pub fn update_neighbors<F: FnOnce(&mut Vec<u32>)>(&self, id: u32, f: F) -> Result<bool, Wedged> {
|
|
413
|
+
if !self.in_range(id) {
|
|
414
|
+
return Ok(false);
|
|
415
|
+
}
|
|
416
|
+
let seq = self.file.seq_atomic(id);
|
|
417
|
+
let _guard = seqlock::write_lock(seq, self.file.self_tag, self.slot_sanitizer(id), self.owner_dead())?;
|
|
418
|
+
let p = self.file.slot_ptr_mut(id);
|
|
419
|
+
let dims = self.file.dims;
|
|
420
|
+
let cap = self.file.layer0_cap;
|
|
421
|
+
unsafe {
|
|
422
|
+
let flags = *p.add(S_FLAGS);
|
|
423
|
+
if flags & FLAG_VALID == 0 || flags & FLAG_DELETED != 0 {
|
|
424
|
+
return Ok(false);
|
|
425
|
+
}
|
|
426
|
+
let degree = u16::from_le((p.add(S_DEGREE) as *const u16).read_unaligned()) as usize;
|
|
427
|
+
let base = p.add(S_VECTOR + dims) as *mut u32;
|
|
428
|
+
let mut list: Vec<u32> = (0..degree.min(cap)).map(|i| u32::from_le(base.add(i).read_unaligned())).collect();
|
|
429
|
+
f(&mut list);
|
|
430
|
+
list.truncate(cap);
|
|
431
|
+
(p.add(S_DEGREE) as *mut u16).write_unaligned((list.len() as u16).to_le());
|
|
432
|
+
for (i, n) in list.iter().enumerate() {
|
|
433
|
+
base.add(i).write_unaligned(n.to_le());
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
Ok(true)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/// Apply a precomputed neighbor list only if the current list still equals `expected` —
|
|
440
|
+
/// the compare and the write share one lock acquisition, so heavy work (distance-based
|
|
441
|
+
/// pruning, which can major-fault) happens OUTSIDE the lock and the critical section
|
|
442
|
+
/// stays microseconds. Returns false when the list changed or the node is gone.
|
|
443
|
+
pub fn set_neighbors_if(&self, id: u32, expected: &[u32], next: &[u32]) -> Result<bool, Wedged> {
|
|
444
|
+
debug_assert!(next.len() <= self.file.layer0_cap);
|
|
445
|
+
if !self.in_range(id) {
|
|
446
|
+
return Ok(false);
|
|
447
|
+
}
|
|
448
|
+
let seq = self.file.seq_atomic(id);
|
|
449
|
+
let _guard = seqlock::write_lock(seq, self.file.self_tag, self.slot_sanitizer(id), self.owner_dead())?;
|
|
450
|
+
let p = self.file.slot_ptr_mut(id);
|
|
451
|
+
let dims = self.file.dims;
|
|
452
|
+
unsafe {
|
|
453
|
+
if *p.add(S_FLAGS) != FLAG_VALID {
|
|
454
|
+
return Ok(false);
|
|
455
|
+
}
|
|
456
|
+
let degree = u16::from_le((p.add(S_DEGREE) as *const u16).read_unaligned()) as usize;
|
|
457
|
+
if degree != expected.len() {
|
|
458
|
+
return Ok(false);
|
|
459
|
+
}
|
|
460
|
+
let base = p.add(S_VECTOR + dims) as *mut u32;
|
|
461
|
+
for (i, want) in expected.iter().enumerate() {
|
|
462
|
+
if u32::from_le(base.add(i).read_unaligned()) != *want {
|
|
463
|
+
return Ok(false);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
(p.add(S_DEGREE) as *mut u16).write_unaligned((next.len() as u16).to_le());
|
|
467
|
+
for (i, n) in next.iter().enumerate() {
|
|
468
|
+
base.add(i).write_unaligned(n.to_le());
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
Ok(true)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/// Replace only the neighbor list (single-writer construction path).
|
|
475
|
+
pub fn write_neighbors(&self, id: u32, neighbors: &[u32]) -> Result<(), Wedged> {
|
|
476
|
+
debug_assert!(neighbors.len() <= self.file.layer0_cap);
|
|
477
|
+
let seq = self.file.seq_atomic(id);
|
|
478
|
+
let _guard = seqlock::write_lock(seq, self.file.self_tag, self.slot_sanitizer(id), self.owner_dead())?;
|
|
479
|
+
let p = self.file.slot_ptr_mut(id);
|
|
480
|
+
let dims = self.file.dims;
|
|
481
|
+
unsafe {
|
|
482
|
+
(p.add(S_DEGREE) as *mut u16).write_unaligned((neighbors.len() as u16).to_le());
|
|
483
|
+
for (i, n) in neighbors.iter().enumerate() {
|
|
484
|
+
(p.add(S_VECTOR + dims + i * 4) as *mut u32).write_unaligned(n.to_le());
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
Ok(())
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/// Mark deleted (traversals skip it), free its upper entry, and return the id to the
|
|
491
|
+
/// plane freelist. Deleting the current entry point re-elects a replacement — without
|
|
492
|
+
/// that, every search returns empty and every insert orphans itself against the dead
|
|
493
|
+
/// entry.
|
|
494
|
+
pub fn delete_node(&self, id: u32) -> Result<(), Wedged> {
|
|
495
|
+
if !self.in_range(id) {
|
|
496
|
+
return Ok(()); // never-allocated or out-of-range ids have nothing to delete
|
|
497
|
+
}
|
|
498
|
+
// capture neighbors before invalidating: they are the best re-election candidates
|
|
499
|
+
let (entry_id, _) = self.file.entry_point();
|
|
500
|
+
let mut candidates: Vec<u32> = Vec::new();
|
|
501
|
+
if entry_id == id {
|
|
502
|
+
self.neighbors_into(id, &mut candidates);
|
|
503
|
+
}
|
|
504
|
+
let upper_idx;
|
|
505
|
+
{
|
|
506
|
+
let seq = self.file.seq_atomic(id);
|
|
507
|
+
let _guard = seqlock::write_lock(seq, self.file.self_tag, self.slot_sanitizer(id), self.owner_dead())?;
|
|
508
|
+
let p = self.file.slot_ptr_mut(id);
|
|
509
|
+
unsafe {
|
|
510
|
+
if *p.add(S_FLAGS) != FLAG_VALID {
|
|
511
|
+
// deleting a never-written or already-deleted id must not free again:
|
|
512
|
+
// a double-push makes the freelist a self-cycle that hands the same id
|
|
513
|
+
// to every subsequent allocation
|
|
514
|
+
return Ok(());
|
|
515
|
+
}
|
|
516
|
+
upper_idx = (p.add(S_UPPER_IDX) as *const u32).read_unaligned();
|
|
517
|
+
(p.add(S_UPPER_IDX) as *mut u32).write_unaligned(NO_UPPER);
|
|
518
|
+
*p.add(S_FLAGS) = FLAG_DELETED;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if upper_idx != NO_UPPER && (upper_idx as u64) < self.file.upper_capacity {
|
|
522
|
+
// empty the entry under its own lock BEFORE freeing: a traversal that already
|
|
523
|
+
// read this node's upper_idx must find a dead entry, not one reallocated to a
|
|
524
|
+
// different node mid-read
|
|
525
|
+
self.rewrite_upper(upper_idx, &[])?;
|
|
526
|
+
}
|
|
527
|
+
self.file.free_upper(upper_idx);
|
|
528
|
+
if entry_id == id {
|
|
529
|
+
self.reelect_entry_point_replacing(&candidates, id);
|
|
530
|
+
}
|
|
531
|
+
self.file.free_id(id);
|
|
532
|
+
Ok(())
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/// Pick a new entry point: the highest-level live node among `preferred`, else the
|
|
536
|
+
/// first live node found scanning the id range (rare path: only when the entry's whole
|
|
537
|
+
/// neighborhood is gone). An empty graph clears the entry.
|
|
538
|
+
/// A node's level without copying its vector or edges (cheap re-election scans).
|
|
539
|
+
fn node_level(&self, id: u32) -> Option<u8> {
|
|
540
|
+
if !self.in_range(id) {
|
|
541
|
+
return None;
|
|
542
|
+
}
|
|
543
|
+
let seq = self.file.seq_atomic(id);
|
|
544
|
+
seqlock::read_consistent(seq, self.file.self_tag, || {
|
|
545
|
+
let p = self.file.slot_ptr(id);
|
|
546
|
+
unsafe {
|
|
547
|
+
if *p.add(S_FLAGS) != FLAG_VALID {
|
|
548
|
+
return None;
|
|
549
|
+
}
|
|
550
|
+
Some(*p.add(S_LEVEL))
|
|
551
|
+
}
|
|
552
|
+
}, self.slot_sanitizer(id), || None, self.owner_dead())
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/// Pick a new entry point: the highest-level live node among `preferred`, else the
|
|
556
|
+
/// highest-level live node found scanning the id range (level reads only — no per-node
|
|
557
|
+
/// vector copies; still O(high-water), which only runs when an entry point vanished
|
|
558
|
+
/// with no live neighborhood). Preferring level keeps the hierarchy navigable — a
|
|
559
|
+
/// level-0 entry degrades every search to a layer-0-only beam. An empty graph clears
|
|
560
|
+
/// the entry.
|
|
561
|
+
pub(crate) fn reelect_entry_point(&self, preferred: &[u32]) {
|
|
562
|
+
self.reelect_entry_point_replacing(preferred, crate::format::NO_ID)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
fn reelect_entry_point_replacing(&self, preferred: &[u32], replacing: u32) {
|
|
566
|
+
let mut best: Option<(u32, u8)> = None;
|
|
567
|
+
// the most recently replaced entry point is the best cheap candidate: usually alive,
|
|
568
|
+
// usually high-level — and it makes the full fallback scan a last resort
|
|
569
|
+
let prev = self.file.previous_entry_point();
|
|
570
|
+
if prev != crate::format::NO_ID && prev != replacing {
|
|
571
|
+
if let Some(level) = self.node_level(prev) {
|
|
572
|
+
best = Some((prev, level));
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
for &cand in preferred {
|
|
576
|
+
if let Some(level) = self.node_level(cand) {
|
|
577
|
+
if best.map(|(_, l)| level > l).unwrap_or(true) {
|
|
578
|
+
best = Some((cand, level));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if best.is_none() {
|
|
583
|
+
let hw = self.file.id_high_water().min(self.file.max_nodes) as u32;
|
|
584
|
+
for cand in 0..hw {
|
|
585
|
+
if let Some(level) = self.node_level(cand) {
|
|
586
|
+
if best.map(|(_, l)| level > l).unwrap_or(true) {
|
|
587
|
+
best = Some((cand, level));
|
|
588
|
+
if level as usize >= MAX_UPPER_LEVELS {
|
|
589
|
+
break; // cannot do better
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
match best {
|
|
596
|
+
Some((cand, level)) => self.file.set_entry_point_if_not_better(cand, level as u32, replacing),
|
|
597
|
+
None => self.file.set_entry_point_if_not_better(crate::format::NO_ID, 0, replacing),
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/// write_node, but only when the slot has never been touched — the check and the write
|
|
602
|
+
/// share ONE seqlock acquisition, so a concurrent live mirror's newer write can never be
|
|
603
|
+
/// overwritten by a backfill scan's older snapshot (a two-step check-then-write left
|
|
604
|
+
/// exactly that window). Returns true when this state was written.
|
|
605
|
+
#[allow(clippy::too_many_arguments)]
|
|
606
|
+
pub fn write_node_if_untouched(
|
|
607
|
+
&self,
|
|
608
|
+
id: u32,
|
|
609
|
+
level: u8,
|
|
610
|
+
vector: &[i8],
|
|
611
|
+
scale: f32,
|
|
612
|
+
inv_mag: f32,
|
|
613
|
+
neighbors: &[u32],
|
|
614
|
+
upper_levels: &[Vec<u32>],
|
|
615
|
+
) -> Result<bool, Wedged> {
|
|
616
|
+
debug_assert!(neighbors.len() <= self.file.layer0_cap);
|
|
617
|
+
debug_assert_eq!(vector.len(), self.file.dims);
|
|
618
|
+
self.file.ensure_high_water(id);
|
|
619
|
+
// the upper entry is allocated before taking the slot lock (allocation is cheap and
|
|
620
|
+
// an unused entry is freed below on the untouched-check failing)
|
|
621
|
+
let upper_idx = if upper_levels.is_empty() { NO_UPPER } else { self.write_upper(upper_levels)? };
|
|
622
|
+
let seq = self.file.seq_atomic(id);
|
|
623
|
+
let written = {
|
|
624
|
+
let _guard = seqlock::write_lock(seq, self.file.self_tag, self.slot_sanitizer(id), self.owner_dead())?;
|
|
625
|
+
let p = self.file.slot_ptr_mut(id);
|
|
626
|
+
let dims = self.file.dims;
|
|
627
|
+
unsafe {
|
|
628
|
+
if *p.add(S_FLAGS) != 0 {
|
|
629
|
+
false
|
|
630
|
+
} else {
|
|
631
|
+
*p.add(S_LEVEL) = level;
|
|
632
|
+
(p.add(S_DEGREE) as *mut u16).write_unaligned((neighbors.len() as u16).to_le());
|
|
633
|
+
(p.add(S_SCALE) as *mut f32).write_unaligned(scale);
|
|
634
|
+
(p.add(S_INV_MAG) as *mut f32).write_unaligned(inv_mag);
|
|
635
|
+
(p.add(S_UPPER_IDX) as *mut u32).write_unaligned(upper_idx);
|
|
636
|
+
std::ptr::copy_nonoverlapping(vector.as_ptr() as *const u8, p.add(S_VECTOR), dims);
|
|
637
|
+
for (i, n) in neighbors.iter().enumerate() {
|
|
638
|
+
(p.add(S_VECTOR + dims + i * 4) as *mut u32).write_unaligned(n.to_le());
|
|
639
|
+
}
|
|
640
|
+
*p.add(S_FLAGS) = FLAG_VALID;
|
|
641
|
+
true
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
if !written {
|
|
646
|
+
self.file.free_upper(upper_idx);
|
|
647
|
+
}
|
|
648
|
+
Ok(written)
|
|
649
|
+
}
|
|
650
|
+
}
|