| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """Tests for CUDA forward compatibility path logic in env_override.py. |
| 4 | |
| 5 | Verifies the opt-in LD_LIBRARY_PATH manipulation for CUDA compat libs, |
| 6 | including env var parsing, path detection, and deduplication. |
| 7 | """ |
| 8 | |
| 9 | import os |
| 10 | from unittest.mock import patch |
| 11 | |
| 12 | import pytest |
| 13 | |
| 14 | # Import the functions directly (they're module-level in env_override) |
| 15 | # We must import them without triggering the module-level side effects, |
| 16 | # so we import the functions by name after the module is already loaded. |
| 17 | from vllm.env_override import ( |
| 18 | _get_torch_cuda_version, |
| 19 | _maybe_set_cuda_compatibility_path, |
| 20 | ) |
| 21 | |
| 22 | |
| 23 | class TestCudaCompatibilityEnvParsing: |
| 24 | """Test VLLM_ENABLE_CUDA_COMPATIBILITY env var parsing.""" |
| 25 | |
| 26 | def test_disabled_by_default(self, monkeypatch): |
| 27 | """Compat path is NOT set when env var is absent.""" |
| 28 | monkeypatch.delenv("VLLM_ENABLE_CUDA_COMPATIBILITY", raising=False) |
| 29 | monkeypatch.delenv("LD_LIBRARY_PATH", raising=False) |
| 30 | _maybe_set_cuda_compatibility_path() |
| 31 | assert ( |
| 32 | "LD_LIBRARY_PATH" not in os.environ |
| 33 | or os.environ.get("LD_LIBRARY_PATH", "") == "" |
| 34 | ) |
| 35 | |
| 36 | @pytest.mark.parametrize("value", ["0", "false", "False", "no", ""]) |
| 37 | def test_disabled_values(self, monkeypatch, value): |
| 38 | """Various falsy values should not activate compat path.""" |
| 39 | monkeypatch.setenv("VLLM_ENABLE_CUDA_COMPATIBILITY", value) |
| 40 | monkeypatch.delenv("LD_LIBRARY_PATH", raising=False) |
| 41 | _maybe_set_cuda_compatibility_path() |
| 42 | # LD_LIBRARY_PATH should not be set (or remain empty) |
| 43 | ld_path = os.environ.get("LD_LIBRARY_PATH", "") |
| 44 | assert "compat" not in ld_path |
| 45 | |
| 46 | @pytest.mark.parametrize("value", ["1", "true", "True", " 1 ", " TRUE "]) |
| 47 | def test_enabled_values_with_valid_path(self, monkeypatch, tmp_path, value): |
| 48 | """Truthy values activate compat path when a valid path exists.""" |
| 49 | compat_dir = tmp_path / "compat" |
| 50 | compat_dir.mkdir() |
| 51 | monkeypatch.setenv("VLLM_ENABLE_CUDA_COMPATIBILITY", value) |
| 52 | monkeypatch.setenv("VLLM_CUDA_COMPATIBILITY_PATH", str(compat_dir)) |
| 53 | monkeypatch.delenv("LD_LIBRARY_PATH", raising=False) |
| 54 | _maybe_set_cuda_compatibility_path() |
| 55 | ld_path = os.environ.get("LD_LIBRARY_PATH", "") |
| 56 | assert str(compat_dir) in ld_path |
| 57 | |
| 58 | |
| 59 | class TestCudaCompatibilityPathDetection: |
| 60 | """Test path detection: custom override, conda, default.""" |
| 61 | |
| 62 | def test_custom_path_override(self, monkeypatch, tmp_path): |
| 63 | """VLLM_CUDA_COMPATIBILITY_PATH takes highest priority.""" |
| 64 | custom_dir = tmp_path / "my-compat" |
| 65 | custom_dir.mkdir() |
| 66 | monkeypatch.setenv("VLLM_ENABLE_CUDA_COMPATIBILITY", "1") |
| 67 | monkeypatch.setenv("VLLM_CUDA_COMPATIBILITY_PATH", str(custom_dir)) |
| 68 | monkeypatch.delenv("LD_LIBRARY_PATH", raising=False) |
| 69 | _maybe_set_cuda_compatibility_path() |
| 70 | ld_path = os.environ.get("LD_LIBRARY_PATH", "") |
| 71 | assert ld_path.startswith(str(custom_dir)) |
| 72 | |
| 73 | def test_conda_prefix_fallback(self, monkeypatch, tmp_path): |
| 74 | """Falls back to $CONDA_PREFIX/cuda-compat if custom not set.""" |
| 75 | conda_dir = tmp_path / "conda-env" |
| 76 | compat_dir = conda_dir / "cuda-compat" |
| 77 | compat_dir.mkdir(parents=True) |
| 78 | monkeypatch.setenv("VLLM_ENABLE_CUDA_COMPATIBILITY", "1") |
| 79 | monkeypatch.delenv("VLLM_CUDA_COMPATIBILITY_PATH", raising=False) |
| 80 | monkeypatch.setenv("CONDA_PREFIX", str(conda_dir)) |
| 81 | monkeypatch.delenv("LD_LIBRARY_PATH", raising=False) |
| 82 | _maybe_set_cuda_compatibility_path() |
| 83 | ld_path = os.environ.get("LD_LIBRARY_PATH", "") |
| 84 | assert str(compat_dir) in ld_path |
| 85 | |
| 86 | def test_no_valid_path_does_nothing(self, monkeypatch): |
| 87 | """When enabled but no valid path exists, LD_LIBRARY_PATH unchanged.""" |
| 88 | monkeypatch.setenv("VLLM_ENABLE_CUDA_COMPATIBILITY", "1") |
| 89 | monkeypatch.setenv("VLLM_CUDA_COMPATIBILITY_PATH", "/nonexistent/path") |
| 90 | monkeypatch.delenv("CONDA_PREFIX", raising=False) |
| 91 | monkeypatch.delenv("LD_LIBRARY_PATH", raising=False) |
| 92 | with patch("vllm.env_override._get_torch_cuda_version", return_value=None): |
| 93 | _maybe_set_cuda_compatibility_path() |
| 94 | assert os.environ.get("LD_LIBRARY_PATH", "") == "" |
| 95 | |
| 96 | def test_default_cuda_path_fallback(self, monkeypatch, tmp_path): |
| 97 | """Falls back to /usr/local/cuda-{ver}/compat via torch version.""" |
| 98 | fake_cuda = tmp_path / "cuda-12.8" / "compat" |
| 99 | fake_cuda.mkdir(parents=True) |
| 100 | monkeypatch.setenv("VLLM_ENABLE_CUDA_COMPATIBILITY", "1") |
| 101 | monkeypatch.delenv("VLLM_CUDA_COMPATIBILITY_PATH", raising=False) |
| 102 | monkeypatch.delenv("CONDA_PREFIX", raising=False) |
| 103 | monkeypatch.delenv("LD_LIBRARY_PATH", raising=False) |
| 104 | with ( |
| 105 | patch("vllm.env_override._get_torch_cuda_version", return_value="12.8"), |
| 106 | patch( |
| 107 | "vllm.env_override.os.path.isdir", |
| 108 | side_effect=lambda p: p == "/usr/local/cuda-12.8/compat" |
| 109 | or os.path.isdir(p), |
| 110 | ), |
| 111 | ): |
| 112 | _maybe_set_cuda_compatibility_path() |
| 113 | ld_path = os.environ.get("LD_LIBRARY_PATH", "") |
| 114 | assert "/usr/local/cuda-12.8/compat" in ld_path |
| 115 | |
| 116 | |
| 117 | class TestCudaCompatibilityLdPathManipulation: |
| 118 | """Test LD_LIBRARY_PATH prepend and deduplication logic.""" |
| 119 | |
| 120 | def test_prepends_to_empty_ld_path(self, monkeypatch, tmp_path): |
| 121 | """Compat path is set when LD_LIBRARY_PATH is empty.""" |
| 122 | compat_dir = tmp_path / "compat" |
| 123 | compat_dir.mkdir() |
| 124 | monkeypatch.setenv("VLLM_ENABLE_CUDA_COMPATIBILITY", "1") |
| 125 | monkeypatch.setenv("VLLM_CUDA_COMPATIBILITY_PATH", str(compat_dir)) |
| 126 | monkeypatch.delenv("LD_LIBRARY_PATH", raising=False) |
| 127 | _maybe_set_cuda_compatibility_path() |
| 128 | assert os.environ["LD_LIBRARY_PATH"] == str(compat_dir) |
| 129 | |
| 130 | def test_prepends_to_existing_ld_path(self, monkeypatch, tmp_path): |
| 131 | """Compat path is prepended before existing entries.""" |
| 132 | compat_dir = tmp_path / "compat" |
| 133 | compat_dir.mkdir() |
| 134 | monkeypatch.setenv("VLLM_ENABLE_CUDA_COMPATIBILITY", "1") |
| 135 | monkeypatch.setenv("VLLM_CUDA_COMPATIBILITY_PATH", str(compat_dir)) |
| 136 | monkeypatch.setenv("LD_LIBRARY_PATH", "/usr/lib:/other/lib") |
| 137 | _maybe_set_cuda_compatibility_path() |
| 138 | ld_path = os.environ["LD_LIBRARY_PATH"] |
| 139 | parts = ld_path.split(os.pathsep) |
| 140 | assert parts[0] == str(compat_dir) |
| 141 | assert "/usr/lib" in parts |
| 142 | assert "/other/lib" in parts |
| 143 | |
| 144 | def test_deduplicates_existing_compat_path(self, monkeypatch, tmp_path): |
| 145 | """If compat path already in LD_LIBRARY_PATH, move to front.""" |
| 146 | compat_dir = tmp_path / "compat" |
| 147 | compat_dir.mkdir() |
| 148 | monkeypatch.setenv("VLLM_ENABLE_CUDA_COMPATIBILITY", "1") |
| 149 | monkeypatch.setenv("VLLM_CUDA_COMPATIBILITY_PATH", str(compat_dir)) |
| 150 | monkeypatch.setenv( |
| 151 | "LD_LIBRARY_PATH", |
| 152 | f"/usr/lib:{compat_dir}:/other/lib", |
| 153 | ) |
| 154 | _maybe_set_cuda_compatibility_path() |
| 155 | ld_path = os.environ["LD_LIBRARY_PATH"] |
| 156 | parts = ld_path.split(os.pathsep) |
| 157 | assert parts[0] == str(compat_dir) |
| 158 | assert parts.count(str(compat_dir)) == 1 |
| 159 | |
| 160 | def test_already_at_front_is_noop(self, monkeypatch, tmp_path): |
| 161 | """If compat path is already first, don't modify LD_LIBRARY_PATH.""" |
| 162 | compat_dir = tmp_path / "compat" |
| 163 | compat_dir.mkdir() |
| 164 | original = f"{compat_dir}:/usr/lib" |
| 165 | monkeypatch.setenv("VLLM_ENABLE_CUDA_COMPATIBILITY", "1") |
| 166 | monkeypatch.setenv("VLLM_CUDA_COMPATIBILITY_PATH", str(compat_dir)) |
| 167 | monkeypatch.setenv("LD_LIBRARY_PATH", original) |
| 168 | _maybe_set_cuda_compatibility_path() |
| 169 | assert os.environ["LD_LIBRARY_PATH"] == original |
| 170 | |
| 171 | |
| 172 | class TestGetTorchCudaVersion: |
| 173 | """Test _get_torch_cuda_version() helper.""" |
| 174 | |
| 175 | def test_returns_string_when_torch_available(self): |
| 176 | """Should return a CUDA version string like '12.8'.""" |
| 177 | version = _get_torch_cuda_version() |
| 178 | # torch is installed in vllm's environment |
| 179 | assert version is None or isinstance(version, str) |
| 180 | |
| 181 | def test_returns_none_when_torch_missing(self): |
| 182 | """Should return None when torch is not importable.""" |
| 183 | with patch( |
| 184 | "vllm.env_override.importlib.util.find_spec", |
| 185 | return_value=None, |
| 186 | ): |
| 187 | assert _get_torch_cuda_version() is None |