rusty_stack/installers/components/
bitsandbytes_multi.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum InstallMethod {
17 Global,
19 Venv,
21 Auto,
23}
24
25impl std::fmt::Display for InstallMethod {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 match self {
28 InstallMethod::Global => write!(f, "global"),
29 InstallMethod::Venv => write!(f, "venv"),
30 InstallMethod::Auto => write!(f, "auto"),
31 }
32 }
33}
34
35#[derive(Debug, Clone)]
37pub struct BitsAndBytesConfig {
38 pub rocm_version: String,
40 pub gpu_arch: String,
42 pub python_bin: String,
44 pub method: InstallMethod,
46 pub dry_run: bool,
48}
49
50impl Default for BitsAndBytesConfig {
51 fn default() -> Self {
52 Self {
53 rocm_version: "7.2.0".to_string(),
54 gpu_arch: "gfx1100".to_string(),
55 python_bin: "python3".to_string(),
56 method: InstallMethod::Auto,
57 dry_run: false,
58 }
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct ShellCommand {
65 pub program: String,
67 pub args: Vec<String>,
69 pub env: Vec<(String, String)>,
71}
72
73impl ShellCommand {
74 pub fn to_command_string(&self) -> String {
76 let env_prefix = self
77 .env
78 .iter()
79 .map(|(k, v)| format!("{k}={v}"))
80 .collect::<Vec<_>>()
81 .join(" ");
82 let cmd = if self.args.is_empty() {
83 self.program.clone()
84 } else {
85 format!("{} {}", self.program, self.args.join(" "))
86 };
87 if env_prefix.is_empty() {
88 cmd
89 } else {
90 format!("{env_prefix} {cmd}")
91 }
92 }
93}
94
95pub struct BitsAndBytesInstaller {
97 config: BitsAndBytesConfig,
98}
99
100impl BitsAndBytesInstaller {
101 pub fn new(config: BitsAndBytesConfig) -> Self {
103 Self { config }
104 }
105
106 pub fn with_defaults() -> Self {
108 Self::new(BitsAndBytesConfig::default())
109 }
110
111 pub fn rocm_fork_url(&self) -> &'static str {
117 "https://github.com/ROCm/bitsandbytes.git"
118 }
119
120 pub fn fallback_fork_url(&self) -> &'static str {
122 "https://github.com/TimDettmers/bitsandbytes.git"
123 }
124
125 pub fn build_git_clone_command(&self, target_dir: &str) -> ShellCommand {
127 ShellCommand {
128 program: "git".to_string(),
129 args: vec![
130 "clone".to_string(),
131 "--recursive".to_string(),
132 self.rocm_fork_url().to_string(),
133 target_dir.to_string(),
134 ],
135 env: vec![],
136 }
137 }
138
139 pub fn build_pypi_install_command(&self) -> ShellCommand {
148 let use_break = self.config.method == InstallMethod::Global
149 || self.config.method == InstallMethod::Auto;
150
151 let mut args = vec!["-m".to_string(), "pip".to_string(), "install".to_string()];
152 if use_break {
153 args.push("--break-system-packages".to_string());
154 }
155 args.extend([
156 "--upgrade".to_string(),
157 "bitsandbytes".to_string(),
158 "--no-cache-dir".to_string(),
159 "--no-deps".to_string(),
160 ]);
161
162 ShellCommand {
163 program: self.config.python_bin.clone(),
164 args,
165 env: vec![],
166 }
167 }
168
169 pub fn build_rocm_env(&self) -> Vec<(String, String)> {
171 let rocm_path = "/opt/rocm".to_string();
172 let version_short = self.rocm_version_short();
173
174 vec![
175 ("BNB_USE_ROCM".to_string(), "1".to_string()),
176 ("BNB_ROCM_ARCH".to_string(), self.config.gpu_arch.clone()),
177 ("ROCM_PATH".to_string(), rocm_path.clone()),
178 ("ROCM_HOME".to_string(), rocm_path.clone()),
179 ("HIP_PATH".to_string(), rocm_path),
180 ("BNB_ROCM_VERSION".to_string(), version_short),
181 ]
182 }
183
184 fn rocm_version_short(&self) -> String {
186 let parts: Vec<&str> = self.config.rocm_version.split('.').collect();
187 if parts.len() >= 2 {
188 format!("{}{}", parts[0], parts[1])
189 } else {
190 "72".to_string()
191 }
192 }
193
194 pub fn build_build_deps_command(&self) -> ShellCommand {
196 let use_break = self.config.method == InstallMethod::Global
197 || self.config.method == InstallMethod::Auto;
198
199 let mut args = vec!["-m".to_string(), "pip".to_string(), "install".to_string()];
200 if use_break {
201 args.push("--break-system-packages".to_string());
202 }
203 args.extend([
204 "scikit-build-core".to_string(),
205 "cmake".to_string(),
206 "ninja".to_string(),
207 ]);
208
209 ShellCommand {
210 program: self.config.python_bin.clone(),
211 args,
212 env: vec![],
213 }
214 }
215
216 pub fn build_source_install_command(&self) -> ShellCommand {
223 let use_break = self.config.method == InstallMethod::Global
224 || self.config.method == InstallMethod::Auto;
225
226 let mut args = vec!["-m".to_string(), "pip".to_string(), "install".to_string()];
227 if use_break {
228 args.push("--break-system-packages".to_string());
229 }
230 args.extend([
231 ".".to_string(),
232 "--no-build-isolation".to_string(),
233 "--no-deps".to_string(),
234 "-Ccmake.define.COMPUTE_BACKEND=hip".to_string(),
235 "-Ccmake.define.ROCM_PATH=/opt/rocm".to_string(),
236 ]);
237
238 ShellCommand {
239 program: self.config.python_bin.clone(),
240 args,
241 env: self.build_rocm_env(),
242 }
243 }
244
245 pub fn build_uninstall_command(&self) -> ShellCommand {
247 ShellCommand {
248 program: self.config.python_bin.clone(),
249 args: vec![
250 "-m".to_string(),
251 "pip".to_string(),
252 "uninstall".to_string(),
253 "-y".to_string(),
254 "bitsandbytes".to_string(),
255 ],
256 env: vec![],
257 }
258 }
259}
260
261#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
272 fn test_pypi_install_command() {
273 let installer = BitsAndBytesInstaller::new(BitsAndBytesConfig {
274 method: InstallMethod::Global,
275 ..Default::default()
276 });
277 let cmd = installer.build_pypi_install_command();
278 assert_eq!(cmd.program, "python3");
279 assert!(cmd.args.contains(&"-m".to_string()));
280 assert!(cmd.args.contains(&"pip".to_string()));
281 assert!(cmd.args.contains(&"install".to_string()));
282 assert!(cmd.args.contains(&"--break-system-packages".to_string()));
283 assert!(cmd.args.contains(&"bitsandbytes".to_string()));
284 assert!(cmd.args.contains(&"--no-cache-dir".to_string()));
285 assert!(cmd.args.contains(&"--no-deps".to_string()));
286 }
287
288 #[test]
289 fn test_pypi_install_command_venv() {
290 let installer = BitsAndBytesInstaller::new(BitsAndBytesConfig {
291 method: InstallMethod::Venv,
292 ..Default::default()
293 });
294 let cmd = installer.build_pypi_install_command();
295 assert!(!cmd.args.contains(&"--break-system-packages".to_string()));
296 assert!(cmd.args.contains(&"bitsandbytes".to_string()));
297 }
298
299 #[test]
300 fn test_rocm_fork_url() {
301 let installer = BitsAndBytesInstaller::with_defaults();
302 assert_eq!(
303 installer.rocm_fork_url(),
304 "https://github.com/ROCm/bitsandbytes.git"
305 );
306 }
307
308 #[test]
309 fn test_fallback_fork_url() {
310 let installer = BitsAndBytesInstaller::with_defaults();
311 assert_eq!(
312 installer.fallback_fork_url(),
313 "https://github.com/TimDettmers/bitsandbytes.git"
314 );
315 }
316
317 #[test]
318 fn test_git_clone_command() {
319 let installer = BitsAndBytesInstaller::with_defaults();
320 let cmd = installer.build_git_clone_command("/tmp/bnb");
321 assert_eq!(cmd.program, "git");
322 assert!(cmd.args.contains(&"clone".to_string()));
323 assert!(cmd.args.contains(&"--recursive".to_string()));
324 assert!(cmd.args.iter().any(|a| a.contains("ROCm/bitsandbytes")));
325 }
326
327 #[test]
328 fn test_build_rocm_env() {
329 let installer = BitsAndBytesInstaller::new(BitsAndBytesConfig {
330 rocm_version: "7.2.0".to_string(),
331 gpu_arch: "gfx1100".to_string(),
332 ..Default::default()
333 });
334 let env = installer.build_rocm_env();
335 assert!(env.iter().any(|(k, v)| k == "BNB_USE_ROCM" && v == "1"));
336 assert!(env
337 .iter()
338 .any(|(k, v)| k == "BNB_ROCM_ARCH" && v == "gfx1100"));
339 assert!(env
340 .iter()
341 .any(|(k, v)| k == "ROCM_PATH" && v == "/opt/rocm"));
342 assert!(env
343 .iter()
344 .any(|(k, v)| k == "BNB_ROCM_VERSION" && v == "72"));
345 }
346
347 #[test]
348 fn test_rocm_version_short() {
349 let installer = BitsAndBytesInstaller::new(BitsAndBytesConfig {
350 rocm_version: "7.2.0".to_string(),
351 ..Default::default()
352 });
353 assert_eq!(installer.rocm_version_short(), "72");
354 }
355
356 #[test]
357 fn test_rocm_version_short_legacy() {
358 let installer = BitsAndBytesInstaller::new(BitsAndBytesConfig {
359 rocm_version: "7.0.0".to_string(),
360 ..Default::default()
361 });
362 assert_eq!(installer.rocm_version_short(), "70");
363 }
364
365 #[test]
366 fn test_build_deps_command() {
367 let installer = BitsAndBytesInstaller::new(BitsAndBytesConfig {
368 method: InstallMethod::Global,
369 ..Default::default()
370 });
371 let cmd = installer.build_build_deps_command();
372 assert!(cmd.args.contains(&"scikit-build-core".to_string()));
373 assert!(cmd.args.contains(&"cmake".to_string()));
374 assert!(cmd.args.contains(&"ninja".to_string()));
375 }
376
377 #[test]
378 fn test_source_install_command() {
379 let installer = BitsAndBytesInstaller::new(BitsAndBytesConfig {
380 rocm_version: "7.2.0".to_string(),
381 gpu_arch: "gfx1100".to_string(),
382 method: InstallMethod::Global,
383 ..Default::default()
384 });
385 let cmd = installer.build_source_install_command();
386 assert_eq!(cmd.program, "python3");
387 assert!(cmd.args.contains(&"--no-build-isolation".to_string()));
388 assert!(cmd.args.contains(&"--no-deps".to_string()));
389 assert!(cmd.args.contains(&".".to_string()));
390 assert!(cmd.args.iter().any(|a| a.contains("COMPUTE_BACKEND=hip")));
391 assert!(cmd.args.iter().any(|a| a.contains("ROCM_PATH=/opt/rocm")));
392 assert!(cmd.env.iter().any(|(k, v)| k == "BNB_USE_ROCM" && v == "1"));
394 }
395
396 #[test]
397 fn test_uninstall_command() {
398 let installer = BitsAndBytesInstaller::with_defaults();
399 let cmd = installer.build_uninstall_command();
400 assert!(cmd.args.contains(&"uninstall".to_string()));
401 assert!(cmd.args.contains(&"-y".to_string()));
402 assert!(cmd.args.contains(&"bitsandbytes".to_string()));
403 }
404}