1use crate::Engine;
18use crate::model::HostBuf;
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 DEFAULT_PINNED_FRAC, MemBudget, SpillCtx, configured_pinned_frac, parse_pinned_frac,
230 };
231 use crate::spill_pread::config_fallbacks;
232 use memra_gguf::{GGUF_MAGIC, GgufFile};
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!(
241 parse_pinned_frac(Some(invalid)).is_err(),
242 "accepted {invalid:?}"
243 );
244 }
245 }
246
247 #[test]
248 fn invalid_pinned_frac_is_counted_and_uses_safe_default() {
249 const CHILD: &str = "MEMRA_INVALID_PINNED_FRAC_TEST_CHILD";
250 const TEST: &str = "spill::tests::invalid_pinned_frac_is_counted_and_uses_safe_default";
251 if std::env::var_os(CHILD).is_some() {
252 assert_eq!(config_fallbacks(), 0);
253 assert_eq!(configured_pinned_frac(), DEFAULT_PINNED_FRAC);
254 assert_eq!(config_fallbacks(), 1);
255 return;
256 }
257
258 for raw in ["1.6", "not-a-number"] {
259 let output = std::process::Command::new(std::env::current_exe().unwrap())
260 .arg(TEST)
261 .arg("--exact")
262 .arg("--nocapture")
263 .env(CHILD, "1")
264 .env("MEMRA_SPILL_PINNED_FRAC", raw)
265 .output()
266 .unwrap();
267 let stdout = String::from_utf8_lossy(&output.stdout);
268 let stderr = String::from_utf8_lossy(&output.stderr);
269 assert!(
270 output.status.success(),
271 "invalid-config child failed for {raw:?}\nstdout:\n{stdout}\nstderr:\n{stderr}"
272 );
273 assert!(
274 stderr.contains(&format!("invalid MEMRA_SPILL_PINNED_FRAC={raw:?}"))
275 && stderr.contains("using 0.6"),
276 "invalid-config warning missing from child stderr:\n{stderr}"
277 );
278 }
279 }
280
281 #[test]
282 fn spill_ctx_keeps_parsed_gguf_inode_after_path_replacement() {
283 let path =
284 std::env::temp_dir().join(format!("memra-spill-inode-{}.gguf", std::process::id()));
285 let mut original = Vec::new();
286 original.extend_from_slice(&GGUF_MAGIC.to_le_bytes());
287 original.extend_from_slice(&3u32.to_le_bytes());
288 original.extend_from_slice(&0i64.to_le_bytes());
289 original.extend_from_slice(&0i64.to_le_bytes());
290 original.resize(32, 0);
291 std::fs::write(&path, &original).unwrap();
292
293 let gguf = GgufFile::open(&path).unwrap();
294 std::fs::remove_file(&path).unwrap();
295 std::fs::write(&path, vec![0xA5u8; original.len()]).unwrap();
296
297 let budget = MemBudget {
298 free_vram: 0,
299 free_pinnable_ram: 0,
300 };
301 let spill = SpillCtx::open(&gguf, &budget).unwrap();
302 assert_eq!(
303 spill.files.len(),
304 1,
305 "single-file GGUF must yield exactly one shard map"
306 );
307 assert!(std::sync::Arc::ptr_eq(&spill.files[0], gguf.opened_file()));
308 assert_eq!(&spill.file_maps[0][..], original.as_slice());
309 assert_eq!(std::fs::read(&path).unwrap(), vec![0xA5u8; original.len()]);
310
311 std::fs::remove_file(path).ok();
312 }
313}