1use ferrum_types::{Device, FerrumError, Result};
7use parking_lot::RwLock;
8use std::collections::HashMap;
9use tracing::{info, warn};
10
11#[derive(Debug, Clone)]
13pub struct DeviceCapability {
14 pub compute_capability: (u32, u32),
16 pub total_memory: usize,
18 pub num_compute_units: u32,
20 pub max_threads_per_block: u32,
22 pub warp_size: u32,
24 pub has_tensor_cores: bool,
26 pub unified_memory: bool,
28 pub max_shared_memory: usize,
30 pub memory_bandwidth: f32,
32 pub peak_tflops: f32,
34}
35
36impl Default for DeviceCapability {
37 fn default() -> Self {
38 Self {
39 compute_capability: (0, 0),
40 total_memory: 0,
41 num_compute_units: 0,
42 max_threads_per_block: 1024,
43 warp_size: 32,
44 has_tensor_cores: false,
45 unified_memory: false,
46 max_shared_memory: 48 * 1024, memory_bandwidth: 0.0,
48 peak_tflops: 0.0,
49 }
50 }
51}
52
53impl DeviceCapability {
54 pub fn cpu() -> Self {
56 Self {
57 compute_capability: (0, 0),
58 total_memory: 0, num_compute_units: 0, max_threads_per_block: 1,
61 warp_size: 1,
62 has_tensor_cores: false,
63 unified_memory: true,
64 max_shared_memory: 0,
65 memory_bandwidth: 0.0,
66 peak_tflops: 0.0,
67 }
68 }
69
70 pub fn apple_silicon(total_memory: usize, gpu_cores: u32) -> Self {
72 Self {
73 compute_capability: (1, 0), total_memory,
75 num_compute_units: gpu_cores,
76 max_threads_per_block: 1024,
77 warp_size: 32, has_tensor_cores: false, unified_memory: true,
80 max_shared_memory: 32 * 1024,
81 memory_bandwidth: 200.0, peak_tflops: 10.0, }
84 }
85
86 pub fn can_fit_model(&self, model_size_bytes: usize) -> bool {
88 let usable_memory = self.total_memory * 70 / 100; model_size_bytes <= usable_memory
91 }
92}
93
94#[derive(Debug, Clone)]
96pub struct DeviceInfo {
97 pub device: Device,
99 pub name: String,
101 pub capability: DeviceCapability,
103 pub used_memory: usize,
105 pub is_available: bool,
107 pub utilization: f32,
109}
110
111impl DeviceInfo {
112 pub fn cpu() -> Self {
114 Self {
115 device: Device::CPU,
116 name: "CPU".to_string(),
117 capability: DeviceCapability::cpu(),
118 used_memory: 0,
119 is_available: true,
120 utilization: 0.0,
121 }
122 }
123
124 pub fn available_memory(&self) -> usize {
126 self.capability
127 .total_memory
128 .saturating_sub(self.used_memory)
129 }
130}
131
132pub struct DeviceManager {
134 devices: RwLock<HashMap<Device, DeviceInfo>>,
136 primary_device: RwLock<Device>,
138}
139
140impl DeviceManager {
141 pub fn new() -> Self {
143 Self {
144 devices: RwLock::new(HashMap::new()),
145 primary_device: RwLock::new(Device::CPU),
146 }
147 }
148
149 pub fn discover_devices(&self) -> Result<()> {
151 info!("Discovering available devices...");
152 let mut devices = self.devices.write();
153 devices.clear();
154
155 let cpu_info = DeviceInfo::cpu();
157 devices.insert(Device::CPU, cpu_info);
158
159 #[cfg(all(feature = "metal", any(target_os = "macos", target_os = "ios")))]
161 {
162 if let Ok(metal_info) = self.detect_metal_devices() {
163 for info in metal_info {
164 info!("Found Metal device: {}", info.name);
165 *self.primary_device.write() = info.device.clone();
166 devices.insert(info.device.clone(), info);
167 }
168 }
169 }
170
171 #[cfg(feature = "cuda")]
173 {
174 if let Ok(cuda_info) = self.detect_cuda_devices() {
175 for (idx, info) in cuda_info.into_iter().enumerate() {
176 info!("Found CUDA device {}: {}", idx, info.name);
177 if idx == 0 {
178 *self.primary_device.write() = info.device.clone();
179 }
180 devices.insert(info.device.clone(), info);
181 }
182 }
183 }
184
185 info!("Discovered {} device(s)", devices.len());
186 Ok(())
187 }
188
189 pub fn get_devices(&self) -> Vec<DeviceInfo> {
191 self.devices.read().values().cloned().collect()
192 }
193
194 pub fn get_device(&self, device: &Device) -> Option<DeviceInfo> {
196 self.devices.read().get(device).cloned()
197 }
198
199 pub fn primary_device(&self) -> Device {
201 self.primary_device.read().clone()
202 }
203
204 pub fn set_primary_device(&self, device: Device) -> Result<()> {
206 if self.devices.read().contains_key(&device) {
207 *self.primary_device.write() = device;
208 Ok(())
209 } else {
210 Err(FerrumError::not_found(format!(
211 "Device {:?} not found",
212 device
213 )))
214 }
215 }
216
217 pub fn get_gpu_devices(&self) -> Vec<DeviceInfo> {
219 self.devices
220 .read()
221 .values()
222 .filter(|info| Self::is_gpu_device(&info.device))
223 .cloned()
224 .collect()
225 }
226
227 pub fn total_gpu_memory(&self) -> usize {
229 self.devices
230 .read()
231 .values()
232 .filter(|info| Self::is_gpu_device(&info.device))
233 .map(|info| info.capability.total_memory)
234 .sum()
235 }
236
237 fn is_gpu_device(device: &Device) -> bool {
239 match device {
240 Device::CPU => false,
241 Device::CUDA(_) => true,
242 Device::ROCm(_) => true,
243 #[cfg(any(target_os = "macos", target_os = "ios"))]
244 Device::Metal => true,
245 }
246 }
247
248 pub fn update_memory_usage(&self, device: &Device, used_bytes: usize) {
250 if let Some(info) = self.devices.write().get_mut(device) {
251 info.used_memory = used_bytes;
252 }
253 }
254
255 pub fn find_devices_for_model(&self, model_size_bytes: usize) -> Vec<DeviceInfo> {
257 self.devices
258 .read()
259 .values()
260 .filter(|info| info.capability.can_fit_model(model_size_bytes))
261 .cloned()
262 .collect()
263 }
264
265 pub fn select_devices_for_parallelism(
267 &self,
268 model_size_bytes: usize,
269 min_devices: usize,
270 max_devices: usize,
271 ) -> Vec<DeviceInfo> {
272 let gpu_devices: Vec<_> = self.get_gpu_devices();
273
274 let fitting_devices: Vec<_> = gpu_devices
276 .iter()
277 .filter(|d| d.capability.can_fit_model(model_size_bytes))
278 .cloned()
279 .collect();
280
281 if fitting_devices.len() >= min_devices {
282 return fitting_devices.into_iter().take(max_devices).collect();
283 }
284
285 let mut sorted_devices = gpu_devices;
287 sorted_devices.sort_by(|a, b| b.capability.total_memory.cmp(&a.capability.total_memory));
288
289 let mut selected = Vec::new();
290 let mut total_memory = 0usize;
291
292 for device in sorted_devices {
293 if selected.len() >= max_devices {
294 break;
295 }
296 total_memory += device.capability.total_memory;
297 selected.push(device);
298
299 let required = model_size_bytes + model_size_bytes / 4; if total_memory >= required && selected.len() >= min_devices {
302 break;
303 }
304 }
305
306 if selected.is_empty() {
308 warn!("No suitable GPUs found, falling back to CPU");
309 if let Some(cpu) = self.get_device(&Device::CPU) {
310 selected.push(cpu);
311 }
312 }
313
314 selected
315 }
316
317 #[cfg(all(feature = "metal", any(target_os = "macos", target_os = "ios")))]
318 fn detect_metal_devices(&self) -> Result<Vec<DeviceInfo>> {
319 Ok(vec![DeviceInfo {
322 device: Device::Metal,
323 name: "Apple GPU".to_string(),
324 capability: DeviceCapability::apple_silicon(16 * 1024 * 1024 * 1024, 76), used_memory: 0,
326 is_available: true,
327 utilization: 0.0,
328 }])
329 }
330
331 #[cfg(feature = "cuda")]
332 fn detect_cuda_devices(&self) -> Result<Vec<DeviceInfo>> {
333 let count = ferrum_kernels::cuda_device_count()
334 .map_err(|error| FerrumError::device(format!("CUDA probe failed: {error}")))?;
335 if count == 0 {
336 Err(FerrumError::unsupported("No CUDA devices found"))
337 } else {
338 Ok((0..count)
339 .map(|idx| DeviceInfo {
340 device: Device::CUDA(idx),
341 name: ferrum_kernels::cuda_device_name(idx)
342 .unwrap_or_else(|_| format!("CUDA Device {idx}")),
343 capability: DeviceCapability {
344 compute_capability: (0, 0),
345 total_memory: 0,
346 unified_memory: false,
347 ..DeviceCapability::default()
348 },
349 used_memory: 0,
350 is_available: true,
351 utilization: 0.0,
352 })
353 .collect())
354 }
355 }
356}
357
358impl Default for DeviceManager {
359 fn default() -> Self {
360 let manager = Self::new();
361 let _ = manager.discover_devices();
362 manager
363 }
364}
365
366static GLOBAL_DEVICE_MANAGER: std::sync::OnceLock<DeviceManager> = std::sync::OnceLock::new();
368
369pub fn global_device_manager() -> &'static DeviceManager {
371 GLOBAL_DEVICE_MANAGER.get_or_init(DeviceManager::default)
372}
373
374#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn test_device_capability_default() {
384 let cap = DeviceCapability::default();
385 assert_eq!(cap.compute_capability, (0, 0));
386 assert!(!cap.has_tensor_cores);
387 }
388
389 #[test]
390 fn test_device_info_cpu() {
391 let info = DeviceInfo::cpu();
392 assert_eq!(info.device, Device::CPU);
393 assert!(info.is_available);
394 }
395
396 #[test]
397 fn test_device_manager_discover() {
398 let manager = DeviceManager::new();
399 manager.discover_devices().unwrap();
400
401 let devices = manager.get_devices();
402 assert!(!devices.is_empty()); }
404
405 #[test]
406 fn test_can_fit_model() {
407 let mut cap = DeviceCapability::default();
408 cap.total_memory = 16 * 1024 * 1024 * 1024; assert!(cap.can_fit_model(10 * 1024 * 1024 * 1024)); assert!(!cap.can_fit_model(15 * 1024 * 1024 * 1024)); }
413}