Skip to main content

xet_runtime/utils/
system_memory.rs

1//! System-memory probing and memory-derived download buffer defaults.
2//!
3//! The download buffer configuration defaults (see `config::groups::reconstruction`) are
4//! derived from the memory actually usable by this process: the minimum of the host's
5//! physical RAM and the effective cgroup memory limit. Inside a container, the cgroup
6//! limit is what matters; `System::total_memory` (and `psutil` in Python) report the
7//! host total, which is how a 1 GiB pod would otherwise size a multi-GB buffer.
8//!
9//! The cgroup limit comes from sysinfo's `Process::cgroup_limits` (sysinfo >= 0.39),
10//! which resolves the cgroup path listed in `/proc/self/cgroup` rather than reading the
11//! controller root, and takes the tightest bound across ancestor cgroups. That covers
12//! cgroup v2 and v1, nested paths (a Slurm step, a systemd slice), and namespaced
13//! container roots.
14
15use std::sync::LazyLock;
16
17use tracing::info;
18
19use crate::utils::ByteSize;
20use crate::utils::configuration_utils::parse_bool_value;
21
22/// Round derived values down to a multiple of this, for clean display values.
23const ROUND_TO: u64 = 8_000_000;
24
25/// Memory available to this process, as best we can determine.
26#[derive(Debug, Clone, Copy)]
27pub struct UsableMemory {
28    /// Total physical memory of the host (or VM), if determinable.
29    pub host_total: Option<u64>,
30    /// Effective cgroup memory limit (Linux only), if one is set.
31    pub cgroup_limit: Option<u64>,
32}
33
34impl UsableMemory {
35    /// The memory figure defaults should be derived from: the minimum of the
36    /// available readings, or `None` if neither could be determined.
37    pub fn usable(&self) -> Option<u64> {
38        match (self.host_total, self.cgroup_limit) {
39            (Some(host), Some(cgroup)) => Some(host.min(cgroup)),
40            (host, cgroup) => host.or(cgroup),
41        }
42    }
43}
44
45/// The three download buffer knobs, derived together so they describe one allocation.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct DownloadBufferDefaults {
48    pub size: ByteSize,
49    pub perfile: ByteSize,
50    pub limit: ByteSize,
51}
52
53/// Static fallback defaults, used when memory cannot be determined or derivation is
54/// disabled via `HF_XET_DISABLE_MEMORY_DERIVED_DOWNLOAD_BUFFERS=1`. These are the historical
55/// constant defaults.
56pub const STATIC_DEFAULTS: DownloadBufferDefaults = DownloadBufferDefaults {
57    size: ByteSize::new(2_000_000_000),
58    perfile: ByteSize::new(512_000_000),
59    limit: ByteSize::new(8_000_000_000),
60};
61
62/// Static fallback for high-performance mode; the historical `HF_XET_HP` constants.
63/// These values also serve as the derivation ceilings below.
64pub const STATIC_HP_DEFAULTS: DownloadBufferDefaults = DownloadBufferDefaults {
65    size: ByteSize::new(16_000_000_000),
66    perfile: ByteSize::new(2_000_000_000),
67    limit: ByteSize::new(64_000_000_000),
68};
69
70/// Divisors applied to usable memory for each knob, with shared floors/ceilings.
71struct Fractions {
72    size_divisor: u64,
73    perfile_divisor: u64,
74    limit_divisor: u64,
75}
76
77/// Standard fractions: anchored so 32 GB of usable memory reproduces the historical
78/// defaults, and >= 256 GB reproduces the historical high-performance values.
79const STANDARD_FRACTIONS: Fractions = Fractions {
80    size_divisor: 16,
81    perfile_divisor: 64,
82    limit_divisor: 4,
83};
84
85/// High-performance fractions: twice as aggressive, same floors and ceilings.
86const HIGH_PERFORMANCE_FRACTIONS: Fractions = Fractions {
87    size_divisor: 8,
88    perfile_divisor: 32,
89    limit_divisor: 2,
90};
91
92/// Floors are the values derived for 1 GiB of usable memory, the smallest environment
93/// the derivation distinguishes; anything smaller receives the same values. At the
94/// floors a single maximum-size term (one unpacked xorb block, <= 64 MiB) fits the
95/// budget once one file download is active (size + perfile = 80 MB), and the floors
96/// satisfy `size + 8 * perfile <= limit` just like the fractions do.
97///
98/// Ceilings are the historical high-performance values. All floors and ceilings are
99/// multiples of `ROUND_TO`, so rounding a clamped value down never violates the clamp.
100const SIZE_FLOOR: u64 = 64_000_000;
101const SIZE_CEILING: u64 = STATIC_HP_DEFAULTS.size.as_u64();
102const PERFILE_FLOOR: u64 = 16_000_000;
103const PERFILE_CEILING: u64 = STATIC_HP_DEFAULTS.perfile.as_u64();
104const LIMIT_FLOOR: u64 = 264_000_000;
105const LIMIT_CEILING: u64 = STATIC_HP_DEFAULTS.limit.as_u64();
106
107/// Derive the three buffer values from a usable-memory figure.
108fn derive_download_buffer_defaults(usable: u64, fractions: &Fractions) -> DownloadBufferDefaults {
109    let apply = |divisor: u64, floor: u64, ceiling: u64| -> u64 {
110        let value = (usable / divisor).clamp(floor, ceiling);
111        (value / ROUND_TO) * ROUND_TO
112    };
113    DownloadBufferDefaults {
114        size: ByteSize::new(apply(fractions.size_divisor, SIZE_FLOOR, SIZE_CEILING)),
115        perfile: ByteSize::new(apply(fractions.perfile_divisor, PERFILE_FLOOR, PERFILE_CEILING)),
116        limit: ByteSize::new(apply(fractions.limit_divisor, LIMIT_FLOOR, LIMIT_CEILING)),
117    }
118}
119
120#[cfg(not(target_family = "wasm"))]
121fn probe_usable_memory() -> UsableMemory {
122    use sysinfo::{MemoryRefreshKind, ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System};
123
124    let mut system =
125        System::new_with_specifics(RefreshKind::nothing().with_memory(MemoryRefreshKind::nothing().with_ram()));
126    let host_total = match system.total_memory() {
127        0 => None,
128        total => Some(total),
129    };
130
131    // `Process::cgroup_limits` resolves this process's cgroup path from /proc and takes
132    // the tightest memory limit across ancestor cgroups (v1 and v2). It returns None on
133    // non-Linux systems. When no limit binds, its total_memory equals the host total, so
134    // filter that case out to keep `cgroup_limit = None` meaning "no limit set".
135    let cgroup_limit = sysinfo::get_current_pid().ok().and_then(|pid| {
136        system.refresh_processes_specifics(ProcessesToUpdate::Some(&[pid]), false, ProcessRefreshKind::nothing());
137        let limits = system.process(pid)?.cgroup_limits()?;
138        Some(limits.total_memory).filter(|&limit| host_total.is_none_or(|host| limit < host))
139    });
140
141    UsableMemory {
142        host_total,
143        cgroup_limit,
144    }
145}
146
147#[cfg(target_family = "wasm")]
148fn probe_usable_memory() -> UsableMemory {
149    UsableMemory {
150        host_total: None,
151        cgroup_limit: None,
152    }
153}
154
155/// The system memory reading, probed once and cached for the process lifetime.
156///
157/// Cached because the `config_group!` default expressions run on every config
158/// construction, and so that every consumer derives from a single reading even if
159/// cgroup limits are edited at runtime.
160pub static USABLE_MEMORY: LazyLock<UsableMemory> = LazyLock::new(probe_usable_memory);
161
162/// The standard and high-performance default sets, derived together from one
163/// [`USABLE_MEMORY`] reading.
164struct DerivedDefaults {
165    standard: DownloadBufferDefaults,
166    high_performance: DownloadBufferDefaults,
167}
168
169/// Computed once on first use. Deliberately does no logging: this LazyLock is
170/// typically first forced by the `XetConfig::new()` call inside logging setup itself
171/// (e.g. `xet_pkg::init_logging`), before any tracing subscriber is installed, so an
172/// event emitted here would be dropped and — the initializer running only once —
173/// never fire again. [`log_derived_defaults`] emits the values after `logging::init`
174/// has installed the subscriber.
175static DERIVED_DEFAULTS: LazyLock<DerivedDefaults> = LazyLock::new(|| match USABLE_MEMORY.usable() {
176    Some(usable) => DerivedDefaults {
177        standard: derive_download_buffer_defaults(usable, &STANDARD_FRACTIONS),
178        high_performance: derive_download_buffer_defaults(usable, &HIGH_PERFORMANCE_FRACTIONS),
179    },
180    None => DerivedDefaults {
181        standard: STATIC_DEFAULTS,
182        high_performance: STATIC_HP_DEFAULTS,
183    },
184});
185
186/// Log the probed memory and the derived download buffer defaults.
187///
188/// Called by `logging::init` once the tracing subscriber is installed; see the note on
189/// [`DERIVED_DEFAULTS`] for why the values cannot be logged where they are computed.
190pub fn log_derived_defaults() {
191    if !memory_derived_defaults_enabled() {
192        info!(
193            "Memory-derived download buffer defaults disabled via HF_XET_DISABLE_MEMORY_DERIVED_DOWNLOAD_BUFFERS; using static defaults"
194        );
195        return;
196    }
197    let memory = &*USABLE_MEMORY;
198    match memory.usable() {
199        Some(usable) => {
200            let standard = DERIVED_DEFAULTS.standard;
201            info!(
202                host_total = ?memory.host_total,
203                cgroup_limit = ?memory.cgroup_limit,
204                usable,
205                "Derived download buffer defaults from system memory: size={} perfile={} limit={}",
206                standard.size,
207                standard.perfile,
208                standard.limit
209            );
210        },
211        None => {
212            info!("System memory could not be determined; using static download buffer defaults");
213        },
214    }
215}
216
217/// Whether memory-derived defaults are enabled: on unless disabled via
218/// `HF_XET_DISABLE_MEMORY_DERIVED_DOWNLOAD_BUFFERS=1`. Read on every call (cheap) so
219/// the switch is testable in-process; only the probe and derived values are cached.
220fn memory_derived_defaults_enabled() -> bool {
221    match std::env::var("HF_XET_DISABLE_MEMORY_DERIVED_DOWNLOAD_BUFFERS") {
222        Ok(value) => !parse_bool_value(&value).unwrap_or(false),
223        Err(_) => true,
224    }
225}
226
227/// The memory-derived defaults for the standard configuration.
228///
229/// Falls back to [`STATIC_DEFAULTS`] when memory cannot be determined, on wasm, or when
230/// disabled via `HF_XET_DISABLE_MEMORY_DERIVED_DOWNLOAD_BUFFERS=1`. A function rather
231/// than a static because the kill switch is consulted on every call.
232pub fn default_download_buffer_sizes() -> DownloadBufferDefaults {
233    if memory_derived_defaults_enabled() {
234        DERIVED_DEFAULTS.standard
235    } else {
236        STATIC_DEFAULTS
237    }
238}
239
240/// The memory-derived defaults for high-performance mode (`HF_XET_HP`).
241///
242/// Falls back to [`STATIC_HP_DEFAULTS`] under the same conditions as
243/// [`default_download_buffer_sizes`].
244pub fn high_performance_download_buffer_sizes() -> DownloadBufferDefaults {
245    if memory_derived_defaults_enabled() {
246        DERIVED_DEFAULTS.high_performance
247    } else {
248        STATIC_HP_DEFAULTS
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    fn triple(d: DownloadBufferDefaults) -> (u64, u64, u64) {
257        (d.size.as_u64(), d.perfile.as_u64(), d.limit.as_u64())
258    }
259
260    #[test]
261    fn test_usable_memory_min() {
262        let mem = |h, c| UsableMemory {
263            host_total: h,
264            cgroup_limit: c,
265        };
266        assert_eq!(mem(Some(10), Some(4)).usable(), Some(4));
267        assert_eq!(mem(Some(4), Some(10)).usable(), Some(4));
268        assert_eq!(mem(Some(10), None).usable(), Some(10));
269        assert_eq!(mem(None, Some(4)).usable(), Some(4));
270        assert_eq!(mem(None, None).usable(), None);
271    }
272
273    #[test]
274    fn test_standard_derivation_table() {
275        // (usable, (size, perfile, limit))
276        let cases: &[(u64, (u64, u64, u64))] = &[
277            (268_435_456, (64_000_000, 16_000_000, 264_000_000)), // 256 MiB: floors
278            (512_000_000, (64_000_000, 16_000_000, 264_000_000)), // still floors
279            (1_073_741_824, (64_000_000, 16_000_000, 264_000_000)), // 1 GiB container ~= floors
280            (2_000_000_000, (120_000_000, 24_000_000, 496_000_000)),
281            (8_000_000_000, (496_000_000, 120_000_000, 2_000_000_000)),
282            (16_000_000_000, (1_000_000_000, 248_000_000, 4_000_000_000)),
283            (32_000_000_000, (2_000_000_000, 496_000_000, 8_000_000_000)), // ~historical defaults
284            (64_000_000_000, (4_000_000_000, 1_000_000_000, 16_000_000_000)),
285            (128_000_000_000, (8_000_000_000, 2_000_000_000, 32_000_000_000)),
286            (256_000_000_000, (16_000_000_000, 2_000_000_000, 64_000_000_000)), // ceilings
287            (2_000_000_000_000, (16_000_000_000, 2_000_000_000, 64_000_000_000)), // 2 TB host
288        ];
289        for &(usable, expected) in cases {
290            assert_eq!(
291                triple(derive_download_buffer_defaults(usable, &STANDARD_FRACTIONS)),
292                expected,
293                "standard, usable={usable}"
294            );
295        }
296        // A huge host derives exactly the historical high-performance constants.
297        assert_eq!(derive_download_buffer_defaults(2_000_000_000_000, &STANDARD_FRACTIONS), STATIC_HP_DEFAULTS);
298    }
299
300    #[test]
301    fn test_high_performance_derivation_table() {
302        let cases: &[(u64, (u64, u64, u64))] = &[
303            (268_435_456, (64_000_000, 16_000_000, 264_000_000)), // floors
304            (512_000_000, (64_000_000, 16_000_000, 264_000_000)), // limit floor binds (u/2 = 256 MB)
305            (8_000_000_000, (1_000_000_000, 248_000_000, 4_000_000_000)),
306            (16_000_000_000, (2_000_000_000, 496_000_000, 8_000_000_000)),
307            (32_000_000_000, (4_000_000_000, 1_000_000_000, 16_000_000_000)),
308            (64_000_000_000, (8_000_000_000, 2_000_000_000, 32_000_000_000)),
309            (128_000_000_000, (16_000_000_000, 2_000_000_000, 64_000_000_000)),
310            (256_000_000_000, (16_000_000_000, 2_000_000_000, 64_000_000_000)),
311            (2_000_000_000_000, (16_000_000_000, 2_000_000_000, 64_000_000_000)),
312        ];
313        for &(usable, expected) in cases {
314            assert_eq!(
315                triple(derive_download_buffer_defaults(usable, &HIGH_PERFORMANCE_FRACTIONS)),
316                expected,
317                "hp, usable={usable}"
318            );
319        }
320    }
321
322    #[test]
323    fn test_derivation_invariants_across_sweep() {
324        // Sweep 256 MB .. 2.5 TB multiplicatively; check invariant, rounding,
325        // clamps, and monotonicity for both fraction sets.
326        for fractions in [&STANDARD_FRACTIONS, &HIGH_PERFORMANCE_FRACTIONS] {
327            let mut prev: Option<(u64, u64, u64)> = None;
328            let mut usable: u64 = 268_435_456;
329            while usable < 2_500_000_000_000 {
330                let (size, perfile, limit) = triple(derive_download_buffer_defaults(usable, fractions));
331
332                // One coherent allocation: base + 8 concurrent files * perfile fits the limit.
333                assert!(
334                    size + 8 * perfile <= limit,
335                    "invariant violated at usable={usable}: {size} + 8*{perfile} > {limit}"
336                );
337
338                // Clean display values.
339                for v in [size, perfile, limit] {
340                    assert_eq!(v % ROUND_TO, 0, "not a multiple of {ROUND_TO} at usable={usable}");
341                }
342
343                // Clamps respected.
344                assert!((SIZE_FLOOR..=SIZE_CEILING).contains(&size));
345                assert!((PERFILE_FLOOR..=PERFILE_CEILING).contains(&perfile));
346                assert!((LIMIT_FLOOR..=LIMIT_CEILING).contains(&limit));
347
348                // Monotonic in usable memory.
349                if let Some((ps, pp, pl)) = prev {
350                    assert!(size >= ps && perfile >= pp && limit >= pl, "not monotonic at usable={usable}");
351                }
352                prev = Some((size, perfile, limit));
353
354                usable = usable * 5 / 4;
355            }
356        }
357    }
358
359    // --- probe and kill switch ---
360
361    #[test]
362    fn test_probe_reports_host_total() {
363        assert!(USABLE_MEMORY.host_total.is_some_and(|t| t > 0), "host total should be detectable on test hosts");
364    }
365
366    #[test]
367    #[serial_test::serial(config_env)]
368    fn test_kill_switch_restores_static_defaults() {
369        let _g = crate::utils::EnvVarGuard::set("HF_XET_DISABLE_MEMORY_DERIVED_DOWNLOAD_BUFFERS", "1");
370        assert_eq!(default_download_buffer_sizes(), STATIC_DEFAULTS);
371        assert_eq!(high_performance_download_buffer_sizes(), STATIC_HP_DEFAULTS);
372    }
373
374    #[test]
375    #[serial_test::serial(config_env)]
376    fn test_derived_defaults_match_probe() {
377        let _g = crate::utils::EnvVarGuard::set("HF_XET_DISABLE_MEMORY_DERIVED_DOWNLOAD_BUFFERS", "0");
378        let expected_std = match USABLE_MEMORY.usable() {
379            Some(u) => derive_download_buffer_defaults(u, &STANDARD_FRACTIONS),
380            None => STATIC_DEFAULTS,
381        };
382        let expected_hp = match USABLE_MEMORY.usable() {
383            Some(u) => derive_download_buffer_defaults(u, &HIGH_PERFORMANCE_FRACTIONS),
384            None => STATIC_HP_DEFAULTS,
385        };
386        assert_eq!(default_download_buffer_sizes(), expected_std);
387        assert_eq!(high_performance_download_buffer_sizes(), expected_hp);
388    }
389}