1#[cfg(feature = "ndarray")]
2use ndarray::prelude::*;
3use rustc_hash::FxHashMap as HashMap;
4
5use super::DMatrix;
6
7pub fn build_name2index_map(_names: &[Box<str>]) -> HashMap<Box<str>, usize> {
8 _names
9 .iter()
10 .enumerate()
11 .map(|(r, name)| (name.clone(), r))
12 .collect()
13}
14
15#[cfg(feature = "ndarray")]
16pub fn ndarray_to_triplets(array: &Array2<f32>) -> Vec<(u64, u64, f32)> {
17 let eps = 1e-6;
18 array
19 .indexed_iter()
20 .filter(|(_, &elem)| elem.abs() > eps)
21 .map(|((row, col), &value)| (row as u64, col as u64, value))
22 .collect::<Vec<(u64, u64, f32)>>()
23}
24
25pub fn dmatrix_to_triplets(matrix: &DMatrix<f32>) -> Vec<(u64, u64, f32)> {
26 let (nrow, _) = matrix.shape();
27 let eps = 1e-6;
28 matrix
29 .iter() .enumerate()
31 .filter(|(_, &elem)| elem.abs() > eps)
32 .map(|(idx, &value)| {
33 let row = idx % nrow;
34 let col = idx / nrow;
35 (row as u64, col as u64, value)
36 })
37 .collect::<Vec<(u64, u64, f32)>>()
38}
39
40pub fn remove_backend_path(path: &str) -> anyhow::Result<()> {
43 let p = std::path::Path::new(path);
44 if p.exists() {
45 if p.is_file() {
46 std::fs::remove_file(p)?;
47 } else {
48 std::fs::remove_dir_all(p)?;
49 }
50 }
51 Ok(())
52}
53
54static RESERVED: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
56
57pub(crate) const PRELOAD_BYTES_PER_NNZ: usize = 12;
60
61#[derive(Debug)]
68pub struct PreloadReservation {
69 bytes: usize,
70 counter: &'static std::sync::atomic::AtomicUsize,
71}
72
73impl PreloadReservation {
74 #[allow(dead_code)] pub fn bytes(&self) -> usize {
79 self.bytes
80 }
81}
82
83impl Clone for PreloadReservation {
84 fn clone(&self) -> Self {
85 self.counter
86 .fetch_add(self.bytes, std::sync::atomic::Ordering::Relaxed);
87 Self {
88 bytes: self.bytes,
89 counter: self.counter,
90 }
91 }
92}
93
94impl Drop for PreloadReservation {
95 fn drop(&mut self) {
96 self.counter
97 .fetch_sub(self.bytes, std::sync::atomic::Ordering::Relaxed);
98 }
99}
100
101#[allow(dead_code)] pub fn preload_reserved_bytes() -> usize {
104 RESERVED.load(std::sync::atomic::Ordering::Relaxed)
105}
106
107pub fn reserve_preload(nnz: usize, what: &str) -> Option<PreloadReservation> {
120 let cost = nnz.saturating_mul(PRELOAD_BYTES_PER_NNZ);
121 if let Some(budget) = preload_budget_override() {
122 return fits_or_warn(cost, budget, nnz, what).then_some(PreloadReservation {
123 bytes: 0,
124 counter: &RESERVED,
125 });
126 }
127 reserve_against(&RESERVED, cost, auto_preload_budget_bytes(), nnz, what)
128}
129
130fn preload_budget_override() -> Option<usize> {
133 std::env::var("LEGUME_PRELOAD_BUDGET_BYTES")
134 .ok()
135 .and_then(|v| v.parse::<usize>().ok())
136}
137
138fn reserve_against(
139 counter: &'static std::sync::atomic::AtomicUsize,
140 cost: usize,
141 budget: usize,
142 nnz: usize,
143 what: &str,
144) -> Option<PreloadReservation> {
145 use std::sync::atomic::Ordering;
146 let reserved = counter.fetch_add(cost, Ordering::Relaxed);
147 if fits_or_warn(reserved.saturating_add(cost), budget, nnz, what) {
148 Some(PreloadReservation {
149 bytes: cost,
150 counter,
151 })
152 } else {
153 counter.fetch_sub(cost, Ordering::Relaxed);
154 None
155 }
156}
157
158fn transpose_grant(wanted: usize, room: usize) -> usize {
163 wanted.min(room).max(wanted.min(SLAB_BUDGET_BYTES))
164}
165
166pub(crate) fn reserve_transpose_band(wanted: usize) -> (usize, Option<PreloadReservation>) {
177 use std::sync::atomic::Ordering;
178 if let Some(budget) = preload_budget_override() {
179 return (transpose_grant(wanted, budget), None);
180 }
181 let budget = auto_preload_budget_bytes();
182 let free_now = available_memory_bytes().map_or(usize::MAX, |avail| avail / 2);
183 let mut reserved = RESERVED.load(Ordering::Relaxed);
184 loop {
185 let room = budget.saturating_sub(reserved).min(free_now);
186 let granted = transpose_grant(wanted, room);
187 match RESERVED.compare_exchange_weak(
188 reserved,
189 reserved.saturating_add(granted),
190 Ordering::Relaxed,
191 Ordering::Relaxed,
192 ) {
193 Ok(_) => {
194 return (
195 granted,
196 Some(PreloadReservation {
197 bytes: granted,
198 counter: &RESERVED,
199 }),
200 )
201 }
202 Err(now) => reserved = now,
203 }
204 }
205}
206
207#[allow(dead_code)] pub fn preload_within_budget(nnz: usize, what: &str) -> bool {
212 reserve_preload(nnz, what).map(std::mem::forget).is_some()
213}
214
215fn fits_or_warn(cost: usize, budget: usize, nnz: usize, what: &str) -> bool {
216 if cost > budget {
217 log::warn!(
218 "skipping {what} preload of {nnz} nnz: {cost} bytes would exceed the \
219 {budget}-byte budget (LEGUME_PRELOAD_BUDGET_BYTES to raise); reads stay on the \
220 streaming path"
221 );
222 false
223 } else {
224 true
225 }
226}
227
228fn auto_preload_budget_bytes() -> usize {
230 static BUDGET: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
231 *BUDGET.get_or_init(|| {
232 const FALLBACK_BYTES: usize = 8 << 30;
233 available_memory_bytes().map_or(FALLBACK_BYTES, |avail| avail / 2)
234 })
235}
236
237fn available_memory_bytes() -> Option<usize> {
240 let mut sys = sysinfo::System::new();
241 sys.refresh_memory();
242 usize::try_from(sys.available_memory())
243 .ok()
244 .filter(|&avail| avail > 0)
245}
246
247#[allow(dead_code)] pub fn auto_preload_enabled() -> bool {
253 !matches!(
254 std::env::var("LEGUME_AUTO_PRELOAD").as_deref(),
255 Ok("0") | Ok("false") | Ok("off")
256 )
257}
258
259pub const SLAB_BUDGET_BYTES: usize = 256 << 20;
264pub const TRIPLET_BYTES: usize = 24;
266
267#[cfg(test)]
268mod preload_reservation_tests {
269 use super::*;
270 use std::sync::atomic::{AtomicUsize, Ordering};
271
272 #[test]
273 fn dropping_a_reservation_gives_it_back() {
274 static COUNTER: AtomicUsize = AtomicUsize::new(0);
275 let a = reserve_against(&COUNTER, 60, 100, 5, "test").expect("fits");
276 assert_eq!(COUNTER.load(Ordering::Relaxed), 60);
277 assert!(reserve_against(&COUNTER, 60, 100, 5, "test").is_none());
280 assert_eq!(COUNTER.load(Ordering::Relaxed), 60);
281 drop(a);
282 assert_eq!(COUNTER.load(Ordering::Relaxed), 0);
283 let b = reserve_against(&COUNTER, 60, 100, 5, "test").expect("fits after release");
285 assert_eq!(b.bytes(), 60);
286 drop(b);
287 assert_eq!(COUNTER.load(Ordering::Relaxed), 0);
288 }
289
290 #[test]
291 fn a_transpose_band_takes_the_room_there_is_but_never_under_a_slab() {
292 let slab = SLAB_BUDGET_BYTES;
293 assert_eq!(transpose_grant(4 * slab, 8 * slab), 4 * slab);
295 assert_eq!(transpose_grant(4 * slab, 2 * slab), 2 * slab);
297 assert_eq!(transpose_grant(4 * slab, 0), slab);
299 assert_eq!(transpose_grant(slab / 2, 0), slab / 2);
300 }
301
302 #[test]
303 fn a_transpose_band_under_one_slab_is_granted_whole() {
304 assert_eq!(reserve_transpose_band(1024).0, 1024);
305 }
306
307 #[test]
308 fn cloning_a_reservation_reserves_again() {
309 static COUNTER: AtomicUsize = AtomicUsize::new(0);
310 let a = reserve_against(&COUNTER, 30, 100, 5, "test").expect("fits");
311 let b = a.clone();
312 assert_eq!(COUNTER.load(Ordering::Relaxed), 60);
313 drop(a);
314 assert_eq!(COUNTER.load(Ordering::Relaxed), 30);
315 drop(b);
316 assert_eq!(COUNTER.load(Ordering::Relaxed), 0);
317 }
318}