1use crate::model::HostBuf;
18use crate::Engine;
19use memmap2::Mmap;
20use std::sync::Arc;
21
22const DEFAULT_PINNED_FRAC: f64 = 0.60;
23
24fn parse_pinned_frac(raw: Option<&str>) -> Result<f64, &'static str> {
25 let frac = raw
26 .unwrap_or("0.60")
27 .parse::<f64>()
28 .map_err(|_| "expected a number")?;
29 if frac.is_finite() && frac > 0.0 && frac <= 1.0 {
30 Ok(frac)
31 } else {
32 Err("expected a finite fraction greater than 0 and at most 1")
33 }
34}
35
36fn configured_pinned_frac() -> f64 {
37 static PINNED_FRAC: std::sync::OnceLock<f64> = std::sync::OnceLock::new();
38 *PINNED_FRAC.get_or_init(|| {
39 let raw = std::env::var("MEMRA_SPILL_PINNED_FRAC").ok();
40 match parse_pinned_frac(raw.as_deref()) {
41 Ok(frac) => frac,
42 Err(reason) => {
43 crate::spill_pread::note_config_fallback();
44 eprintln!(
45 "[spill] invalid MEMRA_SPILL_PINNED_FRAC={:?} ({reason}); using {DEFAULT_PINNED_FRAC}",
46 raw.as_deref().unwrap_or("")
47 );
48 DEFAULT_PINNED_FRAC
49 }
50 }
51 })
52}
53
54#[derive(Clone, Copy, Debug)]
58pub struct MemBudget {
59 pub free_vram: usize,
61 pub free_pinnable_ram: usize,
64}
65
66impl MemBudget {
67 pub fn probe(e: &Engine) -> Result<Self, Box<dyn std::error::Error>> {
68 let (free_vram, _total) = e.ctx().mem_get_info()?; let avail = read_meminfo_kb("MemAvailable")? * 1024; let frac = configured_pinned_frac();
71 Ok(MemBudget {
72 free_vram,
73 free_pinnable_ram: ((avail as f64 * frac) as usize).min(avail),
74 })
75 }
76}
77
78fn read_meminfo_kb(key: &str) -> Result<usize, Box<dyn std::error::Error>> {
80 let s = std::fs::read_to_string("/proc/meminfo")?;
81 for line in s.lines() {
82 if let Some(rest) = line.strip_prefix(key) {
84 let rest = rest.trim_start_matches(':').trim();
85 let kb: usize = rest
86 .split_whitespace()
87 .next()
88 .ok_or("malformed /proc/meminfo line")?
89 .parse()?;
90 return Ok(kb);
91 }
92 }
93 Err(format!("/proc/meminfo: key {key} not found").into())
94}
95
96#[inline]
99pub fn disk_tier_enabled() -> bool {
100 std::env::var("MEMRA_SPILL_DISK").is_ok()
101}
102
103pub struct SpillCtx {
108 pub file_maps: Vec<Arc<Mmap>>,
114 pub files: Vec<Arc<std::fs::File>>,
116 pub pinned_remaining: usize,
118 pub n_pinned: usize,
120 pub n_mmap: usize,
121 pub mmap_bytes: usize,
122}
123
124impl SpillCtx {
125 pub fn open(
130 g: &memra_gguf::GgufFile,
131 budget: &MemBudget,
132 ) -> Result<Self, Box<dyn std::error::Error>> {
133 let mut files = Vec::with_capacity(g.n_shards());
134 let mut file_maps = Vec::with_capacity(g.n_shards());
135 for i in 0..g.n_shards() {
136 let file = g.shard_file(i).clone();
137 let map = unsafe { Mmap::map(file.as_ref())? };
140 let _ = memra_gguf::source::apply_expert_mmap_advice(&map);
141 files.push(file);
142 file_maps.push(Arc::new(map));
143 }
144 Ok(SpillCtx {
145 file_maps,
146 files,
147 pinned_remaining: budget.free_pinnable_ram,
148 n_pinned: 0,
149 n_mmap: 0,
150 mmap_bytes: 0,
151 })
152 }
153}
154
155pub fn place_expert(
161 ctx: &mut SpillCtx,
162 e: &Engine,
163 raw: &[u8],
164 file_off: usize,
165 shard: usize,
166) -> Result<HostBuf, Box<dyn std::error::Error>> {
167 let len = raw.len();
168 if ctx.pinned_remaining >= len {
169 ctx.pinned_remaining -= len;
171 ctx.n_pinned += 1;
172 let mut p = unsafe { e.ctx().alloc_pinned::<u8>(len)? };
173 {
174 let dst = p.as_mut_slice()?;
175 dst.copy_from_slice(raw);
176 }
177 let base = p.as_ptr()? as *const u8;
178 Ok(HostBuf::Pinned {
179 slice: std::sync::Arc::new(p),
180 base,
181 len,
182 })
183 } else {
184 ctx.n_mmap += 1;
186 ctx.mmap_bytes += len;
187 Ok(HostBuf::Mmap {
188 map: ctx.file_maps[shard].clone(),
189 file: ctx.files[shard].clone(),
190 off: file_off,
191 len,
192 })
193 }
194}
195
196pub struct SpillBlock {
201 pub host: HostBuf,
202 pub qtype: i32,
203 pub in_f: usize,
204 pub out_f: usize,
205 pub row_bytes: usize,
206}
207
208impl SpillBlock {
209 #[inline]
211 pub fn bytes(&self) -> &[u8] {
212 self.host.as_bytes()
213 }
214}
215
216pub struct Tiered {
222 pub host: crate::model::HostExps, pub slots: crate::moe_cache::MoeSlotCache, }
225
226#[cfg(all(test, unix))]
227mod tests {
228 use super::{
229 configured_pinned_frac, parse_pinned_frac, MemBudget, SpillCtx, DEFAULT_PINNED_FRAC,
230 };
231 use crate::spill_pread::config_fallbacks;
232 use memra_gguf::{GgufFile, GGUF_MAGIC};
233
234 #[test]
235 fn pinned_frac_accepts_only_finite_values_in_range() {
236 assert_eq!(parse_pinned_frac(None), Ok(DEFAULT_PINNED_FRAC));
237 assert_eq!(parse_pinned_frac(Some("0.25")), Ok(0.25));
238 assert_eq!(parse_pinned_frac(Some("1")), Ok(1.0));
239 for invalid in ["0", "-0.1", "1.6", "NaN", "inf", "not-a-number"] {
240 assert!(parse_pinned_frac(Some(invalid)).is_err(), "accepted {invalid:?}");
241 }
242 }
243
244 #[test]
245 fn invalid_pinned_frac_is_counted_and_uses_safe_default() {
246 const CHILD: &str = "MEMRA_INVALID_PINNED_FRAC_TEST_CHILD";
247 const TEST: &str =
248 "spill::tests::invalid_pinned_frac_is_counted_and_uses_safe_default";
249 if std::env::var_os(CHILD).is_some() {
250 assert_eq!(config_fallbacks(), 0);
251 assert_eq!(configured_pinned_frac(), DEFAULT_PINNED_FRAC);
252 assert_eq!(config_fallbacks(), 1);
253 return;
254 }
255
256 for raw in ["1.6", "not-a-number"] {
257 let output = std::process::Command::new(std::env::current_exe().unwrap())
258 .arg(TEST)
259 .arg("--exact")
260 .arg("--nocapture")
261 .env(CHILD, "1")
262 .env("MEMRA_SPILL_PINNED_FRAC", raw)
263 .output()
264 .unwrap();
265 let stdout = String::from_utf8_lossy(&output.stdout);
266 let stderr = String::from_utf8_lossy(&output.stderr);
267 assert!(
268 output.status.success(),
269 "invalid-config child failed for {raw:?}\nstdout:\n{stdout}\nstderr:\n{stderr}"
270 );
271 assert!(
272 stderr.contains(&format!(
273 "invalid MEMRA_SPILL_PINNED_FRAC={raw:?}"
274 )) && stderr.contains("using 0.6"),
275 "invalid-config warning missing from child stderr:\n{stderr}"
276 );
277 }
278 }
279
280 #[test]
281 fn spill_ctx_keeps_parsed_gguf_inode_after_path_replacement() {
282 let path =
283 std::env::temp_dir().join(format!("memra-spill-inode-{}.gguf", std::process::id()));
284 let mut original = Vec::new();
285 original.extend_from_slice(&GGUF_MAGIC.to_le_bytes());
286 original.extend_from_slice(&3u32.to_le_bytes());
287 original.extend_from_slice(&0i64.to_le_bytes());
288 original.extend_from_slice(&0i64.to_le_bytes());
289 original.resize(32, 0);
290 std::fs::write(&path, &original).unwrap();
291
292 let gguf = GgufFile::open(&path).unwrap();
293 std::fs::remove_file(&path).unwrap();
294 std::fs::write(&path, vec![0xA5u8; original.len()]).unwrap();
295
296 let budget = MemBudget {
297 free_vram: 0,
298 free_pinnable_ram: 0,
299 };
300 let spill = SpillCtx::open(&gguf, &budget).unwrap();
301 assert_eq!(spill.files.len(), 1, "single-file GGUF must yield exactly one shard map");
302 assert!(std::sync::Arc::ptr_eq(&spill.files[0], gguf.opened_file()));
303 assert_eq!(&spill.file_maps[0][..], original.as_slice());
304 assert_eq!(std::fs::read(&path).unwrap(), vec![0xA5u8; original.len()]);
305
306 std::fs::remove_file(path).ok();
307 }
308}