1use std::fs;
4use std::path::{Path, PathBuf};
5
6use super::detect;
7use crate::context::Context;
8use crate::util::archive;
9use crate::util::http;
10use crate::util::system;
11use crate::util::system::GpuVendor;
12
13const LIBTORCH_VERSION: &str = "2.10.0";
18
19struct VariantSpec {
21 label: &'static str,
23 dir_name: &'static str,
25 arch_cuda: &'static str,
27 arch_archs: &'static str,
29 arch_variant: &'static str,
31}
32
33const CPU_SPEC: VariantSpec = VariantSpec {
34 label: "CPU",
35 dir_name: "cpu",
36 arch_cuda: "none",
37 arch_archs: "cpu",
38 arch_variant: "cpu",
39};
40
41const CU126_SPEC: VariantSpec = VariantSpec {
42 label: "CUDA 12.6",
43 dir_name: "cu126",
44 arch_cuda: "12.6",
45 arch_archs: "5.0 5.2 6.0 6.1 7.0 7.5 8.0 8.6 8.9 9.0",
46 arch_variant: "cu126",
47};
48
49const CU128_SPEC: VariantSpec = VariantSpec {
50 label: "CUDA 12.8",
51 dir_name: "cu128",
52 arch_cuda: "12.8",
53 arch_archs: "7.0 7.5 8.0 8.6 8.9 9.0 12.0",
54 arch_variant: "cu128",
55};
56
57const ROCM_ARCHS: &str = "gfx908 gfx90a gfx942 gfx950 gfx1030 gfx1100 gfx1101 \
72 gfx1102 gfx1150 gfx1151 gfx1200 gfx1201";
73
74const ROCM70_SPEC: VariantSpec = VariantSpec {
75 label: "ROCm 7.0",
76 dir_name: "rocm70",
79 arch_cuda: "none",
83 arch_archs: ROCM_ARCHS,
84 arch_variant: "rocm7.0",
89};
90
91const ROCM71_SPEC: VariantSpec = VariantSpec {
92 label: "ROCm 7.1",
93 dir_name: "rocm71",
94 arch_cuda: "none",
95 arch_archs: ROCM_ARCHS,
99 arch_variant: "rocm7.1",
100};
101
102pub enum Variant {
107 Cpu,
108 Cuda126,
109 Cuda128,
110 Rocm70,
111 Rocm71,
112 Auto,
113}
114
115pub struct DownloadOpts {
116 pub variant: Variant,
117 pub custom_path: Option<PathBuf>,
118 pub activate: bool,
119 pub dry_run: bool,
120 pub force_linux: bool,
126}
127
128impl Default for DownloadOpts {
129 fn default() -> Self {
130 Self {
131 variant: Variant::Auto,
132 custom_path: None,
133 activate: true,
134 dry_run: false,
135 force_linux: false,
136 }
137 }
138}
139
140fn absolute_libomp_refs(otool_output: &str) -> Vec<String> {
153 otool_output
154 .lines()
155 .skip(1)
156 .filter_map(|l| l.split_whitespace().next())
157 .filter(|p| p.starts_with('/') && p.ends_with("/libomp.dylib"))
158 .map(str::to_string)
159 .collect()
160}
161
162fn relink_bundled_libomp(lib_dir: &Path) {
185 if !lib_dir.join("libomp.dylib").exists() {
189 return;
190 }
191 let dylibs: Vec<PathBuf> = match fs::read_dir(lib_dir) {
192 Ok(rd) => rd
193 .filter_map(|e| e.ok().map(|e| e.path()))
194 .filter(|p| p.extension().is_some_and(|x| x == "dylib"))
195 .collect(),
196 Err(_) => return,
197 };
198
199 let missing: Vec<&str> = ["otool", "install_name_tool", "codesign"]
200 .into_iter()
201 .filter(|t| !crate::util::system::has_command(t))
202 .collect();
203 if !missing.is_empty() {
204 println!(
205 " note: cannot relink the bundled libomp ({} not found).\n\
206 \x20 Upstream's libtorch_cpu.dylib asks for libomp at an absolute\n\
207 \x20 Homebrew path, so a NATIVE run may fail to start; the docker\n\
208 \x20 path is unaffected. Install the command line tools with\n\
209 \x20 `xcode-select --install` and re-run this download to fix it.",
210 missing.join(", "),
211 );
212 return;
213 }
214
215 let mut patched = 0usize;
216 for f in &dylibs {
217 let out = match std::process::Command::new("otool")
218 .arg("-L")
219 .arg(f)
220 .output()
221 {
222 Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).to_string(),
223 _ => continue,
224 };
225 let refs = absolute_libomp_refs(&out);
226 if refs.is_empty() {
227 continue;
228 }
229 let mut cmd = std::process::Command::new("install_name_tool");
230 for r in &refs {
231 cmd.arg("-change").arg(r).arg("@loader_path/libomp.dylib");
232 }
233 match cmd.arg(f).output() {
234 Ok(o) if o.status.success() => {}
235 other => {
236 println!(
237 " note: install_name_tool failed on {}: {}",
238 f.display(),
239 match other {
240 Ok(o) => String::from_utf8_lossy(&o.stderr).trim().to_string(),
241 Err(e) => e.to_string(),
242 },
243 );
244 continue;
245 }
246 }
247 match std::process::Command::new("codesign")
250 .args(["-f", "-s", "-"])
251 .arg(f)
252 .output()
253 {
254 Ok(o) if o.status.success() => patched += 1,
255 other => println!(
256 " warning: {} was relinked but could NOT be re-signed ({}); \
257 it may fail to load. Re-run this download after \
258 `xcode-select --install`.",
259 f.display(),
260 match other {
261 Ok(o) => String::from_utf8_lossy(&o.stderr).trim().to_string(),
262 Err(e) => e.to_string(),
263 },
264 ),
265 }
266 }
267 if patched > 0 {
268 println!(" relinked {patched} dylib(s) to the bundled libomp");
269 }
270}
271
272fn download_url(spec: &VariantSpec, force_linux: bool) -> Result<String, String> {
273 let (os, arch) = if force_linux {
277 ("linux", "x86_64")
278 } else {
279 (std::env::consts::OS, std::env::consts::ARCH)
280 };
281
282 download_url_for(spec, os, arch)
283}
284
285fn download_url_for(spec: &VariantSpec, os: &str, arch: &str) -> Result<String, String> {
295 match (os, arch) {
296 ("linux", "x86_64") => {}
297 ("macos", "aarch64") => {
298 if spec.arch_cuda != "none" || spec.arch_variant.starts_with("rocm") {
303 return Err("macOS only supports CPU libtorch".into());
304 }
305 }
306 ("macos", _) => {
307 return Err(format!(
308 "macOS libtorch requires Apple Silicon (arm64), got {}.\n\
309 macOS x86_64 was dropped after PyTorch 2.2.",
310 arch
311 ));
312 }
313 ("windows", "x86_64") => {
314 if spec.arch_variant.starts_with("rocm") {
317 return Err(format!(
318 "{} libtorch is not available for Windows.\n\
319 PyTorch publishes ROCm builds for Linux only.",
320 spec.label
321 ));
322 }
323 }
324 _ => {
325 return Err(format!(
326 "Unsupported platform: {} {}.\n\
327 libtorch is available for Linux x86_64, macOS arm64, and Windows x86_64.",
328 os, arch
329 ));
330 }
331 }
332
333 if os == "macos" {
335 return Ok(format!(
336 "https://download.pytorch.org/libtorch/cpu/libtorch-macos-arm64-{}.zip",
337 LIBTORCH_VERSION
338 ));
339 }
340
341 let infix = if os == "windows" { "win-" } else { "" };
346 let filename = format!(
347 "libtorch-{}shared-with-deps-{}%2B{}.zip",
348 infix, LIBTORCH_VERSION, spec.arch_variant
349 );
350
351 let bucket = spec.arch_variant; Ok(format!(
353 "https://download.pytorch.org/libtorch/{}/{}",
354 bucket, filename
355 ))
356}
357
358fn auto_detect_variant() -> &'static VariantSpec {
363 let survey = flodl_hw::survey();
364 if survey.devices.is_empty() {
365 for note in survey.notes.iter().filter(|n| n.kind.explains_absence()) {
371 println!(" {}", note.message);
372 }
373 }
374 variant_for_gpus(&survey.devices)
375}
376
377fn variant_for_gpus(gpus: &[system::GpuInfo]) -> &'static VariantSpec {
383 if gpus.is_empty() {
384 println!(" No GPU detected. Using CPU variant.");
385 return &CPU_SPEC;
386 }
387
388 let amd: Vec<_> = gpus.iter().filter(|g| g.vendor == GpuVendor::Amd).collect();
392 let has_nvidia = gpus.iter().any(|g| g.vendor == GpuVendor::Nvidia);
393 if !amd.is_empty() {
394 if has_nvidia {
395 println!(
396 " Both NVIDIA and AMD GPUs detected. One libtorch build serves\n \
397 one vendor, so the NVIDIA cards are used and the AMD ones stay\n \
398 idle. For the AMD cards instead: fdl libtorch download --rocm 7.0",
399 );
400 } else {
401 return rocm_variant_for(&amd);
402 }
403 }
404
405 let majors: Vec<u32> = gpus.iter().filter_map(|g| g.sm_major()).collect();
408 if majors.is_empty() {
409 let other: Vec<String> = gpus
410 .iter()
411 .map(|g| format!("{} ({})", g.short_name(), g.arch_label()))
412 .collect();
413 println!(
414 " Detected a GPU with no known libtorch variant ({}).\n \
415 Using the CPU variant.",
416 other.join(", "),
417 );
418 return &CPU_SPEC;
419 }
420 let lo_major = majors.iter().copied().min().unwrap_or(0);
421 let hi_major = majors.iter().copied().max().unwrap_or(0);
422
423 if lo_major >= 7 {
425 println!(" Detected Volta+ GPU(s). Using cu128.");
426 &CU128_SPEC
427 } else if hi_major >= 10 {
428 println!(
431 " Mixed GPU architectures (sm_{}.x to sm_{}.x).",
432 lo_major, hi_major
433 );
434 println!(" Using cu126 (broadest pre-Volta coverage).");
435 println!(" For all GPUs, consider: fdl libtorch build");
436 &CU126_SPEC
437 } else {
438 println!(" Detected pre-Volta GPU(s). Using cu126.");
439 &CU126_SPEC
440 }
441}
442
443pub fn rocm_covered(gpus: &[system::GpuInfo]) -> Vec<&system::GpuInfo> {
449 gpus.iter()
450 .filter(|g| g.vendor == GpuVendor::Amd && g.covered_by(ROCM_ARCHS))
451 .collect()
452}
453
454pub fn rocm_archs() -> &'static str {
456 ROCM_ARCHS
457}
458
459fn rocm_variant_for(amd: &[&system::GpuInfo]) -> &'static VariantSpec {
476 let (covered, uncovered): (Vec<_>, Vec<_>) = amd.iter().partition(|g| g.covered_by(ROCM_ARCHS));
477
478 let describe = |gs: &[&&system::GpuInfo]| {
479 gs.iter()
480 .map(|g| format!("{} ({})", g.short_name(), g.arch_label()))
481 .collect::<Vec<_>>()
482 .join(", ")
483 };
484
485 if covered.is_empty() {
486 println!(
487 " Detected AMD GPU(s) ({}) outside the ROCm build's gfx\n \
488 targets, so the CPU variant is selected.\n \
489 Covered targets: {}.",
490 describe(&uncovered),
491 ROCM_ARCHS,
492 );
493 return &CPU_SPEC;
494 }
495 if !uncovered.is_empty() {
496 println!(
497 " Note: {} is not covered by the ROCm build and will be\n \
498 unusable. Covered targets: {}.",
499 describe(&uncovered),
500 ROCM_ARCHS,
501 );
502 }
503 println!(
504 " Detected AMD GPU(s) ({}). Using ROCm 7.0.",
505 describe(&covered)
506 );
507 &ROCM70_SPEC
508}
509
510fn resolve_variant(variant: &Variant) -> &'static VariantSpec {
511 match variant {
512 Variant::Cpu => &CPU_SPEC,
513 Variant::Cuda126 => &CU126_SPEC,
514 Variant::Cuda128 => &CU128_SPEC,
515 Variant::Rocm70 => &ROCM70_SPEC,
516 Variant::Rocm71 => &ROCM71_SPEC,
517 Variant::Auto => auto_detect_variant(),
518 }
519}
520
521pub fn run(opts: DownloadOpts) -> Result<String, String> {
526 let ctx = Context::resolve();
527 run_with_context(opts, &ctx)
528}
529
530pub fn run_with_context(opts: DownloadOpts, ctx: &Context) -> Result<String, String> {
538 let spec = resolve_variant(&opts.variant);
539 let url = download_url(spec, opts.force_linux)?;
540
541 let install_path = if let Some(ref p) = opts.custom_path {
543 p.clone()
544 } else {
545 ctx.root
546 .join(format!("libtorch/precompiled/{}", spec.dir_name))
547 };
548
549 let variant_id = format!("precompiled/{}", spec.dir_name);
550
551 println!();
552 println!(" libtorch {} ({})", LIBTORCH_VERSION, spec.label);
553 println!(" URL: {}", url);
554 println!(" Path: {}", install_path.display());
555
556 if opts.dry_run {
557 println!();
558 println!(" [dry-run] Would download and extract to above path.");
559 return Ok(variant_id);
560 }
561
562 if install_path.exists() {
564 let build_ver_path = install_path.join("build-version");
565 let existing_ver = fs::read_to_string(&build_ver_path)
566 .ok()
567 .map(|s| s.trim().to_string());
568
569 let ver_matches = existing_ver.as_deref().is_some_and(|v| {
571 v == LIBTORCH_VERSION || v.starts_with(&format!("{}+", LIBTORCH_VERSION))
572 });
573
574 if ver_matches {
575 println!();
576 println!(" Already installed (version {}).", LIBTORCH_VERSION);
577 return Ok(variant_id);
578 }
579
580 println!();
581 println!(
582 " Removing existing installation (version: {})...",
583 existing_ver.as_deref().unwrap_or("unknown")
584 );
585 fs::remove_dir_all(&install_path)
586 .map_err(|e| format!("cannot remove {}: {}", install_path.display(), e))?;
587 }
588
589 let stage_root = install_path
602 .parent()
603 .map(Path::to_path_buf)
604 .unwrap_or_else(|| PathBuf::from("."));
605 fs::create_dir_all(&stage_root)
606 .map_err(|e| format!("cannot create {}: {}", stage_root.display(), e))?;
607 let stage = Staging::new(stage_root.join(format!(".fdl-staging-{}", std::process::id())))?;
608
609 let tmp_zip = stage.path().join(format!("libtorch-{}.zip", spec.dir_name));
610
611 println!();
612 println!(" Downloading...");
613 http::download_file(&url, &tmp_zip)?;
614
615 let tmp_extract = stage.path().join("extract");
617 println!(" Extracting...");
618 archive::extract_zip(&tmp_zip, &tmp_extract)?;
619
620 let extracted_lt = tmp_extract.join("libtorch");
622 let source = if extracted_lt.is_dir() {
623 &extracted_lt
624 } else {
625 &tmp_extract
626 };
627
628 fs::create_dir_all(&install_path)
629 .map_err(|e| format!("cannot create {}: {}", install_path.display(), e))?;
630
631 move_contents(source, &install_path)?;
635
636 drop(stage);
641
642 let lib_dir = install_path.join("lib");
644 let has_lib = lib_dir.join("libtorch.so").exists()
645 || lib_dir.join("libtorch.dylib").exists()
646 || lib_dir.join("torch.lib").exists();
647
648 if !has_lib {
649 return Err(format!(
650 "libtorch library not found at {}.\n\
651 The archive structure may have changed.\n\
652 Check: ls {}",
653 lib_dir.display(),
654 lib_dir.display()
655 ));
656 }
657
658 relink_bundled_libomp(&lib_dir);
659
660 let arch_content = format!(
662 "cuda={}\ntorch={}\narchs={}\nsource=precompiled\nvariant={}\n",
663 spec.arch_cuda, LIBTORCH_VERSION, spec.arch_archs, spec.arch_variant
664 );
665 fs::write(install_path.join(".arch"), arch_content)
666 .map_err(|e| format!("cannot write .arch: {}", e))?;
667
668 if opts.activate {
669 detect::set_active(&ctx.root, &variant_id)?;
670 }
671
672 println!();
673 println!(" ================================================");
674 println!(" libtorch {} ({}) installed", LIBTORCH_VERSION, spec.label);
675 println!(" {}", install_path.display());
676 println!(" ================================================");
677
678 if ctx.is_project {
679 println!();
680 println!(" .arch: {}/.arch", install_path.display());
681 if opts.activate {
682 println!(" .active: libtorch/.active -> {}", variant_id);
683 }
684 println!();
685 if detect::variant_vendor(&variant_id).is_some() {
690 println!(" Run 'fdl gpu-test' to verify.");
691 } else {
692 println!(" Run 'fdl test' to verify.");
693 }
694 } else {
695 println!();
696 println!(" Installed to: {}", install_path.display());
697 println!();
698 println!(" To use with tch-rs or flodl, add to your shell profile:");
699 println!();
700 println!(" export LIBTORCH=\"{}\"", install_path.display());
701 let lib = format!("{}/lib", install_path.display());
705 for line in detect::ld_library_path_lines(detect::variant_vendor(&variant_id), &lib) {
706 println!(" {line}");
707 }
708 println!();
709 println!(" Or start a new floDl project:");
710 println!(" fdl init my-project");
711 }
712
713 Ok(variant_id)
714}
715
716struct Staging(PathBuf);
727
728impl Staging {
729 fn new(path: PathBuf) -> Result<Self, String> {
730 let _ = fs::remove_dir_all(&path);
733 fs::create_dir_all(&path)
734 .map_err(|e| format!("cannot create staging dir {}: {}", path.display(), e))?;
735 Ok(Self(path))
736 }
737 fn path(&self) -> &Path {
738 &self.0
739 }
740}
741
742impl Drop for Staging {
743 fn drop(&mut self) {
744 let _ = fs::remove_dir_all(&self.0);
745 }
746}
747
748fn move_contents(src: &Path, dest: &Path) -> Result<(), String> {
749 let entries = fs::read_dir(src).map_err(|e| format!("cannot read {}: {}", src.display(), e))?;
750
751 for entry in entries {
752 let entry = entry.map_err(|e| format!("read_dir error: {}", e))?;
753 let from = entry.path();
754 let name = entry.file_name();
755 let to = dest.join(&name);
756
757 if fs::rename(&from, &to).is_err() {
759 if from.is_dir() {
760 copy_dir_recursive(&from, &to)?;
761 } else {
762 fs::copy(&from, &to)
763 .map_err(|e| format!("copy {} -> {}: {}", from.display(), to.display(), e))?;
764 }
765 }
766 }
767 Ok(())
768}
769
770fn copy_dir_recursive(src: &Path, dest: &Path) -> Result<(), String> {
771 fs::create_dir_all(dest).map_err(|e| format!("cannot create {}: {}", dest.display(), e))?;
772
773 for entry in fs::read_dir(src).map_err(|e| format!("read {}: {}", src.display(), e))? {
774 let entry = entry.map_err(|e| format!("read_dir error: {}", e))?;
775 let from = entry.path();
776 let to = dest.join(entry.file_name());
777
778 if from.is_dir() {
779 copy_dir_recursive(&from, &to)?;
780 } else {
781 fs::copy(&from, &to)
782 .map_err(|e| format!("copy {} -> {}: {}", from.display(), to.display(), e))?;
783 }
784 }
785 Ok(())
786}
787
788#[allow(dead_code)]
790pub fn libtorch_version() -> &'static str {
791 LIBTORCH_VERSION
792}
793
794#[cfg(test)]
795mod tests {
796 use super::*;
797
798 #[test]
804 fn linux_url_has_no_os_infix() {
805 let url = download_url_for(&CU128_SPEC, "linux", "x86_64").unwrap();
806 assert_eq!(
807 url,
808 format!(
809 "https://download.pytorch.org/libtorch/cu128/\
810 libtorch-shared-with-deps-{LIBTORCH_VERSION}%2Bcu128.zip"
811 )
812 );
813 }
814
815 #[test]
816 fn windows_url_carries_the_win_infix() {
817 let url = download_url_for(&CU128_SPEC, "windows", "x86_64").unwrap();
819 assert!(
820 url.contains("libtorch-win-shared-with-deps-"),
821 "windows archives need the `-win-` infix, got {url}"
822 );
823 assert_eq!(
824 url,
825 format!(
826 "https://download.pytorch.org/libtorch/cu128/\
827 libtorch-win-shared-with-deps-{LIBTORCH_VERSION}%2Bcu128.zip"
828 )
829 );
830 }
831
832 #[test]
833 fn windows_cpu_url_carries_the_win_infix() {
834 let url = download_url_for(&CPU_SPEC, "windows", "x86_64").unwrap();
835 assert_eq!(
836 url,
837 format!(
838 "https://download.pytorch.org/libtorch/cpu/\
839 libtorch-win-shared-with-deps-{LIBTORCH_VERSION}%2Bcpu.zip"
840 )
841 );
842 }
843
844 #[test]
845 fn windows_rejects_rocm() {
846 for spec in [&ROCM70_SPEC, &ROCM71_SPEC] {
849 let err = download_url_for(spec, "windows", "x86_64").unwrap_err();
850 assert!(err.contains("not available for Windows"), "got {err}");
851 }
852 }
853
854 #[test]
855 fn linux_accepts_rocm() {
856 for spec in [&ROCM70_SPEC, &ROCM71_SPEC] {
857 let url = download_url_for(spec, "linux", "x86_64").unwrap();
858 let bucket = spec.arch_variant;
859 assert_eq!(
860 url,
861 format!(
862 "https://download.pytorch.org/libtorch/{bucket}/\
863 libtorch-shared-with-deps-{LIBTORCH_VERSION}%2B{bucket}.zip"
864 )
865 );
866 }
867 }
868
869 #[test]
870 fn the_rocm_variants_differ_only_in_runtime_version() {
871 assert_eq!(ROCM70_SPEC.arch_archs, ROCM71_SPEC.arch_archs);
875 assert_ne!(ROCM70_SPEC.arch_variant, ROCM71_SPEC.arch_variant);
876 assert_ne!(ROCM70_SPEC.dir_name, ROCM71_SPEC.dir_name);
877 for spec in [&ROCM70_SPEC, &ROCM71_SPEC] {
880 assert_eq!(
881 detect::variant_vendor(&format!("precompiled/{}", spec.dir_name)),
882 Some(GpuVendor::Amd),
883 "{} must derive the AMD feature",
884 spec.dir_name
885 );
886 }
887 }
888
889 #[test]
890 fn macos_arm_uses_its_own_filename_and_is_cpu_only() {
891 let url = download_url_for(&CPU_SPEC, "macos", "aarch64").unwrap();
892 assert_eq!(
893 url,
894 format!(
895 "https://download.pytorch.org/libtorch/cpu/\
896 libtorch-macos-arm64-{LIBTORCH_VERSION}.zip"
897 )
898 );
899
900 let err = download_url_for(&CU128_SPEC, "macos", "aarch64").unwrap_err();
901 assert!(err.contains("only supports CPU"), "got {err}");
902 }
903
904 #[test]
905 fn macos_rejects_rocm_rather_than_serving_the_cpu_archive() {
906 for spec in [&ROCM70_SPEC, &ROCM71_SPEC] {
911 let err = download_url_for(spec, "macos", "aarch64").unwrap_err();
912 assert!(err.contains("only supports CPU"), "got {err}");
913 }
914 }
915
916 #[test]
917 fn macos_intel_is_rejected_with_a_reason() {
918 let err = download_url_for(&CPU_SPEC, "macos", "x86_64").unwrap_err();
919 assert!(err.contains("Apple Silicon"), "got {err}");
920 }
921
922 #[test]
923 fn unsupported_platform_is_rejected() {
924 let err = download_url_for(&CPU_SPEC, "linux", "aarch64").unwrap_err();
927 assert!(err.contains("Unsupported platform"), "got {err}");
928 }
929
930 const OTOOL_LIBTORCH_CPU: &str = "\
934libtorch/lib/libtorch_cpu.dylib:
935\t@rpath/libtorch_cpu.dylib (compatibility version 0.0.0, current version 0.0.0)
936\t/opt/homebrew/opt/libomp/lib/libomp.dylib (compatibility version 5.0.0, current version 5.0.0)
937\t@rpath/libc10.dylib (compatibility version 0.0.0, current version 0.0.0)
938\t/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 1700.255.0)
939\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1351.0.0)
940";
941
942 #[test]
943 fn the_absolute_libomp_dependency_is_the_only_one_rewritten() {
944 assert_eq!(
949 absolute_libomp_refs(OTOOL_LIBTORCH_CPU),
950 vec!["/opt/homebrew/opt/libomp/lib/libomp.dylib".to_string()],
951 );
952 }
953
954 #[test]
955 fn an_already_relative_libomp_is_left_alone() {
956 let patched = OTOOL_LIBTORCH_CPU.replace(
959 "/opt/homebrew/opt/libomp/lib/libomp.dylib",
960 "@loader_path/libomp.dylib",
961 );
962 assert!(absolute_libomp_refs(&patched).is_empty(), "{patched}");
963 let upstream_fixed = OTOOL_LIBTORCH_CPU.replace("/opt/homebrew/opt/libomp/lib/", "@rpath/");
965 assert!(absolute_libomp_refs(&upstream_fixed).is_empty());
966 }
967
968 #[test]
969 fn a_libomp_at_another_absolute_prefix_still_qualifies() {
970 let intel = OTOOL_LIBTORCH_CPU.replace("/opt/homebrew/opt", "/usr/local/opt");
974 assert_eq!(
975 absolute_libomp_refs(&intel),
976 vec!["/usr/local/opt/libomp/lib/libomp.dylib".to_string()],
977 );
978 }
979
980 #[test]
981 fn a_dump_with_no_dependencies_yields_nothing() {
982 assert!(absolute_libomp_refs("").is_empty());
983 assert!(absolute_libomp_refs("libomp.dylib:\n").is_empty());
984 }
985
986 #[test]
987 fn force_linux_ignores_the_host() {
988 let url = download_url(&CU128_SPEC, true).unwrap();
991 assert!(url.contains("libtorch-shared-with-deps-"), "got {url}");
992 assert!(!url.contains("-win-"), "got {url}");
993 assert!(!url.contains("macos"), "got {url}");
994 }
995
996 fn gpu(vendor: GpuVendor, arch: &str) -> system::GpuInfo {
1000 system::GpuInfo {
1001 index: 0,
1002 vendor,
1003 name: format!("test {arch}"),
1004 arch: flodl_hw::GpuArch::parse(vendor, arch)
1005 .unwrap_or_else(|| panic!("unparsable arch {arch}")),
1006 total_memory_mb: 8192,
1007 }
1008 }
1009
1010 #[test]
1011 fn no_gpu_routes_to_cpu() {
1012 assert_eq!(variant_for_gpus(&[]).arch_variant, "cpu");
1013 }
1014
1015 #[test]
1016 fn a_covered_amd_gpu_routes_to_rocm() {
1017 for arch in [
1022 "gfx908", "gfx90a", "gfx942", "gfx950", "gfx1030", "gfx1100", "gfx1151", "gfx1201",
1023 ] {
1024 let v = variant_for_gpus(&[gpu(GpuVendor::Amd, arch)]);
1025 assert_eq!(v.arch_variant, "rocm7.0", "{arch} should route to ROCm");
1026 }
1027 }
1028
1029 #[test]
1030 fn a_perf_db_only_target_is_not_covered() {
1031 for arch in ["gfx900", "gfx906"] {
1036 let v = variant_for_gpus(&[gpu(GpuVendor::Amd, arch)]);
1037 assert_eq!(v.arch_variant, "cpu", "{arch} ships no kernels");
1038 assert!(rocm_covered(&[gpu(GpuVendor::Amd, arch)]).is_empty());
1039 }
1040 }
1041
1042 #[test]
1043 fn auto_never_picks_the_newer_rocm_bundle() {
1044 for arch in ["gfx942", "gfx950", "gfx1151"] {
1048 assert_eq!(
1049 variant_for_gpus(&[gpu(GpuVendor::Amd, arch)]).arch_variant,
1050 "rocm7.0"
1051 );
1052 }
1053 assert_eq!(resolve_variant(&Variant::Rocm71).arch_variant, "rocm7.1");
1054 }
1055
1056 #[test]
1057 fn an_uncovered_amd_gpu_routes_to_cpu() {
1058 let v = variant_for_gpus(&[gpu(GpuVendor::Amd, "gfx803")]);
1061 assert_eq!(v.arch_variant, "cpu");
1062 }
1063
1064 #[test]
1065 fn a_partly_covered_amd_set_still_routes_to_rocm() {
1066 let v = variant_for_gpus(&[gpu(GpuVendor::Amd, "gfx942"), gpu(GpuVendor::Amd, "gfx803")]);
1067 assert_eq!(v.arch_variant, "rocm7.0");
1068 }
1069
1070 #[test]
1071 fn a_mixed_vendor_box_routes_to_cuda() {
1072 let v = variant_for_gpus(&[
1075 gpu(GpuVendor::Nvidia, "sm_120"),
1076 gpu(GpuVendor::Amd, "gfx1100"),
1077 ]);
1078 assert_eq!(v.arch_variant, "cu128");
1079 }
1080
1081 #[test]
1082 fn rocm_covered_selects_only_supported_amd_devices() {
1083 let gpus = vec![
1086 gpu(GpuVendor::Nvidia, "sm_120"),
1087 gpu(GpuVendor::Amd, "gfx942"),
1088 gpu(GpuVendor::Amd, "gfx803"),
1089 ];
1090 let covered = rocm_covered(&gpus);
1091 assert_eq!(covered.len(), 1);
1092 assert_eq!(covered[0].arch_label(), "gfx942");
1093 }
1094
1095 #[test]
1096 fn nvidia_routing_is_unchanged() {
1097 assert_eq!(
1098 variant_for_gpus(&[gpu(GpuVendor::Nvidia, "sm_120")]).arch_variant,
1099 "cu128"
1100 );
1101 assert_eq!(
1102 variant_for_gpus(&[gpu(GpuVendor::Nvidia, "sm_61")]).arch_variant,
1103 "cu126"
1104 );
1105 }
1106}