Skip to main content

rlx_cpu/
config.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Runtime configuration — compile-time platform defaults + runtime hardware detection.
6//!
7//! Compile-time: target arch/OS sets optimal defaults (cache line, SIMD strategy).
8//! Runtime: sysctl/cpuid refines values (P-core count, L1/L2 sizes).
9//! Env vars: `RLX_*` overrides for manual tuning — or set the same keys in
10//! code via [`rlx_ir::env::set`] / [`RuntimeConfig::install`].
11//!
12//! ```bash
13//! RLX_WORKERS=8           # thread pool size (0 = auto)
14//! RLX_PAR_THRESHOLD=20000 # min elements for parallel dispatch
15//! RLX_SDPA_THRESHOLD=32   # seq len: NEON dots (≤) vs BLAS sgemm (>)
16//! RLX_ARENA_ALIGN=128     # arena buffer alignment in bytes
17//! RLX_VERBOSE=0           # 0=quiet, 1=fusion passes, 2=full graph dump
18//! ```
19
20use std::sync::OnceLock;
21
22// ── Compile-time platform defaults ──────────────────────────────────────
23
24/// Cache line size — known at compile time per platform.
25/// Apple Silicon (Macs, iPhones/iPads, Apple TV/Watch, Vision Pro) uses
26/// 128-byte L1 lines.
27#[cfg(all(target_arch = "aarch64", target_vendor = "apple"))]
28const PLATFORM_CACHE_LINE: usize = 128; // Apple Silicon: 128-byte L1 lines
29
30#[cfg(all(target_arch = "aarch64", not(target_vendor = "apple")))]
31const PLATFORM_CACHE_LINE: usize = 64; // ARM servers (Graviton, Ampere): typically 64
32
33#[cfg(not(target_arch = "aarch64"))]
34const PLATFORM_CACHE_LINE: usize = 64; // x86_64: 64-byte cache lines
35
36/// Default parallel threshold — tuned per platform.
37/// Apple Silicon AMX handles BLAS internally; our par_for is for element-wise ops.
38/// Lower threshold = more parallelism for LayerNorm/GELU on small tensors.
39/// All Apple devices are Apple Silicon, so they share the macOS tuning.
40#[cfg(all(target_arch = "aarch64", target_vendor = "apple"))]
41const PLATFORM_PAR_THRESHOLD: usize = 16_384; // Apple Silicon: AMX does BLAS, parallelize rest earlier
42
43#[cfg(not(all(target_arch = "aarch64", target_vendor = "apple")))]
44const PLATFORM_PAR_THRESHOLD: usize = 30_000;
45
46// ── Runtime hardware detection ──────────────────────────────────────────
47
48/// Detect hardware properties at runtime.
49struct HwInfo {
50    total_cpus: usize,
51    perf_cores: usize, // P-cores (0 = unknown, use total_cpus)
52    l1d_cache: usize,  // L1 data cache bytes (0 = unknown)
53    l2_cache: usize,   // L2 cache bytes (0 = unknown)
54    cache_line: usize, // actual cache line from OS (0 = use compile-time default)
55}
56
57impl HwInfo {
58    fn detect() -> Self {
59        let total = std::thread::available_parallelism()
60            .map(|n| n.get())
61            .unwrap_or(2);
62
63        // `info` is mutated only under per-OS cfg blocks below; on targets
64        // without one (e.g. wasm) it stays as-detected.
65        #[allow(unused_mut)]
66        let mut info = HwInfo {
67            total_cpus: total,
68            perf_cores: 0,
69            l1d_cache: 0,
70            l2_cache: 0,
71            cache_line: 0,
72        };
73
74        #[cfg(target_vendor = "apple")]
75        {
76            info.perf_cores = sysctl_usize("hw.perflevel0.physicalcpu").unwrap_or(0);
77            info.l1d_cache = sysctl_usize("hw.l1dcachesize").unwrap_or(0);
78            info.l2_cache = sysctl_usize("hw.l2cachesize").unwrap_or(0);
79            info.cache_line = sysctl_usize("hw.cachelinesize").unwrap_or(0);
80        }
81
82        #[cfg(target_os = "linux")]
83        {
84            // /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
85            if let Ok(v) = std::fs::read_to_string(
86                "/sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size",
87            ) {
88                info.cache_line = v.trim().parse().unwrap_or(0);
89            }
90            if let Ok(v) = std::fs::read_to_string("/sys/devices/system/cpu/cpu0/cache/index0/size")
91            {
92                // Parse "32K" or "32768"
93                let s = v.trim().to_uppercase();
94                if s.ends_with('K') {
95                    info.l1d_cache = s[..s.len() - 1].parse::<usize>().unwrap_or(0) * 1024;
96                } else {
97                    info.l1d_cache = s.parse().unwrap_or(0);
98                }
99            }
100        }
101
102        info
103    }
104
105    /// Optimal worker count.
106    ///
107    /// Apple Silicon: half of P-cores (avoid E-cores + AMX cache thrashing).
108    /// Elsewhere: ~¾ of logical CPUs — DiT/CNN CPU paths want more outer
109    /// Rayon width once BLAS is pinned to 1 thread.
110    fn optimal_workers(&self) -> usize {
111        let base = if self.perf_cores > 0 {
112            self.perf_cores / 2
113        } else if cfg!(all(target_arch = "aarch64", target_vendor = "apple")) {
114            self.total_cpus / 2
115        } else {
116            self.total_cpus.saturating_mul(3) / 4
117        };
118        base.clamp(1, 32)
119    }
120
121    /// Cache line: prefer runtime-detected, fall back to compile-time.
122    fn cache_line(&self) -> usize {
123        if self.cache_line > 0 {
124            self.cache_line
125        } else {
126            PLATFORM_CACHE_LINE
127        }
128    }
129
130    /// Fusion threshold: intermediates must fit in L1 for monolithic kernels.
131    #[allow(dead_code)]
132    fn fuse_attn_threshold(&self) -> usize {
133        if self.l1d_cache > 0 {
134            // L1 budget: ~60% for intermediates (rest for weights being streamed)
135            // Each fused layer needs: qkv(m×3h) + attn(m×h) + res(m×h) + normed(m×h) + ffn(m×int)
136            // ≈ m × 7h floats for BERT. Must fit in 60% of L1.
137            // Solve for m: m = 0.6 * L1 / (7 * 768 * 4) ≈ L1/36000
138            // For L1=64KB: m ≈ 1.8 → batch*seq ≤ ~2 → threshold ~64 is about right
139            64
140        } else {
141            64
142        }
143    }
144}
145
146#[cfg(target_vendor = "apple")]
147fn sysctl_usize(name: &str) -> Option<usize> {
148    use std::ffi::CString;
149    let cname = CString::new(name).ok()?;
150    let mut val: u64 = 0;
151    let mut len = std::mem::size_of::<u64>();
152    unsafe {
153        unsafe extern "C" {
154            fn sysctlbyname(
155                name: *const i8,
156                oldp: *mut u8,
157                oldlenp: *mut usize,
158                newp: *const u8,
159                newlen: usize,
160            ) -> i32;
161        }
162        let ret = sysctlbyname(
163            cname.as_ptr(),
164            &mut val as *mut u64 as *mut u8,
165            &mut len,
166            std::ptr::null(),
167            0,
168        );
169        if ret == 0 { Some(val as usize) } else { None }
170    }
171}
172
173/// Runtime configuration for the RLX CPU backend.
174#[derive(Debug, Clone)]
175pub struct RuntimeConfig {
176    // ── Thread pool ─────────────────────────────────────────
177    pub pool_workers: usize,
178
179    // ── Parallelization ─────────────────────────────────────
180    pub par_threshold: usize,
181    pub min_rows_per_thread: usize,
182
183    // ── SDPA dispatch ───────────────────────────────────────
184    pub sdpa_seq_threshold: usize,
185
186    // ── Memory planning ─────────────────────────────────────
187    pub arena_alignment: usize,
188
189    // ── Numerical constants ─────────────────────────────────
190    pub ln_eps_default: f32,
191    pub attn_mask_neg_inf: f32,
192    pub score_skip_threshold: f32,
193    pub mask_binary_threshold: f32,
194
195    // ── Diagnostics ─────────────────────────────────────────
196    pub verbose: u8,
197}
198
199impl Default for RuntimeConfig {
200    fn default() -> Self {
201        Self::auto_detect()
202    }
203}
204
205impl RuntimeConfig {
206    /// Auto-detect hardware and apply optimal defaults.
207    pub fn auto_detect() -> Self {
208        let hw = HwInfo::detect();
209
210        Self {
211            pool_workers: hw.optimal_workers(),
212            par_threshold: PLATFORM_PAR_THRESHOLD,
213            min_rows_per_thread: 4,
214            sdpa_seq_threshold: 32,
215            arena_alignment: hw.cache_line(),
216            ln_eps_default: 1e-12,
217            attn_mask_neg_inf: -1e9,
218            score_skip_threshold: 1e-8,
219            mask_binary_threshold: 0.5,
220            verbose: 0,
221        }
222    }
223
224    /// Auto-detect then override from `RLX_*` environment variables.
225    pub fn from_env() -> Self {
226        let mut cfg = Self::auto_detect();
227
228        if let Some(v) = rlx_ir::env::var("RLX_WORKERS")
229            && let Ok(n) = v.parse::<usize>()
230        {
231            cfg.pool_workers = if n == 0 { cfg.pool_workers } else { n.min(32) };
232        }
233        if let Some(v) = rlx_ir::env::var("RLX_PAR_THRESHOLD")
234            && let Ok(n) = v.parse()
235        {
236            cfg.par_threshold = n;
237        }
238        if let Some(v) = rlx_ir::env::var("RLX_SDPA_THRESHOLD")
239            && let Ok(n) = v.parse()
240        {
241            cfg.sdpa_seq_threshold = n;
242        }
243        if let Some(v) = rlx_ir::env::var("RLX_ARENA_ALIGN")
244            && let Ok(n) = v.parse()
245        {
246            cfg.arena_alignment = n;
247        }
248        if let Some(v) = rlx_ir::env::var("RLX_VERBOSE")
249            && let Ok(n) = v.parse()
250        {
251            cfg.verbose = n;
252        }
253
254        if cfg.verbose >= 1 {
255            let hw = HwInfo::detect();
256            eprintln!(
257                "[rlx] hw: {} CPUs ({} P-cores), L1={}KB, L2={}KB, cacheline={}B",
258                hw.total_cpus,
259                hw.perf_cores,
260                hw.l1d_cache / 1024,
261                hw.l2_cache / 1024,
262                hw.cache_line()
263            );
264            eprintln!(
265                "[rlx] config: workers={}, par_thr={}, sdpa_thr={}, align={}",
266                cfg.pool_workers, cfg.par_threshold, cfg.sdpa_seq_threshold, cfg.arena_alignment
267            );
268        }
269
270        cfg
271    }
272
273    /// Push this config into the global [`rlx_ir::env`] override map so all
274    /// RLX backends see the same knobs without setting process env vars.
275    pub fn install(&self) {
276        rlx_ir::env::set("RLX_WORKERS", self.pool_workers.to_string());
277        rlx_ir::env::set("RLX_PAR_THRESHOLD", self.par_threshold.to_string());
278        rlx_ir::env::set("RLX_SDPA_THRESHOLD", self.sdpa_seq_threshold.to_string());
279        rlx_ir::env::set("RLX_ARENA_ALIGN", self.arena_alignment.to_string());
280        rlx_ir::env::set("RLX_VERBOSE", self.verbose.to_string());
281    }
282
283    /// Get or initialize the global singleton config.
284    pub fn global() -> &'static RuntimeConfig {
285        static CONFIG: OnceLock<RuntimeConfig> = OnceLock::new();
286        CONFIG.get_or_init(RuntimeConfig::from_env)
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn auto_detect_sane_defaults() {
296        let cfg = RuntimeConfig::auto_detect();
297        assert!(cfg.pool_workers >= 1);
298        assert!(cfg.pool_workers <= 32);
299        // Platform-appropriate cache line
300        assert!(cfg.arena_alignment >= 64);
301        assert!(cfg.verbose == 0);
302    }
303
304    #[test]
305    fn global_is_consistent() {
306        let a = RuntimeConfig::global();
307        let b = RuntimeConfig::global();
308        assert_eq!(a.pool_workers, b.pool_workers);
309    }
310
311    #[test]
312    fn hw_detection() {
313        let hw = HwInfo::detect();
314        assert!(hw.total_cpus >= 1);
315        // On any Apple platform sysctl should report the cache line size
316        #[cfg(target_vendor = "apple")]
317        assert!(
318            hw.cache_line > 0,
319            "expected sysctl to return cache line size"
320        );
321    }
322}