1const VLLM_SOURCE_VERSION: &str = "0.31.0";
18
19const VLLM_VERSION_POLICY_PY: &str = r#"
25import json, urllib.request, sys, datetime
26lag = int(sys.argv[1]); age = int(sys.argv[2])
27try:
28 from packaging.version import Version
29except Exception:
30 Version = None
31data = json.load(urllib.request.urlopen('https://pypi.org/pypi/vllm/json', timeout=20))
32rels = data.get('releases', {})
33now = datetime.datetime.now(datetime.timezone.utc)
34cands = []
35for ver, files in rels.items():
36 if not files:
37 continue
38 if Version:
39 try:
40 lv = Version(ver)
41 except Exception:
42 continue
43 if lv.is_prerelease:
44 continue
45 ut = files[0].get('upload_time_iso_8601') or files[0].get('upload_time') or ''
46 try:
47 dt = datetime.datetime.fromisoformat(ut.replace('Z', '+00:00'))
48 except Exception:
49 continue
50 # upload_time (legacy) is timezone-naive; coerce to UTC so now-dt works.
51 if dt.tzinfo is None:
52 dt = dt.replace(tzinfo=datetime.timezone.utc)
53 cands.append((ver, dt, lv if Version else None))
54cands.sort(key=lambda x: (x[2] if Version else x[1]), reverse=True)
55for idx, (ver, dt, _lv) in enumerate(cands):
56 if idx < lag:
57 continue
58 if age > 0 and (now - dt).days < age:
59 continue
60 print(ver)
61 sys.exit(0)
62print('')
63"#;
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum InstallMethod {
72 Global,
74 Venv,
76 Auto,
78}
79
80impl std::fmt::Display for InstallMethod {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 match self {
83 InstallMethod::Global => write!(f, "global"),
84 InstallMethod::Venv => write!(f, "venv"),
85 InstallMethod::Auto => write!(f, "auto"),
86 }
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct VllmConfig {
93 pub rocm_version: String,
95 pub gpu_arch: String,
97 pub python_bin: String,
99 pub method: InstallMethod,
101 pub dry_run: bool,
103 pub strict_rocm: bool,
105}
106
107impl Default for VllmConfig {
108 fn default() -> Self {
109 Self {
110 rocm_version: "7.2.0".to_string(),
111 gpu_arch: "gfx1100".to_string(),
112 python_bin: "python3".to_string(),
113 method: InstallMethod::Auto,
114 dry_run: false,
115 strict_rocm: true,
116 }
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct ShellCommand {
123 pub program: String,
125 pub args: Vec<String>,
127 pub env: Vec<(String, String)>,
129}
130
131impl ShellCommand {
132 pub fn to_command_string(&self) -> String {
134 let env_prefix = self
135 .env
136 .iter()
137 .map(|(k, v)| format!("{k}={v}"))
138 .collect::<Vec<_>>()
139 .join(" ");
140 let cmd = if self.args.is_empty() {
141 self.program.clone()
142 } else {
143 format!("{} {}", self.program, self.args.join(" "))
144 };
145 if env_prefix.is_empty() {
146 cmd
147 } else {
148 format!("{env_prefix} {cmd}")
149 }
150 }
151}
152
153pub struct VllmInstaller {
155 config: VllmConfig,
156}
157
158impl VllmInstaller {
159 pub fn new(config: VllmConfig) -> Self {
161 Self { config }
162 }
163
164 pub fn with_defaults() -> Self {
166 Self::new(VllmConfig::default())
167 }
168
169 pub fn dependencies(&self) -> &[&str] {
175 &["pytorch"]
176 }
177
178 pub fn validate_dependencies(&self, installed_components: &[&str]) -> anyhow::Result<()> {
180 for dep in self.dependencies() {
181 if !installed_components.contains(dep) {
182 anyhow::bail!("vLLM requires '{}' to be installed first", dep);
183 }
184 }
185 Ok(())
186 }
187
188 pub fn detect_rocm_mm(&self) -> String {
194 let v = &self.config.rocm_version;
195 let parts: Vec<&str> = v.split('.').collect();
197 if parts.len() >= 2 {
198 format!("{}.{}", parts[0], parts[1])
199 } else {
200 "7.2".to_string()
201 }
202 }
203
204 pub fn rocm_index_url(&self) -> String {
206 let mm = self.detect_rocm_mm();
207 format!("https://repo.radeon.com/rocm/manylinux/rocm-rel-{mm}/")
208 }
209
210 pub fn vllm_wheels_url(&self) -> &'static str {
212 "https://wheels.vllm.ai/rocm/"
213 }
214
215 pub fn build_vllm_install_command(&self) -> ShellCommand {
224 let use_break = self.config.method == InstallMethod::Global
225 || self.config.method == InstallMethod::Auto;
226
227 let mut args = vec!["-m".to_string(), "pip".to_string(), "install".to_string()];
228 if use_break {
229 args.push("--break-system-packages".to_string());
230 }
231 args.extend([
232 "--no-cache-dir".to_string(),
233 "--no-deps".to_string(),
234 "vllm".to_string(),
235 "--extra-index-url".to_string(),
236 self.vllm_wheels_url().to_string(),
237 ]);
238
239 ShellCommand {
240 program: self.config.python_bin.clone(),
241 args,
242 env: vec![],
243 }
244 }
245
246 pub fn build_deps_install_command(&self) -> ShellCommand {
250 let use_break = self.config.method == InstallMethod::Global
251 || self.config.method == InstallMethod::Auto;
252
253 let mut args = vec!["-m".to_string(), "pip".to_string(), "install".to_string()];
254 if use_break {
255 args.push("--break-system-packages".to_string());
256 }
257 args.extend([
258 "--no-cache-dir".to_string(),
259 "--no-deps".to_string(),
260 "--extra-index-url".to_string(),
261 self.vllm_wheels_url().to_string(),
262 ]);
263
264 let deps = [
269 "aiohttp>=3.13.3",
270 "amd-quark>=0.8.99",
271 "anthropic>=0.71.0",
272 "astor",
273 "blake3",
274 "boto3",
275 "botocore",
276 "cachetools",
277 "cbor2",
278 "cloudpickle",
279 "datasets",
280 "docstring-parser",
281 "einops",
282 "evaluate",
283 "fastsafetensors>=0.3.2",
284 "filelock>=3.16.1",
285 "googleapis-common-protos",
286 "hiredis",
287 "httpx-sse",
288 "humanize",
289 "ijson",
290 "importlib-metadata",
291 "jmespath",
292 "joblib",
293 "jsonschema>=4.23.0",
294 "libnacl",
295 "loguru",
296 "mcp",
297 "mistral-common[image]>=1.11.5",
298 "ml-dtypes",
299 "model-hosting-container-standards>=0.1.14,<1.0.0",
300 "msgspec",
301 "colorama",
302 "narwhals",
303 "onnx-ir",
304 "onnxscript",
305 "onnxslim",
306 "openai>=2.0.0",
307 "openai-harmony>=0.0.3",
308 "opentelemetry-api>=1.27.0",
309 "opentelemetry-exporter-otlp>=1.27.0",
310 "opentelemetry-exporter-otlp-proto-common>=1.27.0",
311 "opentelemetry-exporter-otlp-proto-grpc>=1.27.0",
312 "opentelemetry-exporter-otlp-proto-http>=1.27.0",
313 "opentelemetry-proto>=1.27.0",
314 "opentelemetry-sdk>=1.27.0",
315 "opentelemetry-semantic-conventions>=0.59b0",
316 "opentelemetry-semantic-conventions-ai>=0.4.1",
317 "opentelemetry-util-http>=0.59b0",
318 "opencv-python-headless>=4.13.0",
319 "packaging>=24.2",
320 "partial-json-parser",
321 "peft",
322 "pillow",
323 "plotly",
324 "prometheus-client>=0.18.0",
325 "prometheus-fastapi-instrumentator>=8.0.0",
326 "protobuf>=6.33.5,<7.0.0",
327 "psutil",
328 "py-cpuinfo",
329 "pybase64",
330 "pydantic-settings",
331 "pytest-asyncio",
332 "pyjwt",
333 "python-json-logger",
334 "python-dotenv",
335 "python-multipart",
336 "pyyaml",
337 "pyzmq>=25.0.0",
338 "redis",
339 "regex",
340 "requests>=2.26.0",
341 "safetensors>=0.6.2",
342 "s3transfer",
343 "sentencepiece",
344 "setproctitle",
345 "setuptools-rust>=1.9.0",
346 "setuptools-scm>=8",
347 "six>=1.16.0",
348 "sse-starlette",
349 "starlette>=1.0.1",
350 "supervisor",
351 "tiktoken>=0.6.0",
352 "timm>=1.0.17",
353 "tokenizers>=0.21.1",
354 "tqdm",
355 "transformers>=5.5.3",
356 "typing-extensions>=4.10",
357 "uvicorn",
358 "uvloop",
359 "watchfiles",
360 "zipp",
361 "zstandard",
362 ];
363 for dep in deps {
364 args.push(dep.to_string());
365 }
366
367 ShellCommand {
368 program: self.config.python_bin.clone(),
369 args,
370 env: vec![],
371 }
372 }
373
374 pub fn build_versioned_deps_command(&self) -> ShellCommand {
378 let use_break = self.config.method == InstallMethod::Global
379 || self.config.method == InstallMethod::Auto;
380
381 let mut args = vec!["-m".to_string(), "pip".to_string(), "install".to_string()];
382 if use_break {
383 args.push("--break-system-packages".to_string());
384 }
385 args.extend([
386 "--no-cache-dir".to_string(),
387 "--no-deps".to_string(),
388 "--extra-index-url".to_string(),
389 self.vllm_wheels_url().to_string(),
390 ]);
391
392 let deps = [
394 "apache-tvm-ffi==0.1.10",
395 "compressed-tensors==0.17.0",
396 "conch-triton-kernels==1.2.1",
397 "depyf==0.20.0",
398 "diskcache==5.6.3",
399 "fastapi[standard]>=0.133.0,<0.137.0",
400 "grpcio==1.84.0",
401 "grpcio-reflection==1.84.0",
402 "lark==1.2.2",
403 "llguidance>=1.7.0,<1.8.0",
404 "llvmlite>=0.47.0,<0.48.0",
405 "lm-format-enforcer==0.11.3",
406 "numba==0.65.0",
407 "outlines-core==0.2.14",
408 "runai-model-streamer[azure,gcs,s3]==0.15.7",
409 "setuptools>=77.0.3,<80.0.0",
410 "tensorizer==2.10.1",
411 "tilelang==0.1.12",
412 "torch-c-dlpack-ext",
413 "xgrammar>=0.2.1,<1.0.0",
414 "z3-solver>=4.13.0,<4.15.5",
415 ];
416 for dep in deps {
417 args.push(dep.to_string());
418 }
419
420 ShellCommand {
421 program: self.config.python_bin.clone(),
422 args,
423 env: vec![],
424 }
425 }
426
427 pub fn build_integrity_check_command(&self) -> ShellCommand {
433 ShellCommand {
434 program: self.config.python_bin.clone(),
435 args: vec!["-m".to_string(), "pip".to_string(), "check".to_string()],
436 env: vec![],
437 }
438 }
439
440 pub fn triton_cache_env(&self) -> Vec<(String, String)> {
444 let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
445 let triton_home = format!("{home}/.cache/mlstack/triton");
446
447 vec![
448 ("MLSTACK_TRITON_HOME".to_string(), triton_home.clone()),
449 ("TRITON_HOME".to_string(), triton_home.clone()),
450 (
451 "TRITON_CACHE_DIR".to_string(),
452 format!("{triton_home}/cache"),
453 ),
454 ("TRITON_DUMP_DIR".to_string(), format!("{triton_home}/dump")),
455 (
456 "TRITON_OVERRIDE_DIR".to_string(),
457 format!("{triton_home}/override"),
458 ),
459 ]
460 }
461
462 pub fn build_tools_install_command(&self) -> ShellCommand {
467 let use_break = self.config.method == InstallMethod::Global
468 || self.config.method == InstallMethod::Auto;
469 let mut args = vec!["-m".to_string(), "pip".to_string(), "install".to_string()];
470 if use_break {
471 args.push("--break-system-packages".to_string());
472 }
473 args.extend([
474 "--no-cache-dir".to_string(),
475 "cmake".to_string(),
476 "ninja".to_string(),
477 "wheel".to_string(),
478 "setuptools".to_string(),
479 "setuptools-rust".to_string(),
483 "setuptools-scm".to_string(),
484 "patchelf".to_string(),
485 ]);
486 ShellCommand {
487 program: self.config.python_bin.clone(),
488 args,
489 env: vec![],
490 }
491 }
492
493 pub fn build_source_install_command(&self, version: &str) -> ShellCommand {
504 let use_break = self.config.method == InstallMethod::Global
505 || self.config.method == InstallMethod::Auto;
506 let mut args = vec!["-m".to_string(), "pip".to_string(), "install".to_string()];
507 if use_break {
508 args.push("--break-system-packages".to_string());
509 }
510 let req = format!("vllm=={version}");
513 args.extend([
514 "--no-cache-dir".to_string(),
515 "--no-deps".to_string(),
516 "--no-build-isolation".to_string(),
517 "--no-binary".to_string(),
518 "vllm".to_string(),
519 req,
520 ]);
521 ShellCommand {
522 program: self.config.python_bin.clone(),
523 args,
524 env: vec![
525 ("VLLM_TARGET_DEVICE".to_string(), "rocm".to_string()),
528 ("VLLM_USE_ROCM".to_string(), "1".to_string()),
529 ("USE_ROCM".to_string(), "1".to_string()),
530 ("VLLM_VERSION_OVERRIDE".to_string(), version.to_string()),
539 ],
540 }
541 }
542
543 pub fn resolve_version_with_policy(&self, version_lag: u32, min_age_days: u32) -> String {
551 let out = std::process::Command::new(&self.config.python_bin)
552 .args([
553 "-c",
554 VLLM_VERSION_POLICY_PY,
555 &version_lag.to_string(),
556 &min_age_days.to_string(),
557 ])
558 .output();
559 if let Ok(o) = out {
560 if o.status.success() {
561 let v = String::from_utf8_lossy(&o.stdout).trim().to_string();
562 if v.chars()
563 .next()
564 .map(|c| c.is_ascii_digit())
565 .unwrap_or(false)
566 {
567 return v;
568 }
569 }
570 }
571 VLLM_SOURCE_VERSION.to_string()
572 }
573
574 pub fn build_force_reinstall_command(&self) -> ShellCommand {
576 let args = vec![
577 "-m".to_string(),
578 "pip".to_string(),
579 "install".to_string(),
580 "--break-system-packages".to_string(),
581 "--no-cache-dir".to_string(),
582 "--force-reinstall".to_string(),
583 "--no-deps".to_string(),
584 "--extra-index-url".to_string(),
585 self.vllm_wheels_url().to_string(),
586 "vllm".to_string(),
587 ];
588
589 ShellCommand {
590 program: self.config.python_bin.clone(),
591 args,
592 env: vec![],
593 }
594 }
595
596 pub fn build_source_rebuild_command(&self, version: &str) -> ShellCommand {
607 let use_break = self.config.method == InstallMethod::Global
608 || self.config.method == InstallMethod::Auto;
609 let mut args = vec!["-m".to_string(), "pip".to_string(), "install".to_string()];
610 if use_break {
611 args.push("--break-system-packages".to_string());
612 }
613 let req = format!("vllm=={version}");
616 args.extend([
617 "--no-cache-dir".to_string(),
618 "--force-reinstall".to_string(),
619 "--no-deps".to_string(),
620 "--no-build-isolation".to_string(),
621 "--no-binary".to_string(),
622 "vllm".to_string(),
623 req,
624 ]);
625
626 ShellCommand {
627 program: self.config.python_bin.clone(),
628 args,
629 env: vec![
630 ("VLLM_TARGET_DEVICE".to_string(), "rocm".to_string()),
631 ("VLLM_USE_ROCM".to_string(), "1".to_string()),
632 ("USE_ROCM".to_string(), "1".to_string()),
633 ("VLLM_VERSION_OVERRIDE".to_string(), version.to_string()),
634 ],
635 }
636 }
637}
638
639#[cfg(test)]
644mod tests {
645 use super::*;
646
647 #[test]
650 fn test_vllm_install_command() {
651 let installer = VllmInstaller::new(VllmConfig {
652 method: InstallMethod::Global,
653 ..Default::default()
654 });
655 let cmd = installer.build_vllm_install_command();
656 assert_eq!(cmd.program, "python3");
657 assert!(cmd.args.contains(&"-m".to_string()));
658 assert!(cmd.args.contains(&"pip".to_string()));
659 assert!(cmd.args.contains(&"install".to_string()));
660 assert!(cmd.args.contains(&"--break-system-packages".to_string()));
661 assert!(cmd.args.contains(&"--no-cache-dir".to_string()));
662 assert!(cmd.args.contains(&"--no-deps".to_string()));
663 assert!(cmd.args.contains(&"vllm".to_string()));
664 assert!(cmd.args.contains(&"--extra-index-url".to_string()));
665 assert!(cmd.args.iter().any(|a| a == "https://wheels.vllm.ai/rocm/"));
666 }
667
668 #[test]
669 fn test_vllm_install_command_venv() {
670 let installer = VllmInstaller::new(VllmConfig {
671 method: InstallMethod::Venv,
672 ..Default::default()
673 });
674 let cmd = installer.build_vllm_install_command();
675 assert!(!cmd.args.contains(&"--break-system-packages".to_string()));
677 assert!(cmd.args.contains(&"vllm".to_string()));
678 }
679
680 #[test]
681 fn test_vllm_wheels_url() {
682 let installer = VllmInstaller::with_defaults();
683 assert_eq!(installer.vllm_wheels_url(), "https://wheels.vllm.ai/rocm/");
684 }
685
686 #[test]
687 fn test_rocm_index_url() {
688 let installer = VllmInstaller::new(VllmConfig {
689 rocm_version: "7.2.0".to_string(),
690 ..Default::default()
691 });
692 assert_eq!(
693 installer.rocm_index_url(),
694 "https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2/"
695 );
696 }
697
698 #[test]
699 fn test_rocm_index_url_legacy_version() {
700 let installer = VllmInstaller::new(VllmConfig {
701 rocm_version: "7.0.0".to_string(),
702 ..Default::default()
703 });
704 assert_eq!(
705 installer.rocm_index_url(),
706 "https://repo.radeon.com/rocm/manylinux/rocm-rel-7.0/"
707 );
708 }
709
710 #[test]
711 fn test_detect_rocm_mm() {
712 let installer = VllmInstaller::new(VllmConfig {
713 rocm_version: "7.2.0".to_string(),
714 ..Default::default()
715 });
716 assert_eq!(installer.detect_rocm_mm(), "7.2");
717 }
718
719 #[test]
720 fn test_deps_install_command() {
721 let installer = VllmInstaller::new(VllmConfig {
722 method: InstallMethod::Global,
723 ..Default::default()
724 });
725 let cmd = installer.build_deps_install_command();
726 assert!(cmd.args.contains(&"--no-deps".to_string()));
727 assert!(cmd.args.contains(&"transformers>=5.5.3".to_string()));
728 assert!(cmd.args.contains(&"einops".to_string()));
729 assert!(cmd.args.contains(&"amd-quark>=0.8.99".to_string()));
730 assert!(cmd.args.contains(&"evaluate".to_string()));
731 assert!(cmd.args.contains(&"colorama".to_string()));
732 assert!(cmd.args.contains(&"onnx-ir".to_string()));
733 assert!(cmd.args.contains(&"onnxscript".to_string()));
734 assert!(cmd.args.contains(&"boto3".to_string()));
735 assert!(cmd.args.contains(&"httpx-sse".to_string()));
736 assert!(cmd
737 .args
738 .contains(&"opentelemetry-semantic-conventions>=0.59b0".to_string()));
739 assert!(cmd.args.contains(&"protobuf>=6.33.5,<7.0.0".to_string()));
740 assert!(cmd
741 .args
742 .contains(&"model-hosting-container-standards>=0.1.14,<1.0.0".to_string()));
743 assert!(cmd.args.contains(&"uvloop".to_string()));
744 assert!(!cmd.args.iter().any(|arg| arg == "torch"));
745 assert!(!cmd.args.iter().any(|arg| arg == "triton"));
746 assert!(!cmd.args.iter().any(|arg| arg == "triton-kernels"));
747 assert!(!cmd
748 .args
749 .iter()
750 .any(|arg| arg.to_ascii_lowercase().contains("nvidia")));
751 assert!(!cmd
752 .args
753 .iter()
754 .any(|arg| arg.to_ascii_lowercase().contains("cuda")));
755 }
756
757 #[test]
758 fn test_versioned_deps_command() {
759 let installer = VllmInstaller::with_defaults();
760 let cmd = installer.build_versioned_deps_command();
761 assert!(cmd.args.contains(&"--no-deps".to_string()));
762 assert!(cmd
763 .args
764 .iter()
765 .any(|a| a.starts_with("conch-triton-kernels==")));
766 assert!(cmd.args.iter().any(|a| a.starts_with("xgrammar>=")));
767 assert!(cmd
768 .args
769 .iter()
770 .any(|a| a.starts_with("outlines-core==0.2.14")));
771 assert!(cmd.args.iter().any(|a| a.starts_with("llguidance>=1.7.0")));
772 assert!(cmd.args.iter().any(|a| a.starts_with("llvmlite>=0.47.0")));
773 assert!(cmd.args.iter().any(|a| a.starts_with("grpcio==1.84.0")));
774 assert!(cmd.args.contains(&"torch-c-dlpack-ext".to_string()));
775 assert!(cmd.args.contains(&"z3-solver>=4.13.0,<4.15.5".to_string()));
776 assert!(!cmd.args.iter().any(|arg| arg == "torch"));
777 assert!(!cmd.args.iter().any(|arg| arg == "triton"));
778 assert!(!cmd.args.iter().any(|arg| arg == "triton-kernels"));
779 assert!(!cmd
780 .args
781 .iter()
782 .any(|arg| arg.to_ascii_lowercase().contains("nvidia")));
783 assert!(!cmd
784 .args
785 .iter()
786 .any(|arg| arg.to_ascii_lowercase().contains("cuda")));
787 }
788
789 #[test]
790 fn test_integrity_check_command_is_read_only() {
791 let installer = VllmInstaller::with_defaults();
792 let cmd = installer.build_integrity_check_command();
793 assert_eq!(cmd.program, "python3");
794 assert_eq!(cmd.args, vec!["-m", "pip", "check"]);
795 assert!(cmd.env.is_empty());
796 }
797
798 #[test]
799 fn test_force_reinstall_command() {
800 let installer = VllmInstaller::with_defaults();
801 let cmd = installer.build_force_reinstall_command();
802 assert!(cmd.args.contains(&"--force-reinstall".to_string()));
803 assert!(cmd.args.contains(&"--no-deps".to_string()));
804 assert!(cmd.args.contains(&"vllm".to_string()));
805 }
806
807 #[test]
808 fn test_source_rebuild_command() {
809 let installer = VllmInstaller::with_defaults();
810 let cmd = installer.build_source_rebuild_command("0.31.0");
811 assert!(cmd.args.contains(&"--no-build-isolation".to_string()));
812 assert!(cmd.args.contains(&"--no-binary".to_string()));
813 assert!(cmd.args.contains(&"vllm==0.31.0".to_string()));
816 assert!(cmd
817 .env
818 .iter()
819 .any(|(k, v)| k == "VLLM_TARGET_DEVICE" && v == "rocm"));
820 assert!(cmd
821 .env
822 .iter()
823 .any(|(k, v)| k == "VLLM_USE_ROCM" && v == "1"));
824 assert!(cmd
827 .env
828 .iter()
829 .any(|(k, v)| k == "VLLM_VERSION_OVERRIDE" && v == "0.31.0"));
830 }
831
832 #[test]
833 fn test_triton_cache_env() {
834 let installer = VllmInstaller::with_defaults();
835 let env = installer.triton_cache_env();
836 assert!(env.iter().any(|(k, _)| k == "TRITON_HOME"));
837 assert!(env.iter().any(|(k, _)| k == "TRITON_CACHE_DIR"));
838 assert!(env.iter().any(|(k, _)| k == "TRITON_DUMP_DIR"));
839 }
840
841 #[test]
844 fn test_dependencies() {
845 let installer = VllmInstaller::with_defaults();
846 assert!(installer.dependencies().contains(&"pytorch"));
847 }
848
849 #[test]
850 fn test_validate_dependencies_success() {
851 let installer = VllmInstaller::with_defaults();
852 assert!(installer
853 .validate_dependencies(&["pytorch", "rocm"])
854 .is_ok());
855 }
856
857 #[test]
858 fn test_validate_dependencies_missing_pytorch() {
859 let installer = VllmInstaller::with_defaults();
860 let result = installer.validate_dependencies(&["rocm"]);
861 assert!(result.is_err());
862 assert!(result.unwrap_err().to_string().contains("pytorch"));
863 }
864}