rusty_stack/installers/components/
permanent_env.rs1#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct ShellCommand {
17 pub program: String,
19 pub args: Vec<String>,
21 pub env: Vec<(String, String)>,
23}
24
25impl ShellCommand {
26 pub fn to_command_string(&self) -> String {
28 let env_prefix = self
29 .env
30 .iter()
31 .map(|(k, v)| format!("{k}={v}"))
32 .collect::<Vec<_>>()
33 .join(" ");
34 let cmd = if self.args.is_empty() {
35 self.program.clone()
36 } else {
37 format!("{} {}", self.program, self.args.join(" "))
38 };
39 if env_prefix.is_empty() {
40 cmd
41 } else {
42 format!("{env_prefix} {cmd}")
43 }
44 }
45}
46
47#[derive(Debug, Clone)]
49pub struct PermanentEnvConfig {
50 pub python_bin: String,
52 pub rocm_version: String,
54 pub gpu_arch: String,
56 pub hsa_override_gfx_version: String,
58 pub discrete_gpu_list: String,
60 pub install_method: String,
62 pub dry_run: bool,
64}
65
66impl Default for PermanentEnvConfig {
67 fn default() -> Self {
68 Self {
69 python_bin: "python3".to_string(),
70 rocm_version: "7.2.0".to_string(),
71 gpu_arch: "gfx1100".to_string(),
72 hsa_override_gfx_version: "11.0.0".to_string(),
73 discrete_gpu_list: "0".to_string(),
74 install_method: "auto".to_string(),
75 dry_run: false,
76 }
77 }
78}
79
80pub fn default_env_file_path() -> String {
82 format!("{}/.mlstack_env", std::env::var("HOME").unwrap_or_default())
83}
84
85pub struct PermanentEnvInstaller {
87 config: PermanentEnvConfig,
88}
89
90impl PermanentEnvInstaller {
91 pub fn new(config: PermanentEnvConfig) -> Self {
93 Self { config }
94 }
95
96 pub fn with_defaults() -> Self {
98 Self::new(PermanentEnvConfig::default())
99 }
100
101 pub fn env_file_path(&self) -> String {
107 default_env_file_path()
108 }
109
110 pub fn generate_env_file_content(&self) -> String {
114 let _home = std::env::var("HOME").unwrap_or_default();
115 let uid_output = std::process::Command::new("id")
116 .arg("-u")
117 .output()
118 .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
119 .unwrap_or_else(|_| "0".to_string());
120
121 format!(
122 r#"# Permanent ROCm Environment Setup (Generated by rusty-stack)
123export ROCM_VERSION={rocm_version}
124export ROCM_CHANNEL=latest
125export GPU_ARCH={gpu_arch}
126export PYTORCH_ROCM_ARCH={gpu_arch}
127export GPU_ARCHS={gpu_arch}
128export ROCM_HOME=/opt/rocm
129export ROCM_PATH=/opt/rocm
130export HIP_PATH=/opt/rocm
131# Discrete GPUs only (iGPUs filtered): {gpu_list}
132export HIP_VISIBLE_DEVICES={gpu_list}
133export CUDA_VISIBLE_DEVICES={gpu_list}
134export PYTORCH_ROCM_DEVICE={first_gpu}
135export MLSTACK_PYTHON_BIN={python_bin}
136export UV_PYTHON={python_bin}
137export MLSTACK_INSTALL_METHOD={install_method}
138export INSTALL_METHOD={install_method}
139export PYTHONPATH=/opt/rocm/lib:$PYTHONPATH
140export AMDGPU_ASIC_ID_TABLE_PATH=/usr/share/libdrm/amdgpu.ids
141export AMDGPU_ASIC_ID_TABLE_PATHS=/usr/share/libdrm
142
143# Path Settings
144export PATH="/usr/local/bin:/usr/bin:/bin:/opt/rocm/bin:/opt/rocm/hip/bin:$PATH"
145export LD_LIBRARY_PATH="$HOME/.mlstack/libmpi-compat:$HOME/.mlstack/libmpi-compat-user-{uid}:/opt/rocm/lib:/opt/rocm/hip/lib:/opt/rocm/opencl/lib:$LD_LIBRARY_PATH"
146
147# Performance & Compatibility
148export HSA_OVERRIDE_GFX_VERSION={hsa_override}
149export HSA_ENABLE_SDMA=0
150export GPU_MAX_HEAP_SIZE=100
151export GPU_MAX_ALLOC_PERCENT=100
152{hsa_tools_lib_line}
153export MIOPEN_DEBUG_CONV_IMPLICIT_GEMM=1
154export MIOPEN_FIND_MODE=3
155export MIOPEN_FIND_ENFORCE=3
156
157# PyTorch Optimization
158export TORCH_CUDA_ARCH_LIST="7.0;8.0;9.0"
159export PYTORCH_ALLOC_CONF="max_split_size_mb:512"
160export PYTORCH_HIP_ALLOC_CONF="max_split_size_mb:512"
161
162# vLLM RDNA3 Support (v0.15.0+)
163export FLASH_ATTENTION_TRITON_AMD_ENABLE=TRUE
164export VLLM_ROCM_USE_AITER=0
165export VLLM_WORKER_MULTIPROC_METHOD=spawn
166export MLSTACK_TRITON_HOME=$HOME/.cache/mlstack/triton
167export TRITON_HOME=$MLSTACK_TRITON_HOME
168export TRITON_CACHE_DIR=$MLSTACK_TRITON_HOME/cache
169export TRITON_DUMP_DIR=$MLSTACK_TRITON_HOME/dump
170export TRITON_OVERRIDE_DIR=$MLSTACK_TRITON_HOME/override
171
172# Global flags for seamless installs
173export PIP_BREAK_SYSTEM_PACKAGES=1
174export UV_PIP_BREAK_SYSTEM_PACKAGES=1
175export UV_SYSTEM_PYTHON=1
176
177# MPI/UCX Settings (ROCm-aware)
178export OMPI_MCA_opal_cuda_support=true
179export OMPI_MCA_pml=ucx
180export OMPI_MCA_osc=ucx
181export OMPI_MCA_btl=^openib,uct
182export OMPI_MCA_btl_openib_allow_ib=true
183export OMPI_MCA_btl_openib_warn_no_device_params_found=0
184export OMPI_MCA_coll_hcoll_enable=0
185# Prevent UCX ROCm memory registration segfault on multi-GPU systems
186export UCX_MEMTYPE_CACHE=no
187export UCX_LOG_LEVEL=error"
188"#,
189 rocm_version = self.config.rocm_version,
190 gpu_arch = self.config.gpu_arch,
191 gpu_list = self.config.discrete_gpu_list,
192 first_gpu = self
193 .config
194 .discrete_gpu_list
195 .split(',')
196 .next()
197 .unwrap_or("0"),
198 python_bin = self.config.python_bin,
199 install_method = self.config.install_method,
200 hsa_override = self.config.hsa_override_gfx_version,
201 hsa_tools_lib_line = self.generate_hsa_tools_lib_line(),
202 uid = uid_output,
203 )
204 }
205
206 pub fn generate_hsa_tools_lib_line(&self) -> String {
210 if let Some(path) = crate::installers::common::utils::resolve_rocprofiler_tool_path() {
211 format!("export HSA_TOOLS_LIB={}", path)
212 } else {
213 "# HSA_TOOLS_LIB not set (profiler not found)".to_string()
214 }
215 }
216
217 pub fn required_env_vars(&self) -> Vec<(&'static str, String)> {
220 vec![
221 ("ROCM_VERSION", self.config.rocm_version.clone()),
222 ("ROCM_CHANNEL", "latest".to_string()),
223 ("GPU_ARCH", self.config.gpu_arch.clone()),
224 ("PYTORCH_ROCM_ARCH", self.config.gpu_arch.clone()),
225 ("ROCM_HOME", "/opt/rocm".to_string()),
226 ("ROCM_PATH", "/opt/rocm".to_string()),
227 ("HIP_VISIBLE_DEVICES", self.config.discrete_gpu_list.clone()),
228 (
229 "CUDA_VISIBLE_DEVICES",
230 self.config.discrete_gpu_list.clone(),
231 ),
232 ("MLSTACK_PYTHON_BIN", self.config.python_bin.clone()),
233 (
234 "HSA_OVERRIDE_GFX_VERSION",
235 self.config.hsa_override_gfx_version.clone(),
236 ),
237 ("HSA_ENABLE_SDMA", "0".to_string()),
238 ("GPU_MAX_HEAP_SIZE", "100".to_string()),
239 ("GPU_MAX_ALLOC_PERCENT", "100".to_string()),
240 ("MIOPEN_DEBUG_CONV_IMPLICIT_GEMM", "1".to_string()),
241 ("MIOPEN_FIND_MODE", "3".to_string()),
242 ("MIOPEN_FIND_ENFORCE", "3".to_string()),
243 ("PIP_BREAK_SYSTEM_PACKAGES", "1".to_string()),
244 ("UV_PIP_BREAK_SYSTEM_PACKAGES", "1".to_string()),
245 ("UV_SYSTEM_PYTHON", "1".to_string()),
246 ]
247 }
248
249 pub fn build_mkdir_triton_dirs_command(&self) -> ShellCommand {
251 let home = std::env::var("HOME").unwrap_or_default();
252 ShellCommand {
253 program: "mkdir".to_string(),
254 args: vec![
255 "-p".to_string(),
256 format!("{home}/.cache/mlstack/triton/cache"),
257 format!("{home}/.cache/mlstack/triton/dump"),
258 format!("{home}/.cache/mlstack/triton/override"),
259 ],
260 env: vec![],
261 }
262 }
263
264 pub fn build_rocminfo_gpu_detect_command(&self) -> ShellCommand {
270 let rocminfo_path = crate::installers::common::utils::resolve_rocminfo_path();
271 ShellCommand {
272 program: rocminfo_path,
273 args: vec![],
274 env: vec![],
275 }
276 }
277
278 pub fn build_rocm_version_detect_command(&self) -> ShellCommand {
280 ShellCommand {
281 program: "cat".to_string(),
282 args: vec!["/opt/rocm/.info/version".to_string()],
283 env: vec![],
284 }
285 }
286
287 pub fn gpu_arch_from_env() -> Option<String> {
289 std::env::var("GPU_ARCH").ok().filter(|s| !s.is_empty())
290 }
291
292 pub fn default_gpu_arch() -> &'static str {
294 "gfx1100"
295 }
296
297 pub fn gfx_to_hsa_override(gfx: &str) -> Option<String> {
300 let raw = gfx.strip_prefix("gfx")?;
301 match raw.len() {
302 4 => {
303 let major = &raw[0..2];
304 let minor = &raw[2..3];
305 let patch = &raw[3..4];
306 Some(format!("{}.{}.{}", major, minor, patch))
307 }
308 3 => {
309 let major = &raw[0..1];
310 let minor = &raw[1..2];
311 let patch = &raw[2..3];
312 Some(format!("{}.{}.{}", major, minor, patch))
313 }
314 _ => None,
315 }
316 }
317}
318
319#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[test]
332 fn test_env_file_path_is_home_mlstack_env() {
333 let path = default_env_file_path();
334 assert!(
335 path.ends_with("/.mlstack_env"),
336 "Env file must be at ~/.mlstack_env, got: {path}"
337 );
338 }
339
340 #[test]
341 fn test_generate_env_file_contains_required_vars() {
342 let installer = PermanentEnvInstaller::with_defaults();
343 let content = installer.generate_env_file_content();
344
345 assert!(content.contains("export ROCM_VERSION="));
347 assert!(content.contains("export ROCM_CHANNEL=latest"));
348 assert!(content.contains("export GPU_ARCH="));
349 assert!(content.contains("export PYTORCH_ROCM_ARCH="));
350 assert!(content.contains("export ROCM_HOME=/opt/rocm"));
351 assert!(content.contains("export ROCM_PATH=/opt/rocm"));
352 assert!(content.contains("export HIP_PATH=/opt/rocm"));
353 assert!(content.contains("export HIP_VISIBLE_DEVICES="));
354 assert!(content.contains("export CUDA_VISIBLE_DEVICES="));
355 assert!(content.contains("export MLSTACK_PYTHON_BIN="));
356 assert!(content.contains("export UV_PYTHON="));
357 assert!(content.contains("export HSA_OVERRIDE_GFX_VERSION="));
358 assert!(content.contains("export HSA_ENABLE_SDMA=0"));
359 assert!(content.contains("export GPU_MAX_HEAP_SIZE=100"));
360 assert!(content.contains("export GPU_MAX_ALLOC_PERCENT=100"));
361 assert!(content.contains("export MIOPEN_DEBUG_CONV_IMPLICIT_GEMM=1"));
362 assert!(content.contains("export MIOPEN_FIND_MODE=3"));
363 assert!(content.contains("export MIOPEN_FIND_ENFORCE=3"));
364 assert!(content.contains("export PIP_BREAK_SYSTEM_PACKAGES=1"));
365 assert!(content.contains("export UV_PIP_BREAK_SYSTEM_PACKAGES=1"));
366 assert!(content.contains("export UV_SYSTEM_PYTHON=1"));
367 }
368
369 #[test]
370 fn test_env_file_contains_path_settings() {
371 let installer = PermanentEnvInstaller::with_defaults();
372 let content = installer.generate_env_file_content();
373
374 assert!(content.contains("/opt/rocm/bin"));
375 assert!(content.contains("/opt/rocm/lib"));
376 assert!(content.contains("LD_LIBRARY_PATH"));
377 }
378
379 #[test]
380 fn test_env_file_contains_pytorch_optimization() {
381 let installer = PermanentEnvInstaller::with_defaults();
382 let content = installer.generate_env_file_content();
383
384 assert!(content.contains("TORCH_CUDA_ARCH_LIST"));
385 assert!(content.contains("PYTORCH_ALLOC_CONF"));
386 assert!(content.contains("PYTORCH_HIP_ALLOC_CONF"));
387 }
388
389 #[test]
390 fn test_env_file_contains_vllm_settings() {
391 let installer = PermanentEnvInstaller::with_defaults();
392 let content = installer.generate_env_file_content();
393
394 assert!(content.contains("FLASH_ATTENTION_TRITON_AMD_ENABLE=TRUE"));
395 assert!(content.contains("VLLM_ROCM_USE_AITER=0"));
396 assert!(content.contains("VLLM_WORKER_MULTIPROC_METHOD=spawn"));
397 }
398
399 #[test]
400 fn test_env_file_contains_triton_dirs() {
401 let installer = PermanentEnvInstaller::with_defaults();
402 let content = installer.generate_env_file_content();
403
404 assert!(content.contains("MLSTACK_TRITON_HOME"));
405 assert!(content.contains("TRITON_CACHE_DIR"));
406 assert!(content.contains("TRITON_DUMP_DIR"));
407 assert!(content.contains("TRITON_OVERRIDE_DIR"));
408 }
409
410 #[test]
411 fn test_required_env_vars_count() {
412 let installer = PermanentEnvInstaller::with_defaults();
413 let vars = installer.required_env_vars();
414 assert!(
416 vars.len() >= 19,
417 "Expected at least 19 env vars, got {}",
418 vars.len()
419 );
420 }
421
422 #[test]
423 fn test_required_env_vars_values() {
424 let config = PermanentEnvConfig {
425 rocm_version: "7.2.0".to_string(),
426 gpu_arch: "gfx1100".to_string(),
427 hsa_override_gfx_version: "11.0.0".to_string(),
428 discrete_gpu_list: "0,1".to_string(),
429 python_bin: "python3.12".to_string(),
430 install_method: "auto".to_string(),
431 ..Default::default()
432 };
433 let installer = PermanentEnvInstaller::new(config);
434 let vars = installer.required_env_vars();
435
436 let get = |name: &str| -> String {
437 vars.iter()
438 .find(|(n, _)| *n == name)
439 .map(|(_, v)| v.clone())
440 .unwrap_or_default()
441 };
442
443 assert_eq!(get("ROCM_VERSION"), "7.2.0");
444 assert_eq!(get("GPU_ARCH"), "gfx1100");
445 assert_eq!(get("HIP_VISIBLE_DEVICES"), "0,1");
446 assert_eq!(get("MLSTACK_PYTHON_BIN"), "python3.12");
447 assert_eq!(get("HSA_OVERRIDE_GFX_VERSION"), "11.0.0");
448 }
449
450 #[test]
451 fn test_gfx_to_hsa_override_conversion() {
452 assert_eq!(
453 PermanentEnvInstaller::gfx_to_hsa_override("gfx1100"),
454 Some("11.0.0".to_string())
455 );
456 assert_eq!(
457 PermanentEnvInstaller::gfx_to_hsa_override("gfx1030"),
458 Some("10.3.0".to_string())
459 );
460 assert_eq!(
461 PermanentEnvInstaller::gfx_to_hsa_override("gfx1201"),
462 Some("12.0.1".to_string())
463 );
464 assert_eq!(
465 PermanentEnvInstaller::gfx_to_hsa_override("gfx900"),
466 Some("9.0.0".to_string())
467 );
468 assert_eq!(PermanentEnvInstaller::gfx_to_hsa_override("invalid"), None);
469 }
470
471 #[test]
472 fn test_mkdir_triton_dirs_command() {
473 let installer = PermanentEnvInstaller::with_defaults();
474 let cmd = installer.build_mkdir_triton_dirs_command();
475 assert_eq!(cmd.program, "mkdir");
476 assert!(cmd.args.contains(&"-p".to_string()));
477 let cmd_str = cmd.to_command_string();
478 assert!(cmd_str.contains("mlstack/triton/cache"));
479 assert!(cmd_str.contains("mlstack/triton/dump"));
480 assert!(cmd_str.contains("mlstack/triton/override"));
481 }
482
483 #[test]
484 fn test_rocm_version_detect_command() {
485 let installer = PermanentEnvInstaller::with_defaults();
486 let cmd = installer.build_rocm_version_detect_command();
487 assert_eq!(cmd.program, "cat");
488 assert!(cmd.args.contains(&"/opt/rocm/.info/version".to_string()));
489 }
490
491 #[test]
492 fn test_rocminfo_gpu_detect_command_uses_full_path() {
493 let installer = PermanentEnvInstaller::with_defaults();
494 let cmd = installer.build_rocminfo_gpu_detect_command();
495 assert!(
497 cmd.program.contains("rocminfo"),
498 "Program should contain 'rocminfo', got: {}",
499 cmd.program
500 );
501 if std::path::Path::new("/opt/rocm/bin/rocminfo").exists() {
503 assert_eq!(
504 cmd.program, "/opt/rocm/bin/rocminfo",
505 "Must use full path when /opt/rocm/bin/rocminfo exists"
506 );
507 }
508 }
509
510 #[test]
511 fn test_generate_hsa_tools_lib_line() {
512 let installer = PermanentEnvInstaller::with_defaults();
513 let line = installer.generate_hsa_tools_lib_line();
514 assert!(
516 line.contains("HSA_TOOLS_LIB") || line.contains("profiler not found"),
517 "Should reference HSA_TOOLS_LIB"
518 );
519 }
520
521 #[test]
522 fn test_custom_config_values_in_env_file() {
523 let config = PermanentEnvConfig {
524 rocm_version: "7.0.0".to_string(),
525 gpu_arch: "gfx1030".to_string(),
526 discrete_gpu_list: "0".to_string(),
527 python_bin: "python3.11".to_string(),
528 ..Default::default()
529 };
530 let installer = PermanentEnvInstaller::new(config);
531 let content = installer.generate_env_file_content();
532
533 assert!(content.contains("ROCM_VERSION=7.0.0"));
534 assert!(content.contains("GPU_ARCH=gfx1030"));
535 assert!(content.contains("HIP_VISIBLE_DEVICES=0"));
536 assert!(content.contains("MLSTACK_PYTHON_BIN=python3.11"));
537 }
538}