gpugem 0.1.2__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.
- gpugem/__init__.py +32 -0
- gpugem/_defaults.py +80 -0
- gpugem/_deps.py +271 -0
- gpugem/lifting.py +320 -0
- gpugem/loaders.py +205 -0
- gpugem/result.py +50 -0
- gpugem/scaling.py +257 -0
- gpugem/solve_cobra.py +29 -0
- gpugem/solver.py +410 -0
- gpugem-0.1.2.dist-info/METADATA +328 -0
- gpugem-0.1.2.dist-info/RECORD +14 -0
- gpugem-0.1.2.dist-info/WHEEL +5 -0
- gpugem-0.1.2.dist-info/licenses/LICENSE +21 -0
- gpugem-0.1.2.dist-info/top_level.txt +1 -0
gpugem/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""gpuGEM — GPU-accelerated Flux Balance Analysis for genome-scale metabolic models."""
|
|
2
|
+
|
|
3
|
+
from gpugem._deps import DependencyError, require as _require
|
|
4
|
+
|
|
5
|
+
# numpy/scipy are hard dependencies; fail here with guidance, not with a bare ImportError below.
|
|
6
|
+
# cuOpt is deliberately NOT checked at import: it is required only when a solve is requested.
|
|
7
|
+
_require("numpy")
|
|
8
|
+
_require("scipy")
|
|
9
|
+
|
|
10
|
+
from gpugem.solver import FBASolver, solve # noqa: E402
|
|
11
|
+
from gpugem.solve_cobra import solve_cobra # noqa: E402
|
|
12
|
+
from gpugem.result import FBAResult # noqa: E402
|
|
13
|
+
from gpugem.scaling import CoefficientScalingMapping, remap_fluxes, scale_model # noqa: E402
|
|
14
|
+
from gpugem.lifting import LiftingMapping, lift_coupling, lift_mass_balance, map_back # noqa: E402
|
|
15
|
+
from gpugem import loaders # noqa: E402
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.2"
|
|
18
|
+
__all__ = [
|
|
19
|
+
"DependencyError",
|
|
20
|
+
"FBASolver",
|
|
21
|
+
"solve",
|
|
22
|
+
"solve_cobra",
|
|
23
|
+
"FBAResult",
|
|
24
|
+
"CoefficientScalingMapping",
|
|
25
|
+
"scale_model",
|
|
26
|
+
"remap_fluxes",
|
|
27
|
+
"LiftingMapping",
|
|
28
|
+
"lift_mass_balance",
|
|
29
|
+
"lift_coupling",
|
|
30
|
+
"map_back",
|
|
31
|
+
"loaders",
|
|
32
|
+
]
|
gpugem/_defaults.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Default cuOpt solver settings derived from systematic benchmarking on whole-body
|
|
3
|
+
metabolic models across two scale classes.
|
|
4
|
+
|
|
5
|
+
cuOpt's precision enum (verified against cpp/include/.../constants.h):
|
|
6
|
+
-1 = Default, 0 = Single, 1 = Double, 2 = Mixed.
|
|
7
|
+
Earlier revisions of this module named these modes wrongly -- pdlp_precision=1
|
|
8
|
+
was described as "mixed FP32/FP64" when the enum defines it as Double. The
|
|
9
|
+
measurements below are unchanged and were always correct; only the labels were
|
|
10
|
+
wrong. gpuGEM therefore uses DOUBLE precision at both scales: explicitly below
|
|
11
|
+
the threshold, and via cuOpt's Default above it (Default resolves to Double,
|
|
12
|
+
confirmed bit-identical on e_coli_core and again at S85 scale -- identical
|
|
13
|
+
iteration count and max residual with and without the explicit setting).
|
|
14
|
+
|
|
15
|
+
Small models (≤ 100K reactions, e.g. Harvey whole-body model):
|
|
16
|
+
- Single precision (pdlp_precision=0) does not converge at tolerances
|
|
17
|
+
tighter than 1e-4 on these models; double precision is required.
|
|
18
|
+
- Mixed precision (pdlp_precision=2) hits the FP32 precision wall.
|
|
19
|
+
- tol=1e-8 + double precision → zero stoichiometric violations at max residual 1e-6.
|
|
20
|
+
- per_constraint_residual=1 (max-norm convergence) further reduces violations
|
|
21
|
+
vs tightening tolerances alone.
|
|
22
|
+
- PaPILO presolve adds postsolve reconstruction error on well-conditioned models;
|
|
23
|
+
the default PSLP presolve is correct at this scale.
|
|
24
|
+
|
|
25
|
+
Large models (> 100K reactions, e.g. personalised microbiome whole-body models):
|
|
26
|
+
- cuOpt's default PSLP presolve incorrectly reports Infeasible on models with
|
|
27
|
+
extreme stoichiometric coefficient ranges ([1e-6, 2e5]); PaPILO is required.
|
|
28
|
+
- per_constraint_residual=1 reduces violations by ~44% and max residual by ~100×.
|
|
29
|
+
- Tight tolerances do not improve accuracy: the bottleneck is a hardcoded feastol
|
|
30
|
+
in cuOpt's PaPILO integration, not PDLP convergence.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# Models at or below this reaction count use the Harvey-tuned settings.
|
|
34
|
+
_SMALL_MODEL_THRESHOLD = 100_000
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def default_settings(n_vars: int, time_limit: float = 60.0) -> dict:
|
|
38
|
+
"""
|
|
39
|
+
Return the recommended cuOpt parameter dict for a model of the given size.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
n_vars:
|
|
44
|
+
Number of variables (reactions) in the LP.
|
|
45
|
+
time_limit:
|
|
46
|
+
Wall-clock time limit in seconds passed to cuOpt.
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
dict
|
|
51
|
+
Parameter names and values ready to pass to ``SolverSettings.set_parameter``.
|
|
52
|
+
"""
|
|
53
|
+
if n_vars <= _SMALL_MODEL_THRESHOLD:
|
|
54
|
+
# Harvey-scale: double precision + tight tolerances + max-norm convergence.
|
|
55
|
+
# Single precision (pdlp_precision=0) does not converge on these models at
|
|
56
|
+
# tolerances tighter than 1e-4, and Mixed (2) hits the FP32 wall. PaPILO is
|
|
57
|
+
# not used — PSLP is correct on well-conditioned models and PaPILO postsolve
|
|
58
|
+
# adds reconstruction error.
|
|
59
|
+
return {
|
|
60
|
+
"method": 1, # PDLP
|
|
61
|
+
"pdlp_precision": 1, # Double (cuOpt enum 1) — required
|
|
62
|
+
"absolute_primal_tolerance": 1e-8,
|
|
63
|
+
"relative_primal_tolerance": 1e-8,
|
|
64
|
+
"absolute_dual_tolerance": 1e-8,
|
|
65
|
+
"relative_dual_tolerance": 1e-8,
|
|
66
|
+
"per_constraint_residual": 1, # max-norm → better postsolve quality
|
|
67
|
+
"time_limit": time_limit,
|
|
68
|
+
}
|
|
69
|
+
else:
|
|
70
|
+
# Microbiome-scale: PaPILO presolve required + max-norm convergence.
|
|
71
|
+
# Tight tolerances are NOT set — the accuracy floor is PaPILO's hardcoded
|
|
72
|
+
# feastol=1e-5, independent of PDLP tolerance.
|
|
73
|
+
# pdlp_precision is deliberately left unset here: cuOpt's Default (-1)
|
|
74
|
+
# resolves to Double, so this is double precision too, not mixed.
|
|
75
|
+
return {
|
|
76
|
+
"method": 1, # PDLP
|
|
77
|
+
"presolve": 1, # PaPILO — avoids PSLP false-infeasible bug
|
|
78
|
+
"per_constraint_residual": 1, # max-norm convergence
|
|
79
|
+
"time_limit": time_limit,
|
|
80
|
+
}
|
gpugem/_deps.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""External-dependency registry and guided errors.
|
|
2
|
+
|
|
3
|
+
Every external package or executable gpuGEM, its benchmarks and its examples rely on is listed in
|
|
4
|
+
:data:`REGISTRY`. :func:`require` / :func:`require_tool` turn "missing", "fails to load", "too
|
|
5
|
+
old" and "no usable GPU" into one catchable :class:`DependencyError` that says what is wrong, why
|
|
6
|
+
it is needed and how to fix it (specs/016-missing-dependency-errors/contracts/).
|
|
7
|
+
|
|
8
|
+
This module is standard-library only so it can be imported when numpy/scipy/cuOpt are absent.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import glob
|
|
14
|
+
import importlib
|
|
15
|
+
import importlib.metadata
|
|
16
|
+
import re
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
KINDS = ("not_installed", "broken", "version", "no_gpu", "license", "tool_missing", "network")
|
|
23
|
+
|
|
24
|
+
_CUOPT_DOCS = "https://docs.nvidia.com/cuopt/user-guide/latest/introduction.html"
|
|
25
|
+
|
|
26
|
+
_STATE_TEXT = {
|
|
27
|
+
"not_installed": "is not installed",
|
|
28
|
+
"broken": "is installed but fails to load",
|
|
29
|
+
"version": "is not a supported version",
|
|
30
|
+
"no_gpu": "cannot be used: a GPU or driver problem is suspected",
|
|
31
|
+
"license": "is installed but its licence is missing or invalid",
|
|
32
|
+
"tool_missing": "was not found on PATH",
|
|
33
|
+
"network": "could not be reached",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DependencyError(RuntimeError):
|
|
38
|
+
"""A required dependency is missing, broken, too old, or has no usable GPU/licence/network.
|
|
39
|
+
|
|
40
|
+
Attributes
|
|
41
|
+
----------
|
|
42
|
+
dependency : str
|
|
43
|
+
Registry name, e.g. ``"cuopt"``.
|
|
44
|
+
kind : str
|
|
45
|
+
One of ``not_installed, broken, version, no_gpu, license, tool_missing, network``.
|
|
46
|
+
detected, required : str or None
|
|
47
|
+
Installed and minimum versions (``kind == "version"``).
|
|
48
|
+
remedy : str
|
|
49
|
+
What the user should do.
|
|
50
|
+
|
|
51
|
+
The original exception, when there is one, is available as ``__cause__``.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, dependency: str, kind: str, remedy: str, *, purpose: Optional[str] = None,
|
|
55
|
+
detected: Optional[str] = None, required: Optional[str] = None,
|
|
56
|
+
detail: Optional[str] = None, problem: Optional[str] = None):
|
|
57
|
+
if kind not in KINDS:
|
|
58
|
+
raise ValueError("unknown DependencyError kind %r" % (kind,))
|
|
59
|
+
self.dependency = dependency
|
|
60
|
+
self.kind = kind
|
|
61
|
+
self.remedy = remedy
|
|
62
|
+
self.purpose = purpose
|
|
63
|
+
self.detected = detected
|
|
64
|
+
self.required = required
|
|
65
|
+
self.detail = detail
|
|
66
|
+
self.problem = problem
|
|
67
|
+
state = problem or _STATE_TEXT[kind]
|
|
68
|
+
if kind == "version" and not problem:
|
|
69
|
+
state = "is too old (installed %s, required >= %s)" % (detected, required)
|
|
70
|
+
lines = ["gpuGEM cannot run: %s %s." % (dependency, state)]
|
|
71
|
+
if purpose:
|
|
72
|
+
lines.append(" Needed for: %s" % purpose)
|
|
73
|
+
lines.append(" Fix: %s" % remedy)
|
|
74
|
+
if detail:
|
|
75
|
+
lines.append(" (details: %s)" % detail)
|
|
76
|
+
super().__init__("\n".join(lines))
|
|
77
|
+
|
|
78
|
+
def __reduce__(self):
|
|
79
|
+
return (_rebuild, (self.dependency, self.kind, self.remedy, self.purpose,
|
|
80
|
+
self.detected, self.required, self.detail, self.problem))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _rebuild(dependency, kind, remedy, purpose, detected, required, detail, problem):
|
|
84
|
+
return DependencyError(dependency, kind, remedy, purpose=purpose, detected=detected,
|
|
85
|
+
required=required, detail=detail, problem=problem)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class DependencySpec:
|
|
90
|
+
name: str # import name, or executable name for tools
|
|
91
|
+
role: str # required | optional | benchmark | tool | dev
|
|
92
|
+
purpose: str
|
|
93
|
+
install: str
|
|
94
|
+
distributions: tuple = () # pip distribution names to read the version from
|
|
95
|
+
min_version: Optional[str] = None
|
|
96
|
+
extra: Optional[str] = None
|
|
97
|
+
tool: bool = False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# Minimum versions are mirrored in pyproject.toml; tests/test_dependency_inventory.py fails if
|
|
101
|
+
# the two drift apart.
|
|
102
|
+
MIN_VERSIONS = {
|
|
103
|
+
"numpy": "1.24",
|
|
104
|
+
"scipy": "1.10",
|
|
105
|
+
"cuopt": "26.6.0",
|
|
106
|
+
"cobra": "0.29",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_SPECS = [
|
|
110
|
+
DependencySpec(
|
|
111
|
+
"numpy", "required", "array maths used by every gpuGEM function",
|
|
112
|
+
'pip install "numpy>=%s"' % MIN_VERSIONS["numpy"], ("numpy",), MIN_VERSIONS["numpy"]),
|
|
113
|
+
DependencySpec(
|
|
114
|
+
"scipy", "required", "sparse matrices and .mat file reading",
|
|
115
|
+
'pip install "scipy>=%s"' % MIN_VERSIONS["scipy"], ("scipy",), MIN_VERSIONS["scipy"]),
|
|
116
|
+
DependencySpec(
|
|
117
|
+
"cuopt", "required", "the GPU linear-programming solver that gpuGEM wraps",
|
|
118
|
+
'pip install "cuopt-cu12>=%s" --extra-index-url https://pypi.nvidia.com '
|
|
119
|
+
'(other CUDA versions: see %s)' % (MIN_VERSIONS["cuopt"], _CUOPT_DOCS),
|
|
120
|
+
("cuopt-cu12", "cuopt-cu13", "cuopt"), MIN_VERSIONS["cuopt"]),
|
|
121
|
+
DependencySpec(
|
|
122
|
+
"cobra", "required", "reading COBRApy models (gpugem.solve_cobra, loaders.from_cobra)",
|
|
123
|
+
'pip install "cobra>=%s"' % MIN_VERSIONS["cobra"],
|
|
124
|
+
("cobra",), MIN_VERSIONS["cobra"]),
|
|
125
|
+
DependencySpec(
|
|
126
|
+
"gurobipy", "benchmark", "the Gurobi baseline in the benchmarks (needs a Gurobi licence)",
|
|
127
|
+
"pip install gurobipy (a licence is required to solve large models: "
|
|
128
|
+
"https://www.gurobi.com/academia/academic-program-and-licenses/)", ("gurobipy",)),
|
|
129
|
+
DependencySpec(
|
|
130
|
+
"highspy", "benchmark", "the HiGHS CPU baseline in the benchmarks",
|
|
131
|
+
"pip install highspy", ("highspy",)),
|
|
132
|
+
DependencySpec(
|
|
133
|
+
"pandas", "benchmark", "result tables in the benchmark aggregation and figure scripts",
|
|
134
|
+
"pip install pandas", ("pandas",)),
|
|
135
|
+
DependencySpec(
|
|
136
|
+
"matplotlib", "benchmark", "drawing the benchmark figures",
|
|
137
|
+
"pip install matplotlib", ("matplotlib",)),
|
|
138
|
+
DependencySpec(
|
|
139
|
+
"pytest", "dev", "running the test suite",
|
|
140
|
+
'pip install -e ".[dev]"', ("pytest",)),
|
|
141
|
+
DependencySpec(
|
|
142
|
+
"nvidia-smi", "tool", "reading the GPU name for benchmark metadata and diagnosing "
|
|
143
|
+
"GPU problems", "install the NVIDIA driver (https://www.nvidia.com/drivers); "
|
|
144
|
+
"nvidia-smi ships with it", tool=True),
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
REGISTRY = {s.name: s for s in _SPECS}
|
|
148
|
+
|
|
149
|
+
_version_ok = set()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _spec(name):
|
|
153
|
+
try:
|
|
154
|
+
return REGISTRY[name]
|
|
155
|
+
except KeyError:
|
|
156
|
+
raise KeyError("%r is not in gpugem._deps.REGISTRY; add it there" % (name,)) from None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def parse_version(text):
|
|
160
|
+
"""Leading integer components of a version string: ``"26.06.00rc1"`` -> ``(26, 6, 0)``."""
|
|
161
|
+
parts = []
|
|
162
|
+
for piece in str(text).split("."):
|
|
163
|
+
m = re.match(r"\d+", piece)
|
|
164
|
+
if not m:
|
|
165
|
+
break
|
|
166
|
+
parts.append(int(m.group()))
|
|
167
|
+
if m.end() != len(piece):
|
|
168
|
+
break
|
|
169
|
+
return tuple(parts)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _installed_version(spec, module):
|
|
173
|
+
for dist in spec.distributions:
|
|
174
|
+
try:
|
|
175
|
+
return importlib.metadata.version(dist)
|
|
176
|
+
except importlib.metadata.PackageNotFoundError:
|
|
177
|
+
continue
|
|
178
|
+
v = getattr(module, "__version__", None)
|
|
179
|
+
return str(v) if v is not None else None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def gpu_evidence():
|
|
183
|
+
"""Return ``(problem, detail)``: whether there is evidence the NVIDIA GPU/driver is unusable.
|
|
184
|
+
|
|
185
|
+
``problem`` is True only on positive evidence (``nvidia-smi`` fails or lists no GPU, or it is
|
|
186
|
+
absent and no ``/dev/nvidia<N>`` device exists). An absent ``nvidia-smi`` alone is not proof
|
|
187
|
+
(containers can have a working GPU without it) so with a device node present this returns False.
|
|
188
|
+
"""
|
|
189
|
+
exe = shutil.which("nvidia-smi")
|
|
190
|
+
if exe is None:
|
|
191
|
+
if glob.glob("/dev/nvidia[0-9]*"):
|
|
192
|
+
return False, "nvidia-smi not found, but /dev/nvidia* exists"
|
|
193
|
+
return True, "nvidia-smi not found and no /dev/nvidia* device exists"
|
|
194
|
+
try:
|
|
195
|
+
out = subprocess.run([exe, "-L"], capture_output=True, text=True, timeout=30)
|
|
196
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
197
|
+
return True, "nvidia-smi could not be run: %s" % exc
|
|
198
|
+
if out.returncode != 0:
|
|
199
|
+
msg = (out.stderr or out.stdout).strip().splitlines()
|
|
200
|
+
return True, "nvidia-smi exited %d: %s" % (out.returncode, msg[0] if msg else "")
|
|
201
|
+
if "GPU" not in out.stdout:
|
|
202
|
+
return True, "nvidia-smi lists no GPU"
|
|
203
|
+
return False, out.stdout.strip().splitlines()[0]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _no_gpu_error(cause_text, evidence_detail):
|
|
207
|
+
spec = REGISTRY["cuopt"]
|
|
208
|
+
return DependencyError(
|
|
209
|
+
"cuopt", "no_gpu",
|
|
210
|
+
"check that an NVIDIA GPU is visible (run `nvidia-smi`), that the driver supports the CUDA "
|
|
211
|
+
"version of your cuopt build, and that you are not inside a container without GPU access",
|
|
212
|
+
purpose=spec.purpose,
|
|
213
|
+
detail="%s; %s" % (evidence_detail, cause_text))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def require(name):
|
|
217
|
+
"""Import and return the registered module ``name``, or raise :class:`DependencyError`."""
|
|
218
|
+
spec = _spec(name)
|
|
219
|
+
if spec.tool:
|
|
220
|
+
return require_tool(name)
|
|
221
|
+
try:
|
|
222
|
+
module = importlib.import_module(name)
|
|
223
|
+
except ModuleNotFoundError as exc:
|
|
224
|
+
if (exc.name or "").split(".")[0] != name:
|
|
225
|
+
# the package is there but one of ITS imports is not
|
|
226
|
+
raise _broken(spec, exc) from exc
|
|
227
|
+
raise DependencyError(name, "not_installed", spec.install, purpose=spec.purpose) from exc
|
|
228
|
+
except Exception as exc: # ImportError, OSError (shared library), RuntimeError, ...
|
|
229
|
+
raise _broken(spec, exc) from exc
|
|
230
|
+
|
|
231
|
+
if spec.min_version and name not in _version_ok:
|
|
232
|
+
detected = _installed_version(spec, module)
|
|
233
|
+
if detected is not None and parse_version(detected) < parse_version(spec.min_version):
|
|
234
|
+
raise DependencyError(
|
|
235
|
+
name, "version", spec.install.replace("pip install", "pip install --upgrade", 1),
|
|
236
|
+
purpose=spec.purpose, detected=detected, required=spec.min_version)
|
|
237
|
+
_version_ok.add(name)
|
|
238
|
+
return module
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _broken(spec, exc):
|
|
242
|
+
detail = "%s: %s" % (type(exc).__name__, exc)
|
|
243
|
+
if spec.name == "cuopt":
|
|
244
|
+
problem, evidence = gpu_evidence()
|
|
245
|
+
if problem:
|
|
246
|
+
return _no_gpu_error(detail, evidence)
|
|
247
|
+
return DependencyError(
|
|
248
|
+
spec.name, "broken",
|
|
249
|
+
"reinstall it (%s); if the problem persists the installed build does not match this "
|
|
250
|
+
"machine (Python, CUDA runtime or driver version)" % spec.install.split(" (")[0],
|
|
251
|
+
purpose=spec.purpose, detail=detail)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def require_tool(name):
|
|
255
|
+
"""Return the path of registered executable ``name``, or raise :class:`DependencyError`."""
|
|
256
|
+
spec = _spec(name)
|
|
257
|
+
path = shutil.which(name)
|
|
258
|
+
if path is None:
|
|
259
|
+
raise DependencyError(name, "tool_missing", spec.install, purpose=spec.purpose)
|
|
260
|
+
return path
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def diagnose_solver_failure(exc):
|
|
264
|
+
"""Re-raise ``exc`` as a ``no_gpu`` :class:`DependencyError` if the GPU is demonstrably unusable.
|
|
265
|
+
|
|
266
|
+
Called when cuOpt raises while building or solving a model. If there is no positive evidence
|
|
267
|
+
of a GPU/driver problem this returns, and the caller re-raises the original exception.
|
|
268
|
+
"""
|
|
269
|
+
problem, evidence = gpu_evidence()
|
|
270
|
+
if problem:
|
|
271
|
+
raise _no_gpu_error("%s: %s" % (type(exc).__name__, exc), evidence) from exc
|