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.
Files changed (91) hide show
  1. pybinaryguard/__init__.py +78 -0
  2. pybinaryguard/__main__.py +7 -0
  3. pybinaryguard/_compat/__init__.py +0 -0
  4. pybinaryguard/agent/__init__.py +63 -0
  5. pybinaryguard/agent/guard.py +232 -0
  6. pybinaryguard/agent/recommender.py +283 -0
  7. pybinaryguard/agent/schema.py +200 -0
  8. pybinaryguard/agent/simulator.py +430 -0
  9. pybinaryguard/agent/tool_interface.py +474 -0
  10. pybinaryguard/analyzers/__init__.py +209 -0
  11. pybinaryguard/analyzers/base.py +40 -0
  12. pybinaryguard/analyzers/dependency_analyzer.py +336 -0
  13. pybinaryguard/analyzers/elf_analyzer.py +754 -0
  14. pybinaryguard/analyzers/symbol_analyzer.py +280 -0
  15. pybinaryguard/analyzers/wheel_analyzer.py +308 -0
  16. pybinaryguard/cli/__init__.py +5 -0
  17. pybinaryguard/cli/commands.py +414 -0
  18. pybinaryguard/cli/formatters.py +720 -0
  19. pybinaryguard/cli/main.py +250 -0
  20. pybinaryguard/diagnostics/__init__.py +13 -0
  21. pybinaryguard/diagnostics/explainer.py +356 -0
  22. pybinaryguard/diagnostics/findings.py +146 -0
  23. pybinaryguard/diagnostics/suggestions.py +508 -0
  24. pybinaryguard/frameworks/__init__.py +20 -0
  25. pybinaryguard/frameworks/onnxruntime.py +214 -0
  26. pybinaryguard/frameworks/pytorch.py +223 -0
  27. pybinaryguard/frameworks/tensorflow.py +266 -0
  28. pybinaryguard/frameworks/tensorrt.py +189 -0
  29. pybinaryguard/models/__init__.py +19 -0
  30. pybinaryguard/models/enums.py +102 -0
  31. pybinaryguard/models/finding.py +109 -0
  32. pybinaryguard/models/package.py +121 -0
  33. pybinaryguard/models/system.py +118 -0
  34. pybinaryguard/plugins/__init__.py +19 -0
  35. pybinaryguard/plugins/contrib/__init__.py +7 -0
  36. pybinaryguard/plugins/contrib/gstreamer.py +109 -0
  37. pybinaryguard/plugins/contrib/jetson.py +385 -0
  38. pybinaryguard/plugins/contrib/opencv.py +306 -0
  39. pybinaryguard/plugins/contrib/tensorrt.py +426 -0
  40. pybinaryguard/plugins/hooks.py +273 -0
  41. pybinaryguard/plugins/loader.py +190 -0
  42. pybinaryguard/predictor/__init__.py +23 -0
  43. pybinaryguard/predictor/dependency_graph.py +226 -0
  44. pybinaryguard/predictor/linker_simulator.py +161 -0
  45. pybinaryguard/predictor/predictor.py +144 -0
  46. pybinaryguard/predictor/resolver.py +282 -0
  47. pybinaryguard/probes/__init__.py +73 -0
  48. pybinaryguard/probes/base.py +45 -0
  49. pybinaryguard/probes/board_probe.py +248 -0
  50. pybinaryguard/probes/cpu_probe.py +176 -0
  51. pybinaryguard/probes/glibc_probe.py +215 -0
  52. pybinaryguard/probes/gpu_probe.py +430 -0
  53. pybinaryguard/probes/library_probe.py +132 -0
  54. pybinaryguard/probes/os_probe.py +227 -0
  55. pybinaryguard/probes/python_probe.py +135 -0
  56. pybinaryguard/probes/toolchain_probe.py +89 -0
  57. pybinaryguard/probes/venv_probe.py +111 -0
  58. pybinaryguard/profiles/__init__.py +12 -0
  59. pybinaryguard/profiles/engine.py +343 -0
  60. pybinaryguard/rules/__init__.py +24 -0
  61. pybinaryguard/rules/base.py +57 -0
  62. pybinaryguard/rules/builtin/__init__.py +136 -0
  63. pybinaryguard/rules/builtin/arch_rules.py +86 -0
  64. pybinaryguard/rules/builtin/board_profile_rules.py +282 -0
  65. pybinaryguard/rules/builtin/container_rules.py +170 -0
  66. pybinaryguard/rules/builtin/cpu_rules.py +204 -0
  67. pybinaryguard/rules/builtin/cuda_rules.py +760 -0
  68. pybinaryguard/rules/builtin/dependency_rules.py +278 -0
  69. pybinaryguard/rules/builtin/framework_rules.py +252 -0
  70. pybinaryguard/rules/builtin/glibc_rules.py +320 -0
  71. pybinaryguard/rules/builtin/numpy_rules.py +110 -0
  72. pybinaryguard/rules/builtin/predictive_rules.py +165 -0
  73. pybinaryguard/rules/builtin/python_abi_rules.py +259 -0
  74. pybinaryguard/rules/builtin/source_build_rules.py +150 -0
  75. pybinaryguard/rules/builtin/venv_rules.py +159 -0
  76. pybinaryguard/rules/engine.py +123 -0
  77. pybinaryguard/scanner.py +904 -0
  78. pybinaryguard/scoring/__init__.py +19 -0
  79. pybinaryguard/scoring/engine.py +416 -0
  80. pybinaryguard/snapshot/__init__.py +20 -0
  81. pybinaryguard/snapshot/generator.py +168 -0
  82. pybinaryguard/snapshot/lockfile.py +152 -0
  83. pybinaryguard/snapshot/verifier.py +315 -0
  84. pybinaryguard/validators/__init__.py +7 -0
  85. pybinaryguard/validators/import_validator.py +231 -0
  86. pybinaryguard-1.0.0.dist-info/METADATA +876 -0
  87. pybinaryguard-1.0.0.dist-info/RECORD +91 -0
  88. pybinaryguard-1.0.0.dist-info/WHEEL +5 -0
  89. pybinaryguard-1.0.0.dist-info/entry_points.txt +8 -0
  90. pybinaryguard-1.0.0.dist-info/licenses/LICENSE +21 -0
  91. 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