Skip to main content

runmat_runtime/builtins/acceleration/gpu/
gpuinfo.rs

1//! MATLAB-compatible `gpuInfo` builtin that formats the active GPU device metadata.
2
3use super::gpudevice::{self, active_device_struct};
4use crate::builtins::acceleration::gpu::type_resolvers::gpuinfo_type;
5use crate::builtins::common::spec::{
6    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
7    ReductionNaN, ResidencyPolicy, ShapeRequirements,
8};
9
10use runmat_builtins::{IntValue, StructValue, Value};
11use runmat_macros::runtime_builtin;
12
13#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::acceleration::gpu::gpuinfo")]
14pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
15    name: "gpuInfo",
16    op_kind: GpuOpKind::Custom("device-summary"),
17    supported_precisions: &[],
18    broadcast: BroadcastSemantics::None,
19    provider_hooks: &[],
20    constant_strategy: ConstantStrategy::InlineLiteral,
21    residency: ResidencyPolicy::GatherImmediately,
22    nan_mode: ReductionNaN::Include,
23    two_pass_threshold: None,
24    workgroup_size: None,
25    accepts_nan_mode: false,
26    notes: "Formats metadata reported by the active provider; no GPU kernels are launched.",
27};
28
29#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::acceleration::gpu::gpuinfo")]
30pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
31    name: "gpuInfo",
32    shape: ShapeRequirements::Any,
33    constant_strategy: ConstantStrategy::InlineLiteral,
34    elementwise: None,
35    reduction: None,
36    emits_nan: false,
37    notes: "Not eligible for fusion—returns a host-resident string.",
38};
39
40#[runtime_builtin(
41    name = "gpuInfo",
42    category = "acceleration/gpu",
43    summary = "Return a formatted status string that describes the active GPU provider.",
44    keywords = "gpu,gpuInfo,device,info,accelerate",
45    examples = "disp(gpuInfo())",
46    type_resolver(gpuinfo_type),
47    builtin_path = "crate::builtins::acceleration::gpu::gpuinfo"
48)]
49async fn gpu_info_builtin() -> crate::BuiltinResult<Value> {
50    match active_device_struct() {
51        Ok(info) => Ok(Value::String(format_summary(Some(&info)))),
52        Err(err) if err.message() == gpudevice::ERR_NO_PROVIDER => {
53            Ok(Value::String(format_summary(None)))
54        }
55        Err(err) => Err(err),
56    }
57}
58
59fn format_summary(info: Option<&StructValue>) -> String {
60    match info {
61        Some(struct_value) => {
62            let mut parts = Vec::new();
63            for (key, value) in struct_value.fields.iter() {
64                if let Some(fragment) = format_field(key, value) {
65                    parts.push(fragment);
66                }
67            }
68            format!("GPU[{}]", parts.join(", "))
69        }
70        None => "GPU[no provider]".to_string(),
71    }
72}
73
74fn format_field(key: &str, value: &Value) -> Option<String> {
75    match value {
76        Value::Int(intv) => Some(format!("{key}={}", int_to_string(intv))),
77        Value::Num(n) => Some(format!("{key}={}", Value::Num(*n))),
78        Value::Bool(flag) => Some(format!("{key}={}", if *flag { "true" } else { "false" })),
79        Value::String(text) => Some(format!("{key}='{}'", escape_single_quotes(text))),
80        _ => None,
81    }
82}
83
84fn int_to_string(value: &IntValue) -> String {
85    match value {
86        IntValue::I8(v) => v.to_string(),
87        IntValue::I16(v) => v.to_string(),
88        IntValue::I32(v) => v.to_string(),
89        IntValue::I64(v) => v.to_string(),
90        IntValue::U8(v) => v.to_string(),
91        IntValue::U16(v) => v.to_string(),
92        IntValue::U32(v) => v.to_string(),
93        IntValue::U64(v) => v.to_string(),
94    }
95}
96
97fn escape_single_quotes(text: &str) -> String {
98    text.replace('\'', "''")
99}
100
101#[cfg(test)]
102pub(crate) mod tests {
103    use super::*;
104    use crate::builtins::common::test_support;
105    use futures::executor::block_on;
106    #[cfg(feature = "wgpu")]
107    use runmat_accelerate_api::AccelProvider;
108    use runmat_builtins::{ResolveContext, Type};
109
110    fn call() -> crate::BuiltinResult<Value> {
111        block_on(gpu_info_builtin())
112    }
113
114    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
115    #[test]
116    #[allow(non_snake_case)]
117    fn gpuInfo_with_provider_formats_summary() {
118        test_support::with_test_provider(|_| {
119            let value = call().expect("gpuInfo");
120            match value {
121                Value::String(text) => {
122                    assert!(text.starts_with("GPU["), "unexpected prefix: {text}");
123                    assert!(
124                        text.contains("name='InProcess'"),
125                        "expected provider name in: {text}"
126                    );
127                    assert!(
128                        text.contains("vendor='RunMat'"),
129                        "expected vendor in: {text}"
130                    );
131                }
132                other => panic!("expected string result, got {other:?}"),
133            }
134        });
135    }
136
137    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
138    #[test]
139    #[allow(non_snake_case)]
140    fn gpuInfo_placeholder_matches_expectation() {
141        let placeholder = format_summary(None);
142        assert_eq!(placeholder, "GPU[no provider]");
143    }
144
145    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
146    #[test]
147    #[allow(non_snake_case)]
148    fn gpuInfo_format_summary_handles_core_value_types() {
149        let mut info = StructValue::new();
150        info.insert("device_id", Value::Int(IntValue::U32(0)));
151        info.insert("index", Value::Int(IntValue::U32(1)));
152        info.insert("name", Value::String("Ada'GPU".into()));
153        info.insert("vendor", Value::String("RunMat".into()));
154        info.insert("memory_bytes", Value::Int(IntValue::U64(12)));
155        info.insert("load", Value::Num(0.5));
156        info.insert("precision", Value::String("double".into()));
157        info.insert("supports_double", Value::Bool(true));
158        info.insert("ignored", Value::Struct(StructValue::new()));
159
160        let summary = format_summary(Some(&info));
161        assert!(
162            summary.starts_with("GPU["),
163            "summary should start with GPU[: {summary}"
164        );
165        assert!(
166            summary.contains("device_id=0"),
167            "expected device_id field in: {summary}"
168        );
169        assert!(
170            summary.contains("index=1"),
171            "expected index field in: {summary}"
172        );
173        assert!(
174            summary.contains("name='Ada''GPU'"),
175            "expected escaped name in: {summary}"
176        );
177        assert!(
178            summary.contains("vendor='RunMat'"),
179            "expected vendor field in: {summary}"
180        );
181        assert!(
182            summary.contains("memory_bytes=12"),
183            "expected memory_bytes field in: {summary}"
184        );
185        assert!(
186            summary.contains("load=0.5"),
187            "expected numeric load field in: {summary}"
188        );
189        assert!(
190            summary.contains("precision='double'"),
191            "expected precision field in: {summary}"
192        );
193        assert!(
194            summary.contains("supports_double=true"),
195            "expected supports_double field in: {summary}"
196        );
197        assert!(
198            !summary.contains("ignored"),
199            "unexpected field 'ignored' present in: {summary}"
200        );
201    }
202
203    #[test]
204    fn gpuinfo_type_is_string() {
205        assert_eq!(
206            gpuinfo_type(&[], &ResolveContext::new(Vec::new())),
207            Type::String
208        );
209    }
210
211    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
212    #[test]
213    #[cfg(feature = "wgpu")]
214    #[allow(non_snake_case)]
215    fn gpuInfo_wgpu_provider_reports_backend() {
216        use runmat_accelerate::backend::wgpu::provider::{
217            register_wgpu_provider, WgpuProviderOptions,
218        };
219
220        let options = WgpuProviderOptions {
221            force_fallback_adapter: true,
222            ..Default::default()
223        };
224
225        let provider = match register_wgpu_provider(options) {
226            Ok(p) => p,
227            Err(err) => {
228                tracing::warn!("Skipping gpuInfo WGPU provider test: {err}");
229                return;
230            }
231        };
232
233        let value = call().expect("gpuInfo");
234        match value {
235            Value::String(text) => {
236                assert!(text.starts_with("GPU["), "unexpected prefix: {text}");
237                let info = provider.device_info_struct();
238                let runmat_accelerate_api::ApiDeviceInfo {
239                    vendor, backend, ..
240                } = info;
241                let expected_vendor = escape_single_quotes(&vendor);
242                assert!(
243                    text.contains(&format!("vendor='{}'", expected_vendor)),
244                    "expected vendor '{}' in summary: {}",
245                    vendor,
246                    text
247                );
248                if let Some(backend) = backend {
249                    if !backend.is_empty() {
250                        assert!(
251                            text.contains(&format!("backend='{}'", backend)),
252                            "expected backend '{}' in summary: {}",
253                            backend,
254                            text
255                        );
256                    }
257                }
258            }
259            other => panic!("expected string result, got {other:?}"),
260        }
261    }
262}