Skip to main content

rlx_cpu/
config.rs

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