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