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 std::env::var("RLX_SOFT_MEMORY_FRACTION")
221 .ok()
222 .and_then(|s| s.parse::<f64>().ok())
223 .filter(|&f| f > 0.0 && f <= 1.0)
224 .unwrap_or(DEFAULT_SOFT_MEMORY_FRACTION)
225}
226
227pub fn soft_memory_budget_bytes() -> Option<usize> {
230 if let Ok(v) = std::env::var("RLX_SOFT_MEMORY_BUDGET_BYTES") {
231 if let Ok(n) = v.parse::<usize>() {
232 return Some(n);
233 }
234 }
235 available_unified_memory()
236 .map(|total| ((total as f64) * soft_memory_fraction()).floor() as usize)
237}
238
239pub fn process_rss_bytes() -> Option<usize> {
241 #[cfg(target_vendor = "apple")]
242 {
243 use std::mem::MaybeUninit;
244 unsafe extern "C" {
245 fn mach_task_self() -> u32;
246 fn task_info(
247 target_task: u32,
248 flavor: u32,
249 task_info_out: *mut std::os::raw::c_char,
250 task_info_out_cnt: *mut u32,
251 ) -> i32;
252 }
253 const TASK_BASIC_INFO: u32 = 5;
254 #[repr(C)]
255 struct TaskBasicInfo {
256 suspend_count: u32,
257 virtual_size: u64,
258 resident_size: u64,
259 user_time: u64,
260 system_time: u64,
261 policy: i32,
262 }
263 let mut info = MaybeUninit::<TaskBasicInfo>::uninit();
264 let mut count = (std::mem::size_of::<TaskBasicInfo>() / std::mem::size_of::<u32>()) as u32;
265 let rc = unsafe {
266 task_info(
267 mach_task_self(),
268 TASK_BASIC_INFO,
269 info.as_mut_ptr().cast(),
270 &mut count,
271 )
272 };
273 if rc == 0 {
274 Some(unsafe { info.assume_init() }.resident_size as usize)
275 } else {
276 None
277 }
278 }
279 #[cfg(target_os = "linux")]
280 {
281 let statm = std::fs::read_to_string("/proc/self/statm").ok()?;
282 let resident_pages = statm.split_whitespace().nth(1)?.parse::<usize>().ok()?;
283 Some(resident_pages.saturating_mul(page_size()))
284 }
285 #[cfg(not(any(target_vendor = "apple", target_os = "linux")))]
286 {
287 None
288 }
289}
290
291#[cfg(target_os = "linux")]
292fn page_size() -> usize {
293 unsafe extern "C" {
294 fn sysconf(name: i32) -> i64;
295 }
296 const _SC_PAGESIZE: i32 = 30;
297 let ps = unsafe { sysconf(_SC_PAGESIZE) };
298 if ps > 0 { ps as usize } else { 4096 }
299}
300
301pub fn memory_headroom_bytes() -> Option<usize> {
303 let budget = soft_memory_budget_bytes()?;
304 let rss = process_rss_bytes().unwrap_or(0);
305 Some(budget.saturating_sub(rss))
306}
307
308pub fn would_exceed_soft_budget(additional_bytes: usize) -> bool {
311 let Some(budget) = soft_memory_budget_bytes() else {
312 return false;
313 };
314 let rss = process_rss_bytes().unwrap_or(0);
315 exceeds_budget(rss, additional_bytes, budget)
316}
317
318fn exceeds_budget(rss: usize, additional: usize, budget: usize) -> bool {
323 rss.saturating_add(additional) > budget
324}
325
326pub fn llama_decode_bucket_compile_peak_bytes() -> usize {
329 std::env::var("RLX_DECODE_BUCKET_PEAK_BYTES")
330 .ok()
331 .and_then(|s| s.parse::<u64>().ok())
332 .unwrap_or(12 * 1024 * 1024 * 1024)
333 .min(usize::MAX as u64) as usize
334}
335
336pub fn llama_decode_oneshot_compile_peak_bytes() -> usize {
338 std::env::var("RLX_DECODE_ONESHOT_PEAK_BYTES")
339 .ok()
340 .and_then(|s| s.parse::<u64>().ok())
341 .unwrap_or(4 * 1024 * 1024 * 1024)
342 .min(usize::MAX as u64) as usize
343}
344
345pub fn llama_decode_bucket_resident_bytes() -> usize {
356 std::env::var("RLX_DECODE_BUCKET_RESIDENT_BYTES")
357 .ok()
358 .and_then(|s| s.parse::<u64>().ok())
359 .unwrap_or(2 * 1024 * 1024 * 1024)
360 .min(usize::MAX as u64) as usize
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366 use crate::weight_registry::{WeightKind, WeightRegistry};
367 use rlx_ir::*;
368 use std::sync::Arc;
369
370 fn small_graph() -> Graph {
371 let f = DType::F32;
372 let mut g = Graph::new("est");
373 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]);
377 g
378 }
379
380 #[test]
381 fn estimate_sums_components() {
382 let g = small_graph();
383 let mut reg = WeightRegistry::new();
384 reg.register(
385 "w",
386 Shape::new(&[16, 32], DType::F32),
387 Arc::from(vec![0u8; 16 * 32 * 4]),
388 WeightKind::Base,
389 );
390 let est = estimate(&g, ®);
391 assert!(
392 est.activation_bytes >= 256,
393 "activation arena should hold mm output"
394 );
395 assert_eq!(est.weight_bytes, 16 * 32 * 4);
396 assert_eq!(est.input_bytes, 2 * 16 * 4);
397 assert!(
398 est.peak_bytes() >= est.activation_bytes + est.weight_bytes + est.input_bytes
399 || est.peak_bytes() == est.activation_bytes + est.weight_bytes + est.input_bytes
400 );
401 }
402
403 #[test]
404 fn fits_in_passes_with_room() {
405 let g = small_graph();
406 let mut reg = WeightRegistry::new();
407 reg.register(
408 "w",
409 Shape::new(&[16, 32], DType::F32),
410 Arc::from(vec![0u8; 2048]),
411 WeightKind::Base,
412 );
413 let est = estimate(&g, ®);
414 assert!(
415 est.fits_in(1 << 30).is_ok(),
416 "1 GiB budget should fit a tiny graph"
417 );
418 }
419
420 #[test]
421 fn fits_in_reports_deficit() {
422 let g = small_graph();
423 let mut reg = WeightRegistry::new();
424 reg.register(
425 "w",
426 Shape::new(&[16, 32], DType::F32),
427 Arc::from(vec![0u8; 100_000_000]),
428 WeightKind::Base,
429 );
430 let est = estimate(&g, ®);
431 let err = est.fits_in(1024).unwrap_err();
432 assert!(err.peak_bytes > err.budget_bytes);
433 assert!(format!("{err}").contains("exceeds"));
434 }
435
436 #[test]
437 fn available_memory_returns_something_on_macos() {
438 if cfg!(target_vendor = "apple") {
441 let mem = available_unified_memory();
442 assert!(mem.is_some());
443 assert!(mem.unwrap() > 0);
444 }
445 }
446
447 #[test]
448 fn soft_budget_is_fraction_of_physical() {
449 if let Some(total) = available_unified_memory() {
450 let budget = soft_memory_budget_bytes().unwrap();
451 let expect = ((total as f64) * DEFAULT_SOFT_MEMORY_FRACTION).floor() as usize;
452 assert_eq!(budget, expect);
453 }
454 }
455
456 #[test]
457 fn would_exceed_respects_headroom() {
458 let budget = 1_000_usize;
462 let rss = 600_usize;
463 let headroom = budget - rss; assert!(!exceeds_budget(rss, headroom, budget));
467 assert!(exceeds_budget(rss, headroom + 1, budget));
468
469 assert!(!exceeds_budget(budget, 0, budget));
471 assert!(exceeds_budget(budget, 1, budget));
472
473 assert!(exceeds_budget(usize::MAX, 1, budget));
475 }
476
477 #[test]
478 fn would_exceed_soft_budget_smoke() {
479 if soft_memory_budget_bytes().is_some() {
482 assert!(would_exceed_soft_budget(usize::MAX));
483 }
484 }
485}