Skip to main content

flodl_cli/
diagnose.rs

1//! `fdl diagnose` -- system and GPU diagnostics.
2//!
3//! Thin formatting layer over `util::system` and `libtorch::detect`.
4
5use std::fmt::Write;
6use std::path::Path;
7
8use crate::context::Context;
9use crate::libtorch::detect;
10use crate::util::system;
11
12pub fn run(json: bool) {
13    let ctx = Context::resolve();
14    let root = &ctx.root;
15    if json {
16        print_json(root, &ctx);
17    } else {
18        print_report(root, &ctx);
19    }
20}
21
22// ---------------------------------------------------------------------------
23// Human-readable report
24// ---------------------------------------------------------------------------
25
26fn print_report(root: &Path, ctx: &Context) {
27    println!("floDl Diagnostics");
28    println!("=================");
29    println!();
30
31    // Context
32    println!("Context:       {}", ctx.label());
33    println!();
34
35    // System
36    println!("System");
37    let cpu = system::cpu_model().unwrap_or_else(|| "Unknown".into());
38    let threads = system::cpu_threads();
39    let ram_gb = system::ram_total_gb();
40    println!(
41        "  CPU:         {} ({} threads, {}GB RAM)",
42        cpu, threads, ram_gb
43    );
44    if let Some(os) = system::os_version() {
45        println!("  OS:          {}", os);
46    }
47    if system::is_inside_docker() {
48        println!("  Docker:      yes (running inside container)");
49    } else {
50        match system::docker_version() {
51            Some(v) => println!("  Docker:      {}", v),
52            None => println!("  Docker:      not found"),
53        }
54    }
55    println!();
56
57    // GPU
58    //
59    // The full sweep, not just its device list: an empty list has
60    // several causes needing different answers, and the one that matters
61    // most here has NO device at all -- an AMD card physically present
62    // with no ROCm userspace installed. `fdl probe` already prints the
63    // sweep's findings; diagnose is the command users reach for first.
64    println!("GPU");
65    let sweep = flodl_hw::survey();
66    let devices = &sweep.devices;
67    if !devices.is_empty() {
68        if sweep.has_vendor(system::GpuVendor::Nvidia)
69            && let Some(driver) = system::nvidia_driver_version()
70        {
71            println!("  NVIDIA driver: {}", driver);
72        }
73        println!("  Devices:     {}", devices.len());
74        for d in devices {
75            let vram_gb = d.total_memory_mb / 1024;
76            println!(
77                "  [{}] {} -- {}, {}, {}GB VRAM",
78                d.index,
79                d.name,
80                d.vendor,
81                d.arch_label(),
82                vram_gb
83            );
84        }
85    } else {
86        println!("  No GPU devices available");
87    }
88    for note in &sweep.notes {
89        println!("  Note:        {}", note);
90    }
91    println!();
92
93    // libtorch
94    println!("libtorch");
95    match detect::read_active(root) {
96        Some(info) => {
97            println!("  Active:      {}", info.path);
98            if let Some(v) = &info.torch_version {
99                println!("  Version:     {}", v);
100            }
101            // Which stack this build targets is the variant path's to
102            // tell. `.arch`'s `cuda=` is a CUDA TOOLKIT version, which a
103            // ROCm build does not have and writes as `none` exactly like
104            // a CPU build -- and "CUDA: none" under a working AMD install
105            // reads as a broken CUDA rather than a healthy ROCm.
106            match detect::variant_vendor(&info.path) {
107                Some(v) => println!("  Vendor:      {}", v),
108                None => println!("  Vendor:      CPU-only"),
109            }
110            if let Some(c) = info.cuda_version.as_deref().filter(|c| *c != "none") {
111                println!("  CUDA:        {}", c);
112            }
113            if let Some(a) = &info.archs {
114                println!("  Archs:       {}", a);
115            }
116            if let Some(s) = &info.source {
117                println!("  Source:      {}", s);
118            }
119        }
120        None => {
121            println!("  No active variant (run `fdl setup`)");
122        }
123    }
124
125    let variants = detect::list_variants(root);
126    if !variants.is_empty() {
127        println!("  Variants:    {}", variants.join(", "));
128    }
129    println!();
130
131    // Compatibility
132    if !devices.is_empty() {
133        println!("Compatibility");
134        if let Some(info) = detect::read_active(root) {
135            let archs = info.archs.as_deref().unwrap_or("");
136            let mut all_ok = true;
137            for d in devices {
138                if d.covered_by(archs) {
139                    println!(
140                        "  GPU {} ({}, {}):  OK",
141                        d.index,
142                        d.short_name(),
143                        d.arch_label()
144                    );
145                } else {
146                    all_ok = false;
147                    println!(
148                        "  GPU {} ({}, {}):  MISSING -- arch {} not in [{}]",
149                        d.index,
150                        d.short_name(),
151                        d.arch_label(),
152                        // The archs= spelling, not the display one: this
153                        // names the token the user must add to the list.
154                        d.arch.archs_token(),
155                        archs
156                    );
157                }
158            }
159            if all_ok {
160                println!();
161                println!("  All GPUs compatible with active libtorch.");
162            }
163        } else {
164            println!("  Cannot check -- no active libtorch variant.");
165        }
166        println!();
167    }
168}
169
170// ---------------------------------------------------------------------------
171// JSON output
172// ---------------------------------------------------------------------------
173
174fn print_json(root: &Path, ctx: &Context) {
175    let mut b = String::with_capacity(2048);
176    b.push('{');
177
178    // Context
179    let _ = write!(
180        b,
181        "\"context\":{{\"mode\":\"{}\",\"root\":\"{}\"}}",
182        if ctx.is_project { "project" } else { "global" },
183        system::escape_json(&ctx.root.display().to_string())
184    );
185
186    // System
187    let cpu = system::cpu_model().unwrap_or_else(|| "Unknown".into());
188    let _ = write!(
189        b,
190        ",\"system\":{{\"cpu\":\"{}\",\"threads\":{},\"ram_gb\":{}",
191        system::escape_json(&cpu),
192        system::cpu_threads(),
193        system::ram_total_gb()
194    );
195    if let Some(os) = system::os_version() {
196        let _ = write!(b, ",\"os\":\"{}\"", system::escape_json(&os));
197    }
198    if system::is_inside_docker() {
199        b.push_str(",\"docker\":\"container\"");
200    } else if let Some(docker) = system::docker_version() {
201        let _ = write!(b, ",\"docker\":\"{}\"", system::escape_json(&docker));
202    }
203    b.push('}');
204
205    // GPUs. From the full sweep, like the human report: a scripted
206    // consumer needs the findings more than a human does, since it has
207    // no other way to tell "no GPU here" from "an AMD card is present
208    // and its userspace is missing".
209    let sweep = flodl_hw::survey();
210    let devices = &sweep.devices;
211    let archs = detect::read_active(root)
212        .and_then(|info| info.archs)
213        .unwrap_or_default();
214    b.push_str(",\"gpus\":[");
215    for (i, d) in devices.iter().enumerate() {
216        if i > 0 {
217            b.push(',');
218        }
219        let compatible = d.covered_by(&archs);
220        // `sm` is the legacy NVIDIA-only key, kept so an older reader
221        // does not lose the field; `vendor` + `arch` are the
222        // vendor-plural pair every new consumer should read.
223        let _ = write!(
224            b,
225            "{{\"index\":{},\"name\":\"{}\",\"vendor\":\"{}\",\"arch\":\"{}\",\"sm\":\"{}\",\"vram_bytes\":{},\"arch_compatible\":{}}}",
226            d.index,
227            system::escape_json(&d.name),
228            d.vendor.as_str(),
229            d.arch_label(),
230            d.sm_version().unwrap_or_default(),
231            d.vram_bytes(),
232            compatible
233        );
234    }
235    b.push(']');
236
237    // What the sweep learned that the device list cannot express. Empty
238    // array on a healthy rig, so a consumer can read it unconditionally.
239    b.push_str(",\"gpu_notes\":[");
240    for (i, note) in sweep.notes.iter().enumerate() {
241        if i > 0 {
242            b.push(',');
243        }
244        let _ = write!(
245            b,
246            "{{\"vendor\":\"{}\",\"kind\":\"{}\",\"message\":\"{}\"}}",
247            note.vendor.as_str(),
248            note.kind.as_str(),
249            system::escape_json(&note.message),
250        );
251    }
252    b.push(']');
253
254    // libtorch
255    b.push_str(",\"libtorch\":");
256    match detect::read_active(root) {
257        Some(info) => {
258            let _ = write!(b, "{{\"path\":\"{}\"", system::escape_json(&info.path));
259            if let Some(v) = &info.torch_version {
260                let _ = write!(b, ",\"version\":\"{}\"", system::escape_json(v));
261            }
262            // `vendor` is the field to read; `cuda` stays raw, `none` and
263            // all, because it is the verbatim `.arch` value and `fdl
264            // probe` parses that same field back out of remote hosts.
265            // Adding a key is compatible, changing one is not.
266            let _ = write!(
267                b,
268                ",\"vendor\":\"{}\"",
269                match detect::variant_vendor(&info.path) {
270                    Some(v) => v.as_str(),
271                    None => "cpu",
272                }
273            );
274            if let Some(c) = &info.cuda_version {
275                let _ = write!(b, ",\"cuda\":\"{}\"", system::escape_json(c));
276            }
277            if let Some(a) = &info.archs {
278                let _ = write!(b, ",\"archs\":\"{}\"", system::escape_json(a));
279            }
280            if let Some(s) = &info.source {
281                let _ = write!(b, ",\"source\":\"{}\"", system::escape_json(s));
282            }
283            b.push('}');
284        }
285        None => b.push_str("null"),
286    }
287
288    b.push('}');
289    println!("{}", b);
290}