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.
117    ///
118    /// Apple Silicon: half of P-cores (avoid E-cores + AMX cache thrashing).
119    /// Elsewhere: ~¾ of logical CPUs — DiT/CNN CPU paths want more outer
120    /// Rayon width once BLAS is pinned to 1 thread.
121    fn optimal_workers(&self) -> usize {
122        let base = if self.perf_cores > 0 {
123            self.perf_cores / 2
124        } else if cfg!(all(target_arch = "aarch64", target_vendor = "apple")) {
125            self.total_cpus / 2
126        } else {
127            self.total_cpus.saturating_mul(3) / 4
128        };
129        base.clamp(1, 32)
130    }
131
132    /// Cache line: prefer runtime-detected, fall back to compile-time.
133    fn cache_line(&self) -> usize {
134        if self.cache_line > 0 {
135            self.cache_line
136        } else {
137            PLATFORM_CACHE_LINE
138        }
139    }
140
141    /// Fusion threshold: intermediates must fit in L1 for monolithic kernels.
142    #[allow(dead_code)]
143    fn fuse_attn_threshold(&self) -> usize {
144        if self.l1d_cache > 0 {
145            // L1 budget: ~60% for intermediates (rest for weights being streamed)
146            // Each fused layer needs: qkv(m×3h) + attn(m×h) + res(m×h) + normed(m×h) + ffn(m×int)
147            // ≈ m × 7h floats for BERT. Must fit in 60% of L1.
148            // Solve for m: m = 0.6 * L1 / (7 * 768 * 4) ≈ L1/36000
149            // For L1=64KB: m ≈ 1.8 → batch*seq ≤ ~2 → threshold ~64 is about right
150            64
151        } else {
152            64
153        }
154    }
155}
156
157#[cfg(target_vendor = "apple")]
158fn sysctl_usize(name: &str) -> Option<usize> {
159    use std::ffi::CString;
160    let cname = CString::new(name).ok()?;
161    let mut val: u64 = 0;
162    let mut len = std::mem::size_of::<u64>();
163    unsafe {
164        unsafe extern "C" {
165            fn sysctlbyname(
166                name: *const i8,
167                oldp: *mut u8,
168                oldlenp: *mut usize,
169                newp: *const u8,
170                newlen: usize,
171            ) -> i32;
172        }
173        let ret = sysctlbyname(
174            cname.as_ptr(),
175            &mut val as *mut u64 as *mut u8,
176            &mut len,
177            std::ptr::null(),
178            0,
179        );
180        if ret == 0 { Some(val as usize) } else { None }
181    }
182}
183
184/// Runtime configuration for the RLX CPU backend.
185#[derive(Debug, Clone)]
186pub struct RuntimeConfig {
187    // ── Thread pool ─────────────────────────────────────────
188    pub pool_workers: usize,
189
190    // ── Parallelization ─────────────────────────────────────
191    pub par_threshold: usize,
192    pub min_rows_per_thread: usize,
193
194    // ── SDPA dispatch ───────────────────────────────────────
195    pub sdpa_seq_threshold: usize,
196
197    // ── Memory planning ─────────────────────────────────────
198    pub arena_alignment: usize,
199
200    // ── Numerical constants ─────────────────────────────────
201    pub ln_eps_default: f32,
202    pub attn_mask_neg_inf: f32,
203    pub score_skip_threshold: f32,
204    pub mask_binary_threshold: f32,
205
206    // ── Diagnostics ─────────────────────────────────────────
207    pub verbose: u8,
208}
209
210impl Default for RuntimeConfig {
211    fn default() -> Self {
212        Self::auto_detect()
213    }
214}
215
216impl RuntimeConfig {
217    /// Auto-detect hardware and apply optimal defaults.
218    pub fn auto_detect() -> Self {
219        let hw = HwInfo::detect();
220
221        Self {
222            pool_workers: hw.optimal_workers(),
223            par_threshold: PLATFORM_PAR_THRESHOLD,
224            min_rows_per_thread: 4,
225            sdpa_seq_threshold: 32,
226            arena_alignment: hw.cache_line(),
227            ln_eps_default: 1e-12,
228            attn_mask_neg_inf: -1e9,
229            score_skip_threshold: 1e-8,
230            mask_binary_threshold: 0.5,
231            verbose: 0,
232        }
233    }
234
235    /// Auto-detect then override from `RLX_*` environment variables.
236    pub fn from_env() -> Self {
237        let mut cfg = Self::auto_detect();
238
239        if let Some(v) = rlx_ir::env::var("RLX_WORKERS")
240            && let Ok(n) = v.parse::<usize>()
241        {
242            cfg.pool_workers = if n == 0 { cfg.pool_workers } else { n.min(32) };
243        }
244        if let Some(v) = rlx_ir::env::var("RLX_PAR_THRESHOLD")
245            && let Ok(n) = v.parse()
246        {
247            cfg.par_threshold = n;
248        }
249        if let Some(v) = rlx_ir::env::var("RLX_SDPA_THRESHOLD")
250            && let Ok(n) = v.parse()
251        {
252            cfg.sdpa_seq_threshold = n;
253        }
254        if let Some(v) = rlx_ir::env::var("RLX_ARENA_ALIGN")
255            && let Ok(n) = v.parse()
256        {
257            cfg.arena_alignment = n;
258        }
259        if let Some(v) = rlx_ir::env::var("RLX_VERBOSE")
260            && let Ok(n) = v.parse()
261        {
262            cfg.verbose = n;
263        }
264
265        if cfg.verbose >= 1 {
266            let hw = HwInfo::detect();
267            eprintln!(
268                "[rlx] hw: {} CPUs ({} P-cores), L1={}KB, L2={}KB, cacheline={}B",
269                hw.total_cpus,
270                hw.perf_cores,
271                hw.l1d_cache / 1024,
272                hw.l2_cache / 1024,
273                hw.cache_line()
274            );
275            eprintln!(
276                "[rlx] config: workers={}, par_thr={}, sdpa_thr={}, align={}",
277                cfg.pool_workers, cfg.par_threshold, cfg.sdpa_seq_threshold, cfg.arena_alignment
278            );
279        }
280
281        cfg
282    }
283
284    /// Push this config into the global [`rlx_ir::env`] override map so all
285    /// RLX backends see the same knobs without setting process env vars.
286    pub fn install(&self) {
287        rlx_ir::env::set("RLX_WORKERS", self.pool_workers.to_string());
288        rlx_ir::env::set("RLX_PAR_THRESHOLD", self.par_threshold.to_string());
289        rlx_ir::env::set("RLX_SDPA_THRESHOLD", self.sdpa_seq_threshold.to_string());
290        rlx_ir::env::set("RLX_ARENA_ALIGN", self.arena_alignment.to_string());
291        rlx_ir::env::set("RLX_VERBOSE", self.verbose.to_string());
292    }
293
294    /// Get or initialize the global singleton config.
295    pub fn global() -> &'static RuntimeConfig {
296        static CONFIG: OnceLock<RuntimeConfig> = OnceLock::new();
297        CONFIG.get_or_init(RuntimeConfig::from_env)
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    #[test]
306    fn auto_detect_sane_defaults() {
307        let cfg = RuntimeConfig::auto_detect();
308        assert!(cfg.pool_workers >= 1);
309        assert!(cfg.pool_workers <= 32);
310        // Platform-appropriate cache line
311        assert!(cfg.arena_alignment >= 64);
312        assert!(cfg.verbose == 0);
313    }
314
315    #[test]
316    fn global_is_consistent() {
317        let a = RuntimeConfig::global();
318        let b = RuntimeConfig::global();
319        assert_eq!(a.pool_workers, b.pool_workers);
320    }
321
322    #[test]
323    fn hw_detection() {
324        let hw = HwInfo::detect();
325        assert!(hw.total_cpus >= 1);
326        // On any Apple platform sysctl should report the cache line size
327        #[cfg(target_vendor = "apple")]
328        assert!(
329            hw.cache_line > 0,
330            "expected sysctl to return cache line size"
331        );
332    }
333}