1use std::path::{Path, PathBuf};
16
17const FASTVIDEO_REPO: &str = "https://github.com/scooter-lacroix/FastVideo.git";
18pub const FASTVIDEO_COMMIT: &str = "22e448771ebf5c81108f1c57b9c7ef4d5c26d182";
19const FASTVIDEO_BUILD_DIR: &str = "/tmp/FastVideo_ROCm_build";
20
21pub fn max_jobs_for_parallelism(nproc: usize) -> usize {
23 nproc.saturating_sub(1).clamp(1, 4)
24}
25
26fn is_valid_gpu_arch(arch: &str) -> bool {
27 arch.strip_prefix("gfx")
28 .is_some_and(|suffix| !suffix.is_empty() && suffix.chars().all(|ch| ch.is_ascii_hexdigit()))
29}
30
31fn push_normalized_archs(archs: &mut Vec<String>, value: &str) {
32 for arch in value
33 .split(|ch: char| ch == ';' || ch == ',' || ch.is_ascii_whitespace())
34 .map(str::trim)
35 .filter(|arch| is_valid_gpu_arch(arch))
36 {
37 if !archs.iter().any(|existing| existing == arch) {
38 archs.push(arch.to_string());
39 }
40 }
41}
42
43pub fn select_visible_gpu_archs(
45 enumerated_archs: &[String],
46 rocr_visible_devices: Option<&str>,
47 hip_visible_devices: Option<&str>,
48 cuda_visible_devices: Option<&str>,
49) -> Vec<String> {
50 let mask = [
51 rocr_visible_devices,
52 hip_visible_devices,
53 cuda_visible_devices,
54 ]
55 .into_iter()
56 .flatten()
57 .map(str::trim)
58 .find(|value| !value.is_empty());
59
60 let mut selected = Vec::new();
61 if let Some(mask) = mask {
62 for index in mask
63 .split(|ch: char| ch == ',' || ch.is_ascii_whitespace())
64 .filter_map(|value| value.trim().parse::<usize>().ok())
65 {
66 if let Some(arch) = enumerated_archs.get(index) {
67 push_normalized_archs(&mut selected, arch);
68 }
69 }
70 } else {
71 for arch in enumerated_archs {
72 push_normalized_archs(&mut selected, arch);
73 }
74 }
75 selected
76}
77
78fn discrete_pci_id_to_gfx(device_id: &str) -> Option<&'static str> {
79 match device_id
80 .trim()
81 .trim_start_matches("0x")
82 .to_ascii_lowercase()
83 .as_str()
84 {
85 "7550" | "7551" => Some("gfx1201"),
86 "7590" => Some("gfx1200"),
87 "744c" | "7448" | "7449" | "744a" | "744b" | "745e" => Some("gfx1100"),
88 "747e" | "7470" | "7460" | "7461" => Some("gfx1101"),
89 "7480" | "7483" | "7489" | "749f" | "73f0" => Some("gfx1102"),
90 "73bf" | "73af" | "73a5" | "73a1" | "73a2" | "73a3" => Some("gfx1030"),
91 "73df" | "73c3" => Some("gfx1031"),
92 "73ff" | "73ef" | "73e0" | "73e1" | "73e3" => Some("gfx1032"),
93 "743f" | "7424" | "7421" | "7422" | "7423" => Some("gfx1034"),
94 _ => None,
95 }
96}
97
98pub fn detect_discrete_gpu_archs_from_sysfs(drm_root: &Path) -> Vec<String> {
103 let mut cards = match std::fs::read_dir(drm_root) {
104 Ok(entries) => entries
105 .filter_map(Result::ok)
106 .filter_map(|entry| {
107 let name = entry.file_name().to_string_lossy().into_owned();
108 let index = name.strip_prefix("card")?.parse::<usize>().ok()?;
109 Some((index, entry.path()))
110 })
111 .collect::<Vec<_>>(),
112 Err(_) => return Vec::new(),
113 };
114 cards.sort_by_key(|(index, _)| *index);
115
116 cards
117 .into_iter()
118 .filter_map(|(_, card)| {
119 let device = card.join("device");
120 let vendor = std::fs::read_to_string(device.join("vendor")).ok()?;
121 if vendor.trim() != "0x1002" {
122 return None;
123 }
124 let device_id = std::fs::read_to_string(device.join("device")).ok()?;
125 discrete_pci_id_to_gfx(&device_id).map(str::to_string)
126 })
127 .collect()
128}
129
130pub fn resolve_gpu_archs(
132 sysfs_archs: &[String],
133 gpu_archs_env: Option<&str>,
134 gpu_arch_env: Option<&str>,
135 detected_primary: Option<&str>,
136) -> String {
137 let mut archs = Vec::new();
138 for value in sysfs_archs {
139 push_normalized_archs(&mut archs, value);
140 }
141 if !archs.is_empty() {
142 return archs.join(";");
143 }
144
145 for source in [gpu_archs_env, gpu_arch_env, detected_primary]
146 .into_iter()
147 .flatten()
148 {
149 archs.clear();
150 push_normalized_archs(&mut archs, source);
151 if !archs.is_empty() {
152 return archs.join(";");
153 }
154 }
155
156 "gfx1100".to_string()
157}
158
159pub fn resolve_install_user(sudo_user: Option<&str>, user: Option<&str>) -> Option<String> {
161 [sudo_user, user]
162 .into_iter()
163 .flatten()
164 .map(str::trim)
165 .find(|value| !value.is_empty())
166 .map(str::to_string)
167}
168
169pub fn fastvideo_build_warning(gpu_archs: &str) -> String {
171 format!("[FastVideo] Heavy dual-arch HIP compile for {gpu_archs}; MAX_JOBS capped at 4")
172}
173
174pub fn fastvideo_verification_snippet() -> &'static str {
180 r#"import os
181import subprocess
182import sys
183
184probe = r"""
185import fastvideo
186import torch
187from fastvideo_kernel import int8_quant
188
189if not torch.cuda.is_available():
190 raise SystemExit("FastVideo verification requires an available ROCm GPU")
191device_count = torch.cuda.device_count()
192if device_count != 1:
193 raise SystemExit(f"FastVideo verification expected one isolated ROCm GPU, found {device_count}")
194
195device = "cuda:0"
196source = torch.randn(
197 (2, 128),
198 device=device,
199 dtype=torch.float16,
200).contiguous()
201quantized, scale = int8_quant(source)
202torch.cuda.synchronize()
203
204shape_ok = quantized.shape == source.shape
205dtype_ok = quantized.dtype == torch.int8
206scale_finite = bool(torch.isfinite(scale).all().item())
207if not (shape_ok and dtype_ok and scale_finite):
208 raise SystemExit(
209 "FastVideo int8_quant invariant failed on device 0: "
210 f"shape_ok={shape_ok} dtype={quantized.dtype} scale_finite={scale_finite}"
211 )
212
213name = torch.cuda.get_device_name(0)
214print(
215 f"[FastVideo] device=0 name={name} int8_quant=ok "
216 f"shape={tuple(quantized.shape)} dtype={quantized.dtype} "
217 f"scale_finite={scale_finite}"
218)
219"""
220
221visible = os.environ.get("ROCR_VISIBLE_DEVICES", "")
222devices = [device.strip() for device in visible.split(",") if device.strip()]
223if not devices:
224 env = os.environ.copy()
225 env.pop("HIP_VISIBLE_DEVICES", None)
226 env.pop("CUDA_VISIBLE_DEVICES", None)
227 env.pop("GPU_DEVICE_ORDINAL", None)
228 discovered = subprocess.run(
229 [sys.executable, "-c", "import torch; print(torch.cuda.device_count())"],
230 check=True,
231 capture_output=True,
232 text=True,
233 env=env,
234 )
235 devices = [str(device) for device in range(int(discovered.stdout.strip()))]
236if not devices:
237 raise SystemExit("FastVideo verification requires at least one visible ROCm GPU")
238
239for device in devices:
240 env = os.environ.copy()
241 env["ROCR_VISIBLE_DEVICES"] = device
242 env.pop("HIP_VISIBLE_DEVICES", None)
243 env.pop("CUDA_VISIBLE_DEVICES", None)
244 env.pop("GPU_DEVICE_ORDINAL", None)
245 subprocess.run([sys.executable, "-c", probe], check=True, env=env)
246"#
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251pub enum FastVideoInstallStep {
252 Chown,
253 ManagedDependenciesPreflight,
254 FlashAttentionPreflight,
255 SourcePolicyPreflight,
256 KernelInstall,
257 PackageInstall,
258}
259
260impl FastVideoInstallStep {
261 pub const fn requires_sudo(self) -> bool {
262 matches!(self, Self::Chown)
263 }
264}
265
266pub const fn post_checkout_plan() -> [FastVideoInstallStep; 6] {
267 [
268 FastVideoInstallStep::Chown,
269 FastVideoInstallStep::ManagedDependenciesPreflight,
270 FastVideoInstallStep::FlashAttentionPreflight,
271 FastVideoInstallStep::SourcePolicyPreflight,
272 FastVideoInstallStep::KernelInstall,
273 FastVideoInstallStep::PackageInstall,
274 ]
275}
276
277#[derive(Debug, Clone, PartialEq, Eq)]
279pub struct ShellCommand {
280 pub program: String,
282 pub args: Vec<String>,
284 pub env: Vec<(String, String)>,
286 pub working_dir: Option<PathBuf>,
288}
289
290#[derive(Debug, Clone)]
292pub struct FastVideoConfig {
293 pub gpu_archs: String,
295 pub python_bin: String,
297}
298
299impl Default for FastVideoConfig {
300 fn default() -> Self {
301 Self {
302 gpu_archs: "gfx1100".to_string(),
303 python_bin: "python3".to_string(),
304 }
305 }
306}
307
308#[derive(Debug, Clone)]
310pub struct FastVideoInstaller {
311 repo_url: String,
312 commit: String,
313 build_dir: String,
314 config: FastVideoConfig,
315}
316
317impl Default for FastVideoInstaller {
318 fn default() -> Self {
319 Self::new(FastVideoConfig::default())
320 }
321}
322
323impl FastVideoInstaller {
324 pub fn new(config: FastVideoConfig) -> Self {
325 Self {
326 repo_url: FASTVIDEO_REPO.to_string(),
327 commit: FASTVIDEO_COMMIT.to_string(),
328 build_dir: FASTVIDEO_BUILD_DIR.to_string(),
329 config,
330 }
331 }
332
333 pub fn mkdir_build_dir(&self) -> ShellCommand {
334 ShellCommand {
335 program: "mkdir".to_string(),
336 args: vec!["-p".to_string(), self.build_dir.clone()],
337 env: vec![],
338 working_dir: None,
339 }
340 }
341
342 pub fn git_clone(&self) -> ShellCommand {
343 ShellCommand {
344 program: "git".to_string(),
345 args: vec!["clone".to_string(), self.repo_url.clone(), ".".to_string()],
346 env: vec![],
347 working_dir: Some(PathBuf::from(&self.build_dir)),
348 }
349 }
350
351 pub fn git_checkout(&self) -> ShellCommand {
353 ShellCommand {
354 program: "git".to_string(),
355 args: vec![
356 "checkout".to_string(),
357 "--detach".to_string(),
358 self.commit.clone(),
359 ],
360 env: vec![],
361 working_dir: Some(PathBuf::from(&self.build_dir)),
362 }
363 }
364
365 pub fn chown_build_dir(&self, run_user: &str) -> ShellCommand {
367 ShellCommand {
368 program: "chown".to_string(),
369 args: vec![
370 "-R".to_string(),
371 format!("{run_user}:{run_user}"),
372 self.build_dir.clone(),
373 ],
374 env: vec![],
375 working_dir: None,
376 }
377 }
378
379 pub fn managed_dependencies_preflight(&self) -> ShellCommand {
384 let script = r#"import importlib.metadata
385import os
386from pathlib import Path
387import sys
388
389required = (
390 "setuptools==81.0.0",
391 "cmake==4.4.4",
392 "scikit-build-core==1.1.1",
393 "pathspec==1.1.1",
394 "wheel==0.48.0",
395 "ninja==1.13.2",
396 "imageio==2.38.1",
397 "diffusers==0.41.0",
398 "remote-pdb==2.1.0",
399 "ftfy==6.3.1",
400 "wcwidth==0.2.13",
401)
402problems = []
403for pin in required:
404 name, expected = pin.rsplit("==", 1)
405 try:
406 actual = importlib.metadata.version(name)
407 except importlib.metadata.PackageNotFoundError:
408 problems.append(f"{name}: missing (expected {expected})")
409 else:
410 if actual != expected:
411 problems.append(f"{name}: found {actual}, expected {expected}")
412
413try:
414 import torch
415except Exception as exc:
416 problems.append(f"torch: import failed ({exc})")
417else:
418 if not getattr(torch.version, "hip", None):
419 problems.append("torch: Rusty-managed build is not ROCm (torch.version.hip is empty)")
420 torch_root = Path(torch.__file__).resolve().parent
421 for relative in ("include/ATen/ATen.h", "include/torch/extension.h"):
422 header = torch_root / relative
423 if not header.is_file():
424 problems.append(f"torch header missing: {header}")
425
426rocm_root = Path(os.environ.get("ROCM_PATH", "/opt/rocm"))
427hipcc = Path(os.environ.get("HIPCC", "/opt/rocm/bin/hipcc"))
428if not hipcc.is_file() or not os.access(hipcc, os.X_OK):
429 problems.append(f"HIP compiler missing or not executable: {hipcc}")
430for relative in ("include/hip/hip_runtime.h", "include/hip/hip_fp16.h"):
431 header = rocm_root / relative
432 if not header.is_file():
433 problems.append(f"ROCm header missing: {header}")
434
435scripts_dir = Path(sys.executable).resolve().parent
436for executable_name in ("cmake", "ninja"):
437 executable = scripts_dir / executable_name
438 if not executable.is_file() or not os.access(executable, os.X_OK):
439 problems.append(f"managed build executable missing: {executable}")
440
441if problems:
442 details = "\\n - ".join(problems)
443 raise SystemExit(
444 "FastVideo requires dependencies owned by Rusty Stack; FastVideo will not "
445 "modify the managed environment. Repair/reinstall Rusty's managed Python "
446 f"dependencies, then retry:\\n - {details}"
447 )
448print("[FastVideo] Rusty-managed dependency pins verified")
449"#;
450 ShellCommand {
451 program: self.config.python_bin.clone(),
452 args: vec!["-c".to_string(), script.to_string()],
453 env: vec![],
454 working_dir: None,
455 }
456 }
457
458 pub fn source_policy_preflight(&self) -> ShellCommand {
460 let script = r#"from pathlib import Path
461import os
462import re
463import subprocess
464
465root = Path(".")
466violations = []
467
468expected_commit = os.environ.get("FASTVIDEO_EXPECTED_COMMIT", "")
469if not expected_commit:
470 violations.append("FASTVIDEO_EXPECTED_COMMIT is missing")
471else:
472 # git rev-parse proves the checkout is the immutable Rusty pin.
473 actual_commit = subprocess.run(
474 ["git", "rev-parse", "HEAD"],
475 cwd=root,
476 check=True,
477 text=True,
478 capture_output=True,
479 ).stdout.strip()
480 if actual_commit != expected_commit:
481 violations.append(
482 f"commit drift: expected {expected_commit}, found {actual_commit}"
483 )
484
485# git status must report no tracked, ignored, or untracked residue.
486dirty = subprocess.run(
487 ["git", "status", "--porcelain=v1", "--untracked-files=all"],
488 cwd=root,
489 check=True,
490 text=True,
491 capture_output=True,
492).stdout
493if dirty:
494 violations.append("working tree is not pristine:\\n" + dirty.rstrip())
495
496if (root / ".gitmodules").exists():
497 violations.append(".gitmodules is forbidden (CUTLASS/TK gitlinks are not allowed)")
498
499staged_records = subprocess.run(
500 ["git", "ls-files", "--stage", "-z"],
501 cwd=root,
502 check=True,
503 text=True,
504 capture_output=True,
505).stdout.split("\0")
506tracked_paths = []
507gitlinks = []
508for record in staged_records:
509 if not record:
510 continue
511 metadata, relative = record.split("\t", 1)
512 if metadata.startswith("160000 "):
513 gitlinks.append(relative)
514 else:
515 tracked_paths.append(Path(relative))
516if gitlinks:
517 violations.append("gitlinks (mode 160000) are forbidden: " + ", ".join(gitlinks))
518
519forbidden_path_terms = ("cutlass", "thunderkittens", "thunder-kittens")
520for path in tracked_paths:
521 relative = path.as_posix()
522 if any(term in relative.lower() for term in forbidden_path_terms):
523 violations.append(f"forbidden CUTLASS/TK/ThunderKittens path: {relative}")
524
525expected_native = {
526 "fastvideo-kernel/csrc/common_extension.cpp",
527 "fastvideo-kernel/csrc/hip_native/gemm_rocm.cpp",
528 "fastvideo-kernel/csrc/turbodiffusion/quant/quant.cu",
529 "fastvideo-kernel/csrc/turbodiffusion/norm/rmsnorm.cu",
530 "fastvideo-kernel/csrc/turbodiffusion/norm/layernorm.cu",
531}
532native_suffixes = {".cu", ".cpp", ".cc", ".cxx", ".hip"}
533actual_native = {
534 path.relative_to(root).as_posix()
535 for path in (root / "fastvideo-kernel/csrc").rglob("*")
536 if path.is_file() and path.suffix.lower() in native_suffixes
537}
538if actual_native != expected_native:
539 violations.append(
540 "unexpected native sources: expected "
541 + repr(sorted(expected_native))
542 + ", found "
543 + repr(sorted(actual_native))
544 )
545
546dependency_suffixes = {".toml", ".cfg", ".ini", ".txt", ".cmake", ".lock"}
547dependency_names = {
548 "cmakelists.txt",
549 "pip.conf",
550 "pipfile",
551 "pipfile.lock",
552 "poetry.lock",
553 "pyproject.toml",
554 "setup.cfg",
555 "setup.py",
556 "uv.lock",
557 "uv.toml",
558}
559dependency_patterns = (
560 re.compile(r"(?im)^\s*(?:dependencies|requires-dist)\s*=.*(?:nvidia|cuda)"),
561 re.compile(r"(?im)^\s*(?:--)?(?:extra-index-url|index-url)\b.*(?:nvidia|cuda)"),
562 re.compile(r"(?i)\bnvidia[-_.][a-z0-9_.-]+"),
563 re.compile(r"(?i)\b(?:cupy-cuda|cuda-python|pytorch-cuda|torch[^\s\"']*\+cu\d|jax[^\s\"']*cuda)"),
564)
565workflow_root = Path(".github/workflows")
566kernel_root = Path("fastvideo-kernel")
567for relative_path in tracked_paths:
568 path = root / relative_path
569 if not path.is_file():
570 continue
571 relative = relative_path.as_posix()
572 name_lower = path.name.lower()
573 is_dependency_metadata = (
574 path.suffix.lower() in dependency_suffixes
575 or name_lower in dependency_names
576 or name_lower.startswith("requirements")
577 )
578 is_kernel_workflow = (
579 relative_path.is_relative_to(workflow_root)
580 and path.suffix.lower() in {".yml", ".yaml"}
581 and path.stem.lower().startswith("fastvideo-kernel")
582 )
583 is_kernel_template = (
584 relative_path.is_relative_to(kernel_root) and "template" in relative.lower()
585 )
586 if not (is_dependency_metadata or is_kernel_workflow or is_kernel_template):
587 continue
588 try:
589 source_text = path.read_text(errors="strict")
590 except (OSError, UnicodeError):
591 continue
592 if is_dependency_metadata and any(
593 pattern.search(source_text) for pattern in dependency_patterns
594 ):
595 violations.append(f"CUDA/NVIDIA dependency or index URL in {relative}")
596 if (is_kernel_workflow or is_kernel_template) and re.search(
597 r"(?i)\b(?:cuda|nvidia|cutlass|thunderkittens)\b", source_text
598 ):
599 violations.append(f"unexpected CUDA/NVIDIA native workflow/template: {relative}")
600
601if violations:
602 raise SystemExit("FastVideo ROCm source policy failed:\\n - " + "\\n - ".join(violations))
603print("[FastVideo] immutable ROCm source policy verified")
604"#;
605 ShellCommand {
606 program: self.config.python_bin.clone(),
607 args: vec![
608 "-I".to_string(),
609 "-S".to_string(),
610 "-c".to_string(),
611 script.to_string(),
612 ],
613 env: vec![("FASTVIDEO_EXPECTED_COMMIT".to_string(), self.commit.clone())],
614 working_dir: Some(PathBuf::from(&self.build_dir)),
615 }
616 }
617
618 pub fn flash_attention_preflight(&self) -> ShellCommand {
620 let script = r#"from pathlib import Path
621
622marker = Path.home() / ".mlstack/flash-attention/.backend"
623try:
624 backend = marker.read_text().strip()
625except OSError as exc:
626 raise SystemExit(f"FastVideo requires managed flash-attention backend marker {marker}: {exc}")
627
628if backend not in {"ck", "triton"}:
629 raise SystemExit(
630 f"FastVideo requires exact flash-attention backend marker 'ck' or 'triton'; found {backend!r}"
631 )
632
633import flash_attn
634from flash_attn import (
635 flash_attn_func,
636 flash_attn_varlen_func,
637 flash_attn_varlen_qkvpacked_func,
638)
639from flash_attn.flash_attn_interface import (
640 _flash_attn_varlen_backward,
641 _flash_attn_varlen_forward,
642)
643
644selected = "CK" if backend == "ck" else "Triton fallback"
645print(f"[FastVideo] selected flash_attn backend: {selected}")
646"#;
647 ShellCommand {
648 program: self.config.python_bin.clone(),
649 args: vec!["-c".to_string(), script.to_string()],
650 env: vec![],
651 working_dir: None,
652 }
653 }
654
655 pub fn pip_install_kernel(&self) -> ShellCommand {
669 let mut args = vec![
670 "-m".to_string(),
671 "pip".to_string(),
672 "install".to_string(),
673 "--break-system-packages".to_string(),
674 ];
675 args.extend([
676 "-v".to_string(),
677 "--no-build-isolation".to_string(),
678 "--no-deps".to_string(),
679 "--no-index".to_string(),
680 ".".to_string(),
681 ]);
682
683 let cmake_args = format!(
684 "-DCMAKE_HIP_ARCHITECTURES={} -DPython_EXECUTABLE={}",
685 self.config.gpu_archs, self.config.python_bin
686 );
687
688 let max_jobs = std::thread::available_parallelism()
689 .map(|n| max_jobs_for_parallelism(n.get()))
690 .unwrap_or(1);
691
692 let env = vec![
693 ("CMAKE_ARGS".to_string(), cmake_args),
694 ("GPU_ARCHS".to_string(), self.config.gpu_archs.clone()),
695 ("MAX_JOBS".to_string(), max_jobs.to_string()),
696 (
697 "PYTHON_EXECUTABLE".to_string(),
698 self.config.python_bin.clone(),
699 ),
700 ("PIP_NO_INDEX".to_string(), "1".to_string()),
701 ("PIP_DISABLE_PIP_VERSION_CHECK".to_string(), "1".to_string()),
702 ("UV_OFFLINE".to_string(), "1".to_string()),
703 ];
704
705 ShellCommand {
706 program: self.config.python_bin.clone(),
707 args,
708 env,
709 working_dir: Some(PathBuf::from(&self.build_dir).join("fastvideo-kernel")),
710 }
711 }
712
713 pub fn pip_install_package(&self) -> ShellCommand {
715 ShellCommand {
716 program: self.config.python_bin.clone(),
717 args: [
718 "-m",
719 "pip",
720 "install",
721 "--break-system-packages",
722 "--no-build-isolation",
723 "--no-deps",
724 "--no-index",
725 ".",
726 ]
727 .map(String::from)
728 .to_vec(),
729 env: vec![
730 ("PIP_NO_INDEX".to_string(), "1".to_string()),
731 ("PIP_DISABLE_PIP_VERSION_CHECK".to_string(), "1".to_string()),
732 ("UV_OFFLINE".to_string(), "1".to_string()),
733 ],
734 working_dir: Some(PathBuf::from(&self.build_dir)),
735 }
736 }
737
738 pub fn distribution_snapshot(&self, phase: &str, runtime_prefixes: &[&str]) -> ShellCommand {
743 let script = r#"import importlib.metadata
744import json
745import os
746from pathlib import Path
747import re
748import sys
749
750phase = sys.argv[1]
751state_path = Path("/tmp/rusty-fastvideo-distributions.json")
752normalize = lambda value: re.sub(r"[-_.]+", "-", value).lower()
753current = {
754 normalize(dist.metadata["Name"]): dist.version
755 for dist in importlib.metadata.distributions()
756 if dist.metadata.get("Name")
757}
758runtime_prefixes = tuple(
759 prefix
760 for prefix in os.environ["RUSTY_NVIDIA_RUNTIME_PREFIXES"].split(",")
761 if prefix
762)
763forbidden_accelerator = sorted(
764 name
765 for name in current
766 if any(name.startswith(prefix) for prefix in runtime_prefixes) or "cuda" in name
767)
768if forbidden_accelerator:
769 raise SystemExit(
770 "CUDA/NVIDIA distributions are forbidden in Rusty's ROCm environment: "
771 + ", ".join(forbidden_accelerator)
772 )
773
774if phase == "before":
775 state_path.write_text(json.dumps(current, sort_keys=True))
776elif phase == "after":
777 before = json.loads(state_path.read_text())
778 changed = {
779 name
780 for name in set(before) | set(current)
781 if before.get(name) != current.get(name)
782 }
783 unexpected = sorted(changed - {"fastvideo", "fastvideo-kernel"})
784 if unexpected:
785 raise SystemExit(
786 "FastVideo install modified Rusty-managed distributions: " + ", ".join(unexpected)
787 )
788else:
789 raise SystemExit(f"unknown distribution snapshot phase: {phase!r}")
790"#;
791 ShellCommand {
792 program: self.config.python_bin.clone(),
793 args: vec!["-c".to_string(), script.to_string(), phase.to_string()],
794 env: vec![(
795 "RUSTY_NVIDIA_RUNTIME_PREFIXES".to_string(),
796 runtime_prefixes.join(","),
797 )],
798 working_dir: None,
799 }
800 }
801
802 pub fn cleanup(&self) -> ShellCommand {
803 ShellCommand {
804 program: "rm".to_string(),
805 args: vec!["-rf".to_string(), "--".to_string(), self.build_dir.clone()],
806 env: vec![],
807 working_dir: None,
808 }
809 }
810}
811
812#[cfg(test)]
813mod tests {
814 use super::*;
815
816 #[test]
817 fn remediation_contract_is_immutable_read_only_and_offline() {
818 let inst = FastVideoInstaller::new(FastVideoConfig {
819 gpu_archs: "gfx1100;gfx1101".to_string(),
820 python_bin: "/managed/bin/python".to_string(),
821 });
822
823 assert_eq!(FASTVIDEO_COMMIT, "22e448771ebf5c81108f1c57b9c7ef4d5c26d182");
824 assert_eq!(
825 inst.git_checkout().args,
826 ["checkout", "--detach", FASTVIDEO_COMMIT].map(String::from)
827 );
828 assert!(!inst
829 .git_clone()
830 .args
831 .iter()
832 .any(|arg| arg.contains("submodule")));
833
834 let managed = inst.managed_dependencies_preflight();
835 let managed_script = &managed.args[1];
836 for pin in [
837 "setuptools==81.0.0",
838 "cmake==4.4.4",
839 "scikit-build-core==1.1.1",
840 "pathspec==1.1.1",
841 "wheel==0.48.0",
842 "ninja==1.13.2",
843 "imageio==2.38.1",
844 "diffusers==0.41.0",
845 "remote-pdb==2.1.0",
846 "ftfy==6.3.1",
847 "wcwidth==0.2.13",
848 ] {
849 assert!(managed_script.contains(pin), "missing managed pin {pin}");
850 }
851 assert!(managed_script.contains("importlib.metadata"));
852 for anchor in [
853 "torch.version.hip",
854 "/opt/rocm/bin/hipcc",
855 "hip/hip_runtime.h",
856 "ATen/ATen.h",
857 "torch/extension.h",
858 ] {
859 assert!(
860 managed_script.contains(anchor),
861 "missing ROCm prerequisite anchor: {anchor}"
862 );
863 }
864 assert!(!managed_script.contains("pip"));
865 assert!(managed_script.contains("Rusty"));
866
867 let flash = inst.flash_attention_preflight();
868 let flash_script = &flash.args[1];
869 assert!(flash_script.contains("from flash_attn import"));
870 assert!(flash_script.contains("from flash_attn.flash_attn_interface import"));
871 assert!(flash_script.contains("_flash_attn_varlen_forward"));
872 assert!(flash_script.contains("_flash_attn_varlen_backward"));
873
874 let source = inst.source_policy_preflight();
875 assert_eq!(&source.args[..3], ["-I", "-S", "-c"].map(String::from));
876 assert!(source.env.contains(&(
877 "FASTVIDEO_EXPECTED_COMMIT".to_string(),
878 FASTVIDEO_COMMIT.to_string()
879 )));
880 let source_script = &source.args[3].to_ascii_lowercase();
881 for clean_tree_anchor in [
882 "git status",
883 "--porcelain=v1",
884 "--untracked-files=all",
885 "git rev-parse",
886 "fastvideo_expected_commit",
887 ] {
888 assert!(
889 source_script.contains(clean_tree_anchor),
890 "missing clean-tree proof: {clean_tree_anchor}"
891 );
892 }
893 for forbidden in [
894 ".gitmodules",
895 "160000",
896 "cutlass",
897 "thunderkittens",
898 "nvidia",
899 "cuda",
900 "extra-index-url",
901 "index-url",
902 ".github/workflows",
903 "common_extension.cpp",
904 "gemm_rocm.cpp",
905 "quant.cu",
906 "rmsnorm.cu",
907 "layernorm.cu",
908 ] {
909 assert!(
910 source_script.contains(forbidden),
911 "missing source policy {forbidden}"
912 );
913 }
914
915 assert!(source_script.contains("is_kernel_workflow"));
916 assert!(source_script.contains("path.stem.lower().startswith(\"fastvideo-kernel\")"));
917 let dependency_suffixes = source_script
918 .lines()
919 .find(|line| line.starts_with("dependency_suffixes ="))
920 .expect("source policy must define dependency metadata suffixes");
921 assert!(!dependency_suffixes.contains(".yml"));
922 assert!(!dependency_suffixes.contains(".yaml"));
923
924 for cmd in [inst.pip_install_kernel(), inst.pip_install_package()] {
925 assert_eq!(cmd.program, "/managed/bin/python");
926 for required in ["--no-deps", "--no-build-isolation", "--no-index"] {
927 assert!(
928 cmd.args.contains(&required.to_string()),
929 "missing {required}"
930 );
931 }
932 for forbidden in ["--upgrade", "--extra-index-url", "--index-url"] {
933 assert!(!cmd.args.contains(&forbidden.to_string()));
934 }
935 assert!(cmd
936 .env
937 .contains(&("PIP_NO_INDEX".to_string(), "1".to_string())));
938 assert!(cmd
939 .env
940 .contains(&("UV_OFFLINE".to_string(), "1".to_string())));
941 }
942
943 let runtime_prefixes = [
944 "nvidia",
945 "cuda",
946 "cudnn",
947 "cublas",
948 "cufft",
949 "curand",
950 "cusolver",
951 "cusparse",
952 "cusparselt",
953 "nccl",
954 "nvtx",
955 "nvjitlink",
956 "nv-pic",
957 "tensorrt",
958 "pytorch-cuda",
959 "torch-cuda",
960 ];
961 let snapshot = inst.distribution_snapshot("before", &runtime_prefixes);
962 assert_eq!(snapshot.program, "/managed/bin/python");
963 assert!(snapshot.args[1].contains("fastvideo-kernel"));
964 assert!(snapshot.args[1].contains("RUSTY_NVIDIA_RUNTIME_PREFIXES"));
965 let encoded_prefixes = snapshot
966 .env
967 .iter()
968 .find(|(name, _)| name == "RUSTY_NVIDIA_RUNTIME_PREFIXES")
969 .map(|(_, value)| value.as_str())
970 .expect("snapshot must receive canonical runtime prefixes");
971 for forbidden in runtime_prefixes {
972 assert!(
973 encoded_prefixes
974 .split(',')
975 .any(|prefix| prefix == forbidden),
976 "missing canonical runtime distribution pattern {forbidden}"
977 );
978 }
979 }
980
981 #[test]
992 fn source_policy_accepts_pinned_fork_with_unrelated_cuda_workflows() {
993 use std::process::Command;
994 use std::time::{SystemTime, UNIX_EPOCH};
995
996 let repo_str = match std::env::var("FASTVIDEO_POLICY_TEST_REPO") {
997 Ok(s) if !s.trim().is_empty() => s,
998 _ => {
999 eprintln!(
1000 "skip: FASTVIDEO_POLICY_TEST_REPO unset; set it to a FastVideo fork checkout \
1001 to exercise source_policy_accepts_pinned_fork_with_unrelated_cuda_workflows"
1002 );
1003 return;
1004 }
1005 };
1006 let repo = Path::new(&repo_str);
1007 if !repo.is_dir() {
1008 eprintln!(
1009 "skip: FASTVIDEO_POLICY_TEST_REPO={} is not a directory",
1010 repo.display()
1011 );
1012 return;
1013 }
1014
1015 let unique = SystemTime::now()
1016 .duration_since(UNIX_EPOCH)
1017 .expect("system clock must be after Unix epoch")
1018 .as_nanos();
1019 let clone = std::env::temp_dir().join(format!(
1020 "rusty-fastvideo-policy-{}-{unique}",
1021 std::process::id()
1022 ));
1023 let checkout_status = Command::new("git")
1024 .arg("-C")
1025 .arg(repo)
1026 .args(["worktree", "add", "--detach"])
1027 .arg(&clone)
1028 .arg(FASTVIDEO_COMMIT)
1029 .status()
1030 .expect("git must create an immutable FastVideo worktree");
1031 assert!(
1032 checkout_status.success(),
1033 "immutable FastVideo worktree must succeed"
1034 );
1035
1036 let cmd = FastVideoInstaller::default().source_policy_preflight();
1037 let run_policy = || {
1038 Command::new(&cmd.program)
1039 .args(&cmd.args)
1040 .envs(cmd.env.iter().cloned())
1041 .current_dir(&clone)
1042 .output()
1043 .expect("configured Python should execute the source policy")
1044 };
1045 let clean_output = run_policy();
1046 assert!(
1047 clean_output.status.success(),
1048 "pinned fork must pass source policy despite unrelated upstream workflows: {}",
1049 String::from_utf8_lossy(&clean_output.stderr)
1050 );
1051
1052 std::fs::write(
1053 clone.join("sitecustomize.py"),
1054 "raise RuntimeError('must never execute')\n",
1055 )
1056 .expect("write stale untracked source");
1057 let dirty_output = run_policy();
1058 assert!(
1059 !dirty_output.status.success(),
1060 "source policy must reject stale untracked files"
1061 );
1062 assert!(
1063 String::from_utf8_lossy(&dirty_output.stderr).contains("working tree is not pristine"),
1064 "dirty-tree failure must identify the source residue"
1065 );
1066
1067 let remove_status = Command::new("git")
1068 .arg("-C")
1069 .arg(repo)
1070 .args(["worktree", "remove", "--force"])
1071 .arg(&clone)
1072 .status()
1073 .expect("git must remove temporary FastVideo worktree");
1074 assert!(
1075 remove_status.success(),
1076 "temporary worktree removal must succeed"
1077 );
1078 }
1079
1080 #[test]
1081 fn remediation_plan_has_no_patch_or_dependency_write_phase() {
1082 use FastVideoInstallStep::*;
1083 assert_eq!(
1084 post_checkout_plan(),
1085 [
1086 Chown,
1087 ManagedDependenciesPreflight,
1088 FlashAttentionPreflight,
1089 SourcePolicyPreflight,
1090 KernelInstall,
1091 PackageInstall,
1092 ]
1093 );
1094 }
1095
1096 #[test]
1097 fn test_verification_snippet_isolates_each_visible_gpu_with_rocr_only() {
1098 use std::process::Command;
1099
1100 let snippet = fastvideo_verification_snippet();
1101 for anchor in [
1102 "import os",
1103 "import subprocess",
1104 "import fastvideo",
1105 "import torch",
1106 "from fastvideo_kernel import int8_quant",
1107 "torch.cuda.is_available()",
1108 "torch.cuda.device_count()",
1109 "visible = os.environ.get(\"ROCR_VISIBLE_DEVICES\", \"\")",
1110 "devices = [device.strip() for device in visible.split(\",\") if device.strip()]",
1111 "for device in devices:",
1112 "env = os.environ.copy()",
1113 "env[\"ROCR_VISIBLE_DEVICES\"] = device",
1114 "env.pop(\"HIP_VISIBLE_DEVICES\", None)",
1115 "env.pop(\"CUDA_VISIBLE_DEVICES\", None)",
1116 "env.pop(\"GPU_DEVICE_ORDINAL\", None)",
1117 "subprocess.run(",
1118 "device = \"cuda:0\"",
1119 "(2, 128)",
1120 "dtype=torch.float16",
1121 ".contiguous()",
1122 "int8_quant(source)",
1123 "torch.cuda.synchronize()",
1124 "quantized.shape == source.shape",
1125 "quantized.dtype == torch.int8",
1126 "torch.isfinite(scale).all()",
1127 "torch.cuda.get_device_name(0)",
1128 "raise SystemExit",
1129 ] {
1130 assert!(
1131 snippet.contains(anchor),
1132 "missing verification anchor: {anchor}"
1133 );
1134 }
1135
1136 for forbidden in ["rocminfo", "/sys/class/drm"] {
1137 assert!(
1138 !snippet.contains(forbidden),
1139 "snippet must use Python/ROCr visibility, found {forbidden}"
1140 );
1141 }
1142
1143 let compile = format!("compile({snippet:?}, '<fastvideo-verification>', 'exec')");
1144 let output = Command::new(&FastVideoConfig::default().python_bin)
1145 .args(["-c", &compile])
1146 .output()
1147 .expect("configured python3 should run");
1148 assert!(
1149 output.status.success(),
1150 "verification snippet must compile as Python: {}",
1151 String::from_utf8_lossy(&output.stderr)
1152 );
1153 }
1154
1155 #[test]
1156 fn test_git_clone_uses_fork() {
1157 let inst = FastVideoInstaller::new(FastVideoConfig::default());
1158 let clone_cmd = inst.git_clone();
1159 assert!(clone_cmd.args.contains(&"clone".into()));
1160 assert!(
1161 clone_cmd.args.contains(&FASTVIDEO_REPO.into()),
1162 "Should clone from scooter-lacroix fork"
1163 );
1164 }
1165
1166 #[test]
1167 fn test_chown_build_dir_targets_install_user() {
1168 let inst = FastVideoInstaller::new(FastVideoConfig::default());
1169 let cmd = inst.chown_build_dir("ml-user");
1170 assert_eq!(cmd.program, "chown");
1171 assert_eq!(
1172 cmd.args,
1173 ["-R", "ml-user:ml-user", FASTVIDEO_BUILD_DIR].map(String::from)
1174 );
1175 }
1176
1177 #[test]
1178 fn test_pip_install_has_multi_arch_rocm_cmake_args() {
1179 let inst = FastVideoInstaller::new(FastVideoConfig {
1180 gpu_archs: "gfx1100;gfx1101".to_string(),
1181 python_bin: "python3".to_string(),
1182 });
1183 let cmd = inst.pip_install_kernel();
1184 assert!(cmd.args.contains(&"--no-build-isolation".into()));
1185 assert!(cmd.args.contains(&"--no-deps".into()));
1186 let cmake_env = cmd.env.iter().find(|(k, _)| k == "CMAKE_ARGS").unwrap();
1187 assert!(cmake_env
1188 .1
1189 .contains("CMAKE_HIP_ARCHITECTURES=gfx1100;gfx1101"));
1190 assert!(!cmake_env.1.contains("GPU_BACKEND"));
1191 assert!(!cmd.env.iter().any(|(key, _)| key == "GPU_BACKEND"));
1192 assert!(!cmake_env.1.to_ascii_lowercase().contains("tk"));
1193 }
1194
1195 #[test]
1196 fn test_pip_install_sets_same_gpu_archs() {
1197 let inst = FastVideoInstaller::new(FastVideoConfig {
1198 gpu_archs: "gfx1100;gfx1101".to_string(),
1199 python_bin: "python3".to_string(),
1200 });
1201 let cmd = inst.pip_install_kernel();
1202 let gpu_env = cmd.env.iter().find(|(k, _)| k == "GPU_ARCHS").unwrap();
1203 assert_eq!(gpu_env.1, "gfx1100;gfx1101");
1204 }
1205
1206 #[test]
1207 fn test_both_pip_commands_always_allow_selected_interpreter() {
1208 let inst = FastVideoInstaller::default();
1209 assert!(inst
1210 .pip_install_package()
1211 .args
1212 .contains(&"--break-system-packages".to_string()));
1213 assert!(inst
1214 .pip_install_kernel()
1215 .args
1216 .contains(&"--break-system-packages".to_string()));
1217 }
1218
1219 #[test]
1220 fn test_max_jobs_is_cpu_spare_and_capped() {
1221 assert_eq!(max_jobs_for_parallelism(0), 1);
1222 assert_eq!(max_jobs_for_parallelism(1), 1);
1223 assert_eq!(max_jobs_for_parallelism(2), 1);
1224 assert_eq!(max_jobs_for_parallelism(4), 3);
1225 assert_eq!(max_jobs_for_parallelism(8), 4);
1226 }
1227
1228 #[test]
1229 fn test_resolve_gpu_archs_prefers_sysfs() {
1230 let sysfs = vec![" gfx1100 ".to_string(), "gfx1101".to_string()];
1231 assert_eq!(
1232 resolve_gpu_archs(&sysfs, Some("gfx1200"), Some("gfx1030"), Some("gfx900")),
1233 "gfx1100;gfx1101"
1234 );
1235 }
1236
1237 #[test]
1238 fn test_resolve_gpu_archs_normalizes_and_deduplicates_env() {
1239 assert_eq!(
1240 resolve_gpu_archs(&[], Some(" gfx1100, gfx1101;gfx1100\tgfx1101 "), None, None),
1241 "gfx1100;gfx1101"
1242 );
1243 assert_eq!(
1244 resolve_gpu_archs(&[], None, Some("gfx1100, gfx1101"), None),
1245 "gfx1100;gfx1101"
1246 );
1247 }
1248
1249 #[test]
1250 fn test_resolve_gpu_archs_detected_primary_then_default() {
1251 assert_eq!(
1252 resolve_gpu_archs(&[], Some(" , ; "), None, Some(" gfx1101 ")),
1253 "gfx1101"
1254 );
1255 assert_eq!(resolve_gpu_archs(&[], None, None, None), "gfx1100");
1256 }
1257
1258 #[test]
1259 fn test_resolve_install_user_precedence_and_blanks() {
1260 assert_eq!(
1261 resolve_install_user(Some(" sudo-user "), Some("user")),
1262 Some("sudo-user".to_string())
1263 );
1264 assert_eq!(
1265 resolve_install_user(Some(" "), Some(" user ")),
1266 Some("user".to_string())
1267 );
1268 assert_eq!(resolve_install_user(None, None), None);
1269 assert_eq!(resolve_install_user(Some(""), Some(" \t ")), None);
1270 }
1271
1272 #[test]
1273 fn test_heavy_build_warning_is_complete() {
1274 assert_eq!(
1275 fastvideo_build_warning("gfx1100;gfx1101"),
1276 "[FastVideo] Heavy dual-arch HIP compile for gfx1100;gfx1101; MAX_JOBS capped at 4"
1277 );
1278 }
1279
1280 #[test]
1281 fn test_post_checkout_plan_order_and_privileges() {
1282 use FastVideoInstallStep::*;
1283
1284 let plan = post_checkout_plan();
1285 assert_eq!(
1286 plan,
1287 [
1288 Chown,
1289 ManagedDependenciesPreflight,
1290 FlashAttentionPreflight,
1291 SourcePolicyPreflight,
1292 KernelInstall,
1293 PackageInstall,
1294 ]
1295 );
1296 assert!(Chown.requires_sudo());
1297 for step in [
1298 ManagedDependenciesPreflight,
1299 FlashAttentionPreflight,
1300 SourcePolicyPreflight,
1301 KernelInstall,
1302 PackageInstall,
1303 ] {
1304 assert!(!step.requires_sudo());
1305 }
1306 }
1307
1308 #[test]
1309 fn test_flash_attention_preflight_uses_marker_and_required_api() {
1310 let inst = FastVideoInstaller::new(FastVideoConfig {
1311 gpu_archs: "gfx1100".to_string(),
1312 python_bin: "python3.12".to_string(),
1313 });
1314 let cmd = inst.flash_attention_preflight();
1315
1316 assert_eq!(cmd.program, "python3.12");
1317 assert_eq!(cmd.args[0], "-c");
1318 assert_eq!(cmd.working_dir, None);
1319 let script = &cmd.args[1];
1320 for anchor in [
1321 "Path.home()",
1322 ".mlstack/flash-attention/.backend",
1323 "ck",
1324 "triton",
1325 "from flash_attn import",
1326 "flash_attn_func",
1327 "flash_attn_varlen_func",
1328 "flash_attn_varlen_qkvpacked_func",
1329 "_flash_attn_varlen_forward",
1330 "_flash_attn_varlen_backward",
1331 ] {
1332 assert!(
1333 script.contains(anchor),
1334 "missing preflight anchor: {anchor}"
1335 );
1336 }
1337 assert!(!script.contains("fastvideo_kernel"));
1338 assert!(!script.contains("common_extension"));
1339 }
1340
1341 #[test]
1342 fn test_root_package_install_is_dependency_free_from_clone_root() {
1343 let inst = FastVideoInstaller::new(FastVideoConfig {
1344 gpu_archs: "gfx1100".to_string(),
1345 python_bin: "python3.12".to_string(),
1346 });
1347 let cmd = inst.pip_install_package();
1348
1349 assert_eq!(cmd.program, "python3.12");
1350 assert_eq!(
1351 cmd.args,
1352 [
1353 "-m",
1354 "pip",
1355 "install",
1356 "--break-system-packages",
1357 "--no-build-isolation",
1358 "--no-deps",
1359 "--no-index",
1360 ".",
1361 ]
1362 .map(String::from)
1363 );
1364 assert_eq!(cmd.working_dir, Some(PathBuf::from(FASTVIDEO_BUILD_DIR)));
1365 assert!(cmd
1366 .env
1367 .contains(&("PIP_NO_INDEX".to_string(), "1".to_string())));
1368 assert!(cmd
1369 .env
1370 .contains(&("UV_OFFLINE".to_string(), "1".to_string())));
1371 assert!(!cmd.args.iter().any(|arg| {
1372 let arg = arg.to_ascii_lowercase();
1373 arg.contains("cuda") || arg.contains("nvidia") || arg == "torch"
1374 }));
1375 }
1376
1377 #[test]
1378 fn test_visibility_masks_select_discrete_archs() {
1379 let archs = vec!["gfx1100".to_string(), "gfx1101".to_string()];
1380 assert_eq!(
1381 select_visible_gpu_archs(&archs, Some("0,1"), None, None),
1382 archs
1383 );
1384 assert_eq!(
1385 select_visible_gpu_archs(&archs, Some("1"), Some("0"), Some("0")),
1386 vec!["gfx1101".to_string()]
1387 );
1388 assert!(select_visible_gpu_archs(&archs, Some("bad,9"), None, None).is_empty());
1389 }
1390
1391 #[test]
1392 fn test_gpu_arch_validation_rejects_garbage() {
1393 assert_eq!(
1394 resolve_gpu_archs(
1395 &["not-gfx".to_string(), "gfx1101".to_string()],
1396 Some("garbage,gfx12zz,gfx1100"),
1397 None,
1398 None
1399 ),
1400 "gfx1101"
1401 );
1402 assert_eq!(
1403 resolve_gpu_archs(&[], Some("garbage,gfx12zz,gfx1100"), None, None),
1404 "gfx1100"
1405 );
1406 }
1407
1408 #[test]
1409 fn test_cleanup_removes_build_dir() {
1410 let inst = FastVideoInstaller::new(FastVideoConfig::default());
1411 let cleanup = inst.cleanup();
1412 assert_eq!(
1413 cleanup.args,
1414 ["-rf", "--", FASTVIDEO_BUILD_DIR].map(String::from)
1415 );
1416 }
1417
1418 #[test]
1419 fn test_sysfs_detection_excludes_apu_and_preserves_discrete_order() {
1420 use std::time::{SystemTime, UNIX_EPOCH};
1421
1422 let unique = SystemTime::now()
1423 .duration_since(UNIX_EPOCH)
1424 .expect("system clock must be after Unix epoch")
1425 .as_nanos();
1426 let root = std::env::temp_dir().join(format!(
1427 "rusty-fastvideo-sysfs-{}-{unique}",
1428 std::process::id()
1429 ));
1430
1431 for (card, device_id) in [
1432 ("card0", "0x15bf"),
1433 ("card1", "0x744c"),
1434 ("card2", "0x747e"),
1435 ] {
1436 let device = root.join(card).join("device");
1437 std::fs::create_dir_all(&device).expect("create fake DRM device");
1438 std::fs::write(device.join("vendor"), "0x1002\n").expect("write fake vendor");
1439 std::fs::write(device.join("device"), format!("{device_id}\n"))
1440 .expect("write fake device id");
1441 }
1442
1443 assert_eq!(
1444 detect_discrete_gpu_archs_from_sysfs(&root),
1445 vec!["gfx1100".to_string(), "gfx1101".to_string()]
1446 );
1447 std::fs::remove_dir_all(&root).expect("remove fake DRM tree");
1448 }
1449
1450 #[test]
1451 fn test_config_propagates_python_bin() {
1452 let inst = FastVideoInstaller::new(FastVideoConfig {
1453 gpu_archs: "gfx1100".to_string(),
1454 python_bin: "python3.12".to_string(),
1455 });
1456 let cmd = inst.pip_install_kernel();
1457 assert_eq!(cmd.program, "python3.12");
1458 }
1459}