1use crate::expert_pool::gpu_expert_budget_from_vram;
30use crate::weight_registry::WeightRegistry;
31use rlx_ir::Graph;
32use rlx_opt::memory::plan_memory;
33
34#[derive(Debug, Clone)]
35pub struct MemoryEstimate {
36 pub activation_bytes: usize,
39 pub weight_bytes: usize,
41 pub input_bytes: usize,
45}
46
47impl MemoryEstimate {
48 pub fn peak_bytes(&self) -> usize {
49 self.activation_bytes + self.weight_bytes + self.input_bytes
50 }
51
52 pub fn fits_in(&self, budget_bytes: usize) -> Result<(), MemoryDeficit> {
56 let peak = self.peak_bytes();
57 if peak <= budget_bytes {
58 Ok(())
59 } else {
60 Err(MemoryDeficit {
61 budget_bytes,
62 peak_bytes: peak,
63 activation_bytes: self.activation_bytes,
64 weight_bytes: self.weight_bytes,
65 input_bytes: self.input_bytes,
66 })
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
72pub struct MemoryDeficit {
73 pub budget_bytes: usize,
74 pub peak_bytes: usize,
75 pub activation_bytes: usize,
76 pub weight_bytes: usize,
77 pub input_bytes: usize,
78}
79
80impl std::fmt::Display for MemoryDeficit {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 let over = self.peak_bytes - self.budget_bytes;
83 write!(
84 f,
85 "estimated peak {peak_mb:.1} MiB exceeds budget {budget_mb:.1} MiB by {over_mb:.1} MiB \
86 (activation {act_mb:.1}, weights {w_mb:.1}, inputs {in_mb:.1})",
87 peak_mb = self.peak_bytes as f64 / 1024.0 / 1024.0,
88 budget_mb = self.budget_bytes as f64 / 1024.0 / 1024.0,
89 over_mb = over as f64 / 1024.0 / 1024.0,
90 act_mb = self.activation_bytes as f64 / 1024.0 / 1024.0,
91 w_mb = self.weight_bytes as f64 / 1024.0 / 1024.0,
92 in_mb = self.input_bytes as f64 / 1024.0 / 1024.0,
93 )
94 }
95}
96
97impl std::error::Error for MemoryDeficit {}
98
99#[derive(Debug, Clone)]
105pub struct MoeOffloadEstimate {
106 pub expert_param_bytes: usize,
108 pub num_moe_layers: usize,
109 pub num_experts: usize,
110 pub gpu_expert_budget_per_layer: usize,
112 pub all_expert_weight_bytes: usize,
114 pub resident_expert_weight_bytes: usize,
116}
117
118impl MoeOffloadEstimate {
119 pub fn peak_with_offload(&self, base: &MemoryEstimate) -> usize {
121 base.activation_bytes
122 + base.input_bytes
123 + (base.weight_bytes - self.all_expert_weight_bytes)
124 + self.resident_expert_weight_bytes
125 }
126}
127
128pub fn estimate_moe_offload(
130 expert_param_bytes: usize,
131 num_moe_layers: usize,
132 num_experts: usize,
133 max_gpu_experts_per_layer: usize,
134 memory_budget_bytes: usize,
135 reserve_fraction: f32,
136) -> MoeOffloadEstimate {
137 let reserve_bytes = (memory_budget_bytes as f64 * reserve_fraction as f64) as usize;
138 let gpu_budget = gpu_expert_budget_from_vram(
139 memory_budget_bytes,
140 reserve_bytes,
141 expert_param_bytes,
142 num_moe_layers,
143 max_gpu_experts_per_layer,
144 num_experts,
145 );
146 let all_expert = expert_param_bytes
147 .saturating_mul(num_experts)
148 .saturating_mul(num_moe_layers);
149 let resident_expert = expert_param_bytes
150 .saturating_mul(gpu_budget)
151 .saturating_mul(num_moe_layers);
152 MoeOffloadEstimate {
153 expert_param_bytes,
154 num_moe_layers,
155 num_experts,
156 gpu_expert_budget_per_layer: gpu_budget,
157 all_expert_weight_bytes: all_expert,
158 resident_expert_weight_bytes: resident_expert,
159 }
160}
161
162pub fn estimate(graph: &Graph, registry: &WeightRegistry) -> MemoryEstimate {
163 let plan = plan_memory(graph);
164 let mut input_bytes = 0usize;
166 for node in graph.nodes() {
167 if matches!(node.op, rlx_ir::Op::Input { .. }) {
168 input_bytes += node.shape.size_bytes().unwrap_or(0);
169 }
170 }
171 MemoryEstimate {
172 activation_bytes: plan.arena_size,
173 weight_bytes: registry.total_bytes(),
174 input_bytes,
175 }
176}
177
178pub fn available_unified_memory() -> Option<usize> {
182 #[cfg(target_vendor = "apple")]
183 {
184 use std::ffi::CString;
185 let cname = CString::new("hw.memsize").ok()?;
186 let mut val: u64 = 0;
187 let mut len = std::mem::size_of::<u64>();
188 unsafe extern "C" {
189 fn sysctlbyname(
190 name: *const std::os::raw::c_char,
191 oldp: *mut std::os::raw::c_void,
192 oldlenp: *mut usize,
193 newp: *mut std::os::raw::c_void,
194 newlen: usize,
195 ) -> std::os::raw::c_int;
196 }
197 let rc = unsafe {
198 sysctlbyname(
199 cname.as_ptr(),
200 &mut val as *mut u64 as *mut _,
201 &mut len,
202 std::ptr::null_mut(),
203 0,
204 )
205 };
206 if rc == 0 { Some(val as usize) } else { None }
207 }
208 #[cfg(not(target_vendor = "apple"))]
209 {
210 None
211 }
212}
213
214pub const DEFAULT_SOFT_MEMORY_FRACTION: f64 = 0.80;
216
217pub fn soft_memory_fraction() -> f64 {
220 rlx_ir::env::var("RLX_SOFT_MEMORY_FRACTION")
221 .and_then(|s| s.parse::<f64>().ok())
222 .filter(|&f| f > 0.0 && f <= 1.0)
223 .unwrap_or(DEFAULT_SOFT_MEMORY_FRACTION)
224}
225
226pub fn soft_memory_budget_bytes() -> Option<usize> {
229 if let Some(v) = rlx_ir::env::var("RLX_SOFT_MEMORY_BUDGET_BYTES") {
230 if let Ok(n) = v.parse::<usize>() {
231 return Some(n);
232 }
233 }
234 available_unified_memory()
235 .map(|total| ((total as f64) * soft_memory_fraction()).floor() as usize)
236}
237
238pub fn process_rss_bytes() -> Option<usize> {
240 #[cfg(target_vendor = "apple")]
241 {
242 use std::mem::MaybeUninit;
243 unsafe extern "C" {
244 fn mach_task_self() -> u32;
245 fn task_info(
246 target_task: u32,
247 flavor: u32,
248 task_info_out: *mut std::os::raw::c_char,
249 task_info_out_cnt: *mut u32,
250 ) -> i32;
251 }
252 const TASK_BASIC_INFO: u32 = 5;
253 #[repr(C)]
254 struct TaskBasicInfo {
255 suspend_count: u32,
256 virtual_size: u64,
257 resident_size: u64,
258 user_time: u64,
259 system_time: u64,
260 policy: i32,
261 }
262 let mut info = MaybeUninit::<TaskBasicInfo>::uninit();
263 let mut count = (std::mem::size_of::<TaskBasicInfo>() / std::mem::size_of::<u32>()) as u32;
264 let rc = unsafe {
265 task_info(
266 mach_task_self(),
267 TASK_BASIC_INFO,
268 info.as_mut_ptr().cast(),
269 &mut count,
270 )
271 };
272 if rc == 0 {
273 Some(unsafe { info.assume_init() }.resident_size as usize)
274 } else {
275 None
276 }
277 }
278 #[cfg(target_os = "linux")]
279 {
280 let statm = std::fs::read_to_string("/proc/self/statm").ok()?;
281 let resident_pages = statm.split_whitespace().nth(1)?.parse::<usize>().ok()?;
282 Some(resident_pages.saturating_mul(page_size()))
283 }
284 #[cfg(not(any(target_vendor = "apple", target_os = "linux")))]
285 {
286 None
287 }
288}
289
290#[cfg(target_os = "linux")]
291fn page_size() -> usize {
292 unsafe extern "C" {
293 fn sysconf(name: i32) -> i64;
294 }
295 const _SC_PAGESIZE: i32 = 30;
296 let ps = unsafe { sysconf(_SC_PAGESIZE) };
297 if ps > 0 { ps as usize } else { 4096 }
298}
299
300pub fn memory_headroom_bytes() -> Option<usize> {
302 let budget = soft_memory_budget_bytes()?;
303 let rss = process_rss_bytes().unwrap_or(0);
304 Some(budget.saturating_sub(rss))
305}
306
307pub fn would_exceed_soft_budget(additional_bytes: usize) -> bool {
310 let Some(budget) = soft_memory_budget_bytes() else {
311 return false;
312 };
313 let rss = process_rss_bytes().unwrap_or(0);
314 exceeds_budget(rss, additional_bytes, budget)
315}
316
317fn exceeds_budget(rss: usize, additional: usize, budget: usize) -> bool {
322 rss.saturating_add(additional) > budget
323}
324
325pub fn llama_decode_bucket_compile_peak_bytes() -> usize {
328 rlx_ir::env::var("RLX_DECODE_BUCKET_PEAK_BYTES")
329 .and_then(|s| s.parse::<u64>().ok())
330 .unwrap_or(12 * 1024 * 1024 * 1024)
331 .min(usize::MAX as u64) as usize
332}
333
334pub fn llama_decode_oneshot_compile_peak_bytes() -> usize {
336 rlx_ir::env::var("RLX_DECODE_ONESHOT_PEAK_BYTES")
337 .and_then(|s| s.parse::<u64>().ok())
338 .unwrap_or(4 * 1024 * 1024 * 1024)
339 .min(usize::MAX as u64) as usize
340}
341
342pub fn llama_decode_bucket_resident_bytes() -> usize {
353 rlx_ir::env::var("RLX_DECODE_BUCKET_RESIDENT_BYTES")
354 .and_then(|s| s.parse::<u64>().ok())
355 .unwrap_or(2 * 1024 * 1024 * 1024)
356 .min(usize::MAX as u64) as usize
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362 use crate::weight_registry::{WeightKind, WeightRegistry};
363 use rlx_ir::*;
364 use std::sync::Arc;
365
366 fn small_graph() -> Graph {
367 let f = DType::F32;
368 let mut g = Graph::new("est");
369 let x = g.input("x", Shape::new(&[2, 16], f)); let w = g.param("w", Shape::new(&[16, 32], f)); let mm = g.matmul(x, w, Shape::new(&[2, 32], f)); g.set_outputs(vec![mm]);
373 g
374 }
375
376 #[test]
377 fn estimate_sums_components() {
378 let g = small_graph();
379 let mut reg = WeightRegistry::new();
380 reg.register(
381 "w",
382 Shape::new(&[16, 32], DType::F32),
383 Arc::from(vec![0u8; 16 * 32 * 4]),
384 WeightKind::Base,
385 );
386 let est = estimate(&g, ®);
387 assert!(
388 est.activation_bytes >= 256,
389 "activation arena should hold mm output"
390 );
391 assert_eq!(est.weight_bytes, 16 * 32 * 4);
392 assert_eq!(est.input_bytes, 2 * 16 * 4);
393 assert!(
394 est.peak_bytes() >= est.activation_bytes + est.weight_bytes + est.input_bytes
395 || est.peak_bytes() == est.activation_bytes + est.weight_bytes + est.input_bytes
396 );
397 }
398
399 #[test]
400 fn fits_in_passes_with_room() {
401 let g = small_graph();
402 let mut reg = WeightRegistry::new();
403 reg.register(
404 "w",
405 Shape::new(&[16, 32], DType::F32),
406 Arc::from(vec![0u8; 2048]),
407 WeightKind::Base,
408 );
409 let est = estimate(&g, ®);
410 assert!(
411 est.fits_in(1 << 30).is_ok(),
412 "1 GiB budget should fit a tiny graph"
413 );
414 }
415
416 #[test]
417 fn fits_in_reports_deficit() {
418 let g = small_graph();
419 let mut reg = WeightRegistry::new();
420 reg.register(
421 "w",
422 Shape::new(&[16, 32], DType::F32),
423 Arc::from(vec![0u8; 100_000_000]),
424 WeightKind::Base,
425 );
426 let est = estimate(&g, ®);
427 let err = est.fits_in(1024).unwrap_err();
428 assert!(err.peak_bytes > err.budget_bytes);
429 assert!(format!("{err}").contains("exceeds"));
430 }
431
432 #[test]
433 fn available_memory_returns_something_on_macos() {
434 if cfg!(target_vendor = "apple") {
437 let mem = available_unified_memory();
438 assert!(mem.is_some());
439 assert!(mem.unwrap() > 0);
440 }
441 }
442
443 #[test]
444 fn soft_budget_is_fraction_of_physical() {
445 if let Some(total) = available_unified_memory() {
446 let budget = soft_memory_budget_bytes().unwrap();
447 let expect = ((total as f64) * DEFAULT_SOFT_MEMORY_FRACTION).floor() as usize;
448 assert_eq!(budget, expect);
449 }
450 }
451
452 #[test]
453 fn would_exceed_respects_headroom() {
454 let budget = 1_000_usize;
458 let rss = 600_usize;
459 let headroom = budget - rss; assert!(!exceeds_budget(rss, headroom, budget));
463 assert!(exceeds_budget(rss, headroom + 1, budget));
464
465 assert!(!exceeds_budget(budget, 0, budget));
467 assert!(exceeds_budget(budget, 1, budget));
468
469 assert!(exceeds_budget(usize::MAX, 1, budget));
471 }
472
473 #[test]
474 fn would_exceed_soft_budget_smoke() {
475 if soft_memory_budget_bytes().is_some() {
478 assert!(would_exceed_soft_budget(usize::MAX));
479 }
480 }
481}