pybinaryguard 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pybinaryguard/__init__.py +78 -0
- pybinaryguard/__main__.py +7 -0
- pybinaryguard/_compat/__init__.py +0 -0
- pybinaryguard/agent/__init__.py +63 -0
- pybinaryguard/agent/guard.py +232 -0
- pybinaryguard/agent/recommender.py +283 -0
- pybinaryguard/agent/schema.py +200 -0
- pybinaryguard/agent/simulator.py +430 -0
- pybinaryguard/agent/tool_interface.py +474 -0
- pybinaryguard/analyzers/__init__.py +209 -0
- pybinaryguard/analyzers/base.py +40 -0
- pybinaryguard/analyzers/dependency_analyzer.py +336 -0
- pybinaryguard/analyzers/elf_analyzer.py +754 -0
- pybinaryguard/analyzers/symbol_analyzer.py +280 -0
- pybinaryguard/analyzers/wheel_analyzer.py +308 -0
- pybinaryguard/cli/__init__.py +5 -0
- pybinaryguard/cli/commands.py +414 -0
- pybinaryguard/cli/formatters.py +720 -0
- pybinaryguard/cli/main.py +250 -0
- pybinaryguard/diagnostics/__init__.py +13 -0
- pybinaryguard/diagnostics/explainer.py +356 -0
- pybinaryguard/diagnostics/findings.py +146 -0
- pybinaryguard/diagnostics/suggestions.py +508 -0
- pybinaryguard/frameworks/__init__.py +20 -0
- pybinaryguard/frameworks/onnxruntime.py +214 -0
- pybinaryguard/frameworks/pytorch.py +223 -0
- pybinaryguard/frameworks/tensorflow.py +266 -0
- pybinaryguard/frameworks/tensorrt.py +189 -0
- pybinaryguard/models/__init__.py +19 -0
- pybinaryguard/models/enums.py +102 -0
- pybinaryguard/models/finding.py +109 -0
- pybinaryguard/models/package.py +121 -0
- pybinaryguard/models/system.py +118 -0
- pybinaryguard/plugins/__init__.py +19 -0
- pybinaryguard/plugins/contrib/__init__.py +7 -0
- pybinaryguard/plugins/contrib/gstreamer.py +109 -0
- pybinaryguard/plugins/contrib/jetson.py +385 -0
- pybinaryguard/plugins/contrib/opencv.py +306 -0
- pybinaryguard/plugins/contrib/tensorrt.py +426 -0
- pybinaryguard/plugins/hooks.py +273 -0
- pybinaryguard/plugins/loader.py +190 -0
- pybinaryguard/predictor/__init__.py +23 -0
- pybinaryguard/predictor/dependency_graph.py +226 -0
- pybinaryguard/predictor/linker_simulator.py +161 -0
- pybinaryguard/predictor/predictor.py +144 -0
- pybinaryguard/predictor/resolver.py +282 -0
- pybinaryguard/probes/__init__.py +73 -0
- pybinaryguard/probes/base.py +45 -0
- pybinaryguard/probes/board_probe.py +248 -0
- pybinaryguard/probes/cpu_probe.py +176 -0
- pybinaryguard/probes/glibc_probe.py +215 -0
- pybinaryguard/probes/gpu_probe.py +430 -0
- pybinaryguard/probes/library_probe.py +132 -0
- pybinaryguard/probes/os_probe.py +227 -0
- pybinaryguard/probes/python_probe.py +135 -0
- pybinaryguard/probes/toolchain_probe.py +89 -0
- pybinaryguard/probes/venv_probe.py +111 -0
- pybinaryguard/profiles/__init__.py +12 -0
- pybinaryguard/profiles/engine.py +343 -0
- pybinaryguard/rules/__init__.py +24 -0
- pybinaryguard/rules/base.py +57 -0
- pybinaryguard/rules/builtin/__init__.py +136 -0
- pybinaryguard/rules/builtin/arch_rules.py +86 -0
- pybinaryguard/rules/builtin/board_profile_rules.py +282 -0
- pybinaryguard/rules/builtin/container_rules.py +170 -0
- pybinaryguard/rules/builtin/cpu_rules.py +204 -0
- pybinaryguard/rules/builtin/cuda_rules.py +760 -0
- pybinaryguard/rules/builtin/dependency_rules.py +278 -0
- pybinaryguard/rules/builtin/framework_rules.py +252 -0
- pybinaryguard/rules/builtin/glibc_rules.py +320 -0
- pybinaryguard/rules/builtin/numpy_rules.py +110 -0
- pybinaryguard/rules/builtin/predictive_rules.py +165 -0
- pybinaryguard/rules/builtin/python_abi_rules.py +259 -0
- pybinaryguard/rules/builtin/source_build_rules.py +150 -0
- pybinaryguard/rules/builtin/venv_rules.py +159 -0
- pybinaryguard/rules/engine.py +123 -0
- pybinaryguard/scanner.py +904 -0
- pybinaryguard/scoring/__init__.py +19 -0
- pybinaryguard/scoring/engine.py +416 -0
- pybinaryguard/snapshot/__init__.py +20 -0
- pybinaryguard/snapshot/generator.py +168 -0
- pybinaryguard/snapshot/lockfile.py +152 -0
- pybinaryguard/snapshot/verifier.py +315 -0
- pybinaryguard/validators/__init__.py +7 -0
- pybinaryguard/validators/import_validator.py +231 -0
- pybinaryguard-1.0.0.dist-info/METADATA +876 -0
- pybinaryguard-1.0.0.dist-info/RECORD +91 -0
- pybinaryguard-1.0.0.dist-info/WHEEL +5 -0
- pybinaryguard-1.0.0.dist-info/entry_points.txt +8 -0
- pybinaryguard-1.0.0.dist-info/licenses/LICENSE +21 -0
- pybinaryguard-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
"""CUDA / GPU compatibility rules.
|
|
2
|
+
|
|
3
|
+
These rules verify that the GPU driver, CUDA runtime, cuDNN, and GPU
|
|
4
|
+
compute capability are compatible with installed GPU-accelerated packages
|
|
5
|
+
such as PyTorch and TensorFlow.
|
|
6
|
+
|
|
7
|
+
Compatibility data is loaded from JSON files shipped in the
|
|
8
|
+
``pybinaryguard/rules/data/`` directory.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
17
|
+
|
|
18
|
+
from pybinaryguard.models.enums import Severity
|
|
19
|
+
from pybinaryguard.models.finding import Finding
|
|
20
|
+
from pybinaryguard.models.package import PackageBinaryInfo
|
|
21
|
+
from pybinaryguard.models.system import SystemProfile
|
|
22
|
+
from pybinaryguard.rules.base import Rule
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Data loading helpers
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
_DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data")
|
|
29
|
+
|
|
30
|
+
_data_cache: Dict[str, Any] = {}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _load_json(filename: str) -> Any:
|
|
34
|
+
"""Load and cache a JSON file from the ``rules/data/`` directory.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
filename: Name of the JSON file (e.g. ``'cuda_compat_matrix.json'``).
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
The parsed JSON content.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
FileNotFoundError: If the data file does not exist.
|
|
44
|
+
"""
|
|
45
|
+
if filename in _data_cache:
|
|
46
|
+
return _data_cache[filename]
|
|
47
|
+
filepath = os.path.join(_DATA_DIR, filename)
|
|
48
|
+
with open(filepath, "r", encoding="utf-8") as fh:
|
|
49
|
+
data = json.load(fh)
|
|
50
|
+
_data_cache[filename] = data
|
|
51
|
+
return data
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _is_cuda_package(pkg: PackageBinaryInfo) -> bool:
|
|
55
|
+
"""Heuristic: does this package look like a GPU-accelerated build?
|
|
56
|
+
|
|
57
|
+
Checks for a ``+cuXXX`` suffix in the version string or the
|
|
58
|
+
presence of ``libcudart`` in required libraries.
|
|
59
|
+
"""
|
|
60
|
+
if "+cu" in pkg.package_version:
|
|
61
|
+
return True
|
|
62
|
+
for lib in pkg.required_libraries:
|
|
63
|
+
if "libcudart" in lib:
|
|
64
|
+
return True
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _extract_cuda_version_from_version(version: str) -> Optional[Tuple[int, int]]:
|
|
69
|
+
"""Extract CUDA version from a package version string like ``2.1.0+cu118``.
|
|
70
|
+
|
|
71
|
+
Returns ``(11, 8)`` for ``+cu118``, ``(12, 1)`` for ``+cu121``, etc.
|
|
72
|
+
"""
|
|
73
|
+
match = re.search(r"\+cu(\d{2,3})", version)
|
|
74
|
+
if not match:
|
|
75
|
+
return None
|
|
76
|
+
digits = match.group(1)
|
|
77
|
+
if len(digits) == 2:
|
|
78
|
+
return (int(digits[0]), int(digits[1]))
|
|
79
|
+
# 3 digits: e.g. "118" -> (11, 8), "121" -> (12, 1)
|
|
80
|
+
return (int(digits[:2]), int(digits[2:]))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _driver_major(driver_version: str) -> Optional[int]:
|
|
84
|
+
"""Extract the major version from a driver string like ``'535.104.05'``."""
|
|
85
|
+
parts = driver_version.split(".")
|
|
86
|
+
if not parts:
|
|
87
|
+
return None
|
|
88
|
+
try:
|
|
89
|
+
return int(parts[0])
|
|
90
|
+
except (ValueError, TypeError):
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _max_cuda_for_driver(driver_major: int) -> Optional[Tuple[int, int]]:
|
|
95
|
+
"""Look up the maximum CUDA version supported by a given driver major.
|
|
96
|
+
|
|
97
|
+
Returns ``None`` if the driver is not in the compatibility matrix.
|
|
98
|
+
The lookup finds the highest driver key that is <= ``driver_major``.
|
|
99
|
+
"""
|
|
100
|
+
data = _load_json("cuda_compat_matrix.json")
|
|
101
|
+
mapping: Dict[str, list] = data.get("driver_to_max_cuda", {})
|
|
102
|
+
best: Optional[Tuple[int, int]] = None
|
|
103
|
+
best_key = -1
|
|
104
|
+
for key_str, cuda_ver in mapping.items():
|
|
105
|
+
try:
|
|
106
|
+
key_int = int(key_str)
|
|
107
|
+
except (ValueError, TypeError):
|
|
108
|
+
continue
|
|
109
|
+
if key_int <= driver_major and key_int > best_key:
|
|
110
|
+
best_key = key_int
|
|
111
|
+
best = (int(cuda_ver[0]), int(cuda_ver[1]))
|
|
112
|
+
return best
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _fmt_ver(version: Tuple[int, int]) -> str:
|
|
116
|
+
return f"{version[0]}.{version[1]}"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _fmt_ver3(version: Tuple[int, int, int]) -> str:
|
|
120
|
+
return f"{version[0]}.{version[1]}.{version[2]}"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# Rules
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class CUDADriverTooOldRule(Rule):
|
|
129
|
+
"""Detects when the GPU driver does not support the installed CUDA runtime.
|
|
130
|
+
|
|
131
|
+
NVIDIA GPU drivers have a maximum CUDA version they can support.
|
|
132
|
+
If the installed CUDA runtime is newer than the driver supports, GPU
|
|
133
|
+
operations will fail with ``CUDA error: no kernel image is available``.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
rule_id = "CUDA_DRIVER_TOO_OLD"
|
|
137
|
+
description = (
|
|
138
|
+
"Check that the GPU driver supports the installed CUDA runtime "
|
|
139
|
+
"version."
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def is_applicable(self, profile: SystemProfile) -> bool:
|
|
143
|
+
return (
|
|
144
|
+
profile.gpu_available
|
|
145
|
+
and profile.gpu_driver_version is not None
|
|
146
|
+
and profile.cuda_runtime_version is not None
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def evaluate(
|
|
150
|
+
self,
|
|
151
|
+
profile: SystemProfile,
|
|
152
|
+
packages: List[PackageBinaryInfo],
|
|
153
|
+
) -> List[Finding]:
|
|
154
|
+
findings: List[Finding] = []
|
|
155
|
+
if profile.gpu_driver_version is None or profile.cuda_runtime_version is None:
|
|
156
|
+
return findings
|
|
157
|
+
|
|
158
|
+
drv_major = _driver_major(profile.gpu_driver_version)
|
|
159
|
+
if drv_major is None:
|
|
160
|
+
return findings
|
|
161
|
+
|
|
162
|
+
max_cuda = _max_cuda_for_driver(drv_major)
|
|
163
|
+
if max_cuda is None:
|
|
164
|
+
return findings
|
|
165
|
+
|
|
166
|
+
cuda_rt = profile.cuda_runtime_version
|
|
167
|
+
if cuda_rt > max_cuda:
|
|
168
|
+
findings.append(
|
|
169
|
+
Finding(
|
|
170
|
+
rule_id=self.rule_id,
|
|
171
|
+
severity=Severity.CRITICAL,
|
|
172
|
+
title="GPU driver is too old for the installed CUDA runtime",
|
|
173
|
+
explanation=(
|
|
174
|
+
f"Your NVIDIA GPU driver ({profile.gpu_driver_version}) "
|
|
175
|
+
f"supports CUDA up to {_fmt_ver(max_cuda)}, but your "
|
|
176
|
+
f"installed CUDA runtime is {_fmt_ver(cuda_rt)}. "
|
|
177
|
+
f"This means GPU operations will fail because the "
|
|
178
|
+
f"driver cannot execute CUDA {_fmt_ver(cuda_rt)} "
|
|
179
|
+
f"programs."
|
|
180
|
+
),
|
|
181
|
+
technical_detail=(
|
|
182
|
+
f"Driver: {profile.gpu_driver_version} "
|
|
183
|
+
f"(major {drv_major}), "
|
|
184
|
+
f"max CUDA: {_fmt_ver(max_cuda)}, "
|
|
185
|
+
f"CUDA runtime: {_fmt_ver(cuda_rt)}"
|
|
186
|
+
),
|
|
187
|
+
suggestion=(
|
|
188
|
+
f"Option 1 -- upgrade your NVIDIA driver to one "
|
|
189
|
+
f"that supports CUDA {_fmt_ver(cuda_rt)}:\n"
|
|
190
|
+
f" sudo apt install nvidia-driver-XXX # Debian/Ubuntu\n"
|
|
191
|
+
f" sudo dnf install nvidia-driver # Fedora/RHEL\n\n"
|
|
192
|
+
f"Option 2 -- downgrade CUDA to "
|
|
193
|
+
f"{_fmt_ver(max_cuda)} and reinstall GPU packages."
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
return findings
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class CUDARuntimeMismatchRule(Rule):
|
|
201
|
+
"""Detects CUDA major version mismatch between framework and runtime.
|
|
202
|
+
|
|
203
|
+
GPU-accelerated packages like PyTorch and TensorFlow are compiled
|
|
204
|
+
against a specific CUDA version. The major version must match the
|
|
205
|
+
system CUDA runtime; a CUDA 11.x package will not work with a
|
|
206
|
+
CUDA 12.x runtime.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
rule_id = "CUDA_RUNTIME_MISMATCH"
|
|
210
|
+
description = (
|
|
211
|
+
"Check that GPU packages' CUDA build version major matches the "
|
|
212
|
+
"system CUDA runtime major version."
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def is_applicable(self, profile: SystemProfile) -> bool:
|
|
216
|
+
return profile.cuda_runtime_version is not None
|
|
217
|
+
|
|
218
|
+
def evaluate(
|
|
219
|
+
self,
|
|
220
|
+
profile: SystemProfile,
|
|
221
|
+
packages: List[PackageBinaryInfo],
|
|
222
|
+
) -> List[Finding]:
|
|
223
|
+
findings: List[Finding] = []
|
|
224
|
+
cuda_rt = profile.cuda_runtime_version
|
|
225
|
+
if cuda_rt is None:
|
|
226
|
+
return findings
|
|
227
|
+
|
|
228
|
+
for pkg in packages:
|
|
229
|
+
pkg_cuda = pkg.cuda_build_version
|
|
230
|
+
if pkg_cuda is None:
|
|
231
|
+
# Try to extract from version string.
|
|
232
|
+
pkg_cuda = _extract_cuda_version_from_version(
|
|
233
|
+
pkg.package_version
|
|
234
|
+
)
|
|
235
|
+
if pkg_cuda is None:
|
|
236
|
+
continue
|
|
237
|
+
if pkg_cuda[0] != cuda_rt[0]:
|
|
238
|
+
findings.append(
|
|
239
|
+
Finding(
|
|
240
|
+
rule_id=self.rule_id,
|
|
241
|
+
severity=Severity.CRITICAL,
|
|
242
|
+
title=(
|
|
243
|
+
f"{pkg.package_name} CUDA major version mismatch"
|
|
244
|
+
),
|
|
245
|
+
explanation=(
|
|
246
|
+
f"Package {pkg.package_name} "
|
|
247
|
+
f"{pkg.package_version} was built for "
|
|
248
|
+
f"CUDA {_fmt_ver(pkg_cuda)} but your system "
|
|
249
|
+
f"has CUDA {_fmt_ver(cuda_rt)}. Because the "
|
|
250
|
+
f"CUDA major version differs ({pkg_cuda[0]} vs "
|
|
251
|
+
f"{cuda_rt[0]}), the compiled GPU kernels are "
|
|
252
|
+
f"incompatible and will fail to load."
|
|
253
|
+
),
|
|
254
|
+
technical_detail=(
|
|
255
|
+
f"Package CUDA: {_fmt_ver(pkg_cuda)}, "
|
|
256
|
+
f"System CUDA: {_fmt_ver(cuda_rt)}"
|
|
257
|
+
),
|
|
258
|
+
suggestion=(
|
|
259
|
+
f"Install the correct variant for your CUDA "
|
|
260
|
+
f"version:\n"
|
|
261
|
+
f" pip install {pkg.package_name}=="
|
|
262
|
+
f"{_strip_cuda_suffix(pkg.package_version)}"
|
|
263
|
+
f"+cu{cuda_rt[0]}{cuda_rt[1]}\n\n"
|
|
264
|
+
f"Or see the package's installation page for "
|
|
265
|
+
f"CUDA {_fmt_ver(cuda_rt)} wheels."
|
|
266
|
+
),
|
|
267
|
+
package=pkg.package_name,
|
|
268
|
+
package_version=pkg.package_version,
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
return findings
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class CUDAMinorMismatchRule(Rule):
|
|
275
|
+
"""Warns when CUDA minor versions differ between package and runtime.
|
|
276
|
+
|
|
277
|
+
CUDA minor version mismatches are not always fatal (NVIDIA provides
|
|
278
|
+
some forward compatibility), but they can cause subtle issues or
|
|
279
|
+
missing features.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
rule_id = "CUDA_MINOR_MISMATCH"
|
|
283
|
+
description = (
|
|
284
|
+
"Warn when CUDA minor versions differ between a GPU package "
|
|
285
|
+
"and the system runtime."
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def is_applicable(self, profile: SystemProfile) -> bool:
|
|
289
|
+
return profile.cuda_runtime_version is not None
|
|
290
|
+
|
|
291
|
+
def evaluate(
|
|
292
|
+
self,
|
|
293
|
+
profile: SystemProfile,
|
|
294
|
+
packages: List[PackageBinaryInfo],
|
|
295
|
+
) -> List[Finding]:
|
|
296
|
+
findings: List[Finding] = []
|
|
297
|
+
cuda_rt = profile.cuda_runtime_version
|
|
298
|
+
if cuda_rt is None:
|
|
299
|
+
return findings
|
|
300
|
+
|
|
301
|
+
for pkg in packages:
|
|
302
|
+
pkg_cuda = pkg.cuda_build_version
|
|
303
|
+
if pkg_cuda is None:
|
|
304
|
+
pkg_cuda = _extract_cuda_version_from_version(
|
|
305
|
+
pkg.package_version
|
|
306
|
+
)
|
|
307
|
+
if pkg_cuda is None:
|
|
308
|
+
continue
|
|
309
|
+
# Only warn if major matches but minor differs.
|
|
310
|
+
if pkg_cuda[0] == cuda_rt[0] and pkg_cuda[1] != cuda_rt[1]:
|
|
311
|
+
findings.append(
|
|
312
|
+
Finding(
|
|
313
|
+
rule_id=self.rule_id,
|
|
314
|
+
severity=Severity.WARNING,
|
|
315
|
+
title=(
|
|
316
|
+
f"{pkg.package_name} CUDA minor version differs"
|
|
317
|
+
),
|
|
318
|
+
explanation=(
|
|
319
|
+
f"Package {pkg.package_name} "
|
|
320
|
+
f"{pkg.package_version} was built for "
|
|
321
|
+
f"CUDA {_fmt_ver(pkg_cuda)} but your system "
|
|
322
|
+
f"has CUDA {_fmt_ver(cuda_rt)}. While CUDA "
|
|
323
|
+
f"minor versions are sometimes compatible, "
|
|
324
|
+
f"there may be missing features or subtle "
|
|
325
|
+
f"runtime differences."
|
|
326
|
+
),
|
|
327
|
+
technical_detail=(
|
|
328
|
+
f"Package CUDA: {_fmt_ver(pkg_cuda)}, "
|
|
329
|
+
f"System CUDA: {_fmt_ver(cuda_rt)}"
|
|
330
|
+
),
|
|
331
|
+
suggestion=(
|
|
332
|
+
f"For best compatibility, install the wheel "
|
|
333
|
+
f"that matches your CUDA version:\n"
|
|
334
|
+
f" pip install {pkg.package_name}=="
|
|
335
|
+
f"{_strip_cuda_suffix(pkg.package_version)}"
|
|
336
|
+
f"+cu{cuda_rt[0]}{cuda_rt[1]}"
|
|
337
|
+
),
|
|
338
|
+
package=pkg.package_name,
|
|
339
|
+
package_version=pkg.package_version,
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
return findings
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class CUDNNVersionMismatchRule(Rule):
|
|
346
|
+
"""Detects cuDNN version incompatibilities with frameworks.
|
|
347
|
+
|
|
348
|
+
TensorFlow and other frameworks are compiled against a specific
|
|
349
|
+
cuDNN version. A mismatch (especially a major version mismatch)
|
|
350
|
+
leads to errors like ``Could not load library 'libcudnn.so.8'``.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
rule_id = "CUDNN_VERSION_MISMATCH"
|
|
354
|
+
description = (
|
|
355
|
+
"Check that the system cuDNN version is compatible with GPU "
|
|
356
|
+
"framework requirements."
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
def is_applicable(self, profile: SystemProfile) -> bool:
|
|
360
|
+
return profile.cudnn_version is not None
|
|
361
|
+
|
|
362
|
+
def evaluate(
|
|
363
|
+
self,
|
|
364
|
+
profile: SystemProfile,
|
|
365
|
+
packages: List[PackageBinaryInfo],
|
|
366
|
+
) -> List[Finding]:
|
|
367
|
+
findings: List[Finding] = []
|
|
368
|
+
if profile.cudnn_version is None:
|
|
369
|
+
return findings
|
|
370
|
+
|
|
371
|
+
sys_cudnn = profile.cudnn_version # (major, minor, patch)
|
|
372
|
+
|
|
373
|
+
# Check TensorFlow requirements from the compatibility matrix.
|
|
374
|
+
tf_matrix = _try_load_json("tensorflow_cuda_matrix.json")
|
|
375
|
+
for pkg in packages:
|
|
376
|
+
pkg_lower = pkg.package_name.lower()
|
|
377
|
+
if pkg_lower not in ("tensorflow", "tensorflow-gpu"):
|
|
378
|
+
continue
|
|
379
|
+
if tf_matrix is None:
|
|
380
|
+
continue
|
|
381
|
+
# Find the closest matching TF version in the matrix.
|
|
382
|
+
tf_info = _find_framework_entry(
|
|
383
|
+
tf_matrix, pkg.package_version
|
|
384
|
+
)
|
|
385
|
+
if tf_info is None:
|
|
386
|
+
continue
|
|
387
|
+
required_cudnn_str = tf_info.get("cudnn")
|
|
388
|
+
if required_cudnn_str is None:
|
|
389
|
+
continue
|
|
390
|
+
req_parts = required_cudnn_str.split(".")
|
|
391
|
+
if len(req_parts) < 2:
|
|
392
|
+
continue
|
|
393
|
+
req_major = int(req_parts[0])
|
|
394
|
+
req_minor = int(req_parts[1])
|
|
395
|
+
if sys_cudnn[0] != req_major:
|
|
396
|
+
findings.append(
|
|
397
|
+
Finding(
|
|
398
|
+
rule_id=self.rule_id,
|
|
399
|
+
severity=Severity.CRITICAL,
|
|
400
|
+
title=(
|
|
401
|
+
f"{pkg.package_name} needs cuDNN "
|
|
402
|
+
f"{required_cudnn_str}"
|
|
403
|
+
),
|
|
404
|
+
explanation=(
|
|
405
|
+
f"Package {pkg.package_name} "
|
|
406
|
+
f"{pkg.package_version} was built for cuDNN "
|
|
407
|
+
f"{required_cudnn_str} but your system has "
|
|
408
|
+
f"cuDNN {_fmt_ver3(sys_cudnn)}. The cuDNN "
|
|
409
|
+
f"major version must match; otherwise the "
|
|
410
|
+
f"framework will fail to load its GPU kernels."
|
|
411
|
+
),
|
|
412
|
+
technical_detail=(
|
|
413
|
+
f"Required cuDNN: {required_cudnn_str}, "
|
|
414
|
+
f"System cuDNN: {_fmt_ver3(sys_cudnn)}"
|
|
415
|
+
),
|
|
416
|
+
suggestion=(
|
|
417
|
+
f"Install the correct cuDNN version:\n"
|
|
418
|
+
f" sudo apt install libcudnn{req_major} "
|
|
419
|
+
f"# Debian/Ubuntu\n\n"
|
|
420
|
+
f"Or use conda:\n"
|
|
421
|
+
f" conda install -c conda-forge cudnn="
|
|
422
|
+
f"{required_cudnn_str}"
|
|
423
|
+
),
|
|
424
|
+
package=pkg.package_name,
|
|
425
|
+
package_version=pkg.package_version,
|
|
426
|
+
)
|
|
427
|
+
)
|
|
428
|
+
elif sys_cudnn[1] < req_minor:
|
|
429
|
+
findings.append(
|
|
430
|
+
Finding(
|
|
431
|
+
rule_id=self.rule_id,
|
|
432
|
+
severity=Severity.WARNING,
|
|
433
|
+
title=(
|
|
434
|
+
f"{pkg.package_name} prefers cuDNN "
|
|
435
|
+
f"{required_cudnn_str}"
|
|
436
|
+
),
|
|
437
|
+
explanation=(
|
|
438
|
+
f"Package {pkg.package_name} "
|
|
439
|
+
f"{pkg.package_version} was built for cuDNN "
|
|
440
|
+
f"{required_cudnn_str} but your system has "
|
|
441
|
+
f"cuDNN {_fmt_ver3(sys_cudnn)}. While the "
|
|
442
|
+
f"major version matches, the minor version is "
|
|
443
|
+
f"older than expected and some features may "
|
|
444
|
+
f"not work correctly."
|
|
445
|
+
),
|
|
446
|
+
technical_detail=(
|
|
447
|
+
f"Required cuDNN: {required_cudnn_str}, "
|
|
448
|
+
f"System cuDNN: {_fmt_ver3(sys_cudnn)}"
|
|
449
|
+
),
|
|
450
|
+
suggestion=(
|
|
451
|
+
f"Upgrade cuDNN to {required_cudnn_str} or "
|
|
452
|
+
f"later for best compatibility."
|
|
453
|
+
),
|
|
454
|
+
package=pkg.package_name,
|
|
455
|
+
package_version=pkg.package_version,
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
return findings
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class CUDANotFoundRule(Rule):
|
|
462
|
+
"""Warns when a GPU package is installed but no CUDA runtime is found.
|
|
463
|
+
|
|
464
|
+
If a package was built with GPU support but the system has no CUDA
|
|
465
|
+
runtime installed, GPU operations will fail silently (falling back to
|
|
466
|
+
CPU) or raise an error.
|
|
467
|
+
"""
|
|
468
|
+
|
|
469
|
+
rule_id = "CUDA_NOT_FOUND"
|
|
470
|
+
description = (
|
|
471
|
+
"Warn when a GPU-enabled package is installed but CUDA is not "
|
|
472
|
+
"available."
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
def is_applicable(self, profile: SystemProfile) -> bool:
|
|
476
|
+
return profile.cuda_runtime_version is None
|
|
477
|
+
|
|
478
|
+
def evaluate(
|
|
479
|
+
self,
|
|
480
|
+
profile: SystemProfile,
|
|
481
|
+
packages: List[PackageBinaryInfo],
|
|
482
|
+
) -> List[Finding]:
|
|
483
|
+
findings: List[Finding] = []
|
|
484
|
+
for pkg in packages:
|
|
485
|
+
if not _is_cuda_package(pkg):
|
|
486
|
+
continue
|
|
487
|
+
findings.append(
|
|
488
|
+
Finding(
|
|
489
|
+
rule_id=self.rule_id,
|
|
490
|
+
severity=Severity.WARNING,
|
|
491
|
+
title=(
|
|
492
|
+
f"{pkg.package_name} is GPU-enabled but CUDA "
|
|
493
|
+
f"is not found"
|
|
494
|
+
),
|
|
495
|
+
explanation=(
|
|
496
|
+
f"Package {pkg.package_name} "
|
|
497
|
+
f"{pkg.package_version} appears to be built with "
|
|
498
|
+
f"GPU support (it references CUDA libraries), but "
|
|
499
|
+
f"no CUDA runtime was detected on your system. "
|
|
500
|
+
f"GPU acceleration will not work; the package may "
|
|
501
|
+
f"fall back to CPU mode or raise an error."
|
|
502
|
+
),
|
|
503
|
+
technical_detail=(
|
|
504
|
+
f"Package version: {pkg.package_version}, "
|
|
505
|
+
f"CUDA detected: False"
|
|
506
|
+
),
|
|
507
|
+
suggestion=(
|
|
508
|
+
f"If you have an NVIDIA GPU, install the CUDA "
|
|
509
|
+
f"toolkit:\n"
|
|
510
|
+
f" sudo apt install nvidia-cuda-toolkit "
|
|
511
|
+
f"# Debian/Ubuntu\n\n"
|
|
512
|
+
f"If you do not need GPU support, install the "
|
|
513
|
+
f"CPU-only version:\n"
|
|
514
|
+
f" pip install {pkg.package_name}-cpu "
|
|
515
|
+
f"# if available\n"
|
|
516
|
+
f" pip install {pkg.package_name} "
|
|
517
|
+
f"# without +cuXXX suffix"
|
|
518
|
+
),
|
|
519
|
+
package=pkg.package_name,
|
|
520
|
+
package_version=pkg.package_version,
|
|
521
|
+
)
|
|
522
|
+
)
|
|
523
|
+
return findings
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
class ComputeCapabilityLowRule(Rule):
|
|
527
|
+
"""Detects when the GPU's compute capability is below a framework's minimum.
|
|
528
|
+
|
|
529
|
+
Newer versions of TensorFlow and PyTorch drop support for older GPU
|
|
530
|
+
architectures. For example, PyTorch 2.x requires compute capability
|
|
531
|
+
3.7+. Attempting to run on an older GPU produces silent wrong
|
|
532
|
+
results or a ``CUDA error: no kernel image``.
|
|
533
|
+
"""
|
|
534
|
+
|
|
535
|
+
rule_id = "COMPUTE_CAPABILITY_LOW"
|
|
536
|
+
description = (
|
|
537
|
+
"Check that the GPU compute capability meets the framework's "
|
|
538
|
+
"minimum requirement."
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
def is_applicable(self, profile: SystemProfile) -> bool:
|
|
542
|
+
return (
|
|
543
|
+
profile.gpu_available
|
|
544
|
+
and profile.gpu_compute_capability is not None
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
def evaluate(
|
|
548
|
+
self,
|
|
549
|
+
profile: SystemProfile,
|
|
550
|
+
packages: List[PackageBinaryInfo],
|
|
551
|
+
) -> List[Finding]:
|
|
552
|
+
findings: List[Finding] = []
|
|
553
|
+
cc = profile.gpu_compute_capability
|
|
554
|
+
if cc is None:
|
|
555
|
+
return findings
|
|
556
|
+
|
|
557
|
+
for pkg in packages:
|
|
558
|
+
min_cc = _get_min_compute(pkg)
|
|
559
|
+
if min_cc is None:
|
|
560
|
+
continue
|
|
561
|
+
if cc < min_cc:
|
|
562
|
+
findings.append(
|
|
563
|
+
Finding(
|
|
564
|
+
rule_id=self.rule_id,
|
|
565
|
+
severity=Severity.CRITICAL,
|
|
566
|
+
title=(
|
|
567
|
+
f"GPU compute capability too low for "
|
|
568
|
+
f"{pkg.package_name}"
|
|
569
|
+
),
|
|
570
|
+
explanation=(
|
|
571
|
+
f"Package {pkg.package_name} "
|
|
572
|
+
f"{pkg.package_version} requires a GPU with "
|
|
573
|
+
f"compute capability >= {min_cc[0]}.{min_cc[1]}, "
|
|
574
|
+
f"but your GPU "
|
|
575
|
+
f"({profile.gpu_name or 'unknown'}) has "
|
|
576
|
+
f"compute capability {cc[0]}.{cc[1]}. The "
|
|
577
|
+
f"package does not include compiled GPU kernels "
|
|
578
|
+
f"for your architecture, so GPU operations "
|
|
579
|
+
f"will fail."
|
|
580
|
+
),
|
|
581
|
+
technical_detail=(
|
|
582
|
+
f"GPU: {profile.gpu_name or 'unknown'}, "
|
|
583
|
+
f"compute capability: {cc[0]}.{cc[1]}, "
|
|
584
|
+
f"minimum required: {min_cc[0]}.{min_cc[1]}"
|
|
585
|
+
),
|
|
586
|
+
suggestion=(
|
|
587
|
+
f"Option 1 -- use an older version of "
|
|
588
|
+
f"{pkg.package_name} that still supports "
|
|
589
|
+
f"compute capability {cc[0]}.{cc[1]}.\n\n"
|
|
590
|
+
f"Option 2 -- upgrade to a newer NVIDIA GPU "
|
|
591
|
+
f"with compute capability >= "
|
|
592
|
+
f"{min_cc[0]}.{min_cc[1]}."
|
|
593
|
+
),
|
|
594
|
+
package=pkg.package_name,
|
|
595
|
+
package_version=pkg.package_version,
|
|
596
|
+
)
|
|
597
|
+
)
|
|
598
|
+
return findings
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
class CUDALibMissingRule(Rule):
|
|
602
|
+
"""Detects missing CUDA shared libraries.
|
|
603
|
+
|
|
604
|
+
If a package links against CUDA libraries (``libcudart``, ``libcublas``,
|
|
605
|
+
etc.) that are not found on the system, the package will fail at
|
|
606
|
+
import time.
|
|
607
|
+
"""
|
|
608
|
+
|
|
609
|
+
rule_id = "CUDA_LIB_MISSING"
|
|
610
|
+
description = (
|
|
611
|
+
"Check that all required CUDA shared libraries are present."
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
_CUDA_LIB_PREFIXES = (
|
|
615
|
+
"libcuda",
|
|
616
|
+
"libcudart",
|
|
617
|
+
"libcublas",
|
|
618
|
+
"libcufft",
|
|
619
|
+
"libcurand",
|
|
620
|
+
"libcusolver",
|
|
621
|
+
"libcusparse",
|
|
622
|
+
"libnccl",
|
|
623
|
+
"libnvrtc",
|
|
624
|
+
"libnvjitlink",
|
|
625
|
+
"libcudnn",
|
|
626
|
+
"libnvToolsExt",
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
def evaluate(
|
|
630
|
+
self,
|
|
631
|
+
profile: SystemProfile,
|
|
632
|
+
packages: List[PackageBinaryInfo],
|
|
633
|
+
) -> List[Finding]:
|
|
634
|
+
findings: List[Finding] = []
|
|
635
|
+
for pkg in packages:
|
|
636
|
+
if not pkg.missing_libraries:
|
|
637
|
+
continue
|
|
638
|
+
cuda_missing = [
|
|
639
|
+
lib
|
|
640
|
+
for lib in sorted(pkg.missing_libraries)
|
|
641
|
+
if any(lib.startswith(p) for p in self._CUDA_LIB_PREFIXES)
|
|
642
|
+
]
|
|
643
|
+
if not cuda_missing:
|
|
644
|
+
continue
|
|
645
|
+
findings.append(
|
|
646
|
+
Finding(
|
|
647
|
+
rule_id=self.rule_id,
|
|
648
|
+
severity=Severity.CRITICAL,
|
|
649
|
+
title=(
|
|
650
|
+
f"{pkg.package_name} is missing CUDA libraries"
|
|
651
|
+
),
|
|
652
|
+
explanation=(
|
|
653
|
+
f"Package {pkg.package_name} "
|
|
654
|
+
f"{pkg.package_version} requires the following "
|
|
655
|
+
f"CUDA shared libraries that were not found on "
|
|
656
|
+
f"your system: {', '.join(cuda_missing)}. "
|
|
657
|
+
f"Without these libraries the package will fail "
|
|
658
|
+
f"to import."
|
|
659
|
+
),
|
|
660
|
+
technical_detail=(
|
|
661
|
+
f"Missing CUDA libs: {', '.join(cuda_missing)}"
|
|
662
|
+
),
|
|
663
|
+
suggestion=(
|
|
664
|
+
f"Install the CUDA toolkit and cuDNN:\n"
|
|
665
|
+
f" sudo apt install nvidia-cuda-toolkit "
|
|
666
|
+
f"libcudnn8 # Debian/Ubuntu\n\n"
|
|
667
|
+
f"Or ensure LD_LIBRARY_PATH includes CUDA lib "
|
|
668
|
+
f"directories:\n"
|
|
669
|
+
f" export LD_LIBRARY_PATH=/usr/local/cuda/lib64"
|
|
670
|
+
f":$LD_LIBRARY_PATH"
|
|
671
|
+
),
|
|
672
|
+
package=pkg.package_name,
|
|
673
|
+
package_version=pkg.package_version,
|
|
674
|
+
)
|
|
675
|
+
)
|
|
676
|
+
return findings
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
# ---------------------------------------------------------------------------
|
|
680
|
+
# Internal helpers
|
|
681
|
+
# ---------------------------------------------------------------------------
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _try_load_json(filename: str) -> Optional[Dict[str, Any]]:
|
|
685
|
+
"""Load a JSON data file, returning ``None`` on any error."""
|
|
686
|
+
try:
|
|
687
|
+
return _load_json(filename)
|
|
688
|
+
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
|
689
|
+
return None
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _find_framework_entry(
|
|
693
|
+
matrix: Dict[str, Any],
|
|
694
|
+
version: str,
|
|
695
|
+
) -> Optional[Dict[str, Any]]:
|
|
696
|
+
"""Find the closest matching entry in a framework compatibility matrix.
|
|
697
|
+
|
|
698
|
+
Tries an exact match first, then strips the patch version, then picks
|
|
699
|
+
the highest version with the same major.minor.
|
|
700
|
+
"""
|
|
701
|
+
# Strip any +cuXXX suffix for lookup.
|
|
702
|
+
clean = _strip_cuda_suffix(version)
|
|
703
|
+
|
|
704
|
+
# Exact match.
|
|
705
|
+
if clean in matrix:
|
|
706
|
+
return matrix[clean]
|
|
707
|
+
|
|
708
|
+
# Try major.minor.0.
|
|
709
|
+
parts = clean.split(".")
|
|
710
|
+
if len(parts) >= 2:
|
|
711
|
+
base = f"{parts[0]}.{parts[1]}.0"
|
|
712
|
+
if base in matrix:
|
|
713
|
+
return matrix[base]
|
|
714
|
+
|
|
715
|
+
# Pick the highest version with the same major.minor.
|
|
716
|
+
if len(parts) >= 2:
|
|
717
|
+
prefix = f"{parts[0]}.{parts[1]}."
|
|
718
|
+
candidates = [k for k in matrix if k.startswith(prefix)]
|
|
719
|
+
if candidates:
|
|
720
|
+
candidates.sort()
|
|
721
|
+
return matrix[candidates[-1]]
|
|
722
|
+
|
|
723
|
+
return None
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _strip_cuda_suffix(version: str) -> str:
|
|
727
|
+
"""Remove ``+cuXXX`` or similar local version suffix."""
|
|
728
|
+
idx = version.find("+")
|
|
729
|
+
if idx >= 0:
|
|
730
|
+
return version[:idx]
|
|
731
|
+
return version
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _get_min_compute(pkg: PackageBinaryInfo) -> Optional[Tuple[int, int]]:
|
|
735
|
+
"""Determine the minimum GPU compute capability for a package.
|
|
736
|
+
|
|
737
|
+
Uses the PyTorch and TensorFlow compatibility matrices.
|
|
738
|
+
"""
|
|
739
|
+
pkg_lower = pkg.package_name.lower()
|
|
740
|
+
version = _strip_cuda_suffix(pkg.package_version)
|
|
741
|
+
|
|
742
|
+
if pkg_lower in ("torch", "pytorch"):
|
|
743
|
+
matrix = _try_load_json("pytorch_cuda_matrix.json")
|
|
744
|
+
if matrix is not None:
|
|
745
|
+
entry = _find_framework_entry(matrix, version)
|
|
746
|
+
if entry is not None:
|
|
747
|
+
mc = entry.get("min_compute")
|
|
748
|
+
if mc is not None and len(mc) == 2:
|
|
749
|
+
return (int(mc[0]), int(mc[1]))
|
|
750
|
+
|
|
751
|
+
if pkg_lower in ("tensorflow", "tensorflow-gpu"):
|
|
752
|
+
matrix = _try_load_json("tensorflow_cuda_matrix.json")
|
|
753
|
+
if matrix is not None:
|
|
754
|
+
entry = _find_framework_entry(matrix, version)
|
|
755
|
+
if entry is not None:
|
|
756
|
+
mc = entry.get("min_compute")
|
|
757
|
+
if mc is not None and len(mc) == 2:
|
|
758
|
+
return (int(mc[0]), int(mc[1]))
|
|
759
|
+
|
|
760
|
+
return None
|