rusty_stack/installers/components/
flash_attention_ck.rs1use 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 build_git_clone_command(&self) -> ShellCommand {
184 let install_dir = self.config.install_dir();
185 ShellCommand {
186 program: "git".to_string(),
187 args: vec![
188 "clone".to_string(),
189 "https://github.com/ROCmSoftwarePlatform/flash-attention.git".to_string(),
190 install_dir.to_string_lossy().to_string(),
191 ],
192 env: vec![],
193 working_dir: None,
194 }
195 }
196
197 pub fn build_git_checkout_command(&self) -> ShellCommand {
199 ShellCommand {
200 program: "git".to_string(),
201 args: vec!["checkout".to_string(), "main".to_string()],
202 env: vec![],
203 working_dir: Some(self.config.install_dir()),
204 }
205 }
206
207 pub fn build_cmake_command(
222 &self,
223 rocm_env: &RocmEnv,
224 torch_cmake_prefix: &str,
225 ) -> ShellCommand {
226 let rocm_path = rocm_env
227 .path()
228 .map(|p| p.to_string_lossy().to_string())
229 .unwrap_or_else(|| "/opt/rocm".to_string());
230
231 let prefix_path = format!("{};{};/opt/rocm", torch_cmake_prefix, rocm_path);
232
233 let mut args = vec![
234 "..".to_string(),
235 format!("-DCMAKE_PREFIX_PATH={}", prefix_path),
236 "-DCMAKE_BUILD_TYPE=Release".to_string(),
237 format!("-DGPU_TARGETS={}", self.config.gpu_arch.as_str()),
238 "-DCMAKE_CXX_FLAGS=-Wno-error".to_string(),
239 ];
240
241 args.push(format!("-DROCM_PATH={}", rocm_path));
242
243 ShellCommand {
244 program: "cmake".to_string(),
245 args,
246 env: vec![],
247 working_dir: Some(self.config.install_dir().join("build")),
248 }
249 }
250
251 pub fn build_make_command(&self) -> ShellCommand {
262 let jobs = self.config.build_jobs();
263 ShellCommand {
264 program: "cmake".to_string(),
265 args: vec![
266 "--build".to_string(),
267 ".".to_string(),
268 "--config".to_string(),
269 "Release".to_string(),
270 "-j".to_string(),
271 jobs.to_string(),
272 ],
273 env: vec![],
274 working_dir: Some(self.config.install_dir().join("build")),
275 }
276 }
277
278 pub fn build_setup_install_command(&self) -> ShellCommand {
287 let args = vec!["setup_flash_attn_amd.py".to_string(), "install".to_string()];
288
289 ShellCommand {
290 program: self.config.python_bin.clone(),
291 args,
292 env: vec![],
293 working_dir: Some(self.config.install_dir()),
294 }
295 }
296
297 pub fn check_build_output(
306 &self,
307 stdout: &str,
308 stderr: &str,
309 exit_code: i32,
310 ) -> Result<(), String> {
311 if exit_code != 0 {
312 let error_patterns = [
314 "CMake Error",
315 "error:",
316 "Error:",
317 "FAILED:",
318 "fatal error:",
319 "undefined reference",
320 "No such file or directory",
321 "Permission denied",
322 ];
323
324 let combined = format!("{}\n{}", stdout, stderr);
325 let mut detected_errors: Vec<&str> = Vec::new();
326
327 for pattern in &error_patterns {
328 if combined.contains(pattern) {
329 detected_errors.push(*pattern);
330 }
331 }
332
333 if detected_errors.is_empty() {
334 return Err(format!(
335 "Build command failed with exit code {} (no specific error pattern detected)",
336 exit_code
337 ));
338 }
339
340 return Err(format!(
341 "Build failed with exit code {}. Detected errors: {}",
342 exit_code,
343 detected_errors.join(", ")
344 ));
345 }
346
347 Ok(())
348 }
349}
350
351fn dirs_home() -> PathBuf {
353 std::env::var("HOME")
354 .map(PathBuf::from)
355 .unwrap_or_else(|_| PathBuf::from("/root"))
356}
357
358fn num_cpus() -> usize {
360 std::thread::available_parallelism()
361 .map(|n| n.get())
362 .unwrap_or(4)
363}
364
365#[cfg(test)]
370mod tests {
371 use super::*;
372 use std::path::PathBuf;
373
374 #[test]
377 fn test_cmake_command_has_correct_flags() {
378 let installer = FlashAttentionInstaller::new(FlashAttentionConfig {
379 gpu_arch: GpuArch::Known("gfx1100".to_string()),
380 ..Default::default()
381 });
382 let rocm_env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
383 let cmd = installer.build_cmake_command(
384 &rocm_env,
385 "/usr/lib/python3/dist-packages/torch/share/cmake",
386 );
387
388 assert_eq!(cmd.program, "cmake");
389 assert!(cmd.args.contains(&"..".to_string()));
390 assert!(cmd.args.iter().any(|a| a.contains("CMAKE_PREFIX_PATH")));
391 assert!(cmd
392 .args
393 .iter()
394 .any(|a| a.contains("CMAKE_BUILD_TYPE=Release")));
395 assert!(cmd.args.iter().any(|a| a.contains("GPU_TARGETS=gfx1100")));
396 assert!(cmd
397 .args
398 .iter()
399 .any(|a| a.contains("CMAKE_CXX_FLAGS=-Wno-error")));
400 assert!(cmd.args.iter().any(|a| a.contains("ROCM_PATH=/opt/rocm")));
401 }
402
403 #[test]
404 fn test_cmake_command_includes_torch_prefix() {
405 let installer = FlashAttentionInstaller::with_defaults();
406 let rocm_env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
407 let cmd = installer.build_cmake_command(&rocm_env, "/torch/cmake/prefix");
408 let prefix_arg = cmd
409 .args
410 .iter()
411 .find(|a| a.starts_with("-DCMAKE_PREFIX_PATH="));
412 assert!(prefix_arg.is_some());
413 let prefix = prefix_arg.unwrap();
414 assert!(prefix.contains("/torch/cmake/prefix"));
415 assert!(prefix.contains("/opt/rocm"));
416 }
417
418 #[test]
419 fn test_cmake_command_working_dir() {
420 let installer = FlashAttentionInstaller::new(FlashAttentionConfig {
421 install_dir: Some(PathBuf::from("/home/user/ml_stack/flash_attn_amd")),
422 ..Default::default()
423 });
424 let rocm_env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
425 let cmd = installer.build_cmake_command(&rocm_env, "/torch/cmake");
426 assert_eq!(
427 cmd.working_dir,
428 Some(PathBuf::from("/home/user/ml_stack/flash_attn_amd/build"))
429 );
430 }
431
432 #[test]
433 fn test_cmake_command_custom_gpu_arch() {
434 let installer = FlashAttentionInstaller::new(FlashAttentionConfig {
435 gpu_arch: GpuArch::Known("gfx1030".to_string()),
436 ..Default::default()
437 });
438 let rocm_env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
439 let cmd = installer.build_cmake_command(&rocm_env, "/torch/cmake");
440 assert!(cmd.args.iter().any(|a| a.contains("GPU_TARGETS=gfx1030")));
441 }
442
443 #[test]
446 fn test_make_command_correct_flags() {
447 let installer = FlashAttentionInstaller::with_defaults();
448 let cmd = installer.build_make_command();
449
450 assert_eq!(cmd.program, "cmake");
451 assert!(cmd.args.contains(&"--build".to_string()));
452 assert!(cmd.args.contains(&".".to_string()));
453 assert!(cmd.args.contains(&"--config".to_string()));
454 assert!(cmd.args.contains(&"Release".to_string()));
455 assert!(cmd.args.contains(&"-j".to_string()));
456 }
457
458 #[test]
459 fn test_make_command_job_count() {
460 let installer = FlashAttentionInstaller::new(FlashAttentionConfig {
461 build_jobs: Some(8),
462 ..Default::default()
463 });
464 let cmd = installer.build_make_command();
465 assert!(cmd.args.contains(&"8".to_string()));
466 }
467
468 #[test]
469 fn test_make_command_working_dir() {
470 let installer = FlashAttentionInstaller::new(FlashAttentionConfig {
471 install_dir: Some(PathBuf::from("/home/user/ml_stack/flash_attn_amd")),
472 ..Default::default()
473 });
474 let cmd = installer.build_make_command();
475 assert_eq!(
476 cmd.working_dir,
477 Some(PathBuf::from("/home/user/ml_stack/flash_attn_amd/build"))
478 );
479 }
480
481 #[test]
484 fn test_git_clone_command() {
485 let installer = FlashAttentionInstaller::with_defaults();
486 let cmd = installer.build_git_clone_command();
487 assert_eq!(cmd.program, "git");
488 assert!(cmd.args.contains(&"clone".to_string()));
489 assert!(cmd
490 .args
491 .iter()
492 .any(|a| a.contains("ROCmSoftwarePlatform/flash-attention")));
493 }
494
495 #[test]
496 fn test_git_checkout_command() {
497 let installer = FlashAttentionInstaller::with_defaults();
498 let cmd = installer.build_git_checkout_command();
499 assert_eq!(cmd.program, "git");
500 assert!(cmd.args.contains(&"checkout".to_string()));
501 assert!(cmd.args.contains(&"main".to_string()));
502 }
503
504 #[test]
507 fn test_setup_install_command() {
508 let installer = FlashAttentionInstaller::with_defaults();
509 let cmd = installer.build_setup_install_command();
510 assert_eq!(cmd.program, "python3");
511 assert!(cmd.args.contains(&"setup_flash_attn_amd.py".to_string()));
512 assert!(cmd.args.contains(&"install".to_string()));
513 }
514
515 #[test]
518 fn test_dependencies() {
519 let installer = FlashAttentionInstaller::with_defaults();
520 let deps = installer.dependencies();
521 assert!(deps.contains(&"pytorch"));
522 assert!(deps.contains(&"rocm"));
523 }
524
525 #[test]
528 fn test_build_error_detection_cmake_error() {
529 let installer = FlashAttentionInstaller::with_defaults();
530 let result =
531 installer.check_build_output("some output", "CMake Error at CMakeLists.txt:10", 1);
532 assert!(result.is_err());
533 assert!(result.unwrap_err().contains("CMake Error"));
534 }
535
536 #[test]
537 fn test_build_error_detection_make_error() {
538 let installer = FlashAttentionInstaller::with_defaults();
539 let result = installer.check_build_output(
540 "compiling...",
541 "error: 'undefined_var' was not declared",
542 2,
543 );
544 assert!(result.is_err());
545 assert!(result.unwrap_err().contains("error:"));
546 }
547
548 #[test]
549 fn test_build_error_detection_success() {
550 let installer = FlashAttentionInstaller::with_defaults();
551 let result = installer.check_build_output("Build finished successfully", "", 0);
552 assert!(result.is_ok());
553 }
554
555 #[test]
556 fn test_build_error_detection_nonzero_no_pattern() {
557 let installer = FlashAttentionInstaller::with_defaults();
558 let result = installer.check_build_output("clean output", "clean output", 1);
559 assert!(result.is_err());
560 assert!(result.unwrap_err().contains("exit code 1"));
561 }
562
563 #[test]
566 fn test_config_defaults() {
567 let config = FlashAttentionConfig::default();
568 assert_eq!(config.method, InstallMethod::Auto);
569 assert_eq!(config.python_bin, "python3");
570 assert!(!config.force_reinstall);
571 assert!(!config.dry_run);
572 assert!(config.install_dir.is_none());
573 assert_eq!(config.gpu_arch, GpuArch::Default);
574 assert!(config.build_jobs.is_none());
575 }
576
577 #[test]
578 fn test_gpu_arch_default() {
579 assert_eq!(GpuArch::Default.as_str(), "gfx1100");
580 assert_eq!(GpuArch::Known("gfx1030".to_string()).as_str(), "gfx1030");
581 }
582
583 #[test]
584 fn test_command_string_format() {
585 let cmd = ShellCommand {
586 program: "cmake".to_string(),
587 args: vec!["--build".to_string(), ".".to_string()],
588 env: vec![("ROCM_PATH".to_string(), "/opt/rocm".to_string())],
589 working_dir: None,
590 };
591 let s = cmd.to_command_string();
592 assert!(s.contains("ROCM_PATH=/opt/rocm"));
593 assert!(s.contains("cmake --build ."));
594 }
595}