qualia_core_db/platform/platform_scheduler.rs
1//! Platform-aware thread QoS and core-affinity binding.
2//!
3//! Binds the calling thread to the most appropriate processor class for its
4//! workload so the OS scheduler makes the right placement decision:
5//!
6//! | Class | macOS QoS | Linux | Windows |
7//! |--------------------|----------------------------|-------------------|----------------------------|
8//! | UserInteractive | QOS_CLASS_USER_INTERACTIVE | P-cores (affinity)| THREAD_PRIORITY_HIGHEST |
9//! | UserInitiated | QOS_CLASS_USER_INITIATED | P-cores | THREAD_PRIORITY_ABOVE_NORMAL|
10//! | Default | QOS_CLASS_DEFAULT | any | THREAD_PRIORITY_NORMAL |
11//! | Utility | QOS_CLASS_UTILITY | any | THREAD_PRIORITY_BELOW_NORMAL|
12//! | Background | QOS_CLASS_BACKGROUND | E-cores (affinity)| THREAD_PRIORITY_IDLE |
13//!
14//! **Apple Silicon AMP** (P-cores + E-cores) is the primary target.
15//! On Intel/AMD/ARM64 the distinction collapses to thread priority.
16
17#![cfg(not(target_arch = "wasm32"))]
18
19// ──────────────────────────────────────────────────────────────────────────────
20// Darwin QoS FFI
21// ──────────────────────────────────────────────────────────────────────────────
22
23#[cfg(target_os = "macos")]
24mod darwin_qos {
25 /// QoS class values from <sys/qos.h> (Darwin 18+).
26 pub const QOS_CLASS_USER_INTERACTIVE: u32 = 0x21; // 33
27 pub const QOS_CLASS_USER_INITIATED: u32 = 0x19; // 25
28 pub const QOS_CLASS_DEFAULT: u32 = 0x15; // 21
29 pub const QOS_CLASS_UTILITY: u32 = 0x11; // 17
30 pub const QOS_CLASS_BACKGROUND: u32 = 0x09; // 9
31
32 extern "C" {
33 /// Set the QoS class of the calling thread.
34 /// `relative_priority` must be in `[QOS_MIN_RELATIVE_PRIORITY, 0]`
35 /// where `QOS_MIN_RELATIVE_PRIORITY = -15`.
36 pub fn pthread_set_qos_class_self_np(
37 qos_class: u32,
38 relative_priority: libc::c_int,
39 ) -> libc::c_int;
40
41 /// Read back the QoS class of any thread (NULL → calling thread).
42 pub fn pthread_get_qos_class_np(
43 thread: libc::pthread_t,
44 qos_class_out: *mut u32,
45 relative_priority_out: *mut libc::c_int,
46 ) -> libc::c_int;
47 }
48}
49
50// ──────────────────────────────────────────────────────────────────────────────
51// Public API
52// ──────────────────────────────────────────────────────────────────────────────
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum QosClass {
56 /// LLM inference, active SPARQL queries, Phase 8 decode loop.
57 /// → Apple P-cores / Windows HIGHEST / Linux P-core affinity.
58 UserInteractive,
59 /// Active user requests, graph engine hot path.
60 UserInitiated,
61 /// Default — no preference.
62 Default,
63 /// Background sync, slow I/O.
64 Utility,
65 /// WAL flush, Merkle root computation, ambient orchestration.
66 /// → Apple E-cores / Windows IDLE / Linux E-core affinity.
67 Background,
68}
69
70#[derive(Debug)]
71pub enum SchedulerError {
72 Unsupported(String),
73 OsError(i32),
74}
75
76impl std::fmt::Display for SchedulerError {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 match self {
79 SchedulerError::Unsupported(m) => write!(f, "unsupported: {m}"),
80 SchedulerError::OsError(e) => write!(f, "OS error {e}"),
81 }
82 }
83}
84
85/// Bind the **calling thread** to `class`.
86///
87/// Returns `Ok(())` if the platform supports QoS binding, or
88/// `Err(SchedulerError::Unsupported)` on platforms that have no thread
89/// priority API (rare).
90pub fn bind_current_thread(class: QosClass) -> Result<(), SchedulerError> {
91 bind_macos(class)
92 .or_else(|_| bind_linux(class))
93 .or_else(|_| bind_windows(class))
94}
95
96/// Convenience wrapper: bind to UserInteractive (P-core / max priority).
97/// Call from the LLM inference thread and active SPARQL query thread.
98pub fn bind_inference_thread() {
99 let _ = bind_current_thread(QosClass::UserInteractive);
100}
101
102/// Convenience wrapper: bind to Background (E-core / idle priority).
103/// Call from WAL flush, Merkle DAG, ambient orchestration threads.
104pub fn bind_background_thread() {
105 let _ = bind_current_thread(QosClass::Background);
106}
107
108// ──────────────────────────────────────────────────────────────────────────────
109// macOS implementation
110// ──────────────────────────────────────────────────────────────────────────────
111
112fn bind_macos(#[allow(unused_variables)] class: QosClass) -> Result<(), SchedulerError> {
113 #[cfg(target_os = "macos")]
114 {
115 use darwin_qos::*;
116 let qos = match class {
117 QosClass::UserInteractive => QOS_CLASS_USER_INTERACTIVE,
118 QosClass::UserInitiated => QOS_CLASS_USER_INITIATED,
119 QosClass::Default => QOS_CLASS_DEFAULT,
120 QosClass::Utility => QOS_CLASS_UTILITY,
121 QosClass::Background => QOS_CLASS_BACKGROUND,
122 };
123 // SAFETY: pthread_set_qos_class_self_np is safe to call from any thread.
124 let rc = unsafe { pthread_set_qos_class_self_np(qos, 0) };
125 if rc == 0 {
126 return Ok(());
127 }
128 return Err(SchedulerError::OsError(rc));
129 }
130 #[cfg(not(target_os = "macos"))]
131 Err(SchedulerError::Unsupported(
132 "macOS QoS not available on this platform".into(),
133 ))
134}
135
136// ──────────────────────────────────────────────────────────────────────────────
137// Linux implementation — core_affinity for asymmetric multiprocessing
138// ──────────────────────────────────────────────────────────────────────────────
139
140fn bind_linux(#[allow(unused_variables)] class: QosClass) -> Result<(), SchedulerError> {
141 #[cfg(target_os = "linux")]
142 {
143 use core_affinity::CoreId;
144
145 let all_cores = core_affinity::get_core_ids()
146 .ok_or_else(|| SchedulerError::Unsupported("core_affinity unavailable".into()))?;
147
148 if all_cores.is_empty() {
149 return Err(SchedulerError::Unsupported("no cores found".into()));
150 }
151
152 // Heuristic for big.LITTLE / Alder Lake / Sapphire Rapids:
153 // Lower-numbered cores are P-cores; higher-numbered are E-cores.
154 // We split at the midpoint as a conservative estimate.
155 let mid = all_cores.len() / 2;
156 let target = match class {
157 QosClass::UserInteractive | QosClass::UserInitiated => {
158 // P-cores: first half (or all if symmetric)
159 &all_cores[..mid.max(1)]
160 }
161 QosClass::Background | QosClass::Utility => {
162 // E-cores: second half (or all if symmetric)
163 &all_cores[mid..]
164 }
165 QosClass::Default => &all_cores[..],
166 };
167
168 // Pin to the first core in the target set; real production code would
169 // use `sched_setaffinity` with a full mask, but core_affinity exposes
170 // single-core pinning which is sufficient for the inference split.
171 if let Some(core) = target.first() {
172 if core_affinity::set_for_current(*core) {
173 return Ok(());
174 }
175 }
176
177 // Also set Linux thread nice / scheduling policy.
178 let nice_val: libc::c_int = match class {
179 QosClass::UserInteractive => -10,
180 QosClass::UserInitiated => -5,
181 QosClass::Default => 0,
182 QosClass::Utility => 5,
183 QosClass::Background => 19,
184 };
185 // SAFETY: getpid() always succeeds; setpriority is safe with valid args.
186 unsafe {
187 libc::setpriority(libc::PRIO_PROCESS, 0, nice_val);
188 }
189 return Ok(());
190 }
191 #[cfg(not(target_os = "linux"))]
192 Err(SchedulerError::Unsupported(
193 "Linux sched_setaffinity not available".into(),
194 ))
195}
196
197// ──────────────────────────────────────────────────────────────────────────────
198// Windows implementation — SetThreadPriority
199// ──────────────────────────────────────────────────────────────────────────────
200
201fn bind_windows(class: QosClass) -> Result<(), SchedulerError> {
202 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
203 {
204 use windows::Win32::System::Threading::{
205 GetCurrentThread, SetThreadPriority, THREAD_PRIORITY_ABOVE_NORMAL,
206 THREAD_PRIORITY_BELOW_NORMAL, THREAD_PRIORITY_HIGHEST, THREAD_PRIORITY_IDLE,
207 THREAD_PRIORITY_NORMAL,
208 };
209 let priority = match class {
210 QosClass::UserInteractive => THREAD_PRIORITY_HIGHEST,
211 QosClass::UserInitiated => THREAD_PRIORITY_ABOVE_NORMAL,
212 QosClass::Default => THREAD_PRIORITY_NORMAL,
213 QosClass::Utility => THREAD_PRIORITY_BELOW_NORMAL,
214 QosClass::Background => THREAD_PRIORITY_IDLE,
215 };
216 // SAFETY: GetCurrentThread() returns a pseudo-handle that is always valid.
217 let ok = unsafe { SetThreadPriority(GetCurrentThread(), priority) };
218 return if ok.is_ok() {
219 Ok(())
220 } else {
221 Err(SchedulerError::OsError(
222 unsafe { windows::Win32::Foundation::GetLastError() }.0 as i32,
223 ))
224 };
225 }
226 #[cfg(not(all(target_os = "windows", target_arch = "x86_64")))]
227 Err(SchedulerError::Unsupported(
228 "Windows SetThreadPriority not available".into(),
229 ))
230}
231
232// ──────────────────────────────────────────────────────────────────────────────
233// Query current QoS (macOS only)
234// ──────────────────────────────────────────────────────────────────────────────
235
236/// Read the QoS class of the calling thread. Returns `None` on non-macOS.
237pub fn current_qos_class() -> Option<u32> {
238 #[cfg(target_os = "macos")]
239 {
240 let mut cls: u32 = 0;
241 let mut rel: libc::c_int = 0;
242 // SAFETY: pthread_get_qos_class_np with null thread → calling thread.
243 let rc = unsafe {
244 darwin_qos::pthread_get_qos_class_np(libc::pthread_self(), &mut cls, &mut rel)
245 };
246 if rc == 0 {
247 Some(cls)
248 } else {
249 None
250 }
251 }
252 #[cfg(not(target_os = "macos"))]
253 None
254}
255
256// ──────────────────────────────────────────────────────────────────────────────
257// Tests
258// ──────────────────────────────────────────────────────────────────────────────
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 #[test]
265 fn test_bind_does_not_panic() {
266 // On every platform this must complete without panic.
267 // It may return Err(Unsupported) on platforms with no QoS API.
268 let _ = bind_current_thread(QosClass::UserInteractive);
269 let _ = bind_current_thread(QosClass::Background);
270 }
271
272 #[test]
273 fn test_inference_background_helpers() {
274 bind_inference_thread();
275 bind_background_thread();
276 }
277
278 #[test]
279 fn test_current_qos_no_panic() {
280 let _ = current_qos_class(); // May return None on non-macOS
281 }
282}