1use crate::benchmark_logs;
2use crate::state::{Category, Component};
3use std::collections::HashSet;
4use std::env;
5use std::fs;
6use std::path::{Path, PathBuf};
7use std::process::{Command, Stdio};
8
9#[derive(Debug, Clone)]
10pub struct VerificationCommand {
11 pub label: String,
12 pub target_id: String,
13 pub program: String,
14 pub args: Vec<String>,
15 pub modules: Vec<String>,
16}
17
18pub fn python_interpreters() -> Vec<String> {
19 let mut candidates = Vec::new();
20 if let Ok(value) = env::var("MLSTACK_PYTHON_BIN") {
21 push_python_candidate(&mut candidates, value);
22 }
23 if let Ok(value) = env::var("AITER_VENV_PYTHON") {
24 push_python_candidate(&mut candidates, value);
25 }
26 if let Ok(value) = env::var("UV_PYTHON") {
27 push_python_candidate(&mut candidates, value);
28 }
29 for key in [
30 "WANDB_VENV_PYTHON",
31 "MEGATRON_VENV_PYTHON",
32 "MPI4PY_VENV_PYTHON",
33 "FLASH_ATTENTION_VENV_PYTHON",
34 "PYTORCH_VENV_PYTHON",
35 "DEEPSPEED_VENV_PYTHON",
36 "VLLM_VENV_PYTHON",
37 ] {
38 if let Ok(value) = env::var(key) {
39 push_python_candidate(&mut candidates, value);
40 }
41 }
42 let home = resolve_component_user_home();
43 let venv = Path::new(&home)
44 .join("rocm_venv")
45 .join("bin")
46 .join("python");
47 if venv.exists() {
48 candidates.push(venv.to_string_lossy().to_string());
49 }
50
51 let conda_path = Path::new(&home)
53 .join("anaconda3")
54 .join("bin")
55 .join("python");
56 if conda_path.exists() {
57 candidates.push(conda_path.to_string_lossy().to_string());
58 }
59 let miniconda_path = Path::new(&home)
60 .join("miniconda3")
61 .join("bin")
62 .join("python");
63 if miniconda_path.exists() {
64 candidates.push(miniconda_path.to_string_lossy().to_string());
65 }
66
67 if Path::new("/usr/local/bin/python3").exists() {
68 candidates.push("/usr/local/bin/python3".to_string());
69 }
70 if command_exists("python3.13") {
71 candidates.push("python3.13".to_string());
72 }
73 if command_exists("python3.12") {
74 candidates.push("python3.12".to_string());
75 }
76 if command_exists("python3") {
77 candidates.push("python3".to_string());
78 }
79 if command_exists("python") {
80 candidates.push("python".to_string());
81 }
82
83 let mut roots = vec![PathBuf::from(&home)];
84 if let Ok(cwd) = env::current_dir() {
85 roots.push(cwd.clone());
86 roots.push(cwd.join("rusty-stack"));
87 }
88 for root in roots {
89 push_component_venv_candidates(&mut candidates, &root);
90 }
91
92 dedupe_python_candidates_in_order(candidates)
93}
94
95fn dedupe_python_candidates_in_order(candidates: Vec<String>) -> Vec<String> {
96 let mut seen = HashSet::new();
97 let mut ordered = Vec::new();
98 for candidate in candidates {
99 if seen.insert(candidate.clone()) {
100 ordered.push(candidate);
101 }
102 }
103 ordered
104}
105
106fn push_component_venv_candidates(candidates: &mut Vec<String>, root: &Path) {
107 for rel in [
108 "wandb_venv/bin/python",
109 "megatron_rocm_venv/bin/python",
110 "Megatron-LM/megatron_rocm_venv/bin/python",
111 "mpi4py_venv/bin/python",
112 "flash_attention_venv/bin/python",
113 "vllm_venv/bin/python",
114 ".mlstack/venvs/aiter/bin/python",
115 ".mlstack/venvs/vllm/bin/python",
116 "pytorch_rocm_venv/bin/python",
117 ] {
118 let candidate = root.join(rel);
119 if candidate.exists() {
120 candidates.push(candidate.to_string_lossy().to_string());
121 }
122 }
123}
124
125fn push_python_candidate(candidates: &mut Vec<String>, value: String) {
126 let trimmed = value.trim();
127 if trimmed.is_empty() {
128 return;
129 }
130 let candidate = trimmed.to_string();
131 if Path::new(&candidate).exists() || command_exists(&candidate) {
132 candidates.push(candidate);
133 }
134}
135
136pub fn is_component_installed(component: &Component, python_candidates: &[String]) -> bool {
137 if component.category == Category::Verification {
138 return false;
139 }
140 is_component_installed_by_id(&component.id, python_candidates)
141}
142
143pub fn is_component_installed_by_id(component_id: &str, python_candidates: &[String]) -> bool {
144 let home = resolve_component_user_home();
145 let has_benchmark_logs = has_benchmark_log_dirs();
146 match component_id {
147 "rocm" => {
150 let version_exists = path_exists("/opt/rocm/.info/version")
152 || path_exists("/opt/rocm/version")
153 || path_exists("/usr/lib/rocm/.info/version");
154
155 let rocminfo_path = crate::installers::common::utils::resolve_rocminfo_path();
157 let rocminfo_functional = if let Ok(output) = std::process::Command::new(&rocminfo_path)
158 .arg("--version")
159 .output()
160 {
161 output.status.success()
162 } else {
163 false
164 };
165
166 version_exists && rocminfo_functional
169 }
170 "pytorch" => python_any(python_candidates, &["torch"]),
171 "triton" => python_any(python_candidates, &["triton"]),
172 "mpi4py" => python_any(python_candidates, &["mpi4py"]),
173 "deepspeed" => python_any(python_candidates, &["deepspeed"]),
174 "ml-stack-core" => {
175 python_any(python_candidates, &["stans_ml_stack"]) || repo_has_ml_stack_core()
176 }
177 "flash-attn" => {
178 python_any(python_candidates, &["flash_attn", "flash_attn_2"])
179 || path_exists(home_path(&home, &["ml_stack", "flash_attn_amd"]))
180 || path_exists(home_path(&home, &["ml_stack", "flash_attn_amd_direct"]))
181 }
182 "megatron" => {
183 path_exists(home_path(&home, &["megatron", "Megatron-LM"]))
184 || python_any(python_candidates, &["megatron"])
185 }
186 "vllm" => {
187 python_candidates
188 .iter()
189 .any(|python| python_exec(python, vllm_runtime_check_snippet()))
190 || path_exists(home_path(&home, &["vllm_build"]))
191 || path_exists(home_path(&home, &["vllm_py313"]))
192 }
193 "aiter" => python_any(python_candidates, &["aiter"]),
194 "vllm-studio" => {
195 command_exists("vllm-studio") || path_exists(home_path(&home, &["vllm-studio"]))
196 }
197 "onnx" => {
198 python_any(python_candidates, &["onnxruntime"])
199 || path_exists(home_path(&home, &["onnxruntime_build"]))
200 }
201 "bitsandbytes" => {
202 python_any(python_candidates, &["bitsandbytes"])
203 || path_exists(home_path(&home, &["ml_stack", "bitsandbytes"]))
204 }
205 "rocm-smi" => {
207 let rocm_smi_paths = [
208 "/opt/rocm/bin/rocm-smi",
209 "/usr/bin/rocm-smi",
210 "/usr/local/bin/rocm-smi",
211 ];
212 for path in &rocm_smi_paths {
213 if path_exists(path) {
214 if let Ok(output) = std::process::Command::new(path).arg("--showuse").output() {
216 if output.status.success() {
217 return true;
218 }
219 }
220 }
221 }
222 if command_exists("rocm-smi") {
224 if let Ok(output) = std::process::Command::new("rocm-smi")
225 .arg("--showuse")
226 .output()
227 {
228 return output.status.success();
229 }
230 }
231 false
232 }
233 "migraphx" => {
234 python_any(python_candidates, &["migraphx"])
235 || path_exists(home_path(&home, &["migraphx_build"]))
236 }
237 "pytorch-profiler" => python_any(python_candidates, &["torch"]),
238 "wandb" => python_any(python_candidates, &["wandb"]),
239 "fastvideo" => python_any(python_candidates, &["fastvideo"]),
240 "llama-cpp" => {
241 crate::installers::components::llama_cpp::is_llama_cli_functional(&home)
243 }
244 "permanent-env" => env_file_has_permanent(&home_path(&home, &[".mlstack_env"])),
245 "basic-env" => path_exists(home_path(&home, &[".mlstack_env"])),
246 "enhanced-env" => env_file_has_enhanced(&home_path(&home, &[".mlstack_env"])),
247 "vllm-performance" => has_benchmark_logs,
248 "deepspeed-performance" => has_benchmark_logs,
249 "megatron-performance" => has_benchmark_logs,
250 "all-benchmarks" => has_benchmark_logs,
251 "comfyui" => {
252 let default_path = home_path(&home, &["ComfyUI"]);
257 path_exists(default_path.join(".git")) || path_exists(default_path.join("main.py"))
258 }
259 "textgen" => {
260 let default_path = home_path(&home, &["text-generation-webui"]);
263 path_exists(default_path.join(".git")) || path_exists(default_path.join("server.py"))
264 }
265 _ => false,
266 }
267}
268
269pub fn verification_commands(
270 component_id: &str,
271 python_candidates: &[String],
272) -> Vec<VerificationCommand> {
273 match component_id {
274 "verify-basic" => basic_verification_commands(python_candidates),
275 "verify-enhanced" => enhanced_verification_commands(python_candidates),
276 "verify-build" => build_verification_commands(python_candidates),
277 _ => Vec::new(),
278 }
279}
280
281pub fn component_verification_commands(
282 component_id: &str,
283 python_candidates: &[String],
284) -> Vec<VerificationCommand> {
285 match component_id {
286 "rocm" => vec![shell_command("ROCm info", "rocm", "rocminfo", &[])],
287 "pytorch" => vec![python_command(
288 "PyTorch",
289 "pytorch",
290 &["torch"],
291 python_candidates,
292 pytorch_diagnostic_snippet(),
293 )],
294 "triton" => vec![python_command(
295 "Triton",
296 "triton",
297 &["triton"],
298 python_candidates,
299 "import triton; import triton.language as tl; print(f'Triton {triton.__version__} — language OK')",
300 )],
301 "mpi4py" => vec![python_command(
302 "MPI4Py",
303 "mpi4py",
304 &["mpi4py"],
305 python_candidates,
306 "import importlib.metadata as m; import mpi4py; print(getattr(mpi4py, '__version__', m.version('mpi4py')))",
307 )],
308 "deepspeed" => vec![python_command(
309 "DeepSpeed",
310 "deepspeed",
311 &["deepspeed"],
312 python_candidates,
313 "import deepspeed; print(deepspeed.__version__)",
314 )],
315 "ml-stack-core" => vec![python_command(
316 "ML Stack Core",
317 "ml-stack-core",
318 &["stans_ml_stack"],
319 python_candidates,
320 "import stans_ml_stack; print(getattr(stans_ml_stack, '__version__', 'ok'))",
321 )],
322 "flash-attn" => vec![python_command(
323 "Flash Attention",
324 "flash-attn",
325 &["flash_attn", "flash_attention_amd", "flash_attn_2"],
326 python_candidates,
327 "import importlib, sys;\nmodules=[('flash_attn','flash_attn.flash_attn_func'),('flash_attention_amd',None),('flash_attn_2',None)];\nfor mod_name, func_path in modules:\n try:\n m = importlib.import_module(mod_name);\n ver = getattr(m, '__version__', 'unknown');\n if func_path:\n parts = func_path.split('.');\n obj = m;\n [obj := getattr(obj, p) for p in parts[1:]];\n print(f'{mod_name} {ver} — {func_path} OK');\n else:\n print(f'{mod_name} {ver} — imported OK');\n sys.exit(0);\n except SystemExit:\n raise\n except Exception:\n continue;\nprint('No flash attention module found'); sys.exit(1)",
328 )],
329 "megatron" => vec![python_command(
330 "Megatron-LM",
331 "megatron",
332 &["megatron"],
333 python_candidates,
334 "import megatron; print('megatron ok')",
335 )],
336 "vllm" => vec![python_command(
337 "vLLM",
338 "vllm",
339 &["vllm"],
340 python_candidates,
341 vllm_runtime_check_snippet(),
342 )],
343 "aiter" => vec![python_command(
344 "AITER",
345 "aiter",
346 &["aiter"],
347 python_candidates,
348 "import aiter; mods=[x for x in dir(aiter) if not x.startswith('_')]; print(f'AITER imported — {len(mods)} exports available')",
349 )],
350 "vllm-studio" => vec![shell_command(
351 "vLLM Studio",
352 "vllm-studio",
353 "bun",
354 &["--version"],
355 )],
356 "onnx" => vec![python_command(
357 "ONNX Runtime",
358 "onnx",
359 &["onnxruntime"],
360 python_candidates,
361 "import onnxruntime as ort; import sys; print('Version:', ort.__version__); providers=ort.get_available_providers(); print('Providers:', providers); gpu=[p for p in providers if p in ('ROCMExecutionProvider','MIGraphXExecutionProvider','CUDAExecutionProvider')]; sys.exit(0 if gpu else 1)",
362 )],
363 "bitsandbytes" => vec![python_command(
364 "BITSANDBYTES",
365 "bitsandbytes",
366 &["bitsandbytes"],
367 python_candidates,
368 "import bitsandbytes as bnb; import pathlib; import sys; print('Version:', getattr(bnb, '__version__', 'unknown')); path=pathlib.Path(bnb.__file__).parent; libs=list(path.glob('libbitsandbytes_rocm*.so')); print('ROCm Libs:', libs); sys.exit(0 if libs else 1)",
369 )],
370 "rocm-smi" => vec![shell_command(
371 "ROCm SMI",
372 "rocm-smi",
373 "rocm-smi",
374 &["--showproductname"],
375 )],
376 "migraphx" => vec![python_command(
377 "MIGraphX",
378 "migraphx",
379 &["migraphx"],
380 python_candidates,
381 "import migraphx; print(getattr(migraphx, '__version__', 'ok'))",
382 )],
383 "pytorch-profiler" => vec![python_command(
384 "PyTorch Profiler",
385 "pytorch-profiler",
386 &["torch"],
387 python_candidates,
388 "from torch.profiler import profile; print(profile)",
389 )],
390 "wandb" => vec![python_command(
391 "Weights & Biases",
392 "wandb",
393 &["wandb"],
394 python_candidates,
395 "import wandb; print(wandb.__version__)",
396 )],
397 "fastvideo" => vec![python_command(
398 "FastVideo",
399 "fastvideo",
400 &["fastvideo"],
401 python_candidates,
402 "import fastvideo; print(fastvideo.__version__)",
403 )],
404 "llama-cpp" => vec![shell_command(
405 "llama.cpp",
406 "llama-cpp",
407 "llama-cli",
408 &["--help"],
409 )],
410 "basic-env" => vec![shell_command(
411 "Environment file",
412 "basic-env",
413 "bash",
414 &[
415 "-c",
416 "test -f \"$HOME/.mlstack_env\" && echo OK || exit 1",
417 ],
418 )],
419 "enhanced-env" => vec![shell_command(
420 "Enhanced env",
421 "enhanced-env",
422 "bash",
423 &[
424 "-c",
425 "grep -q \"Enhanced ML Stack Environment Setup Script\" \"$HOME/.mlstack_env\" && echo OK || exit 1",
426 ],
427 )],
428 "permanent-env" => vec![shell_command(
429 "Permanent env",
430 "permanent-env",
431 "bash",
432 &[
433 "-c",
434 "grep -qiE \"Permanent ROCm Environment|Permanent ROCm Env|MLSTACK_PYTHON_BIN\" \"$HOME/.mlstack_env\" && echo OK || exit 1",
435 ],
436 )],
437 "mlperf-inference" => vec![benchmark_log_check_command(
439 "MLPerf benchmark logs",
440 "mlperf-inference",
441 "mlperf_inference",
442 "Benchmark logs found",
443 "No benchmark logs yet",
444 )],
445 "rocm-benchmarks" => vec![benchmark_log_check_command(
446 "ROCm benchmark logs",
447 "rocm-benchmarks",
448 "rocm_benchmarks",
449 "Benchmark logs found",
450 "No benchmark logs yet",
451 )],
452 "gpu-memory-bandwidth" => vec![benchmark_log_check_command(
453 "Memory bandwidth logs",
454 "gpu-memory-bandwidth",
455 "gpu_memory_bandwidth",
456 "Benchmark logs found",
457 "No benchmark logs yet",
458 )],
459 "rocm-smi-bench" => vec![benchmark_log_check_command(
460 "ROCm SMI logs",
461 "rocm-smi-bench",
462 "rocm_smi_benchmarks",
463 "Benchmark logs found",
464 "No benchmark logs yet",
465 )],
466 "pytorch-performance" => vec![benchmark_log_check_command(
467 "PyTorch benchmark logs",
468 "pytorch-performance",
469 "pytorch_performance",
470 "Benchmark logs found",
471 "No benchmark logs yet",
472 )],
473 "vllm-performance" => vec![benchmark_log_check_command(
474 "vLLM benchmark logs",
475 "vllm-performance",
476 "vllm_benchmarks",
477 "Benchmark logs found",
478 "No benchmark logs yet",
479 )],
480 "deepspeed-performance" => vec![benchmark_log_check_command(
481 "DeepSpeed benchmark logs",
482 "deepspeed-performance",
483 "deepspeed_benchmarks",
484 "Benchmark logs found",
485 "No benchmark logs yet",
486 )],
487 "megatron-performance" => vec![benchmark_log_check_command(
488 "Megatron benchmark logs",
489 "megatron-performance",
490 "megatron_benchmarks",
491 "Benchmark logs found",
492 "No benchmark logs yet",
493 )],
494 "all-benchmarks" => vec![benchmark_log_check_command(
495 "Full suite logs",
496 "all-benchmarks",
497 "full_benchmarks",
498 "Suite logs found",
499 "No suite logs yet",
500 )],
501 "comfyui" => vec![shell_command(
502 "ComfyUI",
503 "comfyui",
504 "bash",
505 &["-c", "test -f \"$HOME/ComfyUI/main.py\" && echo 'ComfyUI installed' || exit 1"],
506 )],
507 "textgen" => vec![shell_command(
508 "text-generation-webui",
509 "textgen",
510 "bash",
511 &["-c", "test -f \"$HOME/text-generation-webui/server.py\" && echo 'text-generation-webui installed' || exit 1"],
512 )],
513 _ => Vec::new(),
514 }
515}
516
517pub fn modules_available(modules: &[String], python_candidates: &[String]) -> bool {
518 if modules.is_empty() {
519 return true;
520 }
521 let module_refs: Vec<&str> = modules.iter().map(|s| s.as_str()).collect();
522 python_any(python_candidates, &module_refs)
523}
524
525fn basic_verification_commands(python_candidates: &[String]) -> Vec<VerificationCommand> {
526 vec![
527 shell_command("ROCm info", "rocm", "rocminfo", &[]),
528 python_command(
529 "PyTorch",
530 "pytorch",
531 &["torch"],
532 python_candidates,
533 pytorch_diagnostic_snippet(),
534 ),
535 python_command(
536 "Triton",
537 "triton",
538 &["triton"],
539 python_candidates,
540 "import triton; import triton._C; print(f'Triton {triton.__version__} ok')",
541 ),
542 python_command(
543 "MPI4Py",
544 "mpi4py",
545 &["mpi4py"],
546 python_candidates,
547 "import importlib.metadata as m; import mpi4py; print(getattr(mpi4py, '__version__', m.version('mpi4py')))",
548 ),
549 python_command(
550 "DeepSpeed",
551 "deepspeed",
552 &["deepspeed", "einops"],
553 python_candidates,
554 "import deepspeed; import einops; print(deepspeed.__version__)",
555 ),
556 ]
557}
558
559fn enhanced_verification_commands(python_candidates: &[String]) -> Vec<VerificationCommand> {
560 let mut steps = basic_verification_commands(python_candidates);
561 steps.extend(vec![
562 python_command(
563 "Flash Attention",
564 "flash-attn",
565 &["flash_attn", "flash_attention_amd", "flash_attn_2"],
566 python_candidates,
567 "import importlib, sys;\nmodules=[('flash_attn','flash_attn.flash_attn_func'),('flash_attention_amd',None),('flash_attn_2',None)];\nfor mod_name, func_path in modules:\n try:\n m = importlib.import_module(mod_name);\n ver = getattr(m, '__version__', 'unknown');\n if func_path:\n parts = func_path.split('.');\n obj = m;\n [obj := getattr(obj, p) for p in parts[1:]];\n print(f'{mod_name} {ver} — {func_path} OK');\n else:\n print(f'{mod_name} {ver} — imported OK');\n sys.exit(0);\n except SystemExit:\n raise\n except Exception:\n continue;\nprint('No flash attention module found'); sys.exit(1)",
568 ),
569 python_command(
570 "vLLM",
571 "vllm",
572 &["vllm"],
573 python_candidates,
574 vllm_runtime_check_snippet(),
575 ),
576 python_command(
577 "AITER",
578 "aiter",
579 &["aiter"],
580 python_candidates,
581 "import aiter; print(getattr(aiter, '__version__', 'ok'))",
582 ),
583 python_command(
584 "ONNX Runtime",
585 "onnx",
586 &["onnxruntime"],
587 python_candidates,
588 "import onnxruntime as ort; import pathlib; import os; import sys; print('Version:', ort.__version__); base=pathlib.Path(ort.__file__).parent; libs=list(base.rglob('libonnxruntime_providers_rocm.so')); [os.environ.update({'ORT_ROCM_EP_PROVIDER_PATH': str(l)}) for l in libs[:1]]; providers=ort.get_available_providers(); print('Providers:', providers); sys.exit(0 if 'ROCMExecutionProvider' in providers else 1)",
589 ),
590 python_command(
591 "BITSANDBYTES",
592 "bitsandbytes",
593 &["bitsandbytes"],
594 python_candidates,
595 "import bitsandbytes as bnb; import pathlib; import sys; print('Version:', getattr(bnb, '__version__', 'unknown')); path=pathlib.Path(bnb.__file__).parent; libs=list(path.glob('libbitsandbytes_rocm*.so')); print('ROCm Libs:', libs); sys.exit(0 if libs else 1)",
596 ),
597 shell_command("ROCm SMI", "rocm-smi", "rocm-smi", &["--showproductname"]),
598 python_command(
599 "MIGraphX",
600 "migraphx",
601 &["migraphx"],
602 python_candidates,
603 "import migraphx; print(getattr(migraphx, '__version__', 'ok'))",
604 ),
605 python_command(
606 "PyTorch Profiler",
607 "pytorch-profiler",
608 &["torch"],
609 python_candidates,
610 "from torch.profiler import profile; print(profile)",
611 ),
612 python_command(
613 "Weights & Biases",
614 "wandb",
615 &["wandb"],
616 python_candidates,
617 "import wandb; print(wandb.__version__)",
618 ),
619 python_command(
620 "FastVideo",
621 "fastvideo",
622 &["fastvideo"],
623 python_candidates,
624 "import fastvideo; print(fastvideo.__version__)",
625 ),
626 shell_command("vLLM Studio", "vllm-studio", "bun", &["--version"]),
627 ]);
628 steps
629}
630
631fn build_verification_commands(python_candidates: &[String]) -> Vec<VerificationCommand> {
632 let mut steps = basic_verification_commands(python_candidates);
633 steps.extend(vec![
634 python_command(
635 "ONNX Runtime",
636 "onnx",
637 &["onnxruntime"],
638 python_candidates,
639 "import onnxruntime as ort; import pathlib; import os; import sys; print('Version:', ort.__version__); base=pathlib.Path(ort.__file__).parent; libs=list(base.rglob('libonnxruntime_providers_rocm.so')); [os.environ.update({'ORT_ROCM_EP_PROVIDER_PATH': str(l)}) for l in libs[:1]]; providers=ort.get_available_providers(); print('Providers:', providers); sys.exit(0 if 'ROCMExecutionProvider' in providers else 1)",
640 ),
641 python_command(
642 "Flash Attention (build)",
643 "flash-attn",
644 &["flash_attn", "flash_attention_amd", "flash_attn_2"],
645 python_candidates,
646 "import importlib, sys;\nmodules=[('flash_attn','flash_attn.flash_attn_func'),('flash_attention_amd',None),('flash_attn_2',None)];\nfor mod_name, func_path in modules:\n try:\n m = importlib.import_module(mod_name);\n ver = getattr(m, '__version__', 'unknown');\n if func_path:\n parts = func_path.split('.');\n obj = m;\n [obj := getattr(obj, p) for p in parts[1:]];\n print(f'{mod_name} {ver} — {func_path} OK');\n else:\n print(f'{mod_name} {ver} — imported OK');\n sys.exit(0);\n except SystemExit:\n raise\n except Exception:\n continue;\nprint('No flash attention module found'); sys.exit(1)",
647 ),
648 python_command(
649 "MIGraphX",
650 "migraphx",
651 &["migraphx"],
652 python_candidates,
653 "import migraphx; print(getattr(migraphx, '__version__', 'ok'))",
654 ),
655 ]);
656 steps
657}
658
659fn python_command(
660 label: &str,
661 target_id: &str,
662 modules: &[&str],
663 python_candidates: &[String],
664 code: &str,
665) -> VerificationCommand {
666 let program = select_python_for_modules(modules, python_candidates)
667 .or_else(|| python_candidates.first().cloned())
668 .unwrap_or_else(|| "python3".to_string());
669
670 VerificationCommand {
671 label: label.to_string(),
672 target_id: target_id.to_string(),
673 program,
674 args: vec!["-c".to_string(), code.to_string()],
675 modules: modules.iter().map(|m| m.to_string()).collect(),
676 }
677}
678
679fn pytorch_diagnostic_snippet() -> &'static str {
695 r#"import torch, sys, os, ctypes.util
696
697# Tier 1: Basic torch import (already verified by module check)
698print(f'torch version: {torch.__version__}')
699
700# Tier 2: Check if this is a ROCm build
701hip_version = getattr(torch.version, 'hip', None)
702cuda_version = getattr(torch.version, 'cuda', None)
703print(f'hip: {hip_version}')
704print(f'cuda: {cuda_version}')
705
706# Tier 3: Runtime HIP/CUDA availability
707try:
708 hip_available = torch.cuda.is_available()
709 print(f'torch.cuda.is_available(): {hip_available}')
710 if hip_available:
711 device_count = torch.cuda.device_count()
712 print(f'device_count: {device_count}')
713 if device_count > 0:
714 print(f'device_name: {torch.cuda.get_device_name(0)}')
715except Exception as e:
716 hip_available = False
717 print(f'torch.cuda.is_available() error: {e}')
718
719# Tier 4: Diagnostics if HIP not available
720if not hip_available:
721 print('--- ROCm HIP Diagnostics ---')
722
723 # Check LD_LIBRARY_PATH
724 ld_path = os.environ.get('LD_LIBRARY_PATH', '')
725 print(f'LD_LIBRARY_PATH: {ld_path if ld_path else "(not set)"}')
726
727 # Check ROCm_PATH
728 rocm_path = os.environ.get('ROCM_PATH', '')
729 print(f'ROCM_PATH: {rocm_path if rocm_path else "(not set)"}')
730
731 # Check if ROCm libraries exist
732 rocm_lib_dirs = ['/opt/rocm/lib', '/opt/rocm/hip/lib']
733 for d in rocm_lib_dirs:
734 exists = os.path.isdir(d)
735 print(f'{d}: {"exists" if exists else "not found"}')
736
737 # Check if libamdhip64.so can be found
738 hip_lib = ctypes.util.find_library('amdhip64')
739 print(f'libamdhip64: {hip_lib if hip_lib else "not found"}')
740
741 # Check Python executable path
742 print(f'python: {sys.executable}')
743
744 # Provide actionable guidance
745 if hip_version is None and cuda_version is None:
746 print('HINT: torch.version.hip and torch.version.cuda are both None.')
747 print(' This torch build may not have GPU support.')
748 print(' Reinstall with ROCm index: pip install torch --index-url https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/')
749 elif hip_version is not None and not hip_available:
750 print('HINT: torch.version.hip is set but HIP runtime not available.')
751 print(' Possible causes:')
752 print(' 1. LD_LIBRARY_PATH does not include /opt/rocm/lib')
753 print(' 2. Running in a container/environment without GPU access')
754 print(' 3. ROCm drivers not loaded (check with: rocminfo)')
755 print(' Fix: source ~/.mlstack_env or set LD_LIBRARY_PATH=/opt/rocm/lib')
756
757# Exit successfully if torch is importable (even without HIP)
758# This prevents cascading failures to downstream components.
759# The diagnostic output above tells the user if HIP is available.
760sys.exit(0)"#
761}
762
763fn vllm_runtime_check_snippet() -> &'static str {
764 r#"import importlib
765import vllm
766import cachetools
767import cbor2
768import gguf
769import pybase64
770import ijson
771import mistral_common
772import openai_harmony
773
774loaded = []
775errs = []
776for name in ("vllm._C", "vllm._rocm_C"):
777 try:
778 importlib.import_module(name)
779 loaded.append(name)
780 except Exception as exc:
781 errs.append(f"{name}: {exc}")
782
783if not loaded:
784 raise SystemExit("vLLM native extension load failure: " + " | ".join(errs))
785
786print(vllm.__version__)"#
787}
788
789fn shell_command(
790 label: &str,
791 target_id: &str,
792 program: &str,
793 args: &[&str],
794) -> VerificationCommand {
795 VerificationCommand {
796 label: label.to_string(),
797 target_id: target_id.to_string(),
798 program: program.to_string(),
799 args: args.iter().map(|s| s.to_string()).collect(),
800 modules: Vec::new(),
801 }
802}
803
804fn has_benchmark_log_dirs() -> bool {
805 benchmark_logs::benchmark_log_directories()
806 .iter()
807 .any(|dir| {
808 fs::read_dir(dir)
809 .ok()
810 .map(|entries| {
811 entries
812 .filter_map(|entry| entry.ok())
813 .any(|entry| entry.path().is_file())
814 })
815 .unwrap_or(false)
816 })
817}
818
819fn benchmark_log_check_command(
820 label: &str,
821 target_id: &str,
822 pattern: &str,
823 success_message: &str,
824 failure_message: &str,
825) -> VerificationCommand {
826 let script = benchmark_log_check_script(pattern, success_message, failure_message);
827 shell_command(label, target_id, "bash", &["-c", &script])
828}
829
830fn benchmark_log_check_script(
831 pattern: &str,
832 success_message: &str,
833 failure_message: &str,
834) -> String {
835 format!(
836 "dirs=(\"${{MLSTACK_LOG_DIR:-}}\" \"$HOME/.rusty-stack/logs\" \"${{TMPDIR:-/tmp}}/rusty-stack/logs\")\n\
837 found=0\n\
838 for dir in \"${{dirs[@]}}\"; do\n\
839 if [ -n \"$dir\" ] && [ -d \"$dir\" ]; then\n\
840 if ls \"$dir\" | grep -q \"{}\"; then\n\
841 echo \"{}\"\n\
842 found=1\n\
843 break\n\
844 fi\n\
845 fi\n\
846 done\n\
847 if [ \"$found\" -ne 1 ]; then\n\
848 echo \"{}\"\n\
849 fi",
850 pattern, success_message, failure_message
851 )
852}
853
854fn python_any(python_candidates: &[String], modules: &[&str]) -> bool {
855 python_candidates.iter().any(|python| {
856 modules
857 .iter()
858 .any(|module| python_has_module(python, module))
859 })
860}
861
862fn python_has_module(python: &str, module: &str) -> bool {
863 python_exec(
864 python,
865 &format!(
866 "import importlib.util, sys; sys.exit(0 if importlib.util.find_spec('{module}') else 1)"
867 ),
868 )
869}
870
871fn python_exec(python: &str, code: &str) -> bool {
872 let mut cmd = Command::new(python);
873 cmd.arg("-c")
874 .arg(code)
875 .stdout(Stdio::null())
876 .stderr(Stdio::null());
877
878 if let Ok(existing) = env::var("PYTHONPATH") {
879 let mut path = existing;
880 let extra_paths = python_search_paths();
881 if !extra_paths.is_empty() {
882 if !path.is_empty() {
883 path.push(':');
884 }
885 path.push_str(&extra_paths.join(":"));
886 }
887 cmd.env("PYTHONPATH", path);
888 } else {
889 let extra_paths = python_search_paths();
890 if !extra_paths.is_empty() {
891 cmd.env("PYTHONPATH", extra_paths.join(":"));
892 }
893 }
894
895 let home = resolve_component_user_home();
896 let ld_path = component_status_ld_library_path(&home);
897 if !ld_path.is_empty() {
898 cmd.env("LD_LIBRARY_PATH", ld_path);
899 }
900
901 cmd.status().map(|status| status.success()).unwrap_or(false)
902}
903
904fn push_unique_path(paths: &mut Vec<String>, value: &str) {
905 let value = value.trim();
906 if value.is_empty() {
907 return;
908 }
909 if paths.iter().any(|existing| existing == value) {
910 return;
911 }
912 paths.push(value.to_string());
913}
914
915fn component_user_home_from_passwd(user_name: &str) -> Option<String> {
916 let target = user_name.trim();
917 if target.is_empty() {
918 return None;
919 }
920 let passwd = fs::read_to_string("/etc/passwd").ok()?;
921 passwd.lines().find_map(|line| {
922 if line.trim().is_empty() || line.starts_with('#') {
923 return None;
924 }
925 let fields: Vec<&str> = line.split(':').collect();
926 if fields.len() < 6 || fields[0] != target {
927 return None;
928 }
929 let home = fields[5].trim();
930 if home.is_empty() {
931 None
932 } else {
933 Some(home.to_string())
934 }
935 })
936}
937
938fn component_passwd_homes_with_mlstack() -> Vec<String> {
939 let mut homes = Vec::new();
940 let Ok(passwd) = fs::read_to_string("/etc/passwd") else {
941 return homes;
942 };
943
944 for line in passwd.lines() {
945 if line.trim().is_empty() || line.starts_with('#') {
946 continue;
947 }
948 let fields: Vec<&str> = line.split(':').collect();
949 if fields.len() < 6 {
950 continue;
951 }
952 let home = fields[5].trim();
953 if home.is_empty() {
954 continue;
955 }
956 if Path::new(home).join(".mlstack").is_dir()
957 || Path::new(home).join(".mlstack_env").is_file()
958 {
959 push_unique_path(&mut homes, home);
960 }
961 }
962
963 homes
964}
965
966fn component_candidate_homes(preferred_home: &str) -> Vec<String> {
967 let mut homes = Vec::new();
968 push_unique_path(&mut homes, preferred_home);
969
970 if let Ok(value) = env::var("MLSTACK_USER_HOME") {
971 push_unique_path(&mut homes, &value);
972 }
973 if let Ok(value) = env::var("HOME") {
974 push_unique_path(&mut homes, &value);
975 }
976
977 for key in ["SUDO_USER", "USER", "LOGNAME"] {
978 if let Ok(user_name) = env::var(key) {
979 if let Some(home) = component_user_home_from_passwd(&user_name) {
980 push_unique_path(&mut homes, &home);
981 }
982 }
983 }
984
985 for home in component_passwd_homes_with_mlstack() {
986 push_unique_path(&mut homes, &home);
987 }
988
989 homes
990}
991
992fn resolve_component_user_home() -> String {
993 let fallback = env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
994 let candidates = component_candidate_homes(&fallback);
995
996 for home in &candidates {
997 if Path::new(home).join(".mlstack_env").is_file() {
998 return home.clone();
999 }
1000 }
1001 for home in &candidates {
1002 if Path::new(home).join(".mlstack").is_dir() {
1003 return home.clone();
1004 }
1005 }
1006
1007 candidates.into_iter().next().unwrap_or(fallback)
1008}
1009
1010fn component_status_mpi_compat_dirs(home: &str) -> Vec<String> {
1011 let mut dirs = Vec::new();
1012 for home in component_candidate_homes(home) {
1013 let mlstack_dir = Path::new(&home).join(".mlstack");
1014 let primary = mlstack_dir.join("libmpi-compat");
1015 if primary.is_dir() {
1016 dirs.push(primary.to_string_lossy().to_string());
1017 }
1018
1019 if let Ok(entries) = fs::read_dir(&mlstack_dir) {
1020 for entry in entries.flatten() {
1021 let path = entry.path();
1022 if !path.is_dir() {
1023 continue;
1024 }
1025 let Some(name) = path.file_name().and_then(|value| value.to_str()) else {
1026 continue;
1027 };
1028 if name.starts_with("libmpi-compat-user-") {
1029 dirs.push(path.to_string_lossy().to_string());
1030 }
1031 }
1032 }
1033 }
1034
1035 dirs.sort();
1036 dirs.dedup();
1037 dirs
1038}
1039
1040fn component_status_ld_library_path(home: &str) -> String {
1041 let mut paths = Vec::new();
1042
1043 for dir in component_status_mpi_compat_dirs(home) {
1044 push_unique_path(&mut paths, &dir);
1045 }
1046
1047 for rocm_path in ["/opt/rocm/lib", "/opt/rocm/hip/lib", "/opt/rocm/opencl/lib"] {
1048 if Path::new(rocm_path).exists() {
1049 push_unique_path(&mut paths, rocm_path);
1050 }
1051 }
1052
1053 if let Ok(existing) = env::var("LD_LIBRARY_PATH") {
1054 for part in existing.split(':') {
1055 push_unique_path(&mut paths, part);
1056 }
1057 }
1058
1059 paths.join(":")
1060}
1061
1062fn command_exists(command: &str) -> bool {
1063 Command::new("bash")
1064 .arg("-c")
1065 .arg(format!("command -v {}", command))
1066 .stdout(Stdio::null())
1067 .stderr(Stdio::null())
1068 .status()
1069 .map(|status| status.success())
1070 .unwrap_or(false)
1071}
1072
1073pub fn python_search_paths() -> Vec<String> {
1074 let home = resolve_component_user_home();
1075 let mut paths = vec![
1076 format!("{}/pytorch", home),
1077 format!("{}/ml_stack/flash_attn_amd_direct", home),
1078 format!("{}/ml_stack/flash_attn_amd", home),
1079 format!(
1080 "{}/ml_stack/flash_attn_amd/build/lib.linux-x86_64-cpython-313",
1081 home
1082 ),
1083 format!(
1084 "{}/ml_stack/flash_attn_amd/build/lib.linux-x86_64-cpython-312",
1085 home
1086 ),
1087 format!("{}/.local/lib/python3.13/site-packages", home),
1088 format!("{}/.local/lib/python3.12/site-packages", home),
1089 format!("{}/rocm_venv/lib/python3.13/site-packages", home),
1090 format!("{}/rocm_venv/lib/python3.12/site-packages", home),
1091 format!("{}/megatron/Megatron-LM", home),
1092 format!("{}/ml_stack/bitsandbytes/bitsandbytes", home),
1093 ];
1094
1095 let rocm_lib = Path::new("/opt/rocm/lib");
1097 if rocm_lib.exists() {
1098 paths.push(rocm_lib.to_string_lossy().to_string());
1099 if let Ok(entries) = fs::read_dir(rocm_lib) {
1100 for entry in entries.flatten() {
1101 let path = entry.path();
1102 if path.is_dir() {
1103 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
1104 if name.starts_with("python") {
1105 let sp = path.join("site-packages");
1106 if sp.exists() {
1107 paths.push(sp.to_string_lossy().to_string());
1108 }
1109 }
1110 }
1111 }
1112 }
1113 }
1114 }
1115
1116 paths
1117}
1118
1119fn select_python_for_modules(modules: &[&str], python_candidates: &[String]) -> Option<String> {
1120 for key in ["MLSTACK_PYTHON_BIN", "UV_PYTHON"] {
1121 if let Ok(value) = env::var(key) {
1122 let value = value.trim();
1123 if !value.is_empty() {
1124 if modules.is_empty() {
1125 return Some(value.to_string());
1126 }
1127 for module in modules {
1128 if python_has_module(value, module) {
1129 return Some(value.to_string());
1130 }
1131 }
1132 }
1133 }
1134 }
1135
1136 for python in python_candidates {
1137 for module in modules {
1138 if python_has_module(python, module) {
1139 return Some(python.clone());
1140 }
1141 }
1142 }
1143 None
1144}
1145
1146fn path_exists<P: AsRef<Path>>(path: P) -> bool {
1147 path.as_ref().exists()
1148}
1149
1150fn home_path(home: &str, parts: &[&str]) -> PathBuf {
1151 let mut path = PathBuf::from(home);
1152 for part in parts {
1153 path.push(part);
1154 }
1155 path
1156}
1157
1158fn env_file_has_enhanced(path: &Path) -> bool {
1159 if let Ok(contents) = fs::read_to_string(path) {
1160 return contents.contains("Enhanced ML Stack Environment Setup Script");
1161 }
1162 false
1163}
1164
1165fn env_file_has_permanent(path: &Path) -> bool {
1166 if let Ok(contents) = fs::read_to_string(path) {
1167 return contents.contains("Permanent ROCm Environment")
1168 || contents.contains("Permanent ROCm Env")
1169 || (contents.contains("ML Stack Environment File")
1170 && contents.contains("MLSTACK_PYTHON_BIN"));
1171 }
1172 false
1173}
1174
1175fn repo_has_ml_stack_core() -> bool {
1176 if let Ok(root) = env::var("MLSTACK_REPO_ROOT") {
1177 return path_exists(Path::new(&root).join("stans_ml_stack"));
1178 }
1179 false
1180}
1181
1182#[cfg(test)]
1187mod tests {
1188 use super::*;
1189
1190 #[test]
1191 fn test_pytorch_diagnostic_snippet_exits_zero() {
1192 let snippet = pytorch_diagnostic_snippet();
1196 assert!(
1197 snippet.contains("sys.exit(0)"),
1198 "PyTorch diagnostic snippet should exit 0 (not block downstream components)"
1199 );
1200 assert!(
1201 !snippet.contains("sys.exit(1)"),
1202 "PyTorch diagnostic snippet should NOT exit 1 (was the old strict check)"
1203 );
1204 }
1205
1206 #[test]
1207 fn test_pytorch_diagnostic_snippet_checks_hip() {
1208 let snippet = pytorch_diagnostic_snippet();
1210 assert!(
1211 snippet.contains("torch.version"),
1212 "Should check torch.version for HIP info"
1213 );
1214 assert!(
1215 snippet.contains("torch.cuda.is_available()"),
1216 "Should check torch.cuda.is_available() for runtime HIP"
1217 );
1218 }
1219
1220 #[test]
1221 fn test_pytorch_diagnostic_snippet_provides_hints() {
1222 let snippet = pytorch_diagnostic_snippet();
1224 assert!(
1225 snippet.contains("HINT:"),
1226 "Should provide HINT messages for troubleshooting"
1227 );
1228 assert!(
1229 snippet.contains("LD_LIBRARY_PATH"),
1230 "Should mention LD_LIBRARY_PATH as a possible cause"
1231 );
1232 assert!(
1233 snippet.contains("/opt/rocm/lib"),
1234 "Should mention ROCm lib path"
1235 );
1236 }
1237
1238 #[test]
1239 fn test_pytorch_diagnostic_snippet_checks_rocm_libs() {
1240 let snippet = pytorch_diagnostic_snippet();
1242 assert!(
1243 snippet.contains("libamdhip64"),
1244 "Should check for libamdhip64 (core HIP library)"
1245 );
1246 assert!(
1247 snippet.contains("/opt/rocm/lib"),
1248 "Should check /opt/rocm/lib directory"
1249 );
1250 }
1251
1252 #[test]
1253 fn test_pytorch_component_verification_uses_diagnostic() {
1254 let candidates = vec!["python3".to_string()];
1257 let cmds = component_verification_commands("pytorch", &candidates);
1258 assert_eq!(
1259 cmds.len(),
1260 1,
1261 "Should have exactly one verification command"
1262 );
1263 let cmd = &cmds[0];
1264 assert_eq!(cmd.label, "PyTorch");
1265 assert_eq!(cmd.target_id, "pytorch");
1266 let empty = String::new();
1268 let code = cmd.args.get(1).unwrap_or(&empty);
1269 assert!(
1270 !code.contains("sys.exit(0 if getattr(torch.version, 'hip', None) else 1)"),
1271 "Should NOT use old strict HIP check that exits 1"
1272 );
1273 assert!(
1274 code.contains("sys.exit(0)"),
1275 "Should use new diagnostic snippet that always exits 0"
1276 );
1277 }
1278
1279 #[test]
1280 fn test_pytorch_basic_verification_uses_diagnostic() {
1281 let candidates = vec!["python3".to_string()];
1283 let cmds = basic_verification_commands(&candidates);
1284 let pytorch_cmd = cmds.iter().find(|c| c.target_id == "pytorch");
1285 assert!(
1286 pytorch_cmd.is_some(),
1287 "Should have a PyTorch verification command"
1288 );
1289 let cmd = pytorch_cmd.unwrap();
1290 let empty = String::new();
1291 let code = cmd.args.get(1).unwrap_or(&empty);
1292 assert!(
1293 !code.contains("sys.exit(0 if getattr(torch.version, 'hip', None) else 1)"),
1294 "Basic verification should NOT use old strict HIP check"
1295 );
1296 assert!(
1297 code.contains("sys.exit(0)"),
1298 "Basic verification should use new diagnostic snippet"
1299 );
1300 }
1301
1302 #[test]
1303 fn test_pytorch_installation_check_only_imports() {
1304 let candidates = vec!["python3".to_string()];
1309 let _ = is_component_installed_by_id("pytorch", &candidates);
1312 }
1313
1314 #[test]
1315 fn test_verification_command_structure() {
1316 let candidates = vec!["python3".to_string()];
1318 let cmds = component_verification_commands("pytorch", &candidates);
1319 assert_eq!(cmds.len(), 1);
1320 let cmd = &cmds[0];
1321 assert!(!cmd.label.is_empty());
1322 assert!(!cmd.target_id.is_empty());
1323 assert!(!cmd.program.is_empty());
1324 assert_eq!(cmd.args.len(), 2, "Should have -c and the code");
1325 assert_eq!(cmd.args[0], "-c");
1326 assert!(!cmd.args[1].is_empty());
1327 assert!(cmd.modules.contains(&"torch".to_string()));
1328 }
1329}
1330
1331#[cfg(test)]
1332mod component_status_tests;