all-smi 0.24.2

Command-line utility for monitoring GPU hardware. It provides a real-time view of GPU utilization, memory usage, temperature, power consumption, and other metrics.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
// Copyright 2025 Lablup Inc. and Jeongkyu Shin
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Native Apple Silicon GPU reader using macOS native APIs
//!
//! This reader uses IOReport, SMC, and other native macOS APIs to collect
//! Apple Silicon metrics instead of the `powermetrics` command.
//!
//! ## Benefits
//! - No sudo required
//! - Lower latency
//! - More stable (no external process)
//! - Additional metrics (actual temperature, system power)

use crate::device::common::command_executor::execute_command_default;
use crate::device::macos_native::{
    NativeMetricsManager, get_native_metrics_manager, initialize_native_metrics_manager,
};
use crate::device::readers::common_cache::{DetailBuilder, DeviceStaticInfo};
use crate::device::{GpuInfo, GpuReader, ProcessInfo};
use crate::utils::get_hostname;
use chrono::Local;
use once_cell::sync::{Lazy, OnceCell};
use std::sync::{
    Arc, Mutex,
    atomic::{AtomicBool, Ordering},
};
use sysinfo::System;

// Cache GPU info to avoid expensive system_profiler calls on every initialization
static CACHED_GPU_INFO: Lazy<Mutex<Option<DeviceStaticInfo>>> = Lazy::new(|| Mutex::new(None));

// Apple Silicon specific info that needs to be cached separately
struct AppleSiliconInfo {
    gpu_core_count: Option<u32>,
}

/// Apple Silicon GPU reader using native macOS APIs
///
/// This reader uses IOReport and SMC APIs directly instead of spawning
/// the `powermetrics` command, eliminating the need for sudo.
pub struct AppleSiliconNativeGpuReader {
    static_info: OnceCell<DeviceStaticInfo>,
    apple_info: OnceCell<AppleSiliconInfo>,
    initialized: AtomicBool,
    native_manager: OnceCell<Arc<NativeMetricsManager>>,
}

impl Default for AppleSiliconNativeGpuReader {
    fn default() -> Self {
        Self::new()
    }
}

impl AppleSiliconNativeGpuReader {
    pub fn new() -> Self {
        // Initialize native metrics manager if not already done
        // Use 100ms sample interval for responsiveness
        let _ = initialize_native_metrics_manager(100);

        AppleSiliconNativeGpuReader {
            static_info: OnceCell::new(),
            apple_info: OnceCell::new(),
            initialized: AtomicBool::new(false),
            native_manager: OnceCell::new(),
        }
    }

    fn ensure_initialized(&self) {
        if self.initialized.load(Ordering::Acquire) {
            return;
        }

        // Initialize native manager reference
        if let Some(manager) = get_native_metrics_manager() {
            let _ = self.native_manager.set(manager);
        }

        // Check cache first to avoid expensive system_profiler calls
        let mut cache = match CACHED_GPU_INFO.lock() {
            Ok(guard) => guard,
            Err(e) => {
                eprintln!("Failed to acquire lock for Apple Silicon GPU cache: {e}");
                return;
            }
        };

        if let Some(static_info) = cache.as_ref() {
            // Use cached values - safe initialization via OnceCell
            let _ = self.static_info.set(static_info.clone());
            // Extract gpu_core_count from detail if present
            let gpu_core_count = static_info
                .detail
                .get("GPU Core Count")
                .and_then(|s| s.parse::<u32>().ok());
            let _ = self.apple_info.set(AppleSiliconInfo { gpu_core_count });
            self.initialized.store(true, Ordering::Release);
            return;
        }

        // If not cached, fetch the information (this is slow but only happens once)
        let (name, driver_version) = get_gpu_name_and_version();
        let gpu_core_count = get_gpu_core_count();

        // Build DeviceStaticInfo using DetailBuilder
        let mut builder = DetailBuilder::new()
            .insert("gpu_type", "Integrated")
            .insert_optional("driver_version", driver_version.as_ref());

        if let Some(count) = gpu_core_count {
            builder = builder.insert("GPU Core Count", count.to_string());
        }

        let detail = builder.build();
        let static_info = DeviceStaticInfo::with_details(name, None, detail);

        // Store in cache for future use
        *cache = Some(static_info.clone());

        // Update self - safe initialization via OnceCell
        let _ = self.static_info.set(static_info);
        let _ = self.apple_info.set(AppleSiliconInfo { gpu_core_count });
        self.initialized.store(true, Ordering::Release);
    }
}

impl GpuReader for AppleSiliconNativeGpuReader {
    fn get_gpu_info(&self) -> Vec<GpuInfo> {
        // Ensure GPU info is initialized (happens on first call)
        self.ensure_initialized();

        // Try to get metrics from native manager
        let (metrics, combined_power_mw, cpu_temp, gpu_temp) =
            if let Some(manager) = self.native_manager.get() {
                match manager.collect_once() {
                    Ok(data) => {
                        let combined_power = data.combined_power_mw;
                        (
                            GpuMetrics {
                                utilization: Some(data.gpu_active_residency),
                                ane_utilization: Some(data.ane_power_mw),
                                frequency: Some(data.gpu_frequency),
                                power_consumption: Some(data.gpu_power_mw / 1000.0),
                                thermal_pressure_level: data.thermal_pressure_level,
                            },
                            Some(combined_power),
                            data.cpu_temperature,
                            data.gpu_temperature,
                        )
                    }
                    Err(_) => (GpuMetrics::default(), None, None, None),
                }
            } else {
                (GpuMetrics::default(), None, None, None)
            };

        // Get cached static info
        let static_info = match self.static_info.get() {
            Some(info) => info,
            None => {
                // Fallback if initialization failed
                return vec![];
            }
        };
        let apple_info = self.apple_info.get();

        let mut detail = static_info.detail.clone();
        detail.insert("architecture".to_string(), "Apple Silicon".to_string());
        detail.insert("api".to_string(), "Native (IOReport/SMC)".to_string());

        if let Some(ref thermal_level) = metrics.thermal_pressure_level {
            detail.insert("thermal_pressure".to_string(), thermal_level.clone());
        }

        // Add combined power (CPU + GPU + ANE) for metrics export
        if let Some(combined_power) = combined_power_mw {
            detail.insert("combined_power_mw".to_string(), combined_power.to_string());
        }

        // Add temperature metrics from SMC
        if let Some(cpu_t) = cpu_temp {
            detail.insert("cpu_temperature".to_string(), format!("{cpu_t:.1}"));
        }
        if let Some(gpu_t) = gpu_temp {
            detail.insert("gpu_temperature".to_string(), format!("{gpu_t:.1}"));
        }

        // Add unified AI acceleration library labels
        detail.insert("lib_name".to_string(), "Metal".to_string());
        if let Some(driver_ver) = static_info.detail.get("driver_version")
            && driver_ver != "Unknown"
        {
            let lib_ver = driver_ver
                .strip_prefix("Metal ")
                .unwrap_or(driver_ver)
                .to_string();
            detail.insert("lib_version".to_string(), lib_ver);
        }

        // GPU temperature: Apple Silicon's per-die GPU thermistor keys (Tg*) are
        // not always exposed reliably across chip generations. When SMC didn't
        // return a usable value, fall back to the CPU die temperature — CPU and
        // GPU share the same SoC package so the readings are tightly correlated
        // and this is far more meaningful than reporting 0 °C.
        let temperature = gpu_temp.or(cpu_temp).map(|t| t.round() as u32).unwrap_or(0);

        vec![GpuInfo {
            uuid: static_info
                .uuid
                .clone()
                .unwrap_or_else(|| "AppleSiliconGPU".to_string()),
            time: Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
            name: static_info.name.clone(),
            device_type: "GPU".to_string(),
            host_id: get_hostname(),
            hostname: get_hostname(),
            instance: get_hostname(),
            utilization: metrics.utilization.unwrap_or(0.0),
            ane_utilization: metrics.ane_utilization.unwrap_or(0.0),
            dla_utilization: None,
            tensorcore_utilization: None,
            temperature,
            used_memory: get_used_memory(),
            total_memory: get_total_memory(),
            frequency: metrics.frequency.unwrap_or(0),
            power_consumption: metrics.power_consumption.unwrap_or(0.0),
            gpu_core_count: apple_info.and_then(|i| i.gpu_core_count),
            // Apple Silicon reports thermal pressure as a qualitative enum
            // (Nominal / Fair / Serious / Critical) via the `detail` map, not
            // as NVML-style numeric thresholds. Leave these fields empty.
            // NVIDIA-specific hardware details (NUMA, GSP firmware, NvLink,
            // GPM) do not apply to Apple Silicon either.
            temperature_threshold_slowdown: None,
            temperature_threshold_shutdown: None,
            temperature_threshold_max_operating: None,
            temperature_threshold_acoustic: None,
            performance_state: None,
            numa_node_id: None,
            gsp_firmware_mode: None,
            gsp_firmware_version: None,
            nvlink_remote_devices: Vec::new(),
            gpm_metrics: None,
            detail,
        }]
    }

    fn get_process_info(&self) -> Vec<ProcessInfo> {
        // Native APIs don't provide per-process GPU usage
        // Return empty for now - could be enhanced with Metal Performance Shaders API
        vec![]
    }
}

#[derive(Default)]
struct GpuMetrics {
    utilization: Option<f64>,
    ane_utilization: Option<f64>,
    frequency: Option<u32>,
    power_consumption: Option<f64>,
    thermal_pressure_level: Option<String>,
}

fn get_gpu_name_and_version() -> (String, Option<String>) {
    // Try to get GPU name from sysctl first (fast path for name only)
    let gpu_name = if let Ok(output) =
        execute_command_default("sysctl", &["-n", "machdep.cpu.brand_string"])
    {
        let cpu_brand = output.stdout.trim().to_string();
        if cpu_brand.contains("Apple M") {
            let mut name = None;
            for part in cpu_brand.split_whitespace() {
                if part.starts_with("M") && part.chars().nth(1).is_some_and(|c| c.is_numeric()) {
                    let mut gpu_name = format!("Apple {part} GPU");
                    let parts: Vec<&str> = cpu_brand.split_whitespace().collect();
                    if let Some(pos) = parts.iter().position(|&x| x == part)
                        && pos + 1 < parts.len()
                    {
                        let suffix = parts[pos + 1];
                        if suffix == "Pro" || suffix == "Max" || suffix == "Ultra" {
                            gpu_name = format!("Apple {part} {suffix} GPU");
                        }
                    }
                    name = Some(gpu_name);
                    break;
                }
            }
            name.unwrap_or_else(|| "Apple Silicon GPU".to_string())
        } else {
            "Apple Silicon GPU".to_string()
        }
    } else {
        "Apple Silicon GPU".to_string()
    };

    // Get Metal version from macOS version
    let metal_version = get_metal_version_from_framework();

    (gpu_name, metal_version)
}

fn get_metal_version_from_framework() -> Option<String> {
    if let Ok(output) = execute_command_default("sw_vers", &["-productVersion"]) {
        let version_str = output.stdout.trim();
        if let Some(major_version) = version_str.split('.').next()
            && let Ok(major) = major_version.parse::<u32>()
        {
            let metal_version = match major {
                26.. => "Metal 4",
                15..=25 => "Metal 3",
                14 => "Metal 3",
                13 => "Metal 3",
                12 => "Metal 2.4",
                11 => "Metal 2.3",
                _ => "Metal 2",
            };
            return Some(metal_version.to_string());
        }
    }
    Some("Metal 3".to_string())
}

fn get_gpu_core_count() -> Option<u32> {
    if let Ok(output) = execute_command_default("sysctl", &["-n", "machdep.cpu.brand_string"]) {
        let cpu_brand = output.stdout.trim().to_string();

        let core_count = match cpu_brand.as_str() {
            s if s.contains("M1 ")
                && !s.contains("Pro")
                && !s.contains("Max")
                && !s.contains("Ultra") =>
            {
                Some(8)
            }
            s if s.contains("M1 Pro") => Some(16),
            s if s.contains("M1 Max") => Some(32),
            s if s.contains("M1 Ultra") => Some(64),
            s if s.contains("M2 ")
                && !s.contains("Pro")
                && !s.contains("Max")
                && !s.contains("Ultra") =>
            {
                Some(10)
            }
            s if s.contains("M2 Pro") => Some(19),
            s if s.contains("M2 Max") => Some(38),
            s if s.contains("M2 Ultra") => Some(76),
            s if s.contains("M3 ") && !s.contains("Pro") && !s.contains("Max") => Some(10),
            s if s.contains("M3 Pro") => Some(18),
            s if s.contains("M3 Max") => Some(40),
            s if s.contains("M4 ") && !s.contains("Pro") && !s.contains("Max") => Some(10),
            s if s.contains("M4 Pro") => Some(20),
            s if s.contains("M4 Max") => Some(40),
            _ => None,
        };

        if core_count.is_some() {
            return core_count;
        }
    }

    // Fallback to ioreg
    match execute_command_default("ioreg", &["-rc", "AGXAccelerator", "-d1"]) {
        Ok(cmd_output) => parse_ioreg_gpu_cores(&cmd_output.stdout),
        Err(_) => None,
    }
}

fn parse_ioreg_gpu_cores(output_str: &str) -> Option<u32> {
    for line in output_str.lines() {
        if line.contains("\"gpu-core-count\"") {
            let parts: Vec<&str> = line.split_whitespace().collect();
            if parts.len() >= 3
                && let Ok(core_count) = parts[2].parse::<u32>()
            {
                return Some(core_count);
            }
        }
    }
    None
}

// Use a cached System instance for memory info to avoid creating new instances on every call
// Total memory is static so we only need to fetch it once
static CACHED_TOTAL_MEMORY: Lazy<u64> = Lazy::new(|| {
    let mut system = System::new();
    system.refresh_memory();
    system.total_memory()
});

fn get_total_memory() -> u64 {
    *CACHED_TOTAL_MEMORY
}

fn get_used_memory() -> u64 {
    // Use global system instance from utils for memory refresh
    crate::utils::with_global_system(|system| {
        system.refresh_memory();
        system.used_memory()
    })
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_gpu_core_count_parsing() {
        // Test known chip patterns
        let patterns = [
            ("Apple M1", Some(8)),
            ("Apple M1 Pro", Some(16)),
            ("Apple M1 Max", Some(32)),
            ("Apple M1 Ultra", Some(64)),
            ("Apple M2", Some(10)),
            ("Apple M2 Pro", Some(19)),
            ("Apple M3 Pro", Some(18)),
            ("Apple M4 Pro", Some(20)),
        ];

        for (brand, expected) in patterns {
            // We can't directly test the function without mocking sysctl
            // This test documents the expected behavior
            assert!(expected.is_some(), "Expected core count for {brand}");
        }
    }
}