1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
//! Lock-rank enforcement for HNSW graph operations.
//!
//! Defines the global lock ordering invariant and provides runtime
//! checking to prevent deadlocks. The rank system encodes the rule:
//!
//! ```text
//! gpu_vectors_snapshot (rank 5) → vectors (rank 10) → columnar (rank 15)
//! → layers (rank 20) → neighbors (rank 30)
//! ```
//!
//! The `gpu_vectors_snapshot` mutex is acquired before `vectors` in the
//! GPU path (`get_or_refresh_vector_snapshot` takes the mutex first, then
//! calls `with_vectors_read` which takes `vectors`). Writers release
//! `vectors` before reacquiring `gpu_vectors_snapshot` to invalidate, so
//! both call sites observe the same order.
//!
//! Acquiring a lock with lower-or-equal rank than the highest currently
//! held rank is a violation that gets recorded in safety counters.
//!
//! # Release Build Behavior (F-25)
//!
//! In release builds, lock-rank tracking is a complete no-op for maximum
//! search throughput: `record_lock_acquire`/`record_lock_release` discard the
//! rank and touch nothing — no thread-local stack, and (contrary to an earlier
//! version of this note) no atomic counter either. The violation counter is
//! incremented **only** inside the `#[cfg(debug_assertions)]` block. In debug
//! builds, full stack-based tracking is enabled, but it only *warns* via
//! `tracing::warn!` — it never panics — and only for the ranks that actually
//! have a `record_lock_acquire` call site (`GpuVectorsSnapshot`, `Vectors`,
//! `Layers`; `Columnar` and `Neighbors` are `#[allow(dead_code)]` and never
//! recorded).
//!
//! # Higher-level synchronization layered on top of these ranks
//!
//! Some structures sit *above* the lock-rank graph and add their own
//! ordering contracts. The most important one is
//! [`crate::gpu::gpu_csr::CsrCache`], which builds and caches a CSR
//! view of the graph for the GPU dispatch path:
//!
//! - `CsrCache::get_or_rebuild` requires the **`Layers` read lock**
//! (rank 20) to be held for the entire call so concurrent rebuilders
//! observe the same layer topology.
//! - A monotonic `generation` counter (incremented by every
//! `invalidate()`) plus a `built_generation` snapshot solves the ABA
//! problem on dirty-flag caches without introducing a new lock rank:
//! the rebuild commits only when `generation` has not moved during
//! the rebuild window.
//!
//! See `gpu_csr.rs` (`CsrCache` rustdoc, "Caller contract" and
//! "Generation protocol" sections, races #640 and #643) for the
//! detailed protocol and the regression test
//! `gpu_csr_tests::test_csr_cache_concurrent_rebuild_safety`.
//! Audit-2026q2 M5 cross-link.
use HNSW_COUNTERS;
/// Lock rank values — monotonically increasing acquisition order.
///
/// The global lock order is:
/// `gpu_vectors_snapshot → vectors → columnar → layers → neighbors`.
/// Any code path that acquires multiple locks must acquire them
/// in strictly increasing rank order.
pub
// F-25: Thread-local stack only in debug builds to avoid ~10-20ns overhead
// per lock acquire/release in hot search loops.
use RefCell;
thread_local!
/// Records acquisition of a lock at the given rank.
///
/// In debug builds: full thread-local stack tracking with violation detection.
/// In release builds: no-op (zero overhead on hot search paths).
pub
/// Records release of the most recent lock at the given rank.
///
/// In debug builds: pops rank from thread-local stack, detects corruption.
/// In release builds: no-op (zero overhead).
pub
/// Returns the current depth of the lock rank stack for this thread.
///
/// Useful for assertions in tests. Requires debug_assertions (always true in test builds).
// Reason: Debug introspection — available for lock-ordering tests
pub
/// Returns `true` if the current thread is currently holding a lock at `rank`.
///
/// Debug-builds only: callers guarded by `debug_assert!` will compile to
/// nothing in release builds. Use this to encode caller contracts for
/// cache rebuild helpers that must run under a parent lock — e.g. a
/// GPU CSR rebuild is only race-free while the layers read lock is held.
///
/// Returns `true` in release builds (the thread-local stack is not
/// maintained there, so any runtime assertion is a no-op). Callers
/// should always wrap the invocation in `debug_assert!` so the entire
/// check compiles out of release binaries.
// Only reachable via `debug_assert!` in `gpu_csr` (feature-gated) and
// via the locking test module; outside those contexts rustc treats the
// function as dead code. Keep it visible at the crate level so future
// callers can reuse it.
pub
/// Release build stub — never panics, but callers should only invoke this
/// behind `debug_assert!` so the call is compiled out entirely.
pub