Skip to main content

ghostlink_core/
host.rs

1//! Host resource detection and runtime auto-tuning.
2//!
3//! This module detects the local machine's available resources and derives a
4//! conservative runtime profile for worker counts and acceleration strategy.
5//!
6//! Most detection logic now delegates to [`crate::system_profile::SystemProfile`].
7
8use 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/// Selected acceleration path for the current host.
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
19pub enum AccelerationMode {
20    /// GPU-backed execution is available.
21    #[default]
22    Gpu,
23    /// AVX-512 optimized CPU path is preferred.
24    Avx512,
25    /// AVX2 optimized CPU path is preferred.
26    Avx2,
27    /// ARM NEON optimized CPU path is preferred.
28    Neon,
29    /// Generic scalar fallback.
30    Generic,
31}
32
33/// Backend technology hint for the detected accelerator path.
34#[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    /// Human-readable backend label.
47    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/// Hardware probe mode.
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum ProbeMode {
63    /// Fast path: env, sysfs, and visible-device hints only.
64    Fast,
65    /// Full path: includes cached external probes such as `nvidia-smi` and `lspci`.
66    Full,
67}
68
69impl ProbeMode {
70    /// Human-readable label.
71    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    /// Human-readable label for display.
81    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/// Auto-tuned runtime profile for the local host.
93#[derive(Clone, Debug)]
94pub struct RuntimeProfile {
95    /// Node resources advertised to the cluster.
96    pub node_resources: NodeResources,
97    /// Logical CPU cores detected on the host.
98    pub logical_cores: usize,
99    /// Recommended worker count for network and scheduling tasks.
100    pub recommended_workers: usize,
101    /// Preferred acceleration path.
102    pub acceleration_mode: AccelerationMode,
103    /// Detected GPU backend technology.
104    pub gpu_backend: GpuBackend,
105    /// Whether AF_XDP can plausibly be used on this host.
106    pub xdp_supported: bool,
107    /// Best-effort hardware probe source.
108    pub detection_source: String,
109    /// Probe mode used to build this profile.
110    pub probe_mode: ProbeMode,
111}
112
113impl RuntimeProfile {
114    /// Generate a display-oriented summary.
115    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
164/// Detect the local runtime profile for the host.
165pub fn detect_runtime_profile(node_id: impl Into<String>) -> RuntimeProfile {
166    detect_runtime_profile_with_mode(node_id, ProbeMode::Fast)
167}
168
169/// Detect the local runtime profile for the host using a specific probe mode.
170///
171/// Delegates to `SystemProfile` for unified detection logic.
172pub 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
196/// Detect the current host's node resources.
197pub fn detect_local_node_resources(node_id: impl Into<String>) -> NodeResources {
198    detect_runtime_profile(node_id).node_resources
199}
200
201/// Detect the local runtime profile using the full probe path.
202pub 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
235/// Infer GPU compute capability from its marketing name.
236pub 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/// Whether the current host is Apple Silicon (ARM64 Mac).
281#[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/// Whether the current host is Apple Silicon (ARM64 Mac).
291#[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}