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 Default for PermanentEnvInstaller {
91 fn default() -> Self {
92 Self::with_defaults()
93 }
94}
95
96impl PermanentEnvInstaller {
97 pub fn new(config: PermanentEnvConfig) -> Self {
99 Self { config }
100 }
101
102 pub fn with_defaults() -> Self {
104 Self::new(PermanentEnvConfig::default())
105 }
106
107 pub fn env_file_path(&self) -> String {
113 default_env_file_path()
114 }
115
116 pub fn generate_env_file_content(&self) -> String {
120 crate::bootstrap::env_setup::generate_env_file_content(
127 &self.config.discrete_gpu_list,
128 "/opt/rocm",
129 &self.config.rocm_version,
130 "latest",
131 &self.config.gpu_arch,
132 &self.config.hsa_override_gfx_version,
133 &self.config.python_bin,
134 )
135 }
136
137 pub fn generate_hsa_tools_lib_line(&self) -> String {
141 if let Some(path) = crate::installers::common::utils::resolve_rocprofiler_tool_path() {
142 format!("export HSA_TOOLS_LIB={}", path)
143 } else {
144 "# HSA_TOOLS_LIB not set (profiler not found)".to_string()
145 }
146 }
147
148 pub fn required_env_vars(&self) -> Vec<(&'static str, String)> {
151 vec![
152 ("ROCM_VERSION", self.config.rocm_version.clone()),
153 ("ROCM_CHANNEL", "latest".to_string()),
154 ("GPU_ARCH", self.config.gpu_arch.clone()),
155 ("PYTORCH_ROCM_ARCH", self.config.gpu_arch.clone()),
156 ("ROCM_HOME", "/opt/rocm".to_string()),
157 ("ROCM_PATH", "/opt/rocm".to_string()),
158 ("HIP_VISIBLE_DEVICES", self.config.discrete_gpu_list.clone()),
159 (
160 "CUDA_VISIBLE_DEVICES",
161 self.config.discrete_gpu_list.clone(),
162 ),
163 ("MLSTACK_PYTHON_BIN", self.config.python_bin.clone()),
164 (
165 "HSA_OVERRIDE_GFX_VERSION",
166 self.config.hsa_override_gfx_version.clone(),
167 ),
168 ("HSA_ENABLE_SDMA", "0".to_string()),
169 ("GPU_MAX_HEAP_SIZE", "100".to_string()),
170 ("GPU_MAX_ALLOC_PERCENT", "100".to_string()),
171 ("MIOPEN_DEBUG_CONV_IMPLICIT_GEMM", "1".to_string()),
172 ("MIOPEN_FIND_MODE", "3".to_string()),
173 ("MIOPEN_FIND_ENFORCE", "3".to_string()),
174 ("PIP_BREAK_SYSTEM_PACKAGES", "1".to_string()),
175 ("UV_PIP_BREAK_SYSTEM_PACKAGES", "1".to_string()),
176 ("UV_SYSTEM_PYTHON", "1".to_string()),
177 ]
178 }
179
180 pub fn build_mkdir_triton_dirs_command(&self) -> ShellCommand {
182 let home = std::env::var("HOME").unwrap_or_default();
183 ShellCommand {
184 program: "mkdir".to_string(),
185 args: vec![
186 "-p".to_string(),
187 format!("{home}/.cache/mlstack/triton/cache"),
188 format!("{home}/.cache/mlstack/triton/dump"),
189 format!("{home}/.cache/mlstack/triton/override"),
190 ],
191 env: vec![],
192 }
193 }
194
195 pub fn build_rocminfo_gpu_detect_command(&self) -> ShellCommand {
201 let rocminfo_path = crate::installers::common::utils::resolve_rocminfo_path();
202 ShellCommand {
203 program: rocminfo_path,
204 args: vec![],
205 env: vec![],
206 }
207 }
208
209 pub fn build_rocm_version_detect_command(&self) -> ShellCommand {
211 ShellCommand {
212 program: "cat".to_string(),
213 args: vec!["/opt/rocm/.info/version".to_string()],
214 env: vec![],
215 }
216 }
217
218 pub fn gpu_arch_from_env() -> Option<String> {
220 std::env::var("GPU_ARCH").ok().filter(|s| !s.is_empty())
221 }
222
223 pub fn default_gpu_arch() -> &'static str {
225 "gfx1100"
226 }
227
228 pub fn gfx_to_hsa_override(gfx: &str) -> Option<String> {
231 let raw = gfx.strip_prefix("gfx")?;
232 match raw.len() {
233 4 => {
234 let major = &raw[0..2];
235 let minor = &raw[2..3];
236 let patch = &raw[3..4];
237 Some(format!("{}.{}.{}", major, minor, patch))
238 }
239 3 => {
240 let major = &raw[0..1];
241 let minor = &raw[1..2];
242 let patch = &raw[2..3];
243 Some(format!("{}.{}.{}", major, minor, patch))
244 }
245 _ => None,
246 }
247 }
248}
249
250#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
263 fn test_env_file_path_is_home_mlstack_env() {
264 let path = default_env_file_path();
265 assert!(
266 path.ends_with("/.mlstack_env"),
267 "Env file must be at ~/.mlstack_env, got: {path}"
268 );
269 }
270
271 #[test]
272 fn test_generate_env_file_contains_required_vars() {
273 let installer = PermanentEnvInstaller::with_defaults();
274 let content = installer.generate_env_file_content();
275
276 assert!(content.contains("export ROCM_VERSION="));
278 assert!(content.contains("export ROCM_CHANNEL=latest"));
279 assert!(content.contains("export GPU_ARCH="));
280 assert!(content.contains("export PYTORCH_ROCM_ARCH="));
281 assert!(content.contains("export ROCM_HOME=/opt/rocm"));
282 assert!(content.contains("export ROCM_PATH=/opt/rocm"));
283 assert!(content.contains("export HIP_PATH=/opt/rocm"));
284 assert!(content.contains("export HIP_VISIBLE_DEVICES="));
285 assert!(content.contains("export CUDA_VISIBLE_DEVICES="));
286 assert!(content.contains("export MLSTACK_PYTHON_BIN="));
287 assert!(content.contains("export UV_PYTHON="));
288 assert!(content.contains("export HSA_OVERRIDE_GFX_VERSION="));
289 assert!(content.contains("export HSA_ENABLE_SDMA=0"));
290 assert!(content.contains("export GPU_MAX_HEAP_SIZE=100"));
291 assert!(content.contains("export GPU_MAX_ALLOC_PERCENT=100"));
292 assert!(content.contains("export MIOPEN_DEBUG_CONV_IMPLICIT_GEMM=1"));
293 assert!(content.contains("export MIOPEN_FIND_MODE=3"));
294 assert!(content.contains("export MIOPEN_FIND_ENFORCE=3"));
295 assert!(content.contains("export PIP_BREAK_SYSTEM_PACKAGES=1"));
296 assert!(content.contains("export UV_PIP_BREAK_SYSTEM_PACKAGES=1"));
297 assert!(content.contains("export UV_SYSTEM_PYTHON=1"));
298 }
299
300 #[test]
301 fn test_env_file_contains_path_settings() {
302 let installer = PermanentEnvInstaller::with_defaults();
303 let content = installer.generate_env_file_content();
304
305 assert!(content.contains("/opt/rocm/bin"));
306 assert!(content.contains("/opt/rocm/lib"));
307 assert!(content.contains("LD_LIBRARY_PATH"));
308 }
309
310 #[test]
311 fn test_env_file_contains_pytorch_optimization() {
312 let installer = PermanentEnvInstaller::with_defaults();
313 let content = installer.generate_env_file_content();
314
315 assert!(!content.contains("TORCH_CUDA_ARCH_LIST"));
318 assert!(content.contains("PYTORCH_CUDA_ALLOC_CONF"));
319 assert!(content.contains("PYTORCH_HIP_ALLOC_CONF"));
320 }
321
322 #[test]
323 fn test_env_file_contains_vllm_settings() {
324 let installer = PermanentEnvInstaller::with_defaults();
325 let content = installer.generate_env_file_content();
326
327 assert!(content.contains("FLASH_ATTENTION_TRITON_AMD_ENABLE=TRUE"));
328 assert!(content.contains("VLLM_ROCM_USE_AITER=0"));
329 assert!(content.contains("VLLM_WORKER_MULTIPROC_METHOD=spawn"));
330 }
331
332 #[test]
333 fn test_env_file_contains_triton_dirs() {
334 let installer = PermanentEnvInstaller::with_defaults();
335 let content = installer.generate_env_file_content();
336
337 assert!(content.contains("MLSTACK_TRITON_HOME"));
338 assert!(content.contains("TRITON_CACHE_DIR"));
339 assert!(content.contains("TRITON_DUMP_DIR"));
340 assert!(content.contains("TRITON_OVERRIDE_DIR"));
341 }
342
343 #[test]
344 fn test_required_env_vars_count() {
345 let installer = PermanentEnvInstaller::with_defaults();
346 let vars = installer.required_env_vars();
347 assert!(
349 vars.len() >= 19,
350 "Expected at least 19 env vars, got {}",
351 vars.len()
352 );
353 }
354
355 #[test]
356 fn test_required_env_vars_values() {
357 let config = PermanentEnvConfig {
358 rocm_version: "7.2.0".to_string(),
359 gpu_arch: "gfx1100".to_string(),
360 hsa_override_gfx_version: "11.0.0".to_string(),
361 discrete_gpu_list: "0,1".to_string(),
362 python_bin: "python3.12".to_string(),
363 install_method: "auto".to_string(),
364 ..Default::default()
365 };
366 let installer = PermanentEnvInstaller::new(config);
367 let vars = installer.required_env_vars();
368
369 let get = |name: &str| -> String {
370 vars.iter()
371 .find(|(n, _)| *n == name)
372 .map(|(_, v)| v.clone())
373 .unwrap_or_default()
374 };
375
376 assert_eq!(get("ROCM_VERSION"), "7.2.0");
377 assert_eq!(get("GPU_ARCH"), "gfx1100");
378 assert_eq!(get("HIP_VISIBLE_DEVICES"), "0,1");
379 assert_eq!(get("MLSTACK_PYTHON_BIN"), "python3.12");
380 assert_eq!(get("HSA_OVERRIDE_GFX_VERSION"), "11.0.0");
381 }
382
383 #[test]
384 fn test_gfx_to_hsa_override_conversion() {
385 assert_eq!(
386 PermanentEnvInstaller::gfx_to_hsa_override("gfx1100"),
387 Some("11.0.0".to_string())
388 );
389 assert_eq!(
390 PermanentEnvInstaller::gfx_to_hsa_override("gfx1030"),
391 Some("10.3.0".to_string())
392 );
393 assert_eq!(
394 PermanentEnvInstaller::gfx_to_hsa_override("gfx1201"),
395 Some("12.0.1".to_string())
396 );
397 assert_eq!(
398 PermanentEnvInstaller::gfx_to_hsa_override("gfx900"),
399 Some("9.0.0".to_string())
400 );
401 assert_eq!(PermanentEnvInstaller::gfx_to_hsa_override("invalid"), None);
402 }
403
404 #[test]
405 fn test_mkdir_triton_dirs_command() {
406 let installer = PermanentEnvInstaller::with_defaults();
407 let cmd = installer.build_mkdir_triton_dirs_command();
408 assert_eq!(cmd.program, "mkdir");
409 assert!(cmd.args.contains(&"-p".to_string()));
410 let cmd_str = cmd.to_command_string();
411 assert!(cmd_str.contains("mlstack/triton/cache"));
412 assert!(cmd_str.contains("mlstack/triton/dump"));
413 assert!(cmd_str.contains("mlstack/triton/override"));
414 }
415
416 #[test]
417 fn test_rocm_version_detect_command() {
418 let installer = PermanentEnvInstaller::with_defaults();
419 let cmd = installer.build_rocm_version_detect_command();
420 assert_eq!(cmd.program, "cat");
421 assert!(cmd.args.contains(&"/opt/rocm/.info/version".to_string()));
422 }
423
424 #[test]
425 fn test_rocminfo_gpu_detect_command_uses_full_path() {
426 let installer = PermanentEnvInstaller::with_defaults();
427 let cmd = installer.build_rocminfo_gpu_detect_command();
428 assert!(
430 cmd.program.contains("rocminfo"),
431 "Program should contain 'rocminfo', got: {}",
432 cmd.program
433 );
434 if std::path::Path::new("/opt/rocm/bin/rocminfo").exists() {
436 assert_eq!(
437 cmd.program, "/opt/rocm/bin/rocminfo",
438 "Must use full path when /opt/rocm/bin/rocminfo exists"
439 );
440 }
441 }
442
443 #[test]
444 fn test_generate_hsa_tools_lib_line() {
445 let installer = PermanentEnvInstaller::with_defaults();
446 let line = installer.generate_hsa_tools_lib_line();
447 assert!(
449 line.contains("HSA_TOOLS_LIB") || line.contains("profiler not found"),
450 "Should reference HSA_TOOLS_LIB"
451 );
452 }
453
454 #[test]
455 fn test_custom_config_values_in_env_file() {
456 let config = PermanentEnvConfig {
457 rocm_version: "7.0.0".to_string(),
458 gpu_arch: "gfx1030".to_string(),
459 discrete_gpu_list: "0".to_string(),
460 python_bin: "python3.11".to_string(),
461 ..Default::default()
462 };
463 let installer = PermanentEnvInstaller::new(config);
464 let content = installer.generate_env_file_content();
465
466 assert!(content.contains("ROCM_VERSION=7.0.0"));
467 assert!(content.contains("GPU_ARCH=gfx1030"));
468 assert!(content.contains("HIP_VISIBLE_DEVICES=0"));
469 assert!(content.contains("MLSTACK_PYTHON_BIN=python3.11"));
470 }
471}