Skip to main content

rusty_stack/installers/components/
mod.rs

1//! Native Rust installer modules for major components.
2//!
3//! Each module ports the corresponding shell installer script, constructing
4//! the correct system/pip commands matching the original script behavior.
5//! All modules use `installers::common::` for shared operations (package
6//! manager, distro detection, ROCm env, etc.).
7//!
8//! # Dispatch Architecture (VAL-INSTALL-031, VAL-INSTALL-032, VAL-INSTALL-038)
9//!
10//! `is_native_component(id)` returns `true` for all native components.
11//! `get_dependencies(id)` returns the declared dependency IDs for a component.
12//! `topological_sort(ids)` returns components in dependency order.
13//!
14//! # Validation Assertions
15//!
16//! - **VAL-INSTALL-001**: ROCm installer correct package commands
17//! - **VAL-INSTALL-002**: ROCm channel selection correct version pins
18//! - **VAL-INSTALL-003**: PyTorch installer correct wheel selection
19//! - **VAL-INSTALL-004**: Triton installer correct pip command
20//! - **VAL-INSTALL-005**: MPI4Py installer detects system MPI
21//! - **VAL-INSTALL-006**: DeepSpeed installer correct pip command
22//! - **VAL-INSTALL-007**: ML Stack Core installer correct pip command
23//! - **VAL-INSTALL-008**: Flash Attention correct cmake command
24//! - **VAL-INSTALL-009**: Flash Attention correct make command
25//! - **VAL-INSTALL-010**: ONNX Runtime correct cmake command
26//! - **VAL-INSTALL-011**: ONNX Runtime pip install from build output
27//! - **VAL-INSTALL-012**: Megatron correct git clone
28//! - **VAL-INSTALL-013**: Megatron correct pip install post-clone
29//! - **VAL-INSTALL-015**: vLLM correct git clone and pip install
30//! - **VAL-INSTALL-016**: AITER correct git clone and pip install
31//! - **VAL-INSTALL-017**: bitsandbytes correct pip command
32//! - **VAL-INSTALL-018**: ROCm SMI correct package command
33//! - **VAL-INSTALL-019**: MIGraphX correct pip command
34//! - **VAL-INSTALL-020**: PyTorch Profiler correct pip command
35//! - **VAL-INSTALL-021**: W&B correct pip command
36//! - **VAL-INSTALL-022**: vLLM Studio correct git clone
37//! - **VAL-INSTALL-023**: ComfyUI correct git clone and pip install
38//! - **VAL-INSTALL-024**: text-generation-webui correct git clone
39//! - **VAL-INSTALL-025**: App installers correct target directory
40//! - **VAL-INSTALL-026**: Repair invokes correct sub-installers in order
41//! - **VAL-INSTALL-027**: Repair propagates individual failures
42//! - **VAL-INSTALL-028**: Permanent env creates correct venv and symlinks
43//! - **VAL-INSTALL-029**: AMD GPU drivers correct package command
44//! - **VAL-INSTALL-030**: MIGraphX Python correct pip command
45//! - **VAL-INSTALL-031**: installer.rs no bash subprocess for ported components
46//! - **VAL-INSTALL-032**: installer.rs dispatches to correct Rust module per ID
47//! - **VAL-INSTALL-038**: state.rs supports native module routing
48//! - **VAL-INSTALL-039**: All 35 components reference correct Rust modules
49//! - **VAL-INSTALL-040**: Non-installer components unchanged
50//! - **VAL-INSTALL-041**: Megatron declares dependency on PyTorch and MPI4Py
51//! - **VAL-INSTALL-042**: vLLM declares dependency on PyTorch
52//! - **VAL-INSTALL-043**: DeepSpeed declares dependency on PyTorch
53//! - **VAL-INSTALL-044**: Flash Attention declares dependency on PyTorch and ROCm
54//! - **VAL-INSTALL-045**: ONNX Runtime declares dependency on ROCm
55//! - **VAL-INSTALL-046**: AITER declares dependency on PyTorch and ROCm
56//! - **VAL-INSTALL-047**: ComfyUI declares dependency on PyTorch
57//! - **VAL-INSTALL-048**: Dependency graph is acyclic
58//! - **VAL-INSTALL-049**: installer.rs respects dependency ordering
59//! - **VAL-INSTALL-051**: All installer modules have unit tests
60//! - **VAL-INSTALL-052**: installer.rs integration tests exist
61
62pub mod aiter;
63pub mod amdgpu_drivers;
64pub mod bitsandbytes_multi;
65pub mod comfyui;
66pub mod deepspeed;
67pub mod fastvideo;
68pub mod flash_attention_ck;
69pub mod llama_cpp;
70pub mod megatron;
71pub mod migraphx_multi;
72pub mod migraphx_python;
73pub mod ml_stack;
74pub mod mpi4py;
75pub mod onnxruntime;
76pub mod permanent_env;
77pub mod pytorch;
78pub mod pytorch_profiler;
79pub mod rccl;
80pub mod repair;
81pub mod rocm;
82pub mod rocm_smi;
83pub mod textgen;
84pub mod triton;
85pub mod vllm_multi;
86pub mod vllm_studio;
87pub mod wandb;
88
89// Re-export key types for convenience
90pub use aiter::{AiterConfig, AiterInstaller};
91pub use amdgpu_drivers::{AmdgpuConfig, AmdgpuInstaller};
92pub use bitsandbytes_multi::{BitsAndBytesConfig, BitsAndBytesInstaller};
93pub use comfyui::{ComfyuiConfig, ComfyuiInstaller};
94pub use deepspeed::{DeepSpeedConfig, DeepSpeedInstaller};
95pub use flash_attention_ck::{FlashAttentionConfig, FlashAttentionInstaller, GpuArch};
96pub use llama_cpp::{LlamaCppConfig, LlamaCppInstaller};
97pub use megatron::{MegatronConfig, MegatronInstaller};
98pub use migraphx_multi::{MigraphxConfig, MigraphxInstaller, MigraphxSupport};
99pub use migraphx_python::{
100    InstallMethod as MigraphxPythonInstallMethod, MigraphxPythonConfig, MigraphxPythonInstaller,
101};
102pub use ml_stack::{MlStackConfig, MlStackInstaller};
103pub use mpi4py::{Mpi4PyConfig, Mpi4PyInstaller, MpiImplementation};
104pub use onnxruntime::{HipArchs, OnnxRuntimeConfig, OnnxRuntimeInstaller};
105pub use permanent_env::{PermanentEnvConfig, PermanentEnvInstaller};
106pub use pytorch::{PyTorchConfig, PyTorchInstaller, TorchChannel};
107pub use pytorch_profiler::{PytorchProfilerConfig, PytorchProfilerInstaller};
108pub use repair::{RepairConfig, RepairInstaller, RepairResult, RepairStep};
109pub use rocm::{RocmChannel, RocmConfig, RocmInstallType, RocmInstaller};
110pub use rocm_smi::RocmSmiInstaller;
111pub use textgen::{TextgenConfig, TextgenInstaller};
112pub use triton::{TritonBranch, TritonConfig, TritonInstaller};
113pub use vllm_multi::{VllmConfig, VllmInstaller};
114pub use vllm_studio::{VllmStudioConfig, VllmStudioInstaller};
115pub use wandb::{WandbConfig, WandbInstaller};
116
117// ===========================================================================
118// Installer Dispatch (VAL-INSTALL-031, VAL-INSTALL-032, VAL-INSTALL-039)
119// ===========================================================================
120
121/// The set of all component IDs that have been ported to native Rust.
122///
123/// These are the installer components that should NOT spawn bash subprocesses.
124/// Verification and performance components are now routed through native Rust modules.
125///
126/// Note: most of these appear in `state::default_components()`. A few utility
127/// components (`amdgpu-drivers`, `migraphx-python`, `enhanced-env`) are
128/// chain-referenced instead of shown as top-level selectable TUI components.
129pub const NATIVE_COMPONENT_IDS: &[&str] = &[
130    "permanent-env",
131    "rocm",
132    "pytorch",
133    "triton",
134    "mpi4py",
135    "deepspeed",
136    "ml-stack-core",
137    "flash-attn",
138    "flash-attn-triton",
139    "flash-attn-ck",
140    "repair-stack",
141    "rccl-repair",
142    "megatron",
143    "vllm",
144    "aiter",
145    "vllm-studio",
146    "comfyui",
147    "textgen",
148    "onnx",
149    "bitsandbytes",
150    "rocm-smi",
151    "migraphx",
152    "pytorch-profiler",
153    "wandb",
154    // Chain-referenced / utility scripts (ported but not top-level TUI components)
155    "amdgpu-drivers",
156    "migraphx-python",
157    "enhanced-env",
158    // Benchmark components (dispatched via benchmark_runners module)
159    "mlperf-inference",
160    "rocm-benchmarks",
161    "gpu-memory-bandwidth",
162    "rocm-smi-bench",
163    "pytorch-performance",
164    "vllm-performance",
165    "deepspeed-performance",
166    "megatron-performance",
167    "onnx-performance",
168    "rusty-llama-performance",
169    "flash-attention-ck-performance",
170    "all-benchmarks",
171    // FastVideo component
172    "fastvideo",
173    // llama.cpp component (HIP/ROCm CMake source build)
174    "llama-cpp",
175];
176
177/// Returns `true` if the given component ID has been ported to native Rust
178/// and should be dispatched through the Rust installer path instead of
179/// spawning a bash subprocess.
180///
181/// # Validation Assertions
182///
183/// - **VAL-INSTALL-031**: installer.rs no bash subprocess for ported components
184/// - **VAL-INSTALL-039**: All 35 components reference correct Rust modules
185pub fn is_native_component(component_id: &str) -> bool {
186    NATIVE_COMPONENT_IDS.contains(&component_id)
187}
188
189/// Dependency declarations for each native installer component.
190///
191/// Maps component IDs to the list of component IDs they depend on.
192/// Dependencies must be installed before dependents.
193///
194/// # Validation Assertions
195///
196/// - **VAL-INSTALL-041**: Megatron depends on PyTorch and MPI4Py
197/// - **VAL-INSTALL-042**: vLLM depends on PyTorch
198/// - **VAL-INSTALL-043**: DeepSpeed depends on PyTorch
199/// - **VAL-INSTALL-044**: Flash Attention depends on PyTorch and ROCm
200/// - **VAL-INSTALL-045**: ONNX Runtime depends on ROCm
201/// - **VAL-INSTALL-046**: AITER depends on PyTorch and ROCm
202/// - **VAL-INSTALL-047**: ComfyUI depends on PyTorch
203/// - **VAL-CROSS-001**: PyTorch for ROCm depends on ROCm
204pub fn get_dependencies(component_id: &str) -> &'static [&'static str] {
205    match component_id {
206        "megatron" => &["pytorch", "mpi4py"],
207        "vllm" => &["pytorch"],
208        "aiter" => &["pytorch", "rocm"],
209        // Flash Attention: "flash-attn" is the legacy/canonical id. The executor
210        // normalizes it to `flash-attn-triton` (the recommended backend), so it
211        // MUST share Triton's closure — including `aiter`, whose ops.triton the
212        // Triton backend imports at runtime. flash-attn-ck is the concrete CK
213        // backend (forward-only on RDNA3; uses composable_kernel, not aiter).
214        "flash-attn" | "flash-attn-triton" => &["pytorch", "rocm", "aiter"],
215        "flash-attn-ck" => &["pytorch", "rocm"],
216        "onnx" => &["rocm"],
217        "deepspeed" => &["pytorch"],
218        "comfyui" => &["pytorch"],
219        "pytorch" => &["rocm"], // PyTorch for ROCm requires ROCm installed
220        "pytorch-profiler" => &["pytorch"],
221        "llama-cpp" => &["rocm"], // llama.cpp HIP build requires ROCm
222        // All other native components have no cross-component dependencies
223        _ => &[],
224    }
225}
226
227/// Sort component IDs in topological order so dependencies are installed
228/// before dependents.
229///
230/// Uses Kahn's algorithm (BFS-based topological sort).
231///
232/// # Validation Assertions
233///
234/// - **VAL-INSTALL-048**: Dependency graph is acyclic
235/// - **VAL-INSTALL-049**: installer.rs respects dependency ordering
236///
237/// # Returns
238///
239/// `Ok(sorted_ids)` on success, `Err(cycle_description)` if a cycle is detected.
240pub fn topological_sort(component_ids: &[String]) -> Result<Vec<String>, String> {
241    use std::collections::{HashMap, HashSet, VecDeque};
242
243    let id_set: HashSet<&str> = component_ids.iter().map(|s| s.as_str()).collect();
244
245    // Build adjacency list: dep -> list of dependents
246    let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
247    let mut in_degree: HashMap<&str, usize> = HashMap::new();
248
249    for id in component_ids.iter().map(|s| s.as_str()) {
250        in_degree.entry(id).or_insert(0);
251        adj.entry(id).or_default();
252    }
253
254    for id in component_ids.iter().map(|s| s.as_str()) {
255        for dep in get_dependencies(id) {
256            if id_set.contains(dep) {
257                // dep must come before id
258                adj.entry(dep).or_default().push(id);
259                *in_degree.entry(id).or_insert(0) += 1;
260            }
261        }
262    }
263
264    let mut queue: VecDeque<&str> = VecDeque::new();
265    for (&id, &degree) in &in_degree {
266        if degree == 0 {
267            queue.push_back(id);
268        }
269    }
270
271    let mut sorted = Vec::with_capacity(component_ids.len());
272    while let Some(id) = queue.pop_front() {
273        sorted.push(id.to_string());
274        if let Some(dependents) = adj.get(id) {
275            for &dependent in dependents {
276                if let Some(degree) = in_degree.get_mut(dependent) {
277                    *degree -= 1;
278                    if *degree == 0 {
279                        queue.push_back(dependent);
280                    }
281                }
282            }
283        }
284    }
285
286    if sorted.len() != component_ids.len() {
287        let remaining: Vec<&str> = in_degree
288            .iter()
289            .filter(|(_, &d)| d > 0)
290            .map(|(&id, _)| id)
291            .collect();
292        return Err(format!(
293            "Dependency cycle detected among: {}",
294            remaining.join(", ")
295        ));
296    }
297
298    Ok(sorted)
299}
300
301#[cfg(test)]
302mod dispatch_tests {
303    use super::*;
304
305    #[test]
306    fn test_all_native_components_listed() {
307        // 28 installer/action ids + 11 benchmarks + fastvideo + llama-cpp.
308        assert_eq!(NATIVE_COMPONENT_IDS.len(), 41);
309    }
310
311    #[test]
312    fn test_is_native_component_true_for_ported() {
313        for id in NATIVE_COMPONENT_IDS {
314            assert!(is_native_component(id), "Expected '{}' to be native", id);
315        }
316    }
317
318    #[test]
319    fn test_is_native_component_false_for_verification() {
320        assert!(!is_native_component("verify-basic"));
321        assert!(!is_native_component("verify-enhanced"));
322        assert!(!is_native_component("verify-build"));
323    }
324
325    #[test]
326    fn test_is_native_component_true_for_performance() {
327        assert!(is_native_component("mlperf-inference"));
328        assert!(is_native_component("rocm-benchmarks"));
329        assert!(is_native_component("onnx-performance"));
330        assert!(is_native_component("all-benchmarks"));
331        assert!(is_native_component("fastvideo"));
332    }
333
334    #[test]
335    fn test_is_native_component_false_for_unknown() {
336        assert!(!is_native_component("nonexistent"));
337        assert!(!is_native_component(""));
338    }
339
340    #[test]
341    fn test_megatron_dependencies() {
342        let deps = get_dependencies("megatron");
343        assert!(deps.contains(&"pytorch"));
344        assert!(deps.contains(&"mpi4py"));
345    }
346
347    #[test]
348    fn test_vllm_dependencies() {
349        let deps = get_dependencies("vllm");
350        assert!(deps.contains(&"pytorch"));
351    }
352
353    #[test]
354    fn test_deepspeed_dependencies() {
355        let deps = get_dependencies("deepspeed");
356        assert!(deps.contains(&"pytorch"));
357    }
358
359    #[test]
360    fn test_flash_attention_dependencies() {
361        // All three ids share pytorch + rocm. The legacy `flash-attn` id is
362        // normalized to `flash-attn-triton` by the executor (and is the default
363        // survivor of the `flash-attn-backend` exclusive group), so it MUST
364        // carry the Triton closure — including aiter. Cover it explicitly so a
365        // regression in the canonical legacy path cannot pass this suite.
366        for id in &["flash-attn", "flash-attn-triton", "flash-attn-ck"] {
367            let deps = get_dependencies(id);
368            assert!(deps.contains(&"pytorch"), "{id} must depend on pytorch");
369            assert!(deps.contains(&"rocm"), "{id} must depend on rocm");
370        }
371        // FA-Triton (and the legacy id normalized to it) imports aiter.ops.triton
372        // at runtime, so both must pull aiter; CK uses composable_kernel and must not.
373        for id in &["flash-attn", "flash-attn-triton"] {
374            let deps = get_dependencies(id);
375            assert!(
376                deps.contains(&"aiter"),
377                "{id} (Triton closure) must depend on aiter"
378            );
379        }
380        let ck_deps = get_dependencies("flash-attn-ck");
381        assert!(
382            !ck_deps.contains(&"aiter"),
383            "FA-CK must NOT depend on aiter (uses composable_kernel)"
384        );
385    }
386
387    #[test]
388    fn test_onnx_dependencies() {
389        let deps = get_dependencies("onnx");
390        assert!(deps.contains(&"rocm"));
391    }
392
393    #[test]
394    fn test_aiter_dependencies() {
395        let deps = get_dependencies("aiter");
396        assert!(deps.contains(&"pytorch"));
397        assert!(deps.contains(&"rocm"));
398    }
399
400    #[test]
401    fn test_comfyui_dependencies() {
402        let deps = get_dependencies("comfyui");
403        assert!(deps.contains(&"pytorch"));
404    }
405
406    #[test]
407    fn test_llama_cpp_dependencies() {
408        let deps = get_dependencies("llama-cpp");
409        assert!(deps.contains(&"rocm"), "llama-cpp must depend on ROCm");
410    }
411
412    #[test]
413    fn test_no_deps_components() {
414        for id in &[
415            "rocm",
416            "triton",
417            "mpi4py",
418            "ml-stack-core",
419            "permanent-env",
420            "rocm-smi",
421            "bitsandbytes",
422            "wandb",
423            "migraphx",
424            "vllm-studio",
425            "textgen",
426            "repair-stack",
427            "rccl-repair",
428            "amdgpu-drivers",
429            "migraphx-python",
430        ] {
431            assert!(
432                get_dependencies(id).is_empty(),
433                "Expected no dependencies for '{}', got {:?}",
434                id,
435                get_dependencies(id)
436            );
437        }
438    }
439
440    #[test]
441    fn test_pytorch_depends_on_rocm() {
442        let deps = get_dependencies("pytorch");
443        assert!(
444            deps.contains(&"rocm"),
445            "PyTorch for ROCm must depend on ROCm"
446        );
447    }
448
449    #[test]
450    fn test_topological_sort_empty() {
451        let result = topological_sort(&[]);
452        assert!(result.is_ok());
453        assert!(result.unwrap().is_empty());
454    }
455
456    #[test]
457    fn test_topological_sort_no_deps() {
458        let ids = vec![
459            "rocm".to_string(),
460            "pytorch".to_string(),
461            "triton".to_string(),
462        ];
463        let result = topological_sort(&ids);
464        assert!(result.is_ok());
465        assert_eq!(result.unwrap().len(), 3);
466    }
467
468    #[test]
469    fn test_topological_sort_respects_dependencies() {
470        let ids = vec![
471            "megatron".to_string(),
472            "pytorch".to_string(),
473            "mpi4py".to_string(),
474        ];
475        let result = topological_sort(&ids).unwrap();
476        let pytorch_pos = result.iter().position(|s| s == "pytorch").unwrap();
477        let mpi4py_pos = result.iter().position(|s| s == "mpi4py").unwrap();
478        let megatron_pos = result.iter().position(|s| s == "megatron").unwrap();
479        assert!(
480            pytorch_pos < megatron_pos,
481            "pytorch must come before megatron"
482        );
483        assert!(
484            mpi4py_pos < megatron_pos,
485            "mpi4py must come before megatron"
486        );
487    }
488
489    #[test]
490    fn test_topological_sort_complex_graph() {
491        let ids = vec![
492            "megatron".to_string(),
493            "flash-attn-triton".to_string(),
494            "deepspeed".to_string(),
495            "vllm".to_string(),
496            "aiter".to_string(),
497            "onnx".to_string(),
498            "comfyui".to_string(),
499            "pytorch".to_string(),
500            "rocm".to_string(),
501            "mpi4py".to_string(),
502            "triton".to_string(),
503        ];
504        let result = topological_sort(&ids).unwrap();
505        assert_eq!(result.len(), 11);
506
507        // Verify all dependency constraints
508        let pos = |id: &str| result.iter().position(|s| s == id).unwrap();
509        assert!(pos("rocm") < pos("flash-attn-triton"));
510        assert!(pos("rocm") < pos("onnx"));
511        assert!(pos("rocm") < pos("aiter"));
512        assert!(pos("pytorch") < pos("megatron"));
513        assert!(pos("mpi4py") < pos("megatron"));
514        assert!(pos("pytorch") < pos("vllm"));
515        assert!(pos("pytorch") < pos("deepspeed"));
516        assert!(pos("pytorch") < pos("flash-attn-triton"));
517        assert!(pos("pytorch") < pos("aiter"));
518        assert!(pos("pytorch") < pos("comfyui"));
519    }
520
521    #[test]
522    fn test_topological_sort_filters_unrelated_deps() {
523        // Only pytorch and vllm in the set; vllm depends on pytorch
524        let ids = vec!["vllm".to_string(), "pytorch".to_string()];
525        let result = topological_sort(&ids).unwrap();
526        assert_eq!(result.len(), 2);
527        assert_eq!(result[0], "pytorch");
528        assert_eq!(result[1], "vllm");
529    }
530
531    #[test]
532    fn test_dependency_graph_is_acyclic() {
533        // Verify all native components can be sorted together
534        let all_ids: Vec<String> = NATIVE_COMPONENT_IDS.iter().map(|s| s.to_string()).collect();
535        let result = topological_sort(&all_ids);
536        assert!(
537            result.is_ok(),
538            "Dependency graph must be acyclic: {:?}",
539            result
540        );
541        let sorted = result.unwrap();
542        assert_eq!(sorted.len(), NATIVE_COMPONENT_IDS.len());
543    }
544
545    #[test]
546    fn test_native_ids_match_state_component_ids() {
547        // Ensure all NATIVE_COMPONENT_IDS actually correspond to components
548        // defined in state.rs default_components()
549        for id in NATIVE_COMPONENT_IDS {
550            // Just verify they're non-empty strings
551            assert!(!id.is_empty(), "Component ID must not be empty");
552            assert!(
553                id.chars().all(|c| c == '-' || c.is_ascii_alphanumeric()),
554                "Component ID '{}' must be alphanumeric with hyphens",
555                id
556            );
557        }
558    }
559}