inferencekit-cli 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.
- inferencekit/__init__.py +9 -0
- inferencekit/__main__.py +5 -0
- inferencekit/analysis/__init__.py +1 -0
- inferencekit/analysis/ncu_csv.py +284 -0
- inferencekit/analysis/nsys_sqlite.py +518 -0
- inferencekit/canonical.py +104 -0
- inferencekit/checkpoint.py +396 -0
- inferencekit/cli.py +274 -0
- inferencekit/cuda/bw_peak.cu +252 -0
- inferencekit/cuda/torch_bw.py +60 -0
- inferencekit/engines/__init__.py +7 -0
- inferencekit/engines/adapters.py +190 -0
- inferencekit/engines/clip/__init__.py +13 -0
- inferencekit/engines/clip/comfyui.py +749 -0
- inferencekit/engines/clip/diffusers_ltx.py +601 -0
- inferencekit/engines/clip/noise.py +46 -0
- inferencekit/engines/clip/official_ltx.py +316 -0
- inferencekit/engines/clip/server.py +542 -0
- inferencekit/engines/clip/stepdump.py +182 -0
- inferencekit/engines/gates.py +94 -0
- inferencekit/engines/sglang_worker.py +86 -0
- inferencekit/engines/vllm_worker.py +129 -0
- inferencekit/evidence.py +427 -0
- inferencekit/gpuinfo.py +93 -0
- inferencekit/hardware.py +135 -0
- inferencekit/mcp_server.py +178 -0
- inferencekit/ops/__init__.py +30 -0
- inferencekit/ops/bench.py +184 -0
- inferencekit/ops/bench_audio.py +384 -0
- inferencekit/ops/bench_clip.py +819 -0
- inferencekit/ops/bench_image.py +393 -0
- inferencekit/ops/bench_rounds.py +297 -0
- inferencekit/ops/bench_serve.py +866 -0
- inferencekit/ops/ceiling.py +135 -0
- inferencekit/ops/compare.py +378 -0
- inferencekit/ops/correctness.py +153 -0
- inferencekit/ops/doctor.py +486 -0
- inferencekit/ops/gates.py +1772 -0
- inferencekit/ops/gates_clip.py +930 -0
- inferencekit/ops/intake.py +1321 -0
- inferencekit/ops/ledger.py +749 -0
- inferencekit/ops/manifest.py +309 -0
- inferencekit/ops/ncu.py +183 -0
- inferencekit/ops/nsys.py +224 -0
- inferencekit/ops/prompts.py +316 -0
- inferencekit/ops/quality.py +2161 -0
- inferencekit/ops/quality_clip.py +690 -0
- inferencekit/ops/quality_clip_steps.py +341 -0
- inferencekit/ops/report.py +317 -0
- inferencekit/ops/review.py +119 -0
- inferencekit/ops/rocprof.py +89 -0
- inferencekit/ops/roofline.py +392 -0
- inferencekit/ops/store.py +61 -0
- inferencekit/proc.py +172 -0
- inferencekit/prompts/ATTRIBUTION.md +56 -0
- inferencekit/prompts/__init__.py +167 -0
- inferencekit/prompts/build.py +518 -0
- inferencekit/prompts/dev-load.jsonl.gz +0 -0
- inferencekit/prompts/dev-load.manifest.json +212 -0
- inferencekit/prompts/dev.jsonl.gz +0 -0
- inferencekit/prompts/dev.manifest.json +214 -0
- inferencekit/prompts/exclusions.json +54 -0
- inferencekit/quality/__init__.py +23 -0
- inferencekit/quality/client.py +66 -0
- inferencekit/quality/data/gsm8k_LICENSE.txt +21 -0
- inferencekit/quality/fake.py +329 -0
- inferencekit/quality/gsm8k.py +224 -0
- inferencekit/quality/logprobs.py +106 -0
- inferencekit/quality/metrics.py +235 -0
- inferencekit/quality/niah.py +63 -0
- inferencekit/quality/parquet.py +296 -0
- inferencekit/quality/sampling.py +262 -0
- inferencekit/quality/step_replay.py +217 -0
- inferencekit/quality/video_metrics.py +304 -0
- inferencekit/registry.py +206 -0
- inferencekit/review/__init__.py +6 -0
- inferencekit/review/diff.py +163 -0
- inferencekit/review/media.py +181 -0
- inferencekit/review/page.py +920 -0
- inferencekit/review/rundir.py +844 -0
- inferencekit/roofline_headers.py +504 -0
- inferencekit/schemas/evidence.v1.json +130 -0
- inferencekit/schemas/evidence.v2.json +499 -0
- inferencekit/schemas/intake.v1.json +276 -0
- inferencekit/schemas/run-manifest.v1.json +118 -0
- inferencekit/serve/__init__.py +8 -0
- inferencekit/serve/audio_client.py +330 -0
- inferencekit/serve/client.py +446 -0
- inferencekit/serve/clip_client.py +351 -0
- inferencekit/serve/clip_rounds.py +429 -0
- inferencekit/serve/image_client.py +373 -0
- inferencekit/serve/loadgen.py +136 -0
- inferencekit/serve/metrics.py +203 -0
- inferencekit/serve/profiles.py +105 -0
- inferencekit/serve/prompts_io.py +125 -0
- inferencekit/serve/rounds.py +1111 -0
- inferencekit/serve/servers.py +897 -0
- inferencekit/stats.py +249 -0
- inferencekit_cli-0.1.0.dist-info/METADATA +233 -0
- inferencekit_cli-0.1.0.dist-info/RECORD +104 -0
- inferencekit_cli-0.1.0.dist-info/WHEEL +4 -0
- inferencekit_cli-0.1.0.dist-info/entry_points.txt +2 -0
- inferencekit_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
- inferencekit_cli-0.1.0.dist-info/licenses/NOTICE +9 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""correctness: does an optimisation change what the model generates?
|
|
2
|
+
|
|
3
|
+
With greedy decoding (temperature 0) the same model and prompt should give the
|
|
4
|
+
same tokens. Faster kernels change floating-point summation order, so a near-tie
|
|
5
|
+
between the top two logits can flip, and from that position on the two sequences
|
|
6
|
+
follow different contexts. So the useful numbers are:
|
|
7
|
+
* exact match rate: sequences identical end to end;
|
|
8
|
+
* first-divergence index per sequence (later = smaller numerical difference);
|
|
9
|
+
* prefix agreement: mean of (first divergence / length);
|
|
10
|
+
* position-wise agreement (counts matches even after divergence; shown, not judged).
|
|
11
|
+
Optional: per-token logprobs within the agreeing prefix, checked against a tolerance.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import statistics
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from inferencekit import evidence as ev
|
|
21
|
+
from inferencekit.registry import Arg, OpError, operation
|
|
22
|
+
|
|
23
|
+
HIST_BINS = [(0, 0), (1, 7), (8, 31), (32, 127), (128, 511), (512, None)]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_sequences(store: ev.Store, ref: str) -> tuple[list[dict], dict | None]:
|
|
27
|
+
"""[{tokens, logprobs?}] from a JSONL file or an evidence record with data.tokens."""
|
|
28
|
+
p = Path(ref)
|
|
29
|
+
if p.is_file() and p.suffix in (".jsonl", ".json", ".txt"):
|
|
30
|
+
seqs = []
|
|
31
|
+
for n, line in enumerate(p.read_text().splitlines(), 1):
|
|
32
|
+
line = line.strip()
|
|
33
|
+
if not line:
|
|
34
|
+
continue
|
|
35
|
+
try:
|
|
36
|
+
obj = json.loads(line)
|
|
37
|
+
except ValueError:
|
|
38
|
+
raise OpError(f"{p}:{n}: not JSON") from None
|
|
39
|
+
if isinstance(obj, list):
|
|
40
|
+
seqs.append({"tokens": obj})
|
|
41
|
+
elif isinstance(obj, dict) and isinstance(obj.get("tokens"), list):
|
|
42
|
+
seqs.append({"tokens": obj["tokens"], "logprobs": obj.get("logprobs")})
|
|
43
|
+
else:
|
|
44
|
+
raise OpError(f"{p}:{n}: expected a list of token ids or {{\"tokens\": [...]}}")
|
|
45
|
+
return seqs, None
|
|
46
|
+
rec = store.load(ref)
|
|
47
|
+
tokens = rec.get("data", {}).get("tokens")
|
|
48
|
+
if not tokens:
|
|
49
|
+
raise OpError(f"{ref}: record has no data.tokens (run bench decode with record_tokens)")
|
|
50
|
+
if tokens and isinstance(tokens[0], list):
|
|
51
|
+
return [{"tokens": t} for t in tokens], rec
|
|
52
|
+
return [{"tokens": tokens}], rec
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def first_divergence(a: list, b: list) -> int | None:
|
|
56
|
+
"""Index of the first differing token; None if identical (including length)."""
|
|
57
|
+
for i, (x, y) in enumerate(zip(a, b)):
|
|
58
|
+
if x != y:
|
|
59
|
+
return i
|
|
60
|
+
return None if len(a) == len(b) else min(len(a), len(b))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _bin_label(lo, hi):
|
|
64
|
+
return f"{lo}" if lo == hi else (f"{lo}+" if hi is None else f"{lo}-{hi}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@operation("check_correctness", ("correctness",), "Greedy token agreement between a reference and a candidate: "
|
|
68
|
+
"exact matches, first-divergence positions, optional logprob tolerance.",
|
|
69
|
+
[Arg("reference", "string", "Evidence id (with data.tokens) or a JSONL file of token-id lists.",
|
|
70
|
+
required=True),
|
|
71
|
+
Arg("candidate", "string", "Evidence id or JSONL file, same order as the reference.", required=True),
|
|
72
|
+
Arg("min_prefix_agreement", "number", "PASS needs mean prefix agreement >= this (1.0 = identical).",
|
|
73
|
+
default=1.0),
|
|
74
|
+
Arg("logprob_tol", "number", "Max |logprob difference| allowed inside the agreeing prefix "
|
|
75
|
+
"(only when both inputs carry logprobs).")])
|
|
76
|
+
def check_correctness(call, reference, candidate, min_prefix_agreement, logprob_tol):
|
|
77
|
+
store = ev.Store()
|
|
78
|
+
ref, ref_rec = load_sequences(store, reference)
|
|
79
|
+
cand, cand_rec = load_sequences(store, candidate)
|
|
80
|
+
limitations, missing = [], []
|
|
81
|
+
if len(ref) != len(cand):
|
|
82
|
+
limitations.append(f"different sequence counts ({len(ref)} vs {len(cand)}): compared the first "
|
|
83
|
+
f"{min(len(ref), len(cand))}")
|
|
84
|
+
n = min(len(ref), len(cand))
|
|
85
|
+
divergences, prefix_fracs, posmatch, postotal = [], [], 0, 0
|
|
86
|
+
max_lp_diff = None
|
|
87
|
+
for r, c in zip(ref[:n], cand[:n]):
|
|
88
|
+
a, b = r["tokens"], c["tokens"]
|
|
89
|
+
d = first_divergence(a, b)
|
|
90
|
+
divergences.append(d)
|
|
91
|
+
length = max(len(a), len(b)) or 1
|
|
92
|
+
prefix_fracs.append((length if d is None else d) / length)
|
|
93
|
+
posmatch += sum(1 for x, y in zip(a, b) if x == y)
|
|
94
|
+
postotal += length
|
|
95
|
+
if r.get("logprobs") and c.get("logprobs"):
|
|
96
|
+
upto = len(a) if d is None else d
|
|
97
|
+
diffs = [abs(x - y) for x, y in zip(r["logprobs"][:upto], c["logprobs"][:upto])
|
|
98
|
+
if x is not None and y is not None]
|
|
99
|
+
if diffs:
|
|
100
|
+
max_lp_diff = max(max_lp_diff or 0.0, max(diffs))
|
|
101
|
+
if n == 0:
|
|
102
|
+
missing.append("sequences")
|
|
103
|
+
exact = sum(1 for d in divergences if d is None)
|
|
104
|
+
prefix = statistics.mean(prefix_fracs) if prefix_fracs else None
|
|
105
|
+
diverged = [d for d in divergences if d is not None]
|
|
106
|
+
hist = {}
|
|
107
|
+
for lo, hi in HIST_BINS:
|
|
108
|
+
hist[_bin_label(lo, hi)] = sum(1 for d in diverged if d >= lo and (hi is None or d <= hi))
|
|
109
|
+
hist["identical"] = exact
|
|
110
|
+
|
|
111
|
+
if n == 0:
|
|
112
|
+
verdict, why = "UNPROVEN", "no sequences to compare"
|
|
113
|
+
elif prefix is not None and prefix >= min_prefix_agreement - 1e-12:
|
|
114
|
+
verdict, why = "PASS", f"mean prefix agreement {prefix:.4f} >= {min_prefix_agreement}"
|
|
115
|
+
else:
|
|
116
|
+
verdict, why = "FAIL", f"mean prefix agreement {prefix:.4f} < {min_prefix_agreement}"
|
|
117
|
+
if logprob_tol is not None:
|
|
118
|
+
if max_lp_diff is None:
|
|
119
|
+
limitations.append("logprob tolerance requested but inputs carry no logprobs: not checked")
|
|
120
|
+
missing.append("logprobs")
|
|
121
|
+
if verdict == "PASS":
|
|
122
|
+
verdict, why = "UNPROVEN", why + "; logprob tolerance not checked"
|
|
123
|
+
elif max_lp_diff > logprob_tol:
|
|
124
|
+
verdict, why = "FAIL", f"max logprob difference {max_lp_diff:.3g} > {logprob_tol}"
|
|
125
|
+
if n == 1:
|
|
126
|
+
limitations.append("a single sequence: one near-tie decides the result; use several prompts")
|
|
127
|
+
for rec in (ref_rec, cand_rec):
|
|
128
|
+
problem = ev.integrity_problem(rec) if rec else None
|
|
129
|
+
if problem: # an edited input must not become a fresh, verifying PASS
|
|
130
|
+
limitations.append(problem)
|
|
131
|
+
missing.append("input integrity")
|
|
132
|
+
if verdict == "PASS":
|
|
133
|
+
verdict, why = "UNPROVEN", why + "; an input failed its integrity check"
|
|
134
|
+
|
|
135
|
+
metrics = [ev.metric("exact_match_pct", 100 * exact / n if n else None, "%", "rate", n=n),
|
|
136
|
+
ev.metric("prefix_agreement_pct", 100 * prefix if prefix is not None else None, "%", "mean", n=n),
|
|
137
|
+
ev.metric("positionwise_agreement_pct", 100 * posmatch / postotal if postotal else None, "%", "rate",
|
|
138
|
+
n=postotal)]
|
|
139
|
+
if diverged:
|
|
140
|
+
metrics.append(ev.metric("first_divergence_index", statistics.median(diverged), "tokens", "median",
|
|
141
|
+
n=len(diverged)))
|
|
142
|
+
if max_lp_diff is not None:
|
|
143
|
+
metrics.append(ev.metric("max_logprob_abs_diff", max_lp_diff, "nats", "max", n=n))
|
|
144
|
+
summary = f"{verdict}: {exact}/{n} sequences identical, prefix agreement " + (
|
|
145
|
+
f"{100 * prefix:.1f}%" if prefix is not None else "n/a")
|
|
146
|
+
ident = (cand_rec or ref_rec or {}).get("identity") or ev.identity()
|
|
147
|
+
record = ev.build(call, kind="correctness", claim_class="derived", summary=summary, identity_=ident,
|
|
148
|
+
metrics=metrics, missing=missing, limitations=limitations,
|
|
149
|
+
data={"verdict": verdict, "reason": why, "sequences": n,
|
|
150
|
+
"first_divergence": divergences[:1000], "divergence_histogram": hist,
|
|
151
|
+
"reference": ref_rec["evidence_id"] if ref_rec else str(reference),
|
|
152
|
+
"candidate": cand_rec["evidence_id"] if cand_rec else str(candidate)})
|
|
153
|
+
return store.save(record)
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"""doctor: what is this machine, and can it profile?
|
|
2
|
+
|
|
3
|
+
Run it first. Most "my numbers make no sense" problems are environmental: another
|
|
4
|
+
process on the GPU, clocks locked low, a profiler that cannot read hardware
|
|
5
|
+
counters, or an engine version different from the one you think you have.
|
|
6
|
+
|
|
7
|
+
Counter access is read from the driver setting and the process's capabilities, never
|
|
8
|
+
assumed from the user being root: root inside a container often lacks CAP_SYS_ADMIN, and
|
|
9
|
+
then ncu fails with ERR_NVGPUCTRPERM like any other user. ``--probe-profilers`` settles it
|
|
10
|
+
by profiling one tiny kernel with ncu and nsys (when they are installed).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
from importlib import metadata
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from inferencekit import evidence as ev
|
|
26
|
+
from inferencekit.gpuinfo import clocks_state, driver_cuda_version, query_gpus
|
|
27
|
+
from inferencekit.hardware import discover_non_nvidia, profiler_route
|
|
28
|
+
from inferencekit.proc import run, tool_version, which
|
|
29
|
+
from inferencekit.registry import Arg, operation
|
|
30
|
+
|
|
31
|
+
ENGINE_PACKAGES = ["vllm", "sglang", "torch", "transformers", "triton", "flashinfer-python", "flash-attn",
|
|
32
|
+
"xformers", "tensorrt-llm", "llama-cpp-python", "diffusers", "mlx", "mlx-lm", "mlx-audio"]
|
|
33
|
+
|
|
34
|
+
# Runs inside another interpreter (an engine's venv) to read its package versions.
|
|
35
|
+
_VERSION_PROBE = """
|
|
36
|
+
import json, sys
|
|
37
|
+
from importlib import metadata
|
|
38
|
+
out = {}
|
|
39
|
+
for name in json.loads(sys.argv[1]):
|
|
40
|
+
try:
|
|
41
|
+
out[name] = metadata.version(name)
|
|
42
|
+
except metadata.PackageNotFoundError:
|
|
43
|
+
pass
|
|
44
|
+
print(json.dumps(out))
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def engine_versions(python: str | None) -> dict:
|
|
49
|
+
if not python or os.path.realpath(python) == os.path.realpath(sys.executable):
|
|
50
|
+
found = {}
|
|
51
|
+
for name in ENGINE_PACKAGES:
|
|
52
|
+
try:
|
|
53
|
+
found[name] = metadata.version(name)
|
|
54
|
+
except metadata.PackageNotFoundError:
|
|
55
|
+
pass
|
|
56
|
+
return found
|
|
57
|
+
try:
|
|
58
|
+
p = subprocess.run([python, "-c", _VERSION_PROBE, json.dumps(ENGINE_PACKAGES)], capture_output=True,
|
|
59
|
+
text=True, timeout=120)
|
|
60
|
+
return json.loads(p.stdout.strip().splitlines()[-1]) if p.returncode == 0 else {}
|
|
61
|
+
except (OSError, ValueError, IndexError, subprocess.TimeoutExpired):
|
|
62
|
+
return {}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def llamacpp_version() -> dict | None:
|
|
66
|
+
"""``llama-cli --version`` prints e.g. 'version: 4589 (1a2b3c4)' (build number and commit)."""
|
|
67
|
+
for name in ("llama-cli", "llama-bench", "llama-server"):
|
|
68
|
+
path = which(name)
|
|
69
|
+
if not path:
|
|
70
|
+
continue
|
|
71
|
+
r = run([path, "--version"], timeout=30)
|
|
72
|
+
m = re.search(r"version:\s*(\d+)\s*\(([0-9a-f]+)\)", r.stdout + r.stderr)
|
|
73
|
+
if m:
|
|
74
|
+
return {"binary": name, "build": m.group(1), "commit": m.group(2)}
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
PARAMS_FILE = Path("/proc/driver/nvidia/params")
|
|
79
|
+
STATUS_FILE = Path("/proc/self/status")
|
|
80
|
+
CAP_SYS_ADMIN = 21 # bit of the CapEff mask in /proc/<pid>/status
|
|
81
|
+
PROBE_HINT = "run 'inferencekit doctor --probe-profilers' or one small 'ncu capture' to find out"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def profiling_admin_only(path: Path = PARAMS_FILE) -> int | None:
|
|
85
|
+
"""The driver's RmProfilingAdminOnly (1: counters for admins only, 0: for everyone), None if unreadable."""
|
|
86
|
+
try:
|
|
87
|
+
m = re.search(r"RmProfilingAdminOnly:\s*(\d+)", path.read_text())
|
|
88
|
+
except OSError:
|
|
89
|
+
return None
|
|
90
|
+
return int(m.group(1)) if m else None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def has_cap_sys_admin(path: Path = STATUS_FILE) -> bool | None:
|
|
94
|
+
"""Whether this process's effective capabilities include CAP_SYS_ADMIN, None if unreadable."""
|
|
95
|
+
try:
|
|
96
|
+
m = re.search(r"^CapEff:\s*([0-9a-fA-F]+)\s*$", path.read_text(), re.M)
|
|
97
|
+
except OSError:
|
|
98
|
+
return None
|
|
99
|
+
return bool(int(m.group(1), 16) >> CAP_SYS_ADMIN & 1) if m else None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def counter_permission(params_path: Path = PARAMS_FILE, status_path: Path = STATUS_FILE,
|
|
103
|
+
euid: int | None = None) -> dict:
|
|
104
|
+
"""Can this process read GPU performance counters (needed by ncu)? Status allowed | admin_only | unknown.
|
|
105
|
+
|
|
106
|
+
The NVIDIA driver parameter RmProfilingAdminOnly=1 (the Linux default) restricts counters to admin
|
|
107
|
+
users, which the driver checks as CAP_SYS_ADMIN, not as uid 0; ncu then fails with ERR_NVGPUCTRPERM.
|
|
108
|
+
So the answer follows the capability: only RmProfilingAdminOnly=0 gives ``allowed``; under
|
|
109
|
+
RmProfilingAdminOnly=1 a process without CAP_SYS_ADMIN (a container's root usually) is
|
|
110
|
+
``admin_only``, and one with it (root on a VM or bare-metal host, or a non-root process granted the
|
|
111
|
+
capability) is ``unknown`` until a capture is tried (``--probe-profilers``): ncu usually works there,
|
|
112
|
+
but a container runtime or hypervisor can still refuse. Without the parameter the answer is
|
|
113
|
+
``unknown`` too. Nothing is profiled here.
|
|
114
|
+
"""
|
|
115
|
+
if euid is None:
|
|
116
|
+
euid = os.geteuid() if hasattr(os, "geteuid") else None
|
|
117
|
+
admin_only = profiling_admin_only(params_path)
|
|
118
|
+
cap = has_cap_sys_admin(status_path)
|
|
119
|
+
facts = {"rm_profiling_admin_only": admin_only, "euid": euid, "cap_sys_admin": cap}
|
|
120
|
+
who = "root" if euid == 0 else "non-root"
|
|
121
|
+
if admin_only == 0:
|
|
122
|
+
return {"status": "allowed", "reason": "RmProfilingAdminOnly=0: the driver lets every user read counters",
|
|
123
|
+
**facts}
|
|
124
|
+
if admin_only is not None:
|
|
125
|
+
if cap:
|
|
126
|
+
return {"status": "unknown", "reason": f"RmProfilingAdminOnly=1 and this {who} process has CAP_SYS_ADMIN, "
|
|
127
|
+
"which the driver asks for: ncu is expected to work, but a "
|
|
128
|
+
f"container or VM can still refuse (probe needed: {PROBE_HINT})",
|
|
129
|
+
**facts}
|
|
130
|
+
if cap is False and euid == 0:
|
|
131
|
+
reason = ("RmProfilingAdminOnly=1 and this root process lacks CAP_SYS_ADMIN (usual in a container): "
|
|
132
|
+
"ncu fails with ERR_NVGPUCTRPERM")
|
|
133
|
+
elif cap is False:
|
|
134
|
+
reason = ("RmProfilingAdminOnly=1 and this process lacks CAP_SYS_ADMIN: ncu needs CAP_SYS_ADMIN (root "
|
|
135
|
+
"on a VM or bare-metal host, or a container started with it), or the driver option "
|
|
136
|
+
"NVreg_RestrictProfilingToAdminUsers=0")
|
|
137
|
+
elif euid == 0:
|
|
138
|
+
return {"status": "unknown", "reason": "RmProfilingAdminOnly=1 and this process's capabilities are "
|
|
139
|
+
f"unreadable, so CAP_SYS_ADMIN is unknown (probe needed: "
|
|
140
|
+
f"{PROBE_HINT})", **facts}
|
|
141
|
+
else:
|
|
142
|
+
reason = ("RmProfilingAdminOnly=1, and this non-root process's capabilities are unreadable: ncu "
|
|
143
|
+
"needs CAP_SYS_ADMIN, or the driver option NVreg_RestrictProfilingToAdminUsers=0")
|
|
144
|
+
return {"status": "admin_only", "reason": reason, **facts}
|
|
145
|
+
if euid == 0 and cap is False:
|
|
146
|
+
return {"status": "admin_only", "reason": "root without CAP_SYS_ADMIN and no readable RmProfilingAdminOnly "
|
|
147
|
+
"(drivers default it to 1): ncu is expected to fail with "
|
|
148
|
+
"ERR_NVGPUCTRPERM", **facts}
|
|
149
|
+
ncu = which("ncu")
|
|
150
|
+
if ncu:
|
|
151
|
+
r = run([ncu, "--query-metrics"], timeout=120)
|
|
152
|
+
if "ERR_NVGPUCTRPERM" in r.stdout + r.stderr:
|
|
153
|
+
return {"status": "admin_only", "reason": "ncu --query-metrics reported ERR_NVGPUCTRPERM", **facts}
|
|
154
|
+
if r.ok:
|
|
155
|
+
return {"status": "unknown", "reason": "no readable driver parameter; ncu can enumerate metrics but "
|
|
156
|
+
f"counter access is untested (probe needed: {PROBE_HINT})", **facts}
|
|
157
|
+
return {"status": "unknown", "reason": f"no readable /proc/driver/nvidia/params and no ncu (probe needed: "
|
|
158
|
+
f"{PROBE_HINT})", **facts}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# --------------------------------------------------------------------------- --probe-profilers
|
|
162
|
+
|
|
163
|
+
# Programs that launch one small CUDA kernel. First choice: the standard library only. ctypes loads the
|
|
164
|
+
# driver (libcuda.so.1, present on every NVIDIA host), the driver compiles a PTX kernel for whatever GPU this is
|
|
165
|
+
# (PTX is the portable assembly that CUDA compiles to), and the kernel doubles 1M floats. Fallbacks: torch in
|
|
166
|
+
# the engine's interpreter, then a .cu built with nvcc.
|
|
167
|
+
_DRIVER_PROGRAM = r'''import ctypes, sys
|
|
168
|
+
PTX = b"""
|
|
169
|
+
.version 6.0
|
|
170
|
+
.target sm_50
|
|
171
|
+
.address_size 64
|
|
172
|
+
.visible .entry twice(.param .u64 x, .param .u32 n)
|
|
173
|
+
{
|
|
174
|
+
.reg .pred p;
|
|
175
|
+
.reg .b32 r<5>;
|
|
176
|
+
.reg .f32 f<3>;
|
|
177
|
+
.reg .b64 rd<5>;
|
|
178
|
+
ld.param.u64 rd1, [x];
|
|
179
|
+
ld.param.u32 r1, [n];
|
|
180
|
+
mov.u32 r2, %ctaid.x;
|
|
181
|
+
mov.u32 r3, %ntid.x;
|
|
182
|
+
mov.u32 r4, %tid.x;
|
|
183
|
+
mad.lo.s32 r2, r2, r3, r4;
|
|
184
|
+
setp.ge.u32 p, r2, r1;
|
|
185
|
+
@p bra DONE;
|
|
186
|
+
cvta.to.global.u64 rd2, rd1;
|
|
187
|
+
mul.wide.u32 rd3, r2, 4;
|
|
188
|
+
add.s64 rd4, rd2, rd3;
|
|
189
|
+
ld.global.f32 f1, [rd4];
|
|
190
|
+
add.f32 f2, f1, f1;
|
|
191
|
+
st.global.f32 [rd4], f2;
|
|
192
|
+
DONE:
|
|
193
|
+
ret;
|
|
194
|
+
}
|
|
195
|
+
"""
|
|
196
|
+
cuda = ctypes.CDLL("libcuda.so.1")
|
|
197
|
+
def check(rc, what):
|
|
198
|
+
if rc:
|
|
199
|
+
name = ctypes.c_char_p()
|
|
200
|
+
cuda.cuGetErrorName(rc, ctypes.byref(name))
|
|
201
|
+
sys.exit(what + ": " + (name.value or b"error").decode())
|
|
202
|
+
n = 1 << 20
|
|
203
|
+
dev, ctx, mod, fn, ptr = ctypes.c_int(), ctypes.c_void_p(), ctypes.c_void_p(), ctypes.c_void_p(), ctypes.c_uint64()
|
|
204
|
+
check(cuda.cuInit(0), "cuInit")
|
|
205
|
+
check(cuda.cuDeviceGet(ctypes.byref(dev), 0), "cuDeviceGet")
|
|
206
|
+
check(cuda.cuDevicePrimaryCtxRetain(ctypes.byref(ctx), dev), "cuDevicePrimaryCtxRetain")
|
|
207
|
+
check(cuda.cuCtxSetCurrent(ctx), "cuCtxSetCurrent")
|
|
208
|
+
check(cuda.cuModuleLoadData(ctypes.byref(mod), PTX), "cuModuleLoadData")
|
|
209
|
+
check(cuda.cuModuleGetFunction(ctypes.byref(fn), mod, b"twice"), "cuModuleGetFunction")
|
|
210
|
+
check(cuda.cuMemAlloc_v2(ctypes.byref(ptr), ctypes.c_size_t(4 * n)), "cuMemAlloc")
|
|
211
|
+
check(cuda.cuMemsetD32_v2(ptr, ctypes.c_uint(0x3F800000), ctypes.c_size_t(n)), "cuMemsetD32")
|
|
212
|
+
count = ctypes.c_uint32(n)
|
|
213
|
+
args = (ctypes.c_void_p * 2)(ctypes.cast(ctypes.byref(ptr), ctypes.c_void_p),
|
|
214
|
+
ctypes.cast(ctypes.byref(count), ctypes.c_void_p))
|
|
215
|
+
u = ctypes.c_uint
|
|
216
|
+
check(cuda.cuLaunchKernel(fn, u((n + 255) // 256), u(1), u(1), u(256), u(1), u(1), u(0), None, args, None),
|
|
217
|
+
"cuLaunchKernel")
|
|
218
|
+
check(cuda.cuCtxSynchronize(), "cuCtxSynchronize")
|
|
219
|
+
out = ctypes.c_float()
|
|
220
|
+
check(cuda.cuMemcpyDtoH_v2(ctypes.byref(out), ptr, ctypes.c_size_t(4)), "cuMemcpyDtoH")
|
|
221
|
+
check(cuda.cuMemFree_v2(ptr), "cuMemFree")
|
|
222
|
+
if out.value != 2.0:
|
|
223
|
+
sys.exit("the kernel wrote %r, expected 2.0" % out.value)
|
|
224
|
+
print("ok")
|
|
225
|
+
'''
|
|
226
|
+
_TORCH_PROGRAM = ("import torch\n"
|
|
227
|
+
"x = torch.ones(1 << 20, device='cuda')\n"
|
|
228
|
+
"y = (x * 2.0).sum()\n"
|
|
229
|
+
"torch.cuda.synchronize()\n"
|
|
230
|
+
"print(float(y))\n")
|
|
231
|
+
_CU_PROGRAM = r"""#include <cstdio>
|
|
232
|
+
__global__ void twice(float *x, int n) {
|
|
233
|
+
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
|
234
|
+
if (i < n) x[i] = 2.0f * x[i];
|
|
235
|
+
}
|
|
236
|
+
int main() {
|
|
237
|
+
const int n = 1 << 20;
|
|
238
|
+
float *x = nullptr;
|
|
239
|
+
if (cudaMalloc(&x, n * sizeof(float)) != cudaSuccess) { printf("cudaMalloc failed\n"); return 2; }
|
|
240
|
+
twice<<<(n + 255) / 256, 256>>>(x, n);
|
|
241
|
+
cudaError_t e = cudaDeviceSynchronize();
|
|
242
|
+
cudaFree(x);
|
|
243
|
+
if (e != cudaSuccess) { printf("%s\n", cudaGetErrorString(e)); return 3; }
|
|
244
|
+
printf("ok\n");
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
"""
|
|
248
|
+
NCU_METRIC = "sm__cycles_elapsed.avg"
|
|
249
|
+
PROBE_TIMEOUT_S = 300
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _tail(r, n: int = 300) -> str:
|
|
253
|
+
lines = [ln.strip() for ln in (r.stdout + "\n" + r.stderr).splitlines() if ln.strip()]
|
|
254
|
+
errs = [ln for ln in lines if "ERR_" in ln or "==ERROR==" in ln or "error" in ln.lower()]
|
|
255
|
+
return ev.redact_text(" | ".join((errs or lines)[-3:]))[:n] or f"exit {r.returncode}"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _probe_program(python: str | None, work: Path) -> tuple[list[str] | None, str]:
|
|
259
|
+
"""(argv of a program that runs one small CUDA kernel and exits 0, what it is); argv None when no
|
|
260
|
+
candidate ran without a profiler (then the profilers are not tried: a failure would say nothing)."""
|
|
261
|
+
tried = []
|
|
262
|
+
driver = work / "probe_kernel.py"
|
|
263
|
+
driver.write_text(_DRIVER_PROGRAM)
|
|
264
|
+
argv = [sys.executable, "-I", str(driver)]
|
|
265
|
+
r = run(argv, timeout=PROBE_TIMEOUT_S)
|
|
266
|
+
if r.ok:
|
|
267
|
+
return argv, "one PTX kernel launched through the CUDA driver API (Python standard library only)"
|
|
268
|
+
tried.append(f"driver API program: {_tail(r, 160)}")
|
|
269
|
+
py = python or sys.executable
|
|
270
|
+
r = run([py, "-c", _TORCH_PROGRAM], timeout=PROBE_TIMEOUT_S)
|
|
271
|
+
if r.ok:
|
|
272
|
+
return [py, "-c", _TORCH_PROGRAM], f"torch in {Path(py).name}: one elementwise kernel and a reduction"
|
|
273
|
+
tried.append(f"torch program: {_tail(r, 160)}")
|
|
274
|
+
why = "; ".join(tried)
|
|
275
|
+
nvcc = which("nvcc")
|
|
276
|
+
if not nvcc:
|
|
277
|
+
return None, f"no CUDA program could run ({why}; no nvcc to build one)"
|
|
278
|
+
src, exe = work / "probe.cu", work / "probe"
|
|
279
|
+
src.write_text(_CU_PROGRAM)
|
|
280
|
+
b = run([nvcc, "-o", str(exe), str(src)], timeout=PROBE_TIMEOUT_S)
|
|
281
|
+
if not b.ok:
|
|
282
|
+
return None, f"no CUDA program could run ({why}; nvcc build failed: {_tail(b, 160)})"
|
|
283
|
+
r = run([str(exe)], timeout=PROBE_TIMEOUT_S)
|
|
284
|
+
if not r.ok:
|
|
285
|
+
return None, f"no CUDA program could run ({why}; the nvcc-built kernel failed: {_tail(r, 160)})"
|
|
286
|
+
return [str(exe)], "a one-kernel CUDA program built with nvcc"
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _probe_ncu(ncu: str, program: list[str]) -> dict:
|
|
290
|
+
t0 = time.monotonic()
|
|
291
|
+
r = run([ncu, "--launch-count", "1", "--metrics", NCU_METRIC, *program], timeout=PROBE_TIMEOUT_S)
|
|
292
|
+
out = r.stdout + r.stderr
|
|
293
|
+
res = {"ran": True, "returncode": r.returncode, "seconds": round(time.monotonic() - t0, 2)}
|
|
294
|
+
if "ERR_NVGPUCTRPERM" in out:
|
|
295
|
+
return {**res, "capture_ok": False, "error": "ERR_NVGPUCTRPERM: no permission to read GPU counters"}
|
|
296
|
+
value = re.search(re.escape(NCU_METRIC) + r"\s+\S+\s+([\d.,]+)", out)
|
|
297
|
+
if r.ok and value:
|
|
298
|
+
return {**res, "capture_ok": True, "error": None, "metric": NCU_METRIC,
|
|
299
|
+
"value": float(value.group(1).replace(",", ""))}
|
|
300
|
+
if r.ok and NCU_METRIC in out and "No kernels were profiled" not in out:
|
|
301
|
+
return {**res, "capture_ok": True, "error": None, "metric": NCU_METRIC, "value": None}
|
|
302
|
+
return {**res, "capture_ok": False, "error": _tail(r)}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _probe_nsys(nsys: str, program: list[str], work: Path) -> dict:
|
|
306
|
+
t0 = time.monotonic()
|
|
307
|
+
out = work / "probe"
|
|
308
|
+
r = run([nsys, "profile", "--trace=cuda", "--sample=none", "--cpuctxsw=none", "--force-overwrite=true",
|
|
309
|
+
"-o", str(out), *program], timeout=PROBE_TIMEOUT_S)
|
|
310
|
+
res = {"ran": True, "returncode": r.returncode, "seconds": round(time.monotonic() - t0, 2)}
|
|
311
|
+
report = out.with_suffix(".nsys-rep")
|
|
312
|
+
if r.ok and report.is_file():
|
|
313
|
+
return {**res, "capture_ok": True, "error": None, "report_bytes": report.stat().st_size}
|
|
314
|
+
if r.ok:
|
|
315
|
+
return {**res, "capture_ok": False, "error": "nsys exited 0 but wrote no .nsys-rep report"}
|
|
316
|
+
return {**res, "capture_ok": False, "error": _tail(r)}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def profiler_probe(python: str | None) -> dict:
|
|
320
|
+
"""Profile one tiny kernel with ncu (one counter) and nsys (a CUDA trace), when each is installed.
|
|
321
|
+
|
|
322
|
+
``capture_ok`` per tool: True when the capture worked, False with ``error`` when it failed, None when
|
|
323
|
+
it was not tried (tool missing, or no CUDA program could run). Never raises: a probe failure is a finding.
|
|
324
|
+
"""
|
|
325
|
+
tools = {"ncu": which("ncu"), "nsys": which("nsys")}
|
|
326
|
+
out: dict = {"program": None, "ncu": None, "nsys": None}
|
|
327
|
+
if not any(tools.values()):
|
|
328
|
+
for name in tools:
|
|
329
|
+
out[name] = {"ran": False, "capture_ok": None, "error": f"{name} not found"}
|
|
330
|
+
return out
|
|
331
|
+
try:
|
|
332
|
+
work = ev.Store().staging() # under the evidence store, removed below: nothing of the probe is kept
|
|
333
|
+
except OSError as e:
|
|
334
|
+
for name in tools:
|
|
335
|
+
out[name] = {"ran": False, "capture_ok": None, "error": f"no scratch directory for the probe: {e}"[:300]}
|
|
336
|
+
return out
|
|
337
|
+
try:
|
|
338
|
+
program, how = _probe_program(python, work)
|
|
339
|
+
out["program" if program else "program_error"] = how
|
|
340
|
+
for name, path in tools.items():
|
|
341
|
+
if not path:
|
|
342
|
+
out[name] = {"ran": False, "capture_ok": None, "error": f"{name} not found"}
|
|
343
|
+
elif program is None:
|
|
344
|
+
out[name] = {"ran": False, "capture_ok": None, "error": how}
|
|
345
|
+
else:
|
|
346
|
+
try:
|
|
347
|
+
out[name] = _probe_ncu(path, program) if name == "ncu" else _probe_nsys(path, program, work)
|
|
348
|
+
except Exception as e: # noqa: BLE001 -- the probe must never fail doctor
|
|
349
|
+
out[name] = {"ran": True, "capture_ok": False, "error": f"{type(e).__name__}: {e}"[:300]}
|
|
350
|
+
except Exception as e: # noqa: BLE001
|
|
351
|
+
out["error"] = f"{type(e).__name__}: {e}"[:300]
|
|
352
|
+
finally:
|
|
353
|
+
shutil.rmtree(work, ignore_errors=True)
|
|
354
|
+
return out
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def gpu_processes() -> list[dict]:
|
|
358
|
+
"""Other compute processes on the GPU: they steal bandwidth and skew timings."""
|
|
359
|
+
smi = which("nvidia-smi")
|
|
360
|
+
if not smi:
|
|
361
|
+
return []
|
|
362
|
+
r = run([smi, "--query-compute-apps=pid,process_name,used_memory", "--format=csv,noheader,nounits"], timeout=30)
|
|
363
|
+
out = []
|
|
364
|
+
for line in r.stdout.strip().splitlines():
|
|
365
|
+
parts = [p.strip() for p in line.split(",")]
|
|
366
|
+
if len(parts) == 3:
|
|
367
|
+
out.append({"pid": parts[0], "name": Path(parts[1]).name, "used_mib": parts[2]})
|
|
368
|
+
return out
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@operation("doctor", ("doctor",), "Hardware identity and profiling route (NVIDIA, AMD ROCm, Apple Metal); "
|
|
372
|
+
"selected AMD/Apple OS facts; NVIDIA driver, CUDA, clocks, nsys/ncu and profiling-counter permission "
|
|
373
|
+
"(driver setting and CAP_SYS_ADMIN, never assumed from root), engine versions; optionally a tiny ncu and "
|
|
374
|
+
"nsys test capture.",
|
|
375
|
+
[Arg("python", "string", "Interpreter of the engine's environment, to read its package versions "
|
|
376
|
+
"(default: this interpreter). --probe-profilers falls back to its torch when the standard-library "
|
|
377
|
+
"test program cannot run."),
|
|
378
|
+
Arg("probe_profilers", "boolean", "Also profile one tiny CUDA kernel (a standard-library driver-API "
|
|
379
|
+
"program; falls back to torch in --python, then nvcc) with ncu (--launch-count 1, one counter) and "
|
|
380
|
+
"with nsys, when installed, and record whether each capture worked. Each step stops after "
|
|
381
|
+
f"{PROBE_TIMEOUT_S // 60} minutes; a failed capture never fails doctor.", default=False)])
|
|
382
|
+
def doctor(call, python, probe_profilers=False):
|
|
383
|
+
gpus = query_gpus()
|
|
384
|
+
hardware = None if gpus else discover_non_nvidia()
|
|
385
|
+
if hardware and (hardware["devices"] or hardware["platform"] == "unknown"):
|
|
386
|
+
return _vendor_doctor(call, python, probe_profilers, hardware)
|
|
387
|
+
limitations, missing = [], []
|
|
388
|
+
if not gpus:
|
|
389
|
+
missing.append("gpu")
|
|
390
|
+
limitations.append("nvidia-smi not found or no GPU visible: no GPU facts recorded")
|
|
391
|
+
tools = {name: tool_version(name) for name in ("nvcc", "nsys", "ncu")}
|
|
392
|
+
for name, v in tools.items():
|
|
393
|
+
if v is None:
|
|
394
|
+
missing.append(name)
|
|
395
|
+
perm = counter_permission()
|
|
396
|
+
probe = profiler_probe(python) if probe_profilers else None
|
|
397
|
+
if probe is not None:
|
|
398
|
+
ncu = probe.get("ncu") or {}
|
|
399
|
+
if ncu.get("capture_ok"):
|
|
400
|
+
perm = {**perm, "status": "allowed", "reason": f"ncu read {NCU_METRIC} on a test kernel "
|
|
401
|
+
"(--probe-profilers)", "static_reason": perm["reason"]}
|
|
402
|
+
elif ncu.get("capture_ok") is False and "ERR_NVGPUCTRPERM" in (ncu.get("error") or ""):
|
|
403
|
+
perm = {**perm, "status": "admin_only", "reason": "ncu test capture failed with ERR_NVGPUCTRPERM "
|
|
404
|
+
"(--probe-profilers)", "static_reason": perm["reason"]}
|
|
405
|
+
for name in ("ncu", "nsys"):
|
|
406
|
+
r = probe.get(name) or {}
|
|
407
|
+
if r.get("capture_ok") is False:
|
|
408
|
+
limitations.append(f"{name} test capture failed: {r.get('error')}")
|
|
409
|
+
elif r.get("capture_ok") is None and tools.get(name):
|
|
410
|
+
limitations.append(f"{name} test capture not tried: {r.get('error')}")
|
|
411
|
+
if perm["status"] == "admin_only":
|
|
412
|
+
limitations.append(f"profiling counters are admin-only: 'ncu capture' fails without admin rights "
|
|
413
|
+
f"(CAP_SYS_ADMIN); {perm['reason']}")
|
|
414
|
+
elif perm["status"] == "unknown":
|
|
415
|
+
limitations.append(f"profiling counter access unknown: {perm['reason']}")
|
|
416
|
+
engines = engine_versions(python)
|
|
417
|
+
llama = llamacpp_version()
|
|
418
|
+
procs = gpu_processes()
|
|
419
|
+
if procs:
|
|
420
|
+
limitations.append(f"{len(procs)} compute process(es) already on the GPU: timings will be contended")
|
|
421
|
+
clocks = clocks_state(gpus[0]) if gpus else None
|
|
422
|
+
if clocks == "default":
|
|
423
|
+
limitations.append("clock lock state read from application clocks; locks set with 'nvidia-smi -lgc' are "
|
|
424
|
+
"not visible on every driver")
|
|
425
|
+
gpu_rows = [{k: g.get(k) for k in ("index", "name", "memory.total", "compute_cap", "pstate", "persistence_mode",
|
|
426
|
+
"clocks.sm", "clocks.mem", "clocks.max.sm", "clocks.max.mem", "power.limit",
|
|
427
|
+
"temperature.gpu")} for g in gpus]
|
|
428
|
+
ident = ev.identity(gpu={"name": gpus[0].get("name") if gpus else None, "count": len(gpus) or None,
|
|
429
|
+
"driver": gpus[0].get("driver_version") if gpus else None,
|
|
430
|
+
"cuda": driver_cuda_version(), "clocks": clocks})
|
|
431
|
+
name = gpus[0]["name"] if gpus else "no GPU"
|
|
432
|
+
counters = perm["status"] + (" (probe needed)" if perm["status"] == "unknown" else "")
|
|
433
|
+
summary = (f"{len(gpus)}x {name}; nsys {tools['nsys'] or 'missing'}, ncu {tools['ncu'] or 'missing'}; "
|
|
434
|
+
f"counters {counters}; engines: {', '.join(f'{k} {v}' for k, v in engines.items()) or 'none'}")
|
|
435
|
+
if probe is not None:
|
|
436
|
+
words = {True: "ok", False: "FAILED", None: "not tried"}
|
|
437
|
+
summary += "; test capture: " + ", ".join(f"{t} {words[(probe.get(t) or {}).get('capture_ok')]}"
|
|
438
|
+
for t in ("ncu", "nsys"))
|
|
439
|
+
data = {"hardware": hardware or {"platform": "nvidia", "devices": gpu_rows},
|
|
440
|
+
"profiling_route": profiler_route("nvidia" if gpus else "cpu"), "gpus": gpu_rows, "tools": tools, "nvcc_toolkit": tools["nvcc"],
|
|
441
|
+
"profiling_counters": perm, "engines": engines, "llamacpp": llama,
|
|
442
|
+
"gpu_processes": procs,
|
|
443
|
+
# names only: which relevant variables are set, never their values
|
|
444
|
+
"env_set": sorted(k for k in ("CUDA_VISIBLE_DEVICES", "CUDA_HOME", "HF_HOME",
|
|
445
|
+
"VLLM_ATTENTION_BACKEND", "HF_TOKEN") if k in os.environ)}
|
|
446
|
+
if probe is not None:
|
|
447
|
+
data["profiler_probe"] = probe
|
|
448
|
+
record = ev.build(call, kind="environment", claim_class="observed", summary=summary, identity_=ident,
|
|
449
|
+
metrics=[ev.metric("gpu_count", len(gpus), "count", "observed")], missing=missing,
|
|
450
|
+
limitations=limitations, data=data)
|
|
451
|
+
return ev.Store().save(record)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _vendor_doctor(call, python, probe_profilers, hardware):
|
|
455
|
+
"""Report observed vendor facts without running CUDA-only permission/probe code."""
|
|
456
|
+
backend, devices = hardware["platform"], hardware["devices"]
|
|
457
|
+
names = ("rocprofv3", "amd-smi", "hipcc") if backend == "rocm" else ("xcrun",) if backend == "metal" else ()
|
|
458
|
+
tools = {name: tool_version(name) for name in names}
|
|
459
|
+
limitations = list(hardware["limitations"])
|
|
460
|
+
limitations += ["NVIDIA-only ceiling bw and nsys/ncu capture are NOT RUN on this platform.",
|
|
461
|
+
"Other accelerator processes and clock state are not automatically checked here; inspect "
|
|
462
|
+
"vendor tools and record an uncontended device before timing."]
|
|
463
|
+
probe = None
|
|
464
|
+
if probe_profilers:
|
|
465
|
+
probe = {"program": None, "ncu": {"ran": False, "capture_ok": None, "error": "NVIDIA-only probe"},
|
|
466
|
+
"nsys": {"ran": False, "capture_ok": None, "error": "NVIDIA-only probe"}}
|
|
467
|
+
limitations.append("--probe-profilers is NVIDIA-only; vendor capture support is NOT RUN.")
|
|
468
|
+
data = {"gpus": devices, "hardware": hardware, "tools": tools, "nvcc_toolkit": None,
|
|
469
|
+
"profiling_route": profiler_route(backend),
|
|
470
|
+
"profiling_counters": {"status": "not_applicable", "reason": "NVIDIA permission checks do not "
|
|
471
|
+
"apply; vendor counter support/permission is untested"},
|
|
472
|
+
"engines": engine_versions(python), "llamacpp": llamacpp_version(), "gpu_processes": [],
|
|
473
|
+
"env_set": sorted(k for k in ("HIP_VISIBLE_DEVICES", "ROCR_VISIBLE_DEVICES", "CUDA_VISIBLE_DEVICES",
|
|
474
|
+
"ROCM_PATH", "MTL_CAPTURE_ENABLED", "HF_HOME", "HF_TOKEN")
|
|
475
|
+
if k in os.environ)}
|
|
476
|
+
if probe is not None:
|
|
477
|
+
data["profiler_probe"] = probe
|
|
478
|
+
record = ev.build(call, kind="environment", claim_class="observed",
|
|
479
|
+
summary=(f"{len(devices)}x {devices[0]['name']}" if devices else "unknown accelerator count/identity") + "; "
|
|
480
|
+
+ f"platform {backend}; vendor profiling support untested",
|
|
481
|
+
identity_=ev.identity(gpu={"name": devices[0]["name"] if devices else None, "count": len(devices) or None,
|
|
482
|
+
"driver": None, "cuda": None, "clocks": None}),
|
|
483
|
+
metrics=[ev.metric("gpu_count", len(devices), "count", "observed")] if devices else [],
|
|
484
|
+
missing=["profiling counter permission", "gpu processes", "gpu clocks"],
|
|
485
|
+
limitations=limitations, data=data)
|
|
486
|
+
return ev.Store().save(record)
|