1use std::sync::{Mutex, OnceLock};
9use std::time::{Duration, Instant};
10
11use crate::protocol::NodeResources;
12
13const FAST_PROFILE_CACHE_TTL: Duration = Duration::from_secs(5);
14
15static FAST_PROFILE_CACHE: OnceLock<Mutex<Option<CachedRuntimeProfileEntry>>> = OnceLock::new();
16
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
19pub enum AccelerationMode {
20 #[default]
22 Gpu,
23 Avx512,
25 Avx2,
27 Neon,
29 Generic,
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
35pub enum GpuBackend {
36 Cuda,
37 Rocm,
38 Vulkan,
39 Directml,
40 Metal,
41 Npu,
42 Cpu,
43}
44
45impl GpuBackend {
46 pub const fn as_str(self) -> &'static str {
48 match self {
49 Self::Cuda => "cuda",
50 Self::Rocm => "rocm",
51 Self::Vulkan => "vulkan",
52 Self::Directml => "directml",
53 Self::Metal => "metal",
54 Self::Npu => "npu",
55 Self::Cpu => "cpu",
56 }
57 }
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum ProbeMode {
63 Fast,
65 Full,
67}
68
69impl ProbeMode {
70 pub const fn as_str(self) -> &'static str {
72 match self {
73 Self::Fast => "fast",
74 Self::Full => "full",
75 }
76 }
77}
78
79impl AccelerationMode {
80 pub const fn as_str(self) -> &'static str {
82 match self {
83 Self::Gpu => "GPU",
84 Self::Avx512 => "AVX-512",
85 Self::Avx2 => "AVX2",
86 Self::Neon => "NEON",
87 Self::Generic => "Generic CPU",
88 }
89 }
90}
91
92#[derive(Clone, Debug)]
94pub struct RuntimeProfile {
95 pub node_resources: NodeResources,
97 pub logical_cores: usize,
99 pub recommended_workers: usize,
101 pub acceleration_mode: AccelerationMode,
103 pub gpu_backend: GpuBackend,
105 pub xdp_supported: bool,
107 pub detection_source: String,
109 pub probe_mode: ProbeMode,
111}
112
113impl RuntimeProfile {
114 pub fn summary(&self) -> String {
116 format!(
117 "Host Runtime Profile\n\
118 ====================\n\
119 Node ID: {}\n\
120 Logical cores: {}\n\
121 Recommended workers: {}\n\
122 System memory: {:.1} GB\n\
123 GPU VRAM: {:.1} GB\n\
124 Compute capability: {}\n\
125 GPU: {}\n\
126 GPU Backend: {}\n\
127 Acceleration: {}\n\
128 XDP support: {}\n\
129 Detection source: {}\n\
130 Probe mode: {}\n",
131 self.node_resources.id,
132 self.logical_cores,
133 self.recommended_workers,
134 self.node_resources.system_memory_gb,
135 self.node_resources.vram_gb,
136 if self.node_resources.compute_capability.is_empty() {
137 "cpu"
138 } else {
139 &self.node_resources.compute_capability
140 },
141 self.node_resources
142 .gpu_name
143 .as_deref()
144 .unwrap_or("Not detected"),
145 self.gpu_backend.as_str(),
146 self.acceleration_mode.as_str(),
147 if self.xdp_supported {
148 "available"
149 } else {
150 "unavailable"
151 },
152 self.detection_source,
153 self.probe_mode.as_str()
154 )
155 }
156}
157
158#[derive(Clone, Debug)]
159struct CachedRuntimeProfileEntry {
160 captured_at: Instant,
161 profile: RuntimeProfile,
162}
163
164pub fn detect_runtime_profile(node_id: impl Into<String>) -> RuntimeProfile {
166 detect_runtime_profile_with_mode(node_id, ProbeMode::Fast)
167}
168
169pub fn detect_runtime_profile_with_mode(
173 node_id: impl Into<String>,
174 probe_mode: ProbeMode,
175) -> RuntimeProfile {
176 let node_id = node_id.into();
177
178 if let Some(mut profile) = load_cached_runtime_profile(probe_mode) {
179 profile.node_resources.id = node_id;
180 return profile;
181 }
182
183 let sp = match probe_mode {
184 ProbeMode::Fast => crate::system_profile::SystemProfile::detect_fast(),
185 ProbeMode::Full => crate::system_profile::SystemProfile::detect(),
186 };
187
188 let mut profile: RuntimeProfile = sp.into();
189 profile.node_resources.id = node_id;
190 profile.probe_mode = probe_mode;
191
192 store_cached_runtime_profile(&profile);
193 profile
194}
195
196pub fn detect_local_node_resources(node_id: impl Into<String>) -> NodeResources {
198 detect_runtime_profile(node_id).node_resources
199}
200
201pub fn detect_runtime_profile_full(node_id: impl Into<String>) -> RuntimeProfile {
203 detect_runtime_profile_with_mode(node_id, ProbeMode::Full)
204}
205
206fn load_cached_runtime_profile(probe_mode: ProbeMode) -> Option<RuntimeProfile> {
207 if probe_mode != ProbeMode::Fast {
208 return None;
209 }
210
211 let cache = FAST_PROFILE_CACHE.get_or_init(|| Mutex::new(None));
212 let guard = cache.lock().ok()?;
213 let entry = guard.as_ref()?;
214 if entry.captured_at.elapsed() > FAST_PROFILE_CACHE_TTL {
215 return None;
216 }
217
218 Some(entry.profile.clone())
219}
220
221fn store_cached_runtime_profile(profile: &RuntimeProfile) {
222 if profile.probe_mode != ProbeMode::Fast {
223 return;
224 }
225
226 let cache = FAST_PROFILE_CACHE.get_or_init(|| Mutex::new(None));
227 if let Ok(mut guard) = cache.lock() {
228 *guard = Some(CachedRuntimeProfileEntry {
229 captured_at: Instant::now(),
230 profile: profile.clone(),
231 });
232 }
233}
234
235pub fn infer_compute_capability_from_name(name: &str) -> String {
237 let lowered = name.to_ascii_lowercase();
238 if lowered.contains("rtx 50")
239 || lowered.contains("rtx50")
240 || lowered.contains("b100")
241 || lowered.contains("b200")
242 {
243 String::from("9.0")
244 } else if lowered.contains("rtx 40") || lowered.contains("rtx40") {
245 String::from("8.9")
246 } else if lowered.contains("rtx 30") || lowered.contains("rtx30") {
247 String::from("8.6")
248 } else if lowered.contains("arc") {
249 String::from("xe")
250 } else if lowered.contains("radeon")
251 || lowered.contains("amd")
252 || lowered.contains("advanced micro")
253 || lowered.contains("rx ")
254 || lowered.contains("w7900")
255 || lowered.contains("w6800")
256 || lowered.contains("mi250")
257 || lowered.contains("mi300")
258 || lowered.contains("mi350")
259 || lowered.contains("instinct")
260 {
261 if cfg!(feature = "rocm") {
262 String::from("rocm")
263 } else {
264 String::from("gpu")
265 }
266 } else if lowered.contains("intel") || lowered.contains("iris") || lowered.contains("uhd") {
267 String::from("xe")
268 } else if lowered.contains("apple")
269 || lowered.contains("m1")
270 || lowered.contains("m2")
271 || lowered.contains("m3")
272 || lowered.contains("m4")
273 {
274 String::from("apple_metal")
275 } else {
276 String::from("gpu")
277 }
278}
279
280#[cfg(target_os = "macos")]
282pub fn is_apple_silicon() -> bool {
283 std::process::Command::new("sysctl")
284 .arg("hw.optional.arm64")
285 .output()
286 .map(|output| String::from_utf8_lossy(&output.stdout).contains("1"))
287 .unwrap_or(false)
288}
289
290#[cfg(not(target_os = "macos"))]
292pub fn is_apple_silicon() -> bool {
293 false
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn summary_includes_tuning_fields() {
302 let profile = RuntimeProfile {
303 node_resources: NodeResources::new(
304 "node-a",
305 24.0,
306 64.0,
307 "8.9",
308 Some(String::from("RTX4090")),
309 ),
310 logical_cores: 16,
311 recommended_workers: 10,
312 acceleration_mode: AccelerationMode::Gpu,
313 gpu_backend: GpuBackend::Cuda,
314 xdp_supported: true,
315 detection_source: String::from("test"),
316 probe_mode: ProbeMode::Fast,
317 };
318
319 let summary = profile.summary();
320 assert!(summary.contains("Recommended workers: 10"));
321 assert!(summary.contains("Acceleration: GPU"));
322 assert!(summary.contains("Probe mode: fast"));
323 }
324
325 #[test]
326 fn infers_compute_capability_from_gpu_name() {
327 assert_eq!(infer_compute_capability_from_name("RTX 4090"), "8.9");
328 assert_eq!(infer_compute_capability_from_name("Arc A770"), "xe");
329 #[cfg(feature = "rocm")]
330 {
331 assert_eq!(
332 infer_compute_capability_from_name("AMD Radeon RX 7900 XTX"),
333 "rocm"
334 );
335 assert_eq!(
336 infer_compute_capability_from_name("AMD Instinct MI250"),
337 "rocm"
338 );
339 }
340 #[cfg(not(feature = "rocm"))]
341 {
342 assert_eq!(
343 infer_compute_capability_from_name("AMD Radeon RX 7900 XTX"),
344 "gpu"
345 );
346 }
347 }
348
349 #[test]
350 fn fast_profile_cache_reuses_detected_shape() {
351 let first = detect_runtime_profile_with_mode("node-a", ProbeMode::Fast);
352 let second = detect_runtime_profile_with_mode("node-b", ProbeMode::Fast);
353
354 assert_eq!(first.logical_cores, second.logical_cores);
355 assert_eq!(first.acceleration_mode, second.acceleration_mode);
356 assert_eq!(second.node_resources.id, "node-b");
357 }
358}