Skip to main content

llm_manager/backend/
hardware.rs

1use std::fs;
2use std::path::Path;
3
4/// Detected operating system platform.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Platform {
7    Linux,
8    Windows,
9    Macos,
10}
11
12/// GPU vendors
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum GpuVendor {
15    Amd,
16    Nvidia,
17    Intel,
18    Unknown,
19}
20
21/// Detect the current operating system platform.
22pub fn detect_platform() -> Platform {
23    match std::env::consts::OS {
24        "windows" => Platform::Windows,
25        "macos" => Platform::Macos,
26        _ => Platform::Linux,
27    }
28}
29
30/// Check if the current architecture is ARM64.
31pub fn is_arm64() -> bool {
32    cfg!(target_arch = "aarch64")
33}
34
35/// Get the platform as a string slice.
36pub fn platform_name(platform: Platform) -> &'static str {
37    match platform {
38        Platform::Linux => "linux",
39        Platform::Windows => "windows",
40        Platform::Macos => "macos",
41    }
42}
43
44/// Check if a backend variant is available on the given platform.
45pub fn backend_supported(backend: crate::models::Backend, platform: Platform) -> bool {
46    match platform {
47        Platform::Linux => backend.is_linux(),
48        Platform::Windows => backend.is_windows(),
49        Platform::Macos => backend.is_macos(),
50    }
51}
52
53/// Returns paths to all primary DRM card directories (card0, card1, ...).
54fn drm_card_paths() -> Vec<std::path::PathBuf> {
55    let drm_path = Path::new("/sys/class/drm");
56    if !drm_path.exists() {
57        return Vec::new();
58    }
59    fs::read_dir(drm_path)
60        .map(|entries| {
61            entries
62                .flatten()
63                .filter(|e| {
64                    let n = e.file_name();
65                    let s = n.to_string_lossy();
66                    s.starts_with("card") && !s.contains('-')
67                })
68                .map(|e| e.path())
69                .collect()
70        })
71        .unwrap_or_default()
72}
73
74/// Detect all GPU vendors by scanning /sys/class/drm/card*/device/vendor.
75/// Returns a Vec of unique vendors (preserves detection order, deduplicates).
76pub fn detect_gpu_vendors() -> Vec<GpuVendor> {
77    let mut vendors = Vec::new();
78    for card_path in drm_card_paths() {
79        let vendor_path = card_path.join("device/vendor");
80        if let Ok(vendor_id) = fs::read_to_string(vendor_path) {
81            let vendor_id = vendor_id.trim();
82            let vendor = match vendor_id {
83                "0x1002" => GpuVendor::Amd,
84                "0x10de" => GpuVendor::Nvidia,
85                "0x8086" => GpuVendor::Intel,
86                _ => continue,
87            };
88            if !vendors.contains(&vendor) {
89                vendors.push(vendor);
90            }
91        }
92    }
93
94    if vendors.is_empty() {
95        vendors.push(GpuVendor::Unknown);
96    }
97
98    vendors
99}
100
101/// Detect all GPU model names (one per GPU).
102/// For AMD GPUs, includes the GFX target version.
103pub fn detect_gpu_models() -> Vec<Option<String>> {
104    let card_paths = drm_card_paths();
105    if card_paths.is_empty() {
106        return Vec::new();
107    }
108
109    let amd_gfx_targets = detect_amd_gfx_targets();
110    let mut amd_card_idx: usize = 0;
111    let mut models = Vec::new();
112    for card_path in &card_paths {
113        let vendor_path = card_path.join("device/vendor");
114        if let Ok(vendor_id) = fs::read_to_string(vendor_path) {
115            let vendor_id = vendor_id.trim();
116            let vendor = match vendor_id {
117                "0x1002" => GpuVendor::Amd,
118                "0x10de" => GpuVendor::Nvidia,
119                "0x8086" => GpuVendor::Intel,
120                _ => continue,
121            };
122
123            let vendor_name = match vendor {
124                GpuVendor::Amd => "AMD",
125                GpuVendor::Nvidia => "NVIDIA",
126                GpuVendor::Intel => "Intel",
127                GpuVendor::Unknown => continue,
128            };
129
130            if vendor == GpuVendor::Amd {
131                if let Some(gfx) = amd_gfx_targets.get(amd_card_idx % amd_gfx_targets.len()) {
132                    models.push(Some(format!("{} ({})", vendor_name, gfx)));
133                } else {
134                    models.push(Some(vendor_name.to_string()));
135                }
136                amd_card_idx += 1;
137            } else {
138                models.push(Some(vendor_name.to_string()));
139            }
140        }
141    }
142
143    models
144}
145
146/// Format a raw GFX target version value to a string (e.g. 110003 -> "gfx1103").
147/// Returns None for value 0 (CPU node).
148fn gfx_target_to_string(val: u32) -> Option<String> {
149    if val == 0 {
150        return None;
151    }
152    let major = val / 10000;
153    let minor = (val % 10000) / 100;
154    let stepping = val % 100;
155
156    if stepping > 0 {
157        Some(format!("gfx{}{}{}", major, minor, stepping))
158    } else {
159        Some(format!("gfx{}{}", major, minor))
160    }
161}
162
163/// Collect all unique, non-zero AMD GFX target versions from KFD nodes.
164/// Skips CPU nodes (gfx_target_version == 0).
165/// Returns deduplicated targets in detection order.
166pub fn detect_amd_gfx_targets() -> Vec<String> {
167    let kfd_path = Path::new("/sys/class/kfd/kfd/topology/nodes");
168    if !kfd_path.exists() {
169        return Vec::new();
170    }
171
172    let mut targets = Vec::new();
173    if let Ok(entries) = fs::read_dir(kfd_path) {
174        for entry in entries.flatten() {
175            let props_path = entry.path().join("properties");
176            if let Ok(props) = fs::read_to_string(props_path) {
177                for line in props.lines() {
178                    if line.starts_with("gfx_target_version")
179                        && let Some(val_str) = line.split_whitespace().last()
180                            && let Ok(val) = val_str.parse::<u32>()
181                            && let Some(gfx) = gfx_target_to_string(val) {
182                                if !targets.contains(&gfx) {
183                                    targets.push(gfx);
184                                }
185                                break;
186                            }
187                }
188            }
189        }
190    }
191    targets
192}
193
194/// Detect AMD GFX target version (e.g. "gfx1100").
195/// Returns the first non-zero GFX target found, or None.
196pub fn detect_amd_gfx_target() -> Option<String> {
197    detect_amd_gfx_targets().into_iter().next()
198}
199
200/// Get the best Lemonade asset suffix for the detected AMD architecture
201pub fn get_lemonade_gfx_suffix(gfx: &str) -> &'static str {
202    if gfx.starts_with("gfx103") {
203        "gfx103X"
204    } else if gfx.starts_with("gfx110") {
205        "gfx110X"
206    } else if gfx == "gfx1150" {
207        "gfx1150"
208    } else if gfx == "gfx1151" {
209        "gfx1151"
210    } else if gfx.starts_with("gfx120") {
211        "gfx120X"
212    } else {
213        // Fallback to most common recent if unknown
214        "gfx110X"
215    }
216}