1use crate::installers::common::RocmEnv;
16use std::fmt;
17use std::path::PathBuf;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum InstallMethod {
26 Global,
28 Venv,
30 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#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum GpuArch {
47 Known(String),
49 Default,
51}
52
53impl GpuArch {
54 pub fn as_str(&self) -> &str {
56 match self {
57 GpuArch::Known(s) => s,
58 GpuArch::Default => "gfx1100",
59 }
60 }
61}
62
63#[derive(Debug, Clone)]
65pub struct FlashAttentionConfig {
66 pub method: InstallMethod,
68 pub python_bin: String,
70 pub force_reinstall: bool,
72 pub dry_run: bool,
74 pub install_dir: Option<PathBuf>,
76 pub gpu_arch: GpuArch,
78 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 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 pub fn build_jobs(&self) -> usize {
106 self.build_jobs.unwrap_or_else(num_cpus)
107 }
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct ShellCommand {
113 pub program: String,
115 pub args: Vec<String>,
117 pub env: Vec<(String, String)>,
119 pub working_dir: Option<PathBuf>,
121}
122
123impl ShellCommand {
124 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
145pub struct FlashAttentionInstaller {
147 config: FlashAttentionConfig,
148}
149
150impl FlashAttentionInstaller {
151 pub fn new(config: FlashAttentionConfig) -> Self {
153 Self { config }
154 }
155
156 pub fn with_defaults() -> Self {
158 Self::new(FlashAttentionConfig::default())
159 }
160
161 pub fn dependencies(&self) -> &[&str] {
171 &["pytorch", "rocm"]
172 }
173
174 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 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 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 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 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 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 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 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 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
420fn dirs_home() -> PathBuf {
422 std::env::var("HOME")
423 .map(PathBuf::from)
424 .unwrap_or_else(|_| PathBuf::from("/root"))
425}
426
427fn num_cpus() -> usize {
429 std::thread::available_parallelism()
430 .map(|n| n.get())
431 .unwrap_or(4)
432}
433
434#[cfg(test)]
439mod tests {
440 use super::*;
441 use std::path::PathBuf;
442
443 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}