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::common::spec::{
5    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
6    ReductionNaN, ResidencyPolicy, ShapeRequirements,
7};
8#[cfg(feature = "doc_export")]
9use crate::register_builtin_doc_text;
10use crate::{register_builtin_fusion_spec, register_builtin_gpu_spec};
11use runmat_builtins::{IntValue, StructValue, Value};
12use runmat_macros::runtime_builtin;
13
14#[cfg(feature = "doc_export")]
15pub const DOC_MD: &str = r#"---
16title: "gpuInfo"
17category: "acceleration/gpu"
18keywords: ["gpuInfo", "gpu", "device info", "accelerate", "summary"]
19summary: "Return a formatted status string that describes the active GPU provider."
20references:
21  - https://www.mathworks.com/help/parallel-computing/gpudevice.html
22gpu_support:
23  elementwise: false
24  reduction: false
25  precisions: []
26  broadcasting: "none"
27  notes: "Pure query builtin that formats provider metadata. When no provider is registered it returns `GPU[no provider]`."
28fusion:
29  elementwise: false
30  reduction: false
31  max_inputs: 1
32  constants: "inline"
33requires_feature: null
34tested:
35  unit: "builtins::acceleration::gpu::gpuinfo::tests::gpu_info_with_provider_formats_summary"
36  fallback: "builtins::acceleration::gpu::gpuinfo::tests::gpu_info_placeholder_matches_expectation"
37  doc: "builtins::acceleration::gpu::gpuinfo::tests::doc_examples_present"
38  wgpu: "builtins::acceleration::gpu::gpuinfo::tests::gpuInfo_wgpu_provider_reports_backend"
39---
40
41# What does the `gpuInfo` function do in MATLAB / RunMat?
42`gpuInfo()` returns a concise, human-readable string that summarises the active GPU acceleration
43provider. It is a convenience wrapper around [`gpuDevice`](./gpuDevice), intended for logging or
44displaying status information in the REPL and notebooks.
45
46## How does the `gpuInfo` function behave in MATLAB / RunMat?
47- Queries the same device metadata as `gpuDevice()` and formats the fields into
48  `GPU[key=value, ...]`.
49- Includes identifiers (`device_id`, `index`), descriptive strings (`name`, `vendor`, `backend`)
50  and capability hints (`precision`, `supports_double`, `memory_bytes` when available).
51- Escapes string values using MATLAB-style single quote doubling so they are display-friendly.
52- When no acceleration provider is registered, returns the placeholder string `GPU[no provider]`
53  instead of throwing an error, making it safe to call unconditionally.
54- Propagates unexpected errors (for example, if a provider fails while reporting metadata) so they
55  can be diagnosed.
56
57## GPU residency in RunMat (Do I need `gpuArray`?)
58`gpuInfo` is a pure metadata query and never changes residency. Arrays stay wherever they already
59live (GPU or CPU). Use `gpuArray`, `gather`, or RunMat Accelerate's auto-offload heuristics to move
60data between devices as needed.
61
62## Examples of using the `gpuInfo` function in MATLAB / RunMat
63
64### Displaying GPU status in the REPL
65```matlab
66disp(gpuInfo());
67```
68Expected output (in-process provider):
69```matlab
70GPU[device_id=0, index=1, name='InProcess', vendor='RunMat', backend='inprocess', precision='double', supports_double=true]
71```
72
73### Emitting a log line before a computation
74```matlab
75fprintf("Running on %s\n", gpuInfo());
76```
77Expected output:
78```matlab
79Running on GPU[device_id=0, index=1, name='InProcess', vendor='RunMat', backend='inprocess', precision='double', supports_double=true]
80```
81
82### Checking for double precision support quickly
83```matlab
84summary = gpuInfo();
85if contains(summary, "supports_double=true")
86    disp("Double precision kernels available.");
87else
88    disp("Falling back to single precision.");
89end
90```
91
92### Handling missing providers gracefully
93```matlab
94% Safe even when acceleration is disabled
95status = gpuInfo();
96if status == "GPU[no provider]"
97    warning("GPU acceleration is currently disabled.");
98end
99```
100
101### Combining `gpuInfo` with `gpuDevice` for structured data
102```matlab
103info = gpuDevice();
104summary = gpuInfo();
105if isfield(info, 'memory_bytes')
106    fprintf("%s (memory: %.2f GB)\n", summary, info.memory_bytes / 1e9);
107else
108    fprintf("%s (memory: unknown)\n", summary);
109end
110```
111Expected output (when the provider reports `memory_bytes`):
112```matlab
113GPU[device_id=0, index=1, name='InProcess', vendor='RunMat', backend='inprocess', precision='double', supports_double=true] (memory: 15.99 GB)
114```
115If the provider omits memory metadata, the fallback branch prints:
116```matlab
117GPU[device_id=0, index=1, name='InProcess', vendor='RunMat', backend='inprocess', precision='double', supports_double=true] (memory: unknown)
118```
119
120## FAQ
121
122### Does `gpuInfo` change GPU state?
123No. It only reads metadata and formats it into a string.
124
125### Will `gpuInfo` throw an error when no provider is registered?
126No. It returns `GPU[no provider]` so caller code can branch without exception handling.
127
128### How is `gpuInfo` different from `gpuDevice`?
129`gpuDevice` returns a struct that you can inspect programmatically. `gpuInfo` formats the same
130information into a single string that is convenient for logging and display.
131
132### Does the output order of fields stay stable?
133Yes. Fields are emitted in the same order as the `gpuDevice` struct: identifiers, descriptive
134strings, optional metadata, precision, and capability flags.
135
136### Are strings escaped in MATLAB style?
137Yes. Single quotes are doubled (e.g., `Ada'GPU` becomes `Ada''GPU`) so the summary can be pasted
138back into MATLAB code without breaking literal syntax.
139
140## See Also
141[gpuDevice](./gpuDevice), [gpuArray](./gpuArray), [gather](./gather)
142"#;
143
144pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
145    name: "gpuInfo",
146    op_kind: GpuOpKind::Custom("device-summary"),
147    supported_precisions: &[],
148    broadcast: BroadcastSemantics::None,
149    provider_hooks: &[],
150    constant_strategy: ConstantStrategy::InlineLiteral,
151    residency: ResidencyPolicy::GatherImmediately,
152    nan_mode: ReductionNaN::Include,
153    two_pass_threshold: None,
154    workgroup_size: None,
155    accepts_nan_mode: false,
156    notes: "Formats metadata reported by the active provider; no GPU kernels are launched.",
157};
158
159register_builtin_gpu_spec!(GPU_SPEC);
160
161pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
162    name: "gpuInfo",
163    shape: ShapeRequirements::Any,
164    constant_strategy: ConstantStrategy::InlineLiteral,
165    elementwise: None,
166    reduction: None,
167    emits_nan: false,
168    notes: "Not eligible for fusion—returns a host-resident string.",
169};
170
171register_builtin_fusion_spec!(FUSION_SPEC);
172
173#[cfg(feature = "doc_export")]
174register_builtin_doc_text!("gpuInfo", DOC_MD);
175
176#[runtime_builtin(
177    name = "gpuInfo",
178    category = "acceleration/gpu",
179    summary = "Return a formatted status string that describes the active GPU provider.",
180    keywords = "gpu,gpuInfo,device,info,accelerate",
181    examples = "disp(gpuInfo())"
182)]
183fn gpu_info_builtin() -> Result<Value, String> {
184    match active_device_struct() {
185        Ok(info) => Ok(Value::String(format_summary(Some(&info)))),
186        Err(err) if err == gpudevice::ERR_NO_PROVIDER => Ok(Value::String(format_summary(None))),
187        Err(err) => Err(err.to_string()),
188    }
189}
190
191fn format_summary(info: Option<&StructValue>) -> String {
192    match info {
193        Some(struct_value) => {
194            let mut parts = Vec::new();
195            for (key, value) in struct_value.fields.iter() {
196                if let Some(fragment) = format_field(key, value) {
197                    parts.push(fragment);
198                }
199            }
200            format!("GPU[{}]", parts.join(", "))
201        }
202        None => "GPU[no provider]".to_string(),
203    }
204}
205
206fn format_field(key: &str, value: &Value) -> Option<String> {
207    match value {
208        Value::Int(intv) => Some(format!("{key}={}", int_to_string(intv))),
209        Value::Num(n) => Some(format!("{key}={}", Value::Num(*n))),
210        Value::Bool(flag) => Some(format!("{key}={}", if *flag { "true" } else { "false" })),
211        Value::String(text) => Some(format!("{key}='{}'", escape_single_quotes(text))),
212        _ => None,
213    }
214}
215
216fn int_to_string(value: &IntValue) -> String {
217    match value {
218        IntValue::I8(v) => v.to_string(),
219        IntValue::I16(v) => v.to_string(),
220        IntValue::I32(v) => v.to_string(),
221        IntValue::I64(v) => v.to_string(),
222        IntValue::U8(v) => v.to_string(),
223        IntValue::U16(v) => v.to_string(),
224        IntValue::U32(v) => v.to_string(),
225        IntValue::U64(v) => v.to_string(),
226    }
227}
228
229fn escape_single_quotes(text: &str) -> String {
230    text.replace('\'', "''")
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use crate::builtins::common::test_support;
237    #[cfg(feature = "wgpu")]
238    use runmat_accelerate_api::AccelProvider;
239
240    #[test]
241    #[allow(non_snake_case)]
242    fn gpuInfo_with_provider_formats_summary() {
243        test_support::with_test_provider(|_| {
244            let value = gpu_info_builtin().expect("gpuInfo");
245            match value {
246                Value::String(text) => {
247                    assert!(text.starts_with("GPU["), "unexpected prefix: {text}");
248                    assert!(
249                        text.contains("name='InProcess'"),
250                        "expected provider name in: {text}"
251                    );
252                    assert!(
253                        text.contains("vendor='RunMat'"),
254                        "expected vendor in: {text}"
255                    );
256                }
257                other => panic!("expected string result, got {other:?}"),
258            }
259        });
260    }
261
262    #[test]
263    #[allow(non_snake_case)]
264    fn gpuInfo_placeholder_matches_expectation() {
265        let placeholder = format_summary(None);
266        assert_eq!(placeholder, "GPU[no provider]");
267    }
268
269    #[test]
270    #[allow(non_snake_case)]
271    fn gpuInfo_format_summary_handles_core_value_types() {
272        let mut info = StructValue::new();
273        info.insert("device_id", Value::Int(IntValue::U32(0)));
274        info.insert("index", Value::Int(IntValue::U32(1)));
275        info.insert("name", Value::String("Ada'GPU".into()));
276        info.insert("vendor", Value::String("RunMat".into()));
277        info.insert("memory_bytes", Value::Int(IntValue::U64(12)));
278        info.insert("load", Value::Num(0.5));
279        info.insert("precision", Value::String("double".into()));
280        info.insert("supports_double", Value::Bool(true));
281        info.insert("ignored", Value::Struct(StructValue::new()));
282
283        let summary = format_summary(Some(&info));
284        assert!(
285            summary.starts_with("GPU["),
286            "summary should start with GPU[: {summary}"
287        );
288        assert!(
289            summary.contains("device_id=0"),
290            "expected device_id field in: {summary}"
291        );
292        assert!(
293            summary.contains("index=1"),
294            "expected index field in: {summary}"
295        );
296        assert!(
297            summary.contains("name='Ada''GPU'"),
298            "expected escaped name in: {summary}"
299        );
300        assert!(
301            summary.contains("vendor='RunMat'"),
302            "expected vendor field in: {summary}"
303        );
304        assert!(
305            summary.contains("memory_bytes=12"),
306            "expected memory_bytes field in: {summary}"
307        );
308        assert!(
309            summary.contains("load=0.5"),
310            "expected numeric load field in: {summary}"
311        );
312        assert!(
313            summary.contains("precision='double'"),
314            "expected precision field in: {summary}"
315        );
316        assert!(
317            summary.contains("supports_double=true"),
318            "expected supports_double field in: {summary}"
319        );
320        assert!(
321            !summary.contains("ignored"),
322            "unexpected field 'ignored' present in: {summary}"
323        );
324    }
325
326    #[test]
327    #[allow(non_snake_case)]
328    #[cfg(feature = "doc_export")]
329    fn gpuInfo_doc_examples_present() {
330        let blocks = test_support::doc_examples(DOC_MD);
331        assert!(!blocks.is_empty(), "expected at least one MATLAB example");
332    }
333
334    #[test]
335    #[cfg(feature = "wgpu")]
336    #[allow(non_snake_case)]
337    fn gpuInfo_wgpu_provider_reports_backend() {
338        use runmat_accelerate::backend::wgpu::provider::{
339            register_wgpu_provider, WgpuProviderOptions,
340        };
341
342        let options = WgpuProviderOptions {
343            force_fallback_adapter: true,
344            ..Default::default()
345        };
346
347        let provider = match register_wgpu_provider(options) {
348            Ok(p) => p,
349            Err(err) => {
350                eprintln!("Skipping gpuInfo WGPU provider test: {err}");
351                return;
352            }
353        };
354
355        let value = gpu_info_builtin().expect("gpuInfo");
356        match value {
357            Value::String(text) => {
358                assert!(text.starts_with("GPU["), "unexpected prefix: {text}");
359                let info = provider.device_info_struct();
360                let runmat_accelerate_api::ApiDeviceInfo {
361                    vendor, backend, ..
362                } = info;
363                let expected_vendor = escape_single_quotes(&vendor);
364                assert!(
365                    text.contains(&format!("vendor='{}'", expected_vendor)),
366                    "expected vendor '{}' in summary: {}",
367                    vendor,
368                    text
369                );
370                if let Some(backend) = backend {
371                    if !backend.is_empty() {
372                        assert!(
373                            text.contains(&format!("backend='{}'", backend)),
374                            "expected backend '{}' in summary: {}",
375                            backend,
376                            text
377                        );
378                    }
379                }
380            }
381            other => panic!("expected string result, got {other:?}"),
382        }
383    }
384}