gpu-stack-inspector 0.1.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.
- gpu_stack_inspector/__init__.py +8 -0
- gpu_stack_inspector/__main__.py +6 -0
- gpu_stack_inspector/cli.py +335 -0
- gpu_stack_inspector/collectors/__init__.py +1 -0
- gpu_stack_inspector/collectors/environment.py +31 -0
- gpu_stack_inspector/collectors/host.py +48 -0
- gpu_stack_inspector/collectors/nvidia.py +166 -0
- gpu_stack_inspector/collectors/python_env.py +63 -0
- gpu_stack_inspector/collectors/toolkit.py +160 -0
- gpu_stack_inspector/compatibility/__init__.py +1 -0
- gpu_stack_inspector/compatibility/data.json +24 -0
- gpu_stack_inspector/compatibility/loader.py +75 -0
- gpu_stack_inspector/compatibility/versions.py +25 -0
- gpu_stack_inspector/exit_codes.py +22 -0
- gpu_stack_inspector/models.py +349 -0
- gpu_stack_inspector/orchestrator.py +132 -0
- gpu_stack_inspector/privacy.py +237 -0
- gpu_stack_inspector/probes/__init__.py +1 -0
- gpu_stack_inspector/probes/metadata_probe.py +230 -0
- gpu_stack_inspector/probes/metadata_program.py +232 -0
- gpu_stack_inspector/probes/runtime_probe.py +153 -0
- gpu_stack_inspector/probes/runtime_program.py +101 -0
- gpu_stack_inspector/reports/__init__.py +1 -0
- gpu_stack_inspector/reports/json_report.py +43 -0
- gpu_stack_inspector/reports/markdown.py +182 -0
- gpu_stack_inspector/reports/terminal.py +298 -0
- gpu_stack_inspector/rules/__init__.py +1 -0
- gpu_stack_inspector/rules/catalog.py +21 -0
- gpu_stack_inspector/rules/comparison.py +73 -0
- gpu_stack_inspector/rules/compatibility.py +256 -0
- gpu_stack_inspector/rules/differences.py +64 -0
- gpu_stack_inspector/rules/engine.py +101 -0
- gpu_stack_inspector/rules/host.py +259 -0
- gpu_stack_inspector/rules/interpreters.py +422 -0
- gpu_stack_inspector/rules/torch_build.py +227 -0
- gpu_stack_inspector/runner.py +273 -0
- gpu_stack_inspector-0.1.0.dist-info/METADATA +134 -0
- gpu_stack_inspector-0.1.0.dist-info/RECORD +41 -0
- gpu_stack_inspector-0.1.0.dist-info/WHEEL +4 -0
- gpu_stack_inspector-0.1.0.dist-info/entry_points.txt +3 -0
- gpu_stack_inspector-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Bounded, shell-free execution of diagnostic commands.
|
|
2
|
+
|
|
3
|
+
Every external command the tool runs goes through a :class:`CommandRunner`, so that
|
|
4
|
+
collectors can be tested with a fake one and the real one can enforce the same limits
|
|
5
|
+
everywhere: an argument list (never a shell), a timeout, an output cap and no stdin.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import contextlib
|
|
11
|
+
import os
|
|
12
|
+
import signal
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
from collections.abc import Mapping, Sequence
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from enum import StrEnum
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import IO, Protocol
|
|
22
|
+
|
|
23
|
+
from gpu_stack_inspector.models import FailureKind, ProbeFailure
|
|
24
|
+
|
|
25
|
+
DEFAULT_MAX_OUTPUT = 256 * 1024
|
|
26
|
+
"""Bytes kept per stream; diagnostic output larger than this is truncated."""
|
|
27
|
+
|
|
28
|
+
_REAP_TIMEOUT = 2.0
|
|
29
|
+
"""Seconds to wait for a finished command's pipes to close before giving up on them."""
|
|
30
|
+
|
|
31
|
+
_CHUNK = 64 * 1024
|
|
32
|
+
_DETAIL_LIMIT = 200
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Outcome(StrEnum):
|
|
36
|
+
"""How a command ended, as far as the runner can tell."""
|
|
37
|
+
|
|
38
|
+
OK = "ok"
|
|
39
|
+
NONZERO = "nonzero"
|
|
40
|
+
TIMEOUT = "timeout"
|
|
41
|
+
NOT_FOUND = "not_found"
|
|
42
|
+
PERMISSION = "permission"
|
|
43
|
+
OS_ERROR = "os_error"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class CommandResult:
|
|
48
|
+
"""What a command printed and how it ended.
|
|
49
|
+
|
|
50
|
+
``returncode`` is ``None`` when the command never started or was killed after a
|
|
51
|
+
timeout. ``error`` holds the operating system's message when it never started.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
argv: tuple[str, ...]
|
|
55
|
+
outcome: Outcome
|
|
56
|
+
returncode: int | None = None
|
|
57
|
+
stdout: str = ""
|
|
58
|
+
stderr: str = ""
|
|
59
|
+
stdout_truncated: bool = False
|
|
60
|
+
stderr_truncated: bool = False
|
|
61
|
+
duration: float = 0.0
|
|
62
|
+
error: str = ""
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def ok(self) -> bool:
|
|
66
|
+
return self.outcome is Outcome.OK
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class CommandRunner(Protocol):
|
|
70
|
+
"""Runs one command and reports the result; never raises for command failures."""
|
|
71
|
+
|
|
72
|
+
def run(
|
|
73
|
+
self,
|
|
74
|
+
argv: Sequence[str],
|
|
75
|
+
*,
|
|
76
|
+
timeout: float,
|
|
77
|
+
env_overrides: Mapping[str, str] | None = None,
|
|
78
|
+
cwd: Path | None = None,
|
|
79
|
+
) -> CommandResult: ...
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
_FAILURE_KINDS = {
|
|
83
|
+
Outcome.NOT_FOUND: FailureKind.NOT_FOUND,
|
|
84
|
+
Outcome.PERMISSION: FailureKind.PERMISSION_DENIED,
|
|
85
|
+
Outcome.TIMEOUT: FailureKind.TIMEOUT,
|
|
86
|
+
Outcome.NONZERO: FailureKind.NONZERO_EXIT,
|
|
87
|
+
Outcome.OS_ERROR: FailureKind.OS_ERROR,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def probe_failure(probe: str, result: CommandResult) -> ProbeFailure | None:
|
|
92
|
+
"""Describe why ``result`` gave no usable output, or ``None`` if it succeeded.
|
|
93
|
+
|
|
94
|
+
The detail is short and meant for humans; it is redacted with the rest of the
|
|
95
|
+
snapshot before any output is written.
|
|
96
|
+
"""
|
|
97
|
+
if result.ok:
|
|
98
|
+
return None
|
|
99
|
+
program = result.argv[0]
|
|
100
|
+
if result.outcome is Outcome.NOT_FOUND:
|
|
101
|
+
detail = f"{program} was not found"
|
|
102
|
+
elif result.outcome is Outcome.PERMISSION:
|
|
103
|
+
detail = f"permission denied when starting {program}"
|
|
104
|
+
elif result.outcome is Outcome.TIMEOUT:
|
|
105
|
+
detail = f"{program} did not finish within {result.duration:.1f} s and was stopped"
|
|
106
|
+
elif result.outcome is Outcome.NONZERO:
|
|
107
|
+
detail = f"{program} exited with code {result.returncode}"
|
|
108
|
+
# Some tools (nvidia-smi among them) print their errors on stdout.
|
|
109
|
+
first_line = _first_line(result.stderr) or _first_line(result.stdout)
|
|
110
|
+
if first_line:
|
|
111
|
+
detail += f": {first_line}"
|
|
112
|
+
else:
|
|
113
|
+
detail = f"{program} could not be started: {result.error}"
|
|
114
|
+
if len(detail) > _DETAIL_LIMIT:
|
|
115
|
+
detail = detail[: _DETAIL_LIMIT - 1] + "…"
|
|
116
|
+
return ProbeFailure(probe=probe, kind=_FAILURE_KINDS[result.outcome], detail=detail)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _first_line(text: str) -> str:
|
|
120
|
+
return next((line.strip() for line in text.splitlines() if line.strip()), "")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class SubprocessRunner:
|
|
124
|
+
"""The real runner: ``subprocess`` with an argument list, never a shell.
|
|
125
|
+
|
|
126
|
+
Each command runs in its own session (on POSIX), so a timeout or Ctrl+C kills the
|
|
127
|
+
command together with any children it started. Windows only kills the command itself.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def __init__(self, max_output: int = DEFAULT_MAX_OUTPUT) -> None:
|
|
131
|
+
if max_output <= 0:
|
|
132
|
+
raise ValueError("max_output must be positive")
|
|
133
|
+
self.max_output = max_output
|
|
134
|
+
|
|
135
|
+
def run(
|
|
136
|
+
self,
|
|
137
|
+
argv: Sequence[str],
|
|
138
|
+
*,
|
|
139
|
+
timeout: float,
|
|
140
|
+
env_overrides: Mapping[str, str] | None = None,
|
|
141
|
+
cwd: Path | None = None,
|
|
142
|
+
) -> CommandResult:
|
|
143
|
+
args = _checked_argv(argv)
|
|
144
|
+
if timeout <= 0:
|
|
145
|
+
raise ValueError("timeout must be positive")
|
|
146
|
+
env = {**os.environ, **env_overrides} if env_overrides else None
|
|
147
|
+
started = time.monotonic()
|
|
148
|
+
try:
|
|
149
|
+
proc = subprocess.Popen( # noqa: S603 - argument list without a shell
|
|
150
|
+
args,
|
|
151
|
+
stdin=subprocess.DEVNULL,
|
|
152
|
+
stdout=subprocess.PIPE,
|
|
153
|
+
stderr=subprocess.PIPE,
|
|
154
|
+
env=env,
|
|
155
|
+
cwd=cwd,
|
|
156
|
+
start_new_session=sys.platform != "win32",
|
|
157
|
+
)
|
|
158
|
+
except FileNotFoundError as exc:
|
|
159
|
+
return _not_started(args, Outcome.NOT_FOUND, exc, started)
|
|
160
|
+
except PermissionError as exc:
|
|
161
|
+
return _not_started(args, Outcome.PERMISSION, exc, started)
|
|
162
|
+
except OSError as exc:
|
|
163
|
+
return _not_started(args, Outcome.OS_ERROR, exc, started)
|
|
164
|
+
|
|
165
|
+
if proc.stdout is None or proc.stderr is None: # cannot happen with PIPE
|
|
166
|
+
raise RuntimeError("subprocess pipes were not created")
|
|
167
|
+
readers = (
|
|
168
|
+
_CappedReader(proc.stdout, self.max_output),
|
|
169
|
+
_CappedReader(proc.stderr, self.max_output),
|
|
170
|
+
)
|
|
171
|
+
for reader in readers:
|
|
172
|
+
reader.start()
|
|
173
|
+
timed_out = False
|
|
174
|
+
try:
|
|
175
|
+
try:
|
|
176
|
+
proc.wait(timeout=timeout)
|
|
177
|
+
except subprocess.TimeoutExpired:
|
|
178
|
+
timed_out = True
|
|
179
|
+
_kill(proc)
|
|
180
|
+
proc.wait()
|
|
181
|
+
# A child that escaped the process group may hold a pipe open; don't wait
|
|
182
|
+
# for it forever, keep what was read so far.
|
|
183
|
+
deadline = time.monotonic() + _REAP_TIMEOUT
|
|
184
|
+
for reader in readers:
|
|
185
|
+
reader.join(max(0.0, deadline - time.monotonic()))
|
|
186
|
+
except BaseException:
|
|
187
|
+
# Ctrl+C or an internal error: never leave the command running behind us,
|
|
188
|
+
# and reap it so it does not linger as a zombie.
|
|
189
|
+
_kill(proc)
|
|
190
|
+
with contextlib.suppress(subprocess.TimeoutExpired):
|
|
191
|
+
proc.wait(timeout=_REAP_TIMEOUT)
|
|
192
|
+
raise
|
|
193
|
+
|
|
194
|
+
out, err = readers
|
|
195
|
+
return CommandResult(
|
|
196
|
+
argv=args,
|
|
197
|
+
outcome=Outcome.TIMEOUT
|
|
198
|
+
if timed_out
|
|
199
|
+
else (Outcome.OK if proc.returncode == 0 else Outcome.NONZERO),
|
|
200
|
+
returncode=None if timed_out else proc.returncode,
|
|
201
|
+
stdout=_decode(out.data),
|
|
202
|
+
stderr=_decode(err.data),
|
|
203
|
+
stdout_truncated=out.truncated,
|
|
204
|
+
stderr_truncated=err.truncated,
|
|
205
|
+
duration=time.monotonic() - started,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class _CappedReader(threading.Thread):
|
|
210
|
+
"""Drains one pipe, keeping at most ``limit`` bytes.
|
|
211
|
+
|
|
212
|
+
It keeps reading (and discarding) past the limit so the command never blocks on a
|
|
213
|
+
full pipe.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
def __init__(self, stream: IO[bytes], limit: int) -> None:
|
|
217
|
+
super().__init__(daemon=True)
|
|
218
|
+
self._stream = stream
|
|
219
|
+
self._limit = limit
|
|
220
|
+
self._kept = bytearray()
|
|
221
|
+
self.truncated = False
|
|
222
|
+
|
|
223
|
+
def run(self) -> None:
|
|
224
|
+
# os.read returns as soon as any bytes are available; a buffered read() would
|
|
225
|
+
# wait for a full chunk and lose the output of a command whose child keeps the
|
|
226
|
+
# pipe open.
|
|
227
|
+
fd = self._stream.fileno()
|
|
228
|
+
with contextlib.suppress(OSError, ValueError), self._stream:
|
|
229
|
+
while chunk := os.read(fd, _CHUNK):
|
|
230
|
+
room = self._limit - len(self._kept)
|
|
231
|
+
if room > 0:
|
|
232
|
+
self._kept += chunk[:room]
|
|
233
|
+
if len(chunk) > room:
|
|
234
|
+
self.truncated = True
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def data(self) -> bytes:
|
|
238
|
+
return bytes(self._kept)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _checked_argv(argv: Sequence[str]) -> tuple[str, ...]:
|
|
242
|
+
if isinstance(argv, str):
|
|
243
|
+
raise TypeError("argv must be a sequence of str, not a single command string")
|
|
244
|
+
args = tuple(argv)
|
|
245
|
+
if not args:
|
|
246
|
+
raise ValueError("argv is empty")
|
|
247
|
+
for arg in args:
|
|
248
|
+
if not isinstance(arg, str):
|
|
249
|
+
raise TypeError(f"argv items must be str, got {type(arg).__name__}")
|
|
250
|
+
return args
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _not_started(
|
|
254
|
+
args: tuple[str, ...], outcome: Outcome, exc: OSError, started: float
|
|
255
|
+
) -> CommandResult:
|
|
256
|
+
return CommandResult(
|
|
257
|
+
argv=args,
|
|
258
|
+
outcome=outcome,
|
|
259
|
+
error=exc.strerror or str(exc),
|
|
260
|
+
duration=time.monotonic() - started,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _kill(proc: subprocess.Popen[bytes]) -> None:
|
|
265
|
+
if sys.platform == "win32":
|
|
266
|
+
proc.kill()
|
|
267
|
+
return
|
|
268
|
+
with contextlib.suppress(ProcessLookupError, PermissionError): # already gone
|
|
269
|
+
os.killpg(proc.pid, signal.SIGKILL)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _decode(data: bytes) -> str:
|
|
273
|
+
return data.decode("utf-8", errors="replace")
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gpu-stack-inspector
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Explain why your AI code uses the GPU in one Python environment but not another.
|
|
5
|
+
Keywords: cuda,pytorch,gpu,nvidia,conda,venv,diagnostics,troubleshooting
|
|
6
|
+
Author: BhuwanJoshi-01
|
|
7
|
+
Author-email: BhuwanJoshi-01 <bhuwanj779@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Topic :: System :: Hardware
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Dist: rich>=15.0.0
|
|
26
|
+
Requires-Dist: typer>=0.27.3
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
|
+
Project-URL: Homepage, https://bhuwanjoshi-01.github.io/gpu-stack-inspector/
|
|
29
|
+
Project-URL: Documentation, https://github.com/BhuwanJoshi-01/gpu-stack-inspector/blob/main/docs/troubleshooting.md
|
|
30
|
+
Project-URL: Source, https://github.com/BhuwanJoshi-01/gpu-stack-inspector
|
|
31
|
+
Project-URL: Issues, https://github.com/BhuwanJoshi-01/gpu-stack-inspector/issues
|
|
32
|
+
Project-URL: Changelog, https://github.com/BhuwanJoshi-01/gpu-stack-inspector/blob/main/CHANGELOG.md
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# GPU Stack Inspector
|
|
36
|
+
|
|
37
|
+
**Explain why your AI code uses the GPU in one Python environment but not another.**
|
|
38
|
+
|
|
39
|
+
[](https://github.com/BhuwanJoshi-01/gpu-stack-inspector/actions/workflows/ci.yml)
|
|
40
|
+
[](https://github.com/BhuwanJoshi-01/gpu-stack-inspector/blob/main/LICENSE)
|
|
41
|
+
[](https://bhuwanjoshi-01.github.io/gpu-stack-inspector/)
|
|
42
|
+
|
|
43
|
+
`nvidia-smi` shows your GPU, yet `torch.cuda.is_available()` is `True` in one environment and `False` in another.
|
|
44
|
+
GPU Stack Inspector inspects several Python interpreters from one command, shows what differs between them, and
|
|
45
|
+
explains which difference matters, with the evidence:
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
> **Status:** 0.1.0 is the first release, validated on one laptop with five real environments. Reports from
|
|
50
|
+
> other machines are very welcome.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
The tool needs Python 3.11 or newer; the interpreters it inspects can be Python 3.8 or newer.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
uv tool install git+https://github.com/BhuwanJoshi-01/gpu-stack-inspector # until the PyPI release
|
|
58
|
+
pipx install git+https://github.com/BhuwanJoshi-01/gpu-stack-inspector # or with pipx
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Use
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
gpu-stack-inspector compare --python ~/envs/works/bin/python --python ~/envs/broken/bin/python
|
|
65
|
+
gpu-stack-inspector inspect --python ~/envs/broken/bin/python # one environment in detail
|
|
66
|
+
gpu-stack-inspector probe --runtime --python ~/envs/broken/bin/python # opt-in: really runs a tiny CUDA operation
|
|
67
|
+
gpu-stack-inspector report --python ~/envs/broken/bin/python --output gpu-report.md # redacted, for an issue
|
|
68
|
+
gpu-stack-inspector rules list # every check; `rules explain CODE` for one
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Without `--python` it inspects the `python` your shell runs. See the [command reference](https://github.com/BhuwanJoshi-01/gpu-stack-inspector/blob/main/docs/cli.md).
|
|
72
|
+
|
|
73
|
+
## What it checks
|
|
74
|
+
|
|
75
|
+
- **The machine**: driver tools, whether the driver answers and which CUDA versions it supports, the GPUs and their
|
|
76
|
+
compute capability, `CUDA_VISIBLE_DEVICES`.
|
|
77
|
+
- **Each interpreter**: whether it starts, whether it is the one your shell runs, whether torch is installed where it
|
|
78
|
+
is needed, and which build it is (CUDA with its version, CPU-only, ROCm), decided from the version tag,
|
|
79
|
+
`torch/version.py`, the CUDA libraries in `torch/lib`, the `nvidia-*` requirements and conda's records, without
|
|
80
|
+
importing torch.
|
|
81
|
+
- **Compatibility**: the driver against the CUDA version torch was built for and against nvcc, using dated,
|
|
82
|
+
source-tagged NVIDIA compatibility data.
|
|
83
|
+
- **The comparison**: every field that differs, and whether the environments have different PyTorch builds.
|
|
84
|
+
- **At runtime** (`probe --runtime` only): importing torch and running an operation on the GPU, step by step.
|
|
85
|
+
|
|
86
|
+
Every finding has a stable code, the evidence it rests on, what to do next and the command to check it afterwards.
|
|
87
|
+
[Troubleshooting](https://github.com/BhuwanJoshi-01/gpu-stack-inspector/blob/main/docs/troubleshooting.md) explains the common ones.
|
|
88
|
+
|
|
89
|
+
## What it promises
|
|
90
|
+
|
|
91
|
+
- **Read-only.** It never installs, upgrades or changes anything, and never needs `sudo`. It writes nothing except a
|
|
92
|
+
report file you name.
|
|
93
|
+
- **Your environments stay untouched.** A fixed probe program reads each interpreter's metadata. torch is only
|
|
94
|
+
imported with `probe --runtime`, because creating a CUDA context uses GPU memory and can hang on a broken system.
|
|
95
|
+
- **Unknown is not a pass.** A check that could not run is unknown or not tested, never green.
|
|
96
|
+
- **Shareable output.** Home paths, user and host names, secrets and tokens are redacted before anything is printed
|
|
97
|
+
or written, JSON included, and each report says what was redacted. No network access, no telemetry.
|
|
98
|
+
|
|
99
|
+
Details in [privacy](https://github.com/BhuwanJoshi-01/gpu-stack-inspector/blob/main/docs/privacy.md).
|
|
100
|
+
|
|
101
|
+
## Tested on
|
|
102
|
+
|
|
103
|
+
An Ubuntu 26.04 laptop with an RTX 3050 (driver 595.99.02) and five real environments: conda and uv, Python 3.8 to
|
|
104
|
+
3.13, CUDA 11.3 to 13.0 builds and a CPU-only build. CI runs the tests on Linux and Windows and the probe in Python
|
|
105
|
+
3.8 to 3.10 and PyPy. Windows and WSL with a GPU, containers and macOS are **not validated**; see the
|
|
106
|
+
[support matrix](https://github.com/BhuwanJoshi-01/gpu-stack-inspector/blob/main/docs/support-matrix.md).
|
|
107
|
+
|
|
108
|
+
## Alternatives
|
|
109
|
+
|
|
110
|
+
If you only need to check a single environment, these may already be enough:
|
|
111
|
+
|
|
112
|
+
- [cuda-doctor](https://pypi.org/project/cuda-doctor/) checks the GPU, driver, toolkit, compiler and PyTorch of the
|
|
113
|
+
environment it is installed in.
|
|
114
|
+
- [env-doctor](https://github.com/mitulgarg/env-doctor) checks the driver, CUDA, cuDNN and frameworks, and compares
|
|
115
|
+
machines in a fleet dashboard.
|
|
116
|
+
- [cudadoc](https://www.cudadoc.com/docs/quickstart) includes a GPU validation suite.
|
|
117
|
+
|
|
118
|
+
GPU Stack Inspector focuses on comparing **environments on the same machine**.
|
|
119
|
+
|
|
120
|
+
## Documentation
|
|
121
|
+
|
|
122
|
+
[Command reference](https://github.com/BhuwanJoshi-01/gpu-stack-inspector/blob/main/docs/cli.md) · [Troubleshooting](https://github.com/BhuwanJoshi-01/gpu-stack-inspector/blob/main/docs/troubleshooting.md) · [Privacy](https://github.com/BhuwanJoshi-01/gpu-stack-inspector/blob/main/docs/privacy.md) ·
|
|
123
|
+
[Support matrix](https://github.com/BhuwanJoshi-01/gpu-stack-inspector/blob/main/docs/support-matrix.md) · [JSON output](https://github.com/BhuwanJoshi-01/gpu-stack-inspector/blob/main/docs/json-output.md) · [Architecture](https://github.com/BhuwanJoshi-01/gpu-stack-inspector/blob/main/docs/architecture.md) ·
|
|
124
|
+
[Sample reports](https://github.com/BhuwanJoshi-01/gpu-stack-inspector/tree/main/docs/sample-reports) · [Roadmap](https://github.com/BhuwanJoshi-01/gpu-stack-inspector/blob/main/docs/roadmap.md)
|
|
125
|
+
|
|
126
|
+
## Contributing
|
|
127
|
+
|
|
128
|
+
Test reports, fixtures from unusual systems, bug reports and pull requests are welcome; see
|
|
129
|
+
[CONTRIBUTING.md](https://github.com/BhuwanJoshi-01/gpu-stack-inspector/blob/main/CONTRIBUTING.md). Security problems, including a report that leaks private data, go through
|
|
130
|
+
[SECURITY.md](https://github.com/BhuwanJoshi-01/gpu-stack-inspector/blob/main/SECURITY.md).
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
[MIT](https://github.com/BhuwanJoshi-01/gpu-stack-inspector/blob/main/LICENSE)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
gpu_stack_inspector/__init__.py,sha256=dJXcPa3gAk1Lr3JjLxEZHQ9pS80p6Y7ylewqH0okWMU,312
|
|
2
|
+
gpu_stack_inspector/__main__.py,sha256=tmc5ufsSfSmgDN2QuJOsq8RlW70WjKaz1gFINtSd4Ug,128
|
|
3
|
+
gpu_stack_inspector/cli.py,sha256=_D5XO1-7v8q7PnKVrSWgfeQ31dNeo8sCbpeXFF3ISug,11627
|
|
4
|
+
gpu_stack_inspector/collectors/__init__.py,sha256=UvHX8m7P7cdXpAqQoW8ZoY5h3A2G3u3ilKxbFtOdHe8,82
|
|
5
|
+
gpu_stack_inspector/collectors/environment.py,sha256=yKeNL2eGTA9W4qLPG6SU8lmBfUQM4wdO4yGxGdm9fSw,964
|
|
6
|
+
gpu_stack_inspector/collectors/host.py,sha256=Duub9aC848bijkFUWgdjMkhVV_N9ofBot-dBtOwIO8c,1529
|
|
7
|
+
gpu_stack_inspector/collectors/nvidia.py,sha256=dRwbY3-4oKxtsjc76W8HVkJFFMGw83OJjvy525YSXIo,6474
|
|
8
|
+
gpu_stack_inspector/collectors/python_env.py,sha256=f2TI9feAhLv3qOMS1gkeasSHifxZ-uvUIyjg7mumLD8,2445
|
|
9
|
+
gpu_stack_inspector/collectors/toolkit.py,sha256=UblkT0BMMbHQHzOcENeldrHHVBaBxBqSW5SqQEiSekM,6033
|
|
10
|
+
gpu_stack_inspector/compatibility/__init__.py,sha256=TJYv_PUowuJROTrGnI06F40l0UGntQtDfxqf6D4uxXk,66
|
|
11
|
+
gpu_stack_inspector/compatibility/data.json,sha256=zhPNBcwwBECbsYFO0P_JQLYAuJgeiDLwcG02AEodLpA,1061
|
|
12
|
+
gpu_stack_inspector/compatibility/loader.py,sha256=w1rmv6P5o0DYhzPGPqpUWHyHtWEPMfzky6UzuSnal_Q,2490
|
|
13
|
+
gpu_stack_inspector/compatibility/versions.py,sha256=X05dB9ktYeIAHCW7ocnb9R9OjTzolUFQYHpilxG9qzY,834
|
|
14
|
+
gpu_stack_inspector/exit_codes.py,sha256=Ho_9iLzwyhdg2X2gysPa07QhlZaSYET7HBJJT43W_F0,624
|
|
15
|
+
gpu_stack_inspector/models.py,sha256=AikJcsR9urr7pVkCcnTLUQlytyzeS2egUzghwMsHOcA,11139
|
|
16
|
+
gpu_stack_inspector/orchestrator.py,sha256=BwA4GmyB8FPXBjD4GdnRuYl47CR9edSvOXc1pX9c0Bc,4906
|
|
17
|
+
gpu_stack_inspector/privacy.py,sha256=HX-PxSRlCrpbOKQZhfriaI9ew8YLhUmsr_-SRAW0JGs,9617
|
|
18
|
+
gpu_stack_inspector/probes/__init__.py,sha256=frWsRtpoGKb9mbZ2PaMHWWQd705IsB_55isbjJPrI6E,82
|
|
19
|
+
gpu_stack_inspector/probes/metadata_probe.py,sha256=zAyonIfxsMLuGL2yjR-HBu2RXAhTNwHX5iyRIMIfIAw,9034
|
|
20
|
+
gpu_stack_inspector/probes/metadata_program.py,sha256=XWlbw5Rf2aZig3OuBYMcCrlM5T-LhBGt1f2qbFlo3cs,8626
|
|
21
|
+
gpu_stack_inspector/probes/runtime_probe.py,sha256=0cWM1tTMdEqWPyRNPdr8At-Byl1COFCZTE4kU08OPVc,5793
|
|
22
|
+
gpu_stack_inspector/probes/runtime_program.py,sha256=xofX8l4rrcJeB5Vsv6H97lUch9Jh5izo-KkZn3ke8Oo,3642
|
|
23
|
+
gpu_stack_inspector/reports/__init__.py,sha256=6iGY7rFTa5ktGL0b0TGufqpyHRiWWr3BpfEg1SKJ1bY,89
|
|
24
|
+
gpu_stack_inspector/reports/json_report.py,sha256=duYjE2qViu7xNX1HNjnkMjkv-DED_mAHbvl-Br4-MTw,1730
|
|
25
|
+
gpu_stack_inspector/reports/markdown.py,sha256=74rcrvfLPTpJtoK-romtoYehlJy-ev9S2MmejUXCbWQ,7472
|
|
26
|
+
gpu_stack_inspector/reports/terminal.py,sha256=XClXXDOfYTZ8fPS5FLUqUVbGa4AZHCI91DQSjuL6sb4,11330
|
|
27
|
+
gpu_stack_inspector/rules/__init__.py,sha256=fD9lbPmNoP91YhOhDY9xYicy1lmaX-nPtT05J-fpSSc,95
|
|
28
|
+
gpu_stack_inspector/rules/catalog.py,sha256=5c1co1lmEKrZC_uycEtVuzBYTMNgnMapg0RxPr_HIBY,705
|
|
29
|
+
gpu_stack_inspector/rules/comparison.py,sha256=5nsECvfWBElFpjmblobvByzQUmyjPtwZtqKzM-pXbr0,2892
|
|
30
|
+
gpu_stack_inspector/rules/compatibility.py,sha256=jgApZOh4rRtvq28bJ5Jveex1YGLldGM0OIrv3F-Cjug,10268
|
|
31
|
+
gpu_stack_inspector/rules/differences.py,sha256=z8qiHPXZICizaiRU39SGOafuReM60mQKifGQEWwq4Do,2594
|
|
32
|
+
gpu_stack_inspector/rules/engine.py,sha256=DOH8UVkatvW-YRwmdnQLuS07DBi5U8KH0HwzMYG_2q4,3582
|
|
33
|
+
gpu_stack_inspector/rules/host.py,sha256=tO8kccA7ufnKayACP2zDDLgrYZu-_Aw9b-0G6kbLZx0,10695
|
|
34
|
+
gpu_stack_inspector/rules/interpreters.py,sha256=kL1TloMD9AhdVkJK_1ShY-bPtj27mq95Zpsd6oh2Nkk,19567
|
|
35
|
+
gpu_stack_inspector/rules/torch_build.py,sha256=hjmZMc2D4LODDM63CxGuJNfPhY2z0wzI8aQiqTjxyIg,9348
|
|
36
|
+
gpu_stack_inspector/runner.py,sha256=ZT95pEgGacd3kWyjVXEvXU0WDP_eQl41XID7HrhyjK8,9204
|
|
37
|
+
gpu_stack_inspector-0.1.0.dist-info/licenses/LICENSE,sha256=qpnoghKC1sfa6tVZTac6xDaNwFgOvSqQIYH0s3szqFc,1071
|
|
38
|
+
gpu_stack_inspector-0.1.0.dist-info/WHEEL,sha256=MWx7psoF7KNST9G72pGtiBYuaXRXQXBk9pw5R6FzYko,80
|
|
39
|
+
gpu_stack_inspector-0.1.0.dist-info/entry_points.txt,sha256=G__M6nBl-XGQjLr_CkHrjTyOTbrTibNu4cpf122PjJQ,70
|
|
40
|
+
gpu_stack_inspector-0.1.0.dist-info/METADATA,sha256=bXLePLnBTa5nIt1Z7suHPNBMqUM76OxqN9gFJCPTtAI,8389
|
|
41
|
+
gpu_stack_inspector-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BhuwanJoshi-01
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|