Skip to main content

rusty_stack/installers/components/
flash_attention_ck.rs

1//! Flash Attention CK installer — ports `scripts/install_flash_attention_ck.sh`.
2//!
3//! Constructs correct cmake commands with HIP compiler flags and ROCm path,
4//! make commands with job count, and pip install from build output.
5//!
6//! Flash Attention CK depends on PyTorch (ROCm-enabled) and ROCm.
7//!
8//! # Validation Assertions
9//!
10//! - **VAL-INSTALL-008**: Flash Attention correct cmake command
11//! - **VAL-INSTALL-009**: Flash Attention correct make command
12//! - **VAL-INSTALL-014**: Source builds parse build output for errors
13//! - **VAL-INSTALL-044**: Flash Attention declares dependency on PyTorch and ROCm
14
15use crate::installers::common::RocmEnv;
16use std::fmt;
17use std::path::PathBuf;
18
19// ===========================================================================
20// Types
21// ===========================================================================
22
23/// Installation method.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum InstallMethod {
26    /// Install globally.
27    Global,
28    /// Install in a virtual environment.
29    Venv,
30    /// Try global, fallback to venv.
31    Auto,
32}
33
34impl fmt::Display for InstallMethod {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        match self {
37            InstallMethod::Global => write!(f, "global"),
38            InstallMethod::Venv => write!(f, "venv"),
39            InstallMethod::Auto => write!(f, "auto"),
40        }
41    }
42}
43
44/// GPU architecture specification for building.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum GpuArch {
47    /// Detected or specified GPU architecture (e.g., "gfx1100").
48    Known(String),
49    /// Default fallback architecture.
50    Default,
51}
52
53impl GpuArch {
54    /// Get the architecture string.
55    pub fn as_str(&self) -> &str {
56        match self {
57            GpuArch::Known(s) => s,
58            GpuArch::Default => "gfx1100",
59        }
60    }
61}
62
63/// Configuration for the Flash Attention CK installer.
64#[derive(Debug, Clone)]
65pub struct FlashAttentionConfig {
66    /// Installation method.
67    pub method: InstallMethod,
68    /// Python binary to use.
69    pub python_bin: String,
70    /// Whether to force reinstall.
71    pub force_reinstall: bool,
72    /// Whether to run in dry-run mode.
73    pub dry_run: bool,
74    /// Installation directory (defaults to `$HOME/ml_stack/flash_attn_amd`).
75    pub install_dir: Option<PathBuf>,
76    /// GPU architecture override.
77    pub gpu_arch: GpuArch,
78    /// Number of parallel build jobs (defaults to nproc).
79    pub build_jobs: Option<usize>,
80}
81
82impl Default for FlashAttentionConfig {
83    fn default() -> Self {
84        Self {
85            method: InstallMethod::Auto,
86            python_bin: "python3".to_string(),
87            force_reinstall: false,
88            dry_run: false,
89            install_dir: None,
90            gpu_arch: GpuArch::Default,
91            build_jobs: None,
92        }
93    }
94}
95
96impl FlashAttentionConfig {
97    /// Get the installation directory path.
98    pub fn install_dir(&self) -> PathBuf {
99        self.install_dir
100            .clone()
101            .unwrap_or_else(|| dirs_home().join("ml_stack").join("flash_attn_amd"))
102    }
103
104    /// Get the number of build jobs (defaults to num_cpus).
105    pub fn build_jobs(&self) -> usize {
106        self.build_jobs.unwrap_or_else(num_cpus)
107    }
108}
109
110/// A constructed shell command.
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct ShellCommand {
113    /// The program to run.
114    pub program: String,
115    /// Arguments to pass.
116    pub args: Vec<String>,
117    /// Environment variables to set.
118    pub env: Vec<(String, String)>,
119    /// Working directory for the command.
120    pub working_dir: Option<PathBuf>,
121}
122
123impl ShellCommand {
124    /// Format as a shell command string.
125    pub fn to_command_string(&self) -> String {
126        let env_prefix = self
127            .env
128            .iter()
129            .map(|(k, v)| format!("{k}={v}"))
130            .collect::<Vec<_>>()
131            .join(" ");
132        let cmd = if self.args.is_empty() {
133            self.program.clone()
134        } else {
135            format!("{} {}", self.program, self.args.join(" "))
136        };
137        if env_prefix.is_empty() {
138            cmd
139        } else {
140            format!("{env_prefix} {cmd}")
141        }
142    }
143}
144
145/// The Flash Attention CK installer.
146pub struct FlashAttentionInstaller {
147    config: FlashAttentionConfig,
148}
149
150impl FlashAttentionInstaller {
151    /// Create a new Flash Attention installer with the given config.
152    pub fn new(config: FlashAttentionConfig) -> Self {
153        Self { config }
154    }
155
156    /// Create with default config.
157    pub fn with_defaults() -> Self {
158        Self::new(FlashAttentionConfig::default())
159    }
160
161    // -----------------------------------------------------------------------
162    // Dependencies (VAL-INSTALL-044)
163    // -----------------------------------------------------------------------
164
165    /// Get the list of required dependencies.
166    ///
167    /// Flash Attention CK depends on:
168    /// - PyTorch (ROCm-enabled)
169    /// - ROCm
170    pub fn dependencies(&self) -> &[&str] {
171        &["pytorch", "rocm"]
172    }
173
174    // -----------------------------------------------------------------------
175    // ROCm build environment (VAL-INSTALL-008)
176    // -----------------------------------------------------------------------
177
178    /// Get the ROCm environment variables needed for the build.
179    ///
180    /// The original script sets:
181    /// - HSA_OVERRIDE_GFX_VERSION=11.0.0
182    /// - PYTORCH_ROCM_ARCH=gfx1100 (or detected)
183    /// - ROCM_PATH=/opt/rocm
184    /// - PATH=/opt/rocm/bin:$PATH
185    /// - LD_LIBRARY_PATH=/opt/rocm/lib:$LD_LIBRARY_PATH
186    /// - HSA_TOOLS_LIB (if rocprofiler library exists)
187    pub fn rocm_build_env(&self, rocm_env: &RocmEnv) -> Vec<(String, String)> {
188        let rocm_path = rocm_env
189            .path()
190            .map(|p| p.to_string_lossy().to_string())
191            .unwrap_or_else(|| "/opt/rocm".to_string());
192
193        let mut env = vec![
194            ("HSA_OVERRIDE_GFX_VERSION".to_string(), "11.0.0".to_string()),
195            (
196                "PYTORCH_ROCM_ARCH".to_string(),
197                self.config.gpu_arch.as_str().to_string(),
198            ),
199            ("ROCM_PATH".to_string(), rocm_path.clone()),
200            (
201                "PATH".to_string(),
202                format!(
203                    "{}/bin:{}",
204                    rocm_path,
205                    std::env::var("PATH").unwrap_or_default()
206                ),
207            ),
208            (
209                "LD_LIBRARY_PATH".to_string(),
210                format!(
211                    "{}/lib:{}",
212                    rocm_path,
213                    std::env::var("LD_LIBRARY_PATH").unwrap_or_default()
214                ),
215            ),
216        ];
217
218        // HSA_TOOLS_LIB - check for rocprofiler library
219        // Try the ROCm 7.x layout first (lib/rocprofiler-sdk/), then the old layout (lib/)
220        let profiler_lib = rocm_env.path().and_then(|p| {
221            let new_layout = p.join("lib/rocprofiler-sdk/librocprofiler-sdk-tool.so");
222            let old_layout = p.join("lib/librocprofiler-sdk-tool.so");
223            if new_layout.exists() {
224                Some(new_layout)
225            } else if old_layout.exists() {
226                Some(old_layout)
227            } else {
228                None
229            }
230        });
231        if let Some(lib) = profiler_lib {
232            env.push((
233                "HSA_TOOLS_LIB".to_string(),
234                lib.to_string_lossy().to_string(),
235            ));
236        } else {
237            env.push(("HSA_TOOLS_LIB".to_string(), "0".to_string()));
238        }
239
240        env
241    }
242
243    // -----------------------------------------------------------------------
244    // Git clone command
245    // -----------------------------------------------------------------------
246
247    /// Construct the git clone command for Flash Attention.
248    ///
249    /// The original script clones from:
250    /// `https://github.com/ROCmSoftwarePlatform/flash-attention.git`
251    /// and checks out the `main` branch.
252    pub fn build_git_clone_command(&self) -> ShellCommand {
253        let install_dir = self.config.install_dir();
254        ShellCommand {
255            program: "git".to_string(),
256            args: vec![
257                "clone".to_string(),
258                "https://github.com/ROCmSoftwarePlatform/flash-attention.git".to_string(),
259                install_dir.to_string_lossy().to_string(),
260            ],
261            env: vec![],
262            working_dir: None,
263        }
264    }
265
266    /// Construct the git checkout command.
267    pub fn build_git_checkout_command(&self) -> ShellCommand {
268        ShellCommand {
269            program: "git".to_string(),
270            args: vec!["checkout".to_string(), "main".to_string()],
271            env: vec![],
272            working_dir: Some(self.config.install_dir()),
273        }
274    }
275
276    // -----------------------------------------------------------------------
277    // CMake command (VAL-INSTALL-008)
278    // -----------------------------------------------------------------------
279
280    /// Construct the cmake configure command.
281    ///
282    /// The original script configures cmake with:
283    /// - CMAKE_PREFIX_PATH: torch cmake prefix + ROCm path
284    /// - CMAKE_BUILD_TYPE=Release
285    /// - GPU_TARGETS=gfx1100 (or detected)
286    /// - CMAKE_CXX_FLAGS="-Wno-error"
287    /// - ROCM_PATH=/opt/rocm
288    ///
289    /// Working directory: `<install_dir>/build`
290    pub fn build_cmake_command(
291        &self,
292        rocm_env: &RocmEnv,
293        torch_cmake_prefix: &str,
294    ) -> ShellCommand {
295        let rocm_path = rocm_env
296            .path()
297            .map(|p| p.to_string_lossy().to_string())
298            .unwrap_or_else(|| "/opt/rocm".to_string());
299
300        let prefix_path = format!("{};{};/opt/rocm", torch_cmake_prefix, rocm_path);
301
302        let mut args = vec![
303            "..".to_string(),
304            format!("-DCMAKE_PREFIX_PATH={}", prefix_path),
305            "-DCMAKE_BUILD_TYPE=Release".to_string(),
306            format!("-DGPU_TARGETS={}", self.config.gpu_arch.as_str()),
307            "-DCMAKE_CXX_FLAGS=-Wno-error".to_string(),
308        ];
309
310        args.push(format!("-DROCM_PATH={}", rocm_path));
311
312        ShellCommand {
313            program: "cmake".to_string(),
314            args,
315            env: vec![],
316            working_dir: Some(self.config.install_dir().join("build")),
317        }
318    }
319
320    // -----------------------------------------------------------------------
321    // Make / build command (VAL-INSTALL-009)
322    // -----------------------------------------------------------------------
323
324    /// Construct the cmake --build command.
325    ///
326    /// The original script uses:
327    /// `cmake --build . --config Release -j $(nproc)`
328    ///
329    /// Working directory: `<install_dir>/build`
330    pub fn build_make_command(&self) -> ShellCommand {
331        let jobs = self.config.build_jobs();
332        ShellCommand {
333            program: "cmake".to_string(),
334            args: vec![
335                "--build".to_string(),
336                ".".to_string(),
337                "--config".to_string(),
338                "Release".to_string(),
339                "-j".to_string(),
340                jobs.to_string(),
341            ],
342            env: vec![],
343            working_dir: Some(self.config.install_dir().join("build")),
344        }
345    }
346
347    // -----------------------------------------------------------------------
348    // Setup.py install command
349    // -----------------------------------------------------------------------
350
351    /// Construct the Python setup.py install command.
352    ///
353    /// The original script runs:
354    /// `python3 setup_flash_attn_amd.py install`
355    pub fn build_setup_install_command(&self) -> ShellCommand {
356        let args = vec!["setup_flash_attn_amd.py".to_string(), "install".to_string()];
357
358        ShellCommand {
359            program: self.config.python_bin.clone(),
360            args,
361            env: vec![],
362            working_dir: Some(self.config.install_dir()),
363        }
364    }
365
366    // -----------------------------------------------------------------------
367    // Build error detection (VAL-INSTALL-014)
368    // -----------------------------------------------------------------------
369
370    /// Check build output for errors.
371    ///
372    /// Detects common cmake and make failure patterns in the output.
373    /// Returns an error message if a failure is detected.
374    pub fn check_build_output(
375        &self,
376        stdout: &str,
377        stderr: &str,
378        exit_code: i32,
379    ) -> Result<(), String> {
380        if exit_code != 0 {
381            // Check for specific error patterns
382            let error_patterns = [
383                "CMake Error",
384                "error:",
385                "Error:",
386                "FAILED:",
387                "fatal error:",
388                "undefined reference",
389                "No such file or directory",
390                "Permission denied",
391            ];
392
393            let combined = format!("{}\n{}", stdout, stderr);
394            let mut detected_errors: Vec<&str> = Vec::new();
395
396            for pattern in &error_patterns {
397                if combined.contains(pattern) {
398                    detected_errors.push(*pattern);
399                }
400            }
401
402            if detected_errors.is_empty() {
403                return Err(format!(
404                    "Build command failed with exit code {} (no specific error pattern detected)",
405                    exit_code
406                ));
407            }
408
409            return Err(format!(
410                "Build failed with exit code {}. Detected errors: {}",
411                exit_code,
412                detected_errors.join(", ")
413            ));
414        }
415
416        Ok(())
417    }
418}
419
420/// Get the home directory.
421fn dirs_home() -> PathBuf {
422    std::env::var("HOME")
423        .map(PathBuf::from)
424        .unwrap_or_else(|_| PathBuf::from("/root"))
425}
426
427/// Get the number of CPUs.
428fn num_cpus() -> usize {
429    std::thread::available_parallelism()
430        .map(|n| n.get())
431        .unwrap_or(4)
432}
433
434// ===========================================================================
435// Tests
436// ===========================================================================
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441    use std::path::PathBuf;
442
443    // --- VAL-INSTALL-008: Flash Attention correct cmake command ---
444
445    #[test]
446    fn test_cmake_command_has_correct_flags() {
447        let installer = FlashAttentionInstaller::new(FlashAttentionConfig {
448            gpu_arch: GpuArch::Known("gfx1100".to_string()),
449            ..Default::default()
450        });
451        let rocm_env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
452        let cmd = installer.build_cmake_command(
453            &rocm_env,
454            "/usr/lib/python3/dist-packages/torch/share/cmake",
455        );
456
457        assert_eq!(cmd.program, "cmake");
458        assert!(cmd.args.contains(&"..".to_string()));
459        assert!(cmd.args.iter().any(|a| a.contains("CMAKE_PREFIX_PATH")));
460        assert!(cmd
461            .args
462            .iter()
463            .any(|a| a.contains("CMAKE_BUILD_TYPE=Release")));
464        assert!(cmd.args.iter().any(|a| a.contains("GPU_TARGETS=gfx1100")));
465        assert!(cmd
466            .args
467            .iter()
468            .any(|a| a.contains("CMAKE_CXX_FLAGS=-Wno-error")));
469        assert!(cmd.args.iter().any(|a| a.contains("ROCM_PATH=/opt/rocm")));
470    }
471
472    #[test]
473    fn test_cmake_command_includes_torch_prefix() {
474        let installer = FlashAttentionInstaller::with_defaults();
475        let rocm_env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
476        let cmd = installer.build_cmake_command(&rocm_env, "/torch/cmake/prefix");
477        let prefix_arg = cmd
478            .args
479            .iter()
480            .find(|a| a.starts_with("-DCMAKE_PREFIX_PATH="));
481        assert!(prefix_arg.is_some());
482        let prefix = prefix_arg.unwrap();
483        assert!(prefix.contains("/torch/cmake/prefix"));
484        assert!(prefix.contains("/opt/rocm"));
485    }
486
487    #[test]
488    fn test_cmake_command_working_dir() {
489        let installer = FlashAttentionInstaller::new(FlashAttentionConfig {
490            install_dir: Some(PathBuf::from("/home/user/ml_stack/flash_attn_amd")),
491            ..Default::default()
492        });
493        let rocm_env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
494        let cmd = installer.build_cmake_command(&rocm_env, "/torch/cmake");
495        assert_eq!(
496            cmd.working_dir,
497            Some(PathBuf::from("/home/user/ml_stack/flash_attn_amd/build"))
498        );
499    }
500
501    #[test]
502    fn test_cmake_command_custom_gpu_arch() {
503        let installer = FlashAttentionInstaller::new(FlashAttentionConfig {
504            gpu_arch: GpuArch::Known("gfx1030".to_string()),
505            ..Default::default()
506        });
507        let rocm_env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
508        let cmd = installer.build_cmake_command(&rocm_env, "/torch/cmake");
509        assert!(cmd.args.iter().any(|a| a.contains("GPU_TARGETS=gfx1030")));
510    }
511
512    // --- VAL-INSTALL-009: Flash Attention correct make command ---
513
514    #[test]
515    fn test_make_command_correct_flags() {
516        let installer = FlashAttentionInstaller::with_defaults();
517        let cmd = installer.build_make_command();
518
519        assert_eq!(cmd.program, "cmake");
520        assert!(cmd.args.contains(&"--build".to_string()));
521        assert!(cmd.args.contains(&".".to_string()));
522        assert!(cmd.args.contains(&"--config".to_string()));
523        assert!(cmd.args.contains(&"Release".to_string()));
524        assert!(cmd.args.contains(&"-j".to_string()));
525    }
526
527    #[test]
528    fn test_make_command_job_count() {
529        let installer = FlashAttentionInstaller::new(FlashAttentionConfig {
530            build_jobs: Some(8),
531            ..Default::default()
532        });
533        let cmd = installer.build_make_command();
534        assert!(cmd.args.contains(&"8".to_string()));
535    }
536
537    #[test]
538    fn test_make_command_working_dir() {
539        let installer = FlashAttentionInstaller::new(FlashAttentionConfig {
540            install_dir: Some(PathBuf::from("/home/user/ml_stack/flash_attn_amd")),
541            ..Default::default()
542        });
543        let cmd = installer.build_make_command();
544        assert_eq!(
545            cmd.working_dir,
546            Some(PathBuf::from("/home/user/ml_stack/flash_attn_amd/build"))
547        );
548    }
549
550    // --- Git clone command ---
551
552    #[test]
553    fn test_git_clone_command() {
554        let installer = FlashAttentionInstaller::with_defaults();
555        let cmd = installer.build_git_clone_command();
556        assert_eq!(cmd.program, "git");
557        assert!(cmd.args.contains(&"clone".to_string()));
558        assert!(cmd
559            .args
560            .iter()
561            .any(|a| a.contains("ROCmSoftwarePlatform/flash-attention")));
562    }
563
564    #[test]
565    fn test_git_checkout_command() {
566        let installer = FlashAttentionInstaller::with_defaults();
567        let cmd = installer.build_git_checkout_command();
568        assert_eq!(cmd.program, "git");
569        assert!(cmd.args.contains(&"checkout".to_string()));
570        assert!(cmd.args.contains(&"main".to_string()));
571    }
572
573    // --- ROCm build environment ---
574
575    #[test]
576    fn test_rocm_build_env() {
577        let installer = FlashAttentionInstaller::with_defaults();
578        let rocm_env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
579        let env = installer.rocm_build_env(&rocm_env);
580        assert!(env
581            .iter()
582            .any(|(k, v)| k == "HSA_OVERRIDE_GFX_VERSION" && v == "11.0.0"));
583        assert!(env
584            .iter()
585            .any(|(k, v)| k == "PYTORCH_ROCM_ARCH" && v == "gfx1100"));
586        assert!(env
587            .iter()
588            .any(|(k, v)| k == "ROCM_PATH" && v == "/opt/rocm"));
589        assert!(env.iter().any(|(k, _)| k == "HSA_TOOLS_LIB"));
590    }
591
592    // --- Setup.py install command ---
593
594    #[test]
595    fn test_setup_install_command() {
596        let installer = FlashAttentionInstaller::with_defaults();
597        let cmd = installer.build_setup_install_command();
598        assert_eq!(cmd.program, "python3");
599        assert!(cmd.args.contains(&"setup_flash_attn_amd.py".to_string()));
600        assert!(cmd.args.contains(&"install".to_string()));
601    }
602
603    // --- VAL-INSTALL-044: Flash Attention declares dependency on PyTorch and ROCm ---
604
605    #[test]
606    fn test_dependencies() {
607        let installer = FlashAttentionInstaller::with_defaults();
608        let deps = installer.dependencies();
609        assert!(deps.contains(&"pytorch"));
610        assert!(deps.contains(&"rocm"));
611    }
612
613    // --- VAL-INSTALL-014: Build error detection ---
614
615    #[test]
616    fn test_build_error_detection_cmake_error() {
617        let installer = FlashAttentionInstaller::with_defaults();
618        let result =
619            installer.check_build_output("some output", "CMake Error at CMakeLists.txt:10", 1);
620        assert!(result.is_err());
621        assert!(result.unwrap_err().contains("CMake Error"));
622    }
623
624    #[test]
625    fn test_build_error_detection_make_error() {
626        let installer = FlashAttentionInstaller::with_defaults();
627        let result = installer.check_build_output(
628            "compiling...",
629            "error: 'undefined_var' was not declared",
630            2,
631        );
632        assert!(result.is_err());
633        assert!(result.unwrap_err().contains("error:"));
634    }
635
636    #[test]
637    fn test_build_error_detection_success() {
638        let installer = FlashAttentionInstaller::with_defaults();
639        let result = installer.check_build_output("Build finished successfully", "", 0);
640        assert!(result.is_ok());
641    }
642
643    #[test]
644    fn test_build_error_detection_nonzero_no_pattern() {
645        let installer = FlashAttentionInstaller::with_defaults();
646        let result = installer.check_build_output("clean output", "clean output", 1);
647        assert!(result.is_err());
648        assert!(result.unwrap_err().contains("exit code 1"));
649    }
650
651    // --- Config defaults ---
652
653    #[test]
654    fn test_config_defaults() {
655        let config = FlashAttentionConfig::default();
656        assert_eq!(config.method, InstallMethod::Auto);
657        assert_eq!(config.python_bin, "python3");
658        assert!(!config.force_reinstall);
659        assert!(!config.dry_run);
660        assert!(config.install_dir.is_none());
661        assert_eq!(config.gpu_arch, GpuArch::Default);
662        assert!(config.build_jobs.is_none());
663    }
664
665    #[test]
666    fn test_gpu_arch_default() {
667        assert_eq!(GpuArch::Default.as_str(), "gfx1100");
668        assert_eq!(GpuArch::Known("gfx1030".to_string()).as_str(), "gfx1030");
669    }
670
671    #[test]
672    fn test_command_string_format() {
673        let cmd = ShellCommand {
674            program: "cmake".to_string(),
675            args: vec!["--build".to_string(), ".".to_string()],
676            env: vec![("ROCM_PATH".to_string(), "/opt/rocm".to_string())],
677            working_dir: None,
678        };
679        let s = cmd.to_command_string();
680        assert!(s.contains("ROCM_PATH=/opt/rocm"));
681        assert!(s.contains("cmake --build ."));
682    }
683}