Skip to main content

rusty_stack/installers/components/
vllm_multi.rs

1//! vLLM installer — ports `scripts/install_vllm_multi.sh`.
2//!
3//! Constructs correct git clone URL/branch + pip install commands for vLLM
4//! with ROCm support. vLLM depends on PyTorch (validated via preflight check).
5//!
6//! # Validation Assertions
7//!
8//! - **VAL-INSTALL-015**: vLLM correct git clone and pip install
9//! - **VAL-INSTALL-042**: vLLM declares dependency on PyTorch
10
11/// Fallback vLLM version — used ONLY when PyPI is unreachable or no release
12/// satisfies the user's version/age gate. Otherwise the installer resolves a
13/// live version from PyPI (see `resolve_version_with_policy`) so no manual bump
14/// is needed across releases. Also provides the `VLLM_VERSION_OVERRIDE` value
15/// that suppresses vLLM's `+rocmNNN` build label (avoids pip's filename-vs-
16/// metadata cascade).
17const VLLM_SOURCE_VERSION: &str = "0.25.1";
18
19/// Python: query PyPI for vLLM releases, apply the version-lag + age gate, and
20/// print the selected version (empty if none qualifies). Run as
21/// `python -c <this> <lag> <age>` (argv[1]=lag, argv[2]=age). Drops pre-releases,
22/// sorts by version descending, then picks the newest release that is BOTH
23/// beyond the newest `lag` AND at least `age` days old.
24const 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// ===========================================================================
66// Types
67// ===========================================================================
68
69/// Installation method for vLLM.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum InstallMethod {
72    /// Install globally.
73    Global,
74    /// Install in a virtual environment.
75    Venv,
76    /// Try global, fallback to venv.
77    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/// Configuration for the vLLM installer.
91#[derive(Debug, Clone)]
92pub struct VllmConfig {
93    /// ROCm version string (e.g., "7.2.0").
94    pub rocm_version: String,
95    /// GPU architecture (e.g., "gfx1100").
96    pub gpu_arch: String,
97    /// Python binary to use.
98    pub python_bin: String,
99    /// Installation method.
100    pub method: InstallMethod,
101    /// Whether to run in dry-run mode.
102    pub dry_run: bool,
103    /// Whether strict ROCm mode is enabled.
104    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/// A constructed shell command.
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct ShellCommand {
123    /// The program to run.
124    pub program: String,
125    /// Arguments to pass.
126    pub args: Vec<String>,
127    /// Environment variables to set.
128    pub env: Vec<(String, String)>,
129}
130
131impl ShellCommand {
132    /// Format as a shell command string.
133    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
153/// The vLLM installer.
154pub struct VllmInstaller {
155    config: VllmConfig,
156}
157
158impl VllmInstaller {
159    /// Create a new vLLM installer with the given config.
160    pub fn new(config: VllmConfig) -> Self {
161        Self { config }
162    }
163
164    /// Create with default config.
165    pub fn with_defaults() -> Self {
166        Self::new(VllmConfig::default())
167    }
168
169    // -----------------------------------------------------------------------
170    // Dependencies (VAL-INSTALL-042)
171    // -----------------------------------------------------------------------
172
173    /// vLLM depends on PyTorch.
174    pub fn dependencies(&self) -> &[&str] {
175        &["pytorch"]
176    }
177
178    /// Validate that all dependencies are satisfied.
179    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    // -----------------------------------------------------------------------
189    // ROCm detection helpers
190    // -----------------------------------------------------------------------
191
192    /// Detect ROCm major.minor version.
193    pub fn detect_rocm_mm(&self) -> String {
194        let v = &self.config.rocm_version;
195        // Extract major.minor from version string
196        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    /// Get the ROCm PyTorch wheel index URL.
205    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    /// Get the vLLM ROCm wheels extra index URL.
211    pub fn vllm_wheels_url(&self) -> &'static str {
212        "https://wheels.vllm.ai/rocm/"
213    }
214
215    // -----------------------------------------------------------------------
216    // Command construction (VAL-INSTALL-015)
217    // -----------------------------------------------------------------------
218
219    /// Construct the pip install command for vLLM from ROCm wheels.
220    ///
221    /// The original script uses:
222    /// `pip install --no-cache-dir --no-deps vllm --extra-index-url https://wheels.vllm.ai/rocm/`
223    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    /// Construct the pip install command for vLLM dependencies.
247    ///
248    /// Installs the core dependencies needed by vLLM (excluding torch and xformers).
249    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        // vLLM 0.25.x direct deps from package metadata, excluding torch/triton/
265        // xformers and CUDA/NVIDIA packages. `--no-deps` is intentional: vLLM is
266        // torch-adjacent, so Rusty must not let transitive deps replace ROCm
267        // stack packages or pull CUDA runtime wheels.
268        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    /// Construct the pip install command for vLLM versioned dependencies.
375    ///
376    /// These have specific version pins from the original script.
377    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        // Versioned vLLM 0.25.x deps from package metadata, excluding torch/CUDA.
393        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.78.0",
401            "grpcio-reflection==1.78.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.10",
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    /// Validate that the vLLM dependency closure is internally consistent.
428    ///
429    /// This is intentionally read-only. It catches stale or missing packages
430    /// after Rusty's `--no-deps` installs without allowing pip to mutate torch,
431    /// ROCm, CUDA/NVIDIA libraries, or any other stack component.
432    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    /// Construct the Triton cache environment setup command.
441    ///
442    /// Sets up writable cache directories for Triton kernel compilation.
443    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    /// Install the build tools vLLM's source build needs (cmake/ninja/wheel/
463    /// setuptools/patchelf). The env already supplies ROCm dev headers + torch;
464    /// these are the remaining pip-installable build tools. (rust/cargo, if an
465    /// optional dep ever needs it, is a system package — not pip-installable.)
466    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            // vLLM's sdist setup.py imports these at metadata-generation time
480            // (`ModuleNotFoundError: No module named 'setuptools_rust'`). cargo/
481            // rustc are a system dep (present at /usr/bin/cargo) — not pip.
482            "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    /// Build vLLM FROM SOURCE against the env's installed torch.
494    ///
495    /// The prebuilt ROCm wheel is compiled against a specific torch (e.g.
496    /// 2.11.0) and ABI-breaks against the env's torch (e.g. 2.12.1+rocm7.2) —
497    /// `vllm._C.abi3.so: undefined symbol: _ZNR5torch7Library4_defE…`. Building
498    /// the sdist with `--no-build-isolation` compiles the C/HIP extensions
499    /// against the ACTUAL installed torch, so the symbols match. `--no-deps`
500    /// guarantees pip NEVER swaps the env's torch (vLLM pins torch==<other>).
501    /// Heavy: compiles C++/HIP (tens of minutes). VLLM_TARGET_DEVICE=rocm picks
502    /// the ROCm backend. Non-negotiable: the env's torch is the only torch.
503    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        // `--no-binary vllm` = build from sdist (NOT the prebuilt CUDA wheel);
511        // the pinned `vllm==<ver>` is the requirement pip installs.
512        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                // ROCm build backend — the REAL ROCm selector (independent of the
526                // version label). Guarantees C/HIP compilation, never CUDA.
527                ("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                // Suppress vLLM's `+rocmNNN` build label. setup.py appends
531                // +rocmNNN (from the local ROCm version) which mismatches the
532                // PyPI sdist filename (X.Y.Z) → pip cascades through every
533                // release downloading 36MB each. VLLM_VERSION_OVERRIDE makes
534                // get_vllm_version() return the exact version EARLY (before the
535                // append), so metadata == filename → no cascade. The version is
536                // pinned (VLLM_SOURCE_VERSION) so the override matches the sdist
537                // pip fetches. ROCm is still guaranteed by VLLM_TARGET_DEVICE.
538                ("VLLM_VERSION_OVERRIDE".to_string(), version.to_string()),
539            ],
540        }
541    }
542
543    /// Resolve the vLLM version to build, applying the user's supply-chain gate.
544    ///
545    /// Queries PyPI's release list (versions + upload dates), drops pre-releases,
546    /// then picks the newest release satisfying BOTH: (a) not within the
547    /// `version_lag` newest (lag=1 = skip the latest), AND (b) at least
548    /// `min_age_days` old (0 = no age requirement). Falls back to
549    /// `VLLM_SOURCE_VERSION` if PyPI is unreachable or no release qualifies.
550    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    /// Construct the force-reinstall command for vLLM wheel repair.
575    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    /// Construct the source rebuild command for vLLM.
597    ///
598    /// Used as a fallback when wheel install fails. Accepts the
599    /// policy-resolved `version` (the same value `build_source_install_command`
600    /// pins) so the fallback honors the `resolve_version_with_policy`
601    /// version-lag / min-age supply-chain gate instead of silently installing
602    /// the newest bare `vllm` from source. `VLLM_VERSION_OVERRIDE` is set for
603    /// the same reason as the primary source-install path: it suppresses the
604    /// `+rocmNNN` build label that mismatches the sdist filename and forces pip
605    /// to cascade through every release.
606    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        // `--no-binary vllm` = build from sdist (NOT the prebuilt CUDA wheel);
614        // the pinned `vllm==<ver>` is the requirement pip installs.
615        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// ===========================================================================
640// Tests
641// ===========================================================================
642
643#[cfg(test)]
644mod tests {
645    use super::*;
646
647    // --- VAL-INSTALL-015: vLLM correct git clone and pip install ---
648
649    #[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        // Venv should NOT have --break-system-packages
676        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.78.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.25.1");
811        assert!(cmd.args.contains(&"--no-build-isolation".to_string()));
812        assert!(cmd.args.contains(&"--no-binary".to_string()));
813        // The fallback must pin the resolved version (supply-chain gate honored),
814        // not install bare `vllm`.
815        assert!(cmd.args.contains(&"vllm==0.25.1".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        // VLLM_VERSION_OVERRIDE must mirror the pinned version so pip does not
825        // cascade through releases (same contract as the primary source install).
826        assert!(cmd
827            .env
828            .iter()
829            .any(|(k, v)| k == "VLLM_VERSION_OVERRIDE" && v == "0.25.1"));
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    // --- VAL-INSTALL-042: vLLM declares dependency on PyTorch ---
842
843    #[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}