rusty_stack/installers/components/
aiter.rs1use std::path::PathBuf;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum InstallMethod {
20 Global,
22 Venv,
24 Auto,
26}
27
28impl std::fmt::Display for InstallMethod {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 match self {
31 InstallMethod::Global => write!(f, "global"),
32 InstallMethod::Venv => write!(f, "venv"),
33 InstallMethod::Auto => write!(f, "auto"),
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
40pub struct AiterConfig {
41 pub rocm_version: String,
43 pub gpu_arch: String,
45 pub python_bin: String,
47 pub method: InstallMethod,
49 pub dry_run: bool,
51 pub strict_rocm: bool,
53 pub repo_url: Option<String>,
55}
56
57impl Default for AiterConfig {
58 fn default() -> Self {
59 Self {
60 rocm_version: "7.2.0".to_string(),
61 gpu_arch: "gfx1100".to_string(),
62 python_bin: "python3".to_string(),
63 method: InstallMethod::Auto,
64 dry_run: false,
65 strict_rocm: true,
66 repo_url: None,
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct ShellCommand {
74 pub program: String,
76 pub args: Vec<String>,
78 pub env: Vec<(String, String)>,
80 pub working_dir: Option<PathBuf>,
82}
83
84impl ShellCommand {
85 pub fn to_command_string(&self) -> String {
87 let env_prefix = self
88 .env
89 .iter()
90 .map(|(k, v)| format!("{k}={v}"))
91 .collect::<Vec<_>>()
92 .join(" ");
93 let cmd = if self.args.is_empty() {
94 self.program.clone()
95 } else {
96 format!("{} {}", self.program, self.args.join(" "))
97 };
98 if env_prefix.is_empty() {
99 cmd
100 } else {
101 format!("{env_prefix} {cmd}")
102 }
103 }
104}
105
106pub struct AiterInstaller {
108 config: AiterConfig,
109}
110
111impl AiterInstaller {
112 pub fn new(config: AiterConfig) -> Self {
114 Self { config }
115 }
116
117 pub fn with_defaults() -> Self {
119 Self::new(AiterConfig::default())
120 }
121
122 pub fn dependencies(&self) -> &[&str] {
128 &["pytorch", "rocm"]
129 }
130
131 pub fn validate_dependencies(&self, installed_components: &[&str]) -> anyhow::Result<()> {
133 for dep in self.dependencies() {
134 if !installed_components.contains(dep) {
135 anyhow::bail!("AITER requires '{}' to be installed first", dep);
136 }
137 }
138 Ok(())
139 }
140
141 pub fn git_clone_url(&self) -> &str {
147 self.config
148 .repo_url
149 .as_deref()
150 .unwrap_or("https://github.com/ROCm/aiter.git")
151 }
152
153 pub fn build_git_clone_command(&self, target_dir: &str) -> ShellCommand {
155 ShellCommand {
156 program: "git".to_string(),
157 args: vec![
158 "clone".to_string(),
159 "--recursive".to_string(),
160 self.git_clone_url().to_string(),
161 target_dir.to_string(),
162 ],
163 env: vec![],
164 working_dir: None,
165 }
166 }
167
168 pub fn build_deps_install_command(&self) -> ShellCommand {
177 let use_break = self.config.method == InstallMethod::Global
178 || self.config.method == InstallMethod::Auto;
179
180 let mut args = vec!["-m".to_string(), "pip".to_string(), "install".to_string()];
181 if use_break {
182 args.push("--break-system-packages".to_string());
183 }
184 args.extend(["--no-cache-dir".to_string(), "--upgrade".to_string()]);
185
186 let deps = [
187 "packaging",
188 "pybind11",
189 "pandas",
190 "einops",
191 "psutil",
192 "numpy",
193 "setuptools",
194 "wheel",
195 "typing-extensions",
196 "cmake",
197 "ninja",
198 ];
199 for dep in deps {
200 args.push(dep.to_string());
201 }
202
203 ShellCommand {
204 program: self.config.python_bin.clone(),
205 args,
206 env: vec![],
207 working_dir: None,
208 }
209 }
210
211 pub fn build_pip_install_command(&self, src_dir: &str) -> ShellCommand {
227 let use_break = self.config.method == InstallMethod::Global
228 || self.config.method == InstallMethod::Auto;
229
230 let mut args = vec!["-m".to_string(), "pip".to_string(), "install".to_string()];
231 if use_break {
232 args.push("--break-system-packages".to_string());
233 }
234 args.extend([
235 "--no-cache-dir".to_string(),
236 "--no-deps".to_string(),
237 ".".to_string(),
238 ]);
239
240 let primary_arch = self.resolve_primary_arch();
241 let rocm_path = "/opt/rocm".to_string();
242
243 ShellCommand {
244 program: self.config.python_bin.clone(),
245 args,
246 env: vec![
247 ("GPU_ARCH".to_string(), primary_arch.clone()),
248 ("PYTORCH_ROCM_ARCH".to_string(), primary_arch),
249 ("GPU_ARCHS".to_string(), self.config.gpu_arch.clone()),
250 ("ROCM_PATH".to_string(), rocm_path),
251 (
252 "HSA_OVERRIDE_GFX_VERSION".to_string(),
253 self.config
254 .gpu_arch
255 .split(';')
256 .next()
257 .map(|a| {
258 let raw = a.trim().strip_prefix("gfx").unwrap_or("1100");
259 match raw.len() {
260 4 => format!("{}.{}.{}", &raw[0..2], &raw[2..3], &raw[3..4]),
261 3 => format!("{}.{}.{}", &raw[0..1], &raw[1..2], &raw[2..3]),
262 _ => "11.0.0".to_string(),
263 }
264 })
265 .unwrap_or_else(|| "11.0.0".to_string()),
266 ),
267 ],
268 working_dir: Some(PathBuf::from(src_dir)),
269 }
270 }
271 pub fn aiter_jit_env(&self) -> Vec<(String, String)> {
273 let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
274 let jit_dir = format!("{home}/.mlstack/aiter/jit");
275 vec![("AITER_JIT_DIR".to_string(), jit_dir)]
276 }
277
278 pub fn resolve_primary_arch(&self) -> String {
280 self.config
281 .gpu_arch
282 .split(';')
283 .next()
284 .unwrap_or("gfx1100")
285 .trim()
286 .to_string()
287 }
288}
289
290#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
301 fn test_git_clone_url() {
302 let installer = AiterInstaller::with_defaults();
303 assert_eq!(
304 installer.git_clone_url(),
305 "https://github.com/ROCm/aiter.git"
306 );
307 }
308
309 #[test]
310 fn test_git_clone_url_custom() {
311 let installer = AiterInstaller::new(AiterConfig {
312 repo_url: Some("https://example.com/aiter.git".to_string()),
313 ..Default::default()
314 });
315 assert_eq!(installer.git_clone_url(), "https://example.com/aiter.git");
316 }
317
318 #[test]
319 fn test_git_clone_command() {
320 let installer = AiterInstaller::with_defaults();
321 let cmd = installer.build_git_clone_command("/tmp/aiter");
322 assert_eq!(cmd.program, "git");
323 assert!(cmd.args.contains(&"clone".to_string()));
324 assert!(cmd.args.contains(&"--recursive".to_string()));
325 assert!(cmd.args.iter().any(|a| a.contains("ROCm/aiter")));
326 assert!(cmd.args.contains(&"/tmp/aiter".to_string()));
327 }
328
329 #[test]
330 fn test_deps_install_command() {
331 let installer = AiterInstaller::new(AiterConfig {
332 method: InstallMethod::Global,
333 ..Default::default()
334 });
335 let cmd = installer.build_deps_install_command();
336 assert_eq!(cmd.program, "python3");
337 assert!(cmd.args.contains(&"packaging".to_string()));
338 assert!(cmd.args.contains(&"pybind11".to_string()));
339 assert!(cmd.args.contains(&"pandas".to_string()));
340 assert!(cmd.args.contains(&"einops".to_string()));
341 assert!(cmd.args.contains(&"cmake".to_string()));
342 assert!(cmd.args.contains(&"ninja".to_string()));
343 assert!(cmd.args.contains(&"--break-system-packages".to_string()));
344 }
345
346 #[test]
347 fn test_deps_install_command_venv() {
348 let installer = AiterInstaller::new(AiterConfig {
349 method: InstallMethod::Venv,
350 ..Default::default()
351 });
352 let cmd = installer.build_deps_install_command();
353 assert!(!cmd.args.contains(&"--break-system-packages".to_string()));
354 }
355
356 #[test]
357 fn test_pip_install_command() {
358 let installer = AiterInstaller::new(AiterConfig {
359 method: InstallMethod::Global,
360 ..Default::default()
361 });
362 let cmd = installer.build_pip_install_command("/tmp/aiter");
363 assert_eq!(cmd.program, "python3");
364 assert!(cmd.args.contains(&"--no-cache-dir".to_string()));
365 assert!(cmd.args.contains(&".".to_string()));
366 assert!(cmd.args.contains(&"--break-system-packages".to_string()));
367 assert!(!cmd.args.contains(&"--no-build-isolation".to_string()));
369 assert!(cmd.env.iter().any(|(k, _)| k == "GPU_ARCH"));
371 assert!(cmd.env.iter().any(|(k, _)| k == "PYTORCH_ROCM_ARCH"));
372 assert!(cmd.env.iter().any(|(k, _)| k == "ROCM_PATH"));
373 assert!(cmd.env.iter().any(|(k, _)| k == "HSA_OVERRIDE_GFX_VERSION"));
374 }
375
376 #[test]
377 fn test_aiter_jit_env() {
378 let installer = AiterInstaller::with_defaults();
379 let env = installer.aiter_jit_env();
380 assert!(env.iter().any(|(k, _)| k == "AITER_JIT_DIR"));
381 assert!(env.iter().any(|(_, v)| v.contains(".mlstack/aiter/jit")));
382 }
383
384 #[test]
385 fn test_resolve_primary_arch() {
386 let installer = AiterInstaller::new(AiterConfig {
387 gpu_arch: "gfx1100;gfx1101;gfx1102".to_string(),
388 ..Default::default()
389 });
390 assert_eq!(installer.resolve_primary_arch(), "gfx1100");
391 }
392
393 #[test]
394 fn test_resolve_primary_arch_single() {
395 let installer = AiterInstaller::new(AiterConfig {
396 gpu_arch: "gfx1100".to_string(),
397 ..Default::default()
398 });
399 assert_eq!(installer.resolve_primary_arch(), "gfx1100");
400 }
401
402 #[test]
405 fn test_dependencies() {
406 let installer = AiterInstaller::with_defaults();
407 assert!(installer.dependencies().contains(&"pytorch"));
408 assert!(installer.dependencies().contains(&"rocm"));
409 }
410
411 #[test]
412 fn test_validate_dependencies_success() {
413 let installer = AiterInstaller::with_defaults();
414 assert!(installer
415 .validate_dependencies(&["pytorch", "rocm"])
416 .is_ok());
417 }
418
419 #[test]
420 fn test_validate_dependencies_missing_pytorch() {
421 let installer = AiterInstaller::with_defaults();
422 let result = installer.validate_dependencies(&["rocm"]);
423 assert!(result.is_err());
424 assert!(result.unwrap_err().to_string().contains("pytorch"));
425 }
426
427 #[test]
428 fn test_validate_dependencies_missing_rocm() {
429 let installer = AiterInstaller::with_defaults();
430 let result = installer.validate_dependencies(&["pytorch"]);
431 assert!(result.is_err());
432 assert!(result.unwrap_err().to_string().contains("rocm"));
433 }
434}