cascade-profiler 0.1.0__tar.gz
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.
- cascade_profiler-0.1.0/PKG-INFO +69 -0
- cascade_profiler-0.1.0/README.md +45 -0
- cascade_profiler-0.1.0/cascade_profiler/__init__.py +14 -0
- cascade_profiler-0.1.0/cascade_profiler/align.py +116 -0
- cascade_profiler-0.1.0/cascade_profiler/clocks.py +93 -0
- cascade_profiler-0.1.0/cascade_profiler/legacy.py +589 -0
- cascade_profiler-0.1.0/cascade_profiler/modal_support.py +70 -0
- cascade_profiler-0.1.0/cascade_profiler/model_info.py +20 -0
- cascade_profiler-0.1.0/cascade_profiler/nsys.py +434 -0
- cascade_profiler-0.1.0/cascade_profiler/pipeline.py +235 -0
- cascade_profiler-0.1.0/cascade_profiler/planning.py +49 -0
- cascade_profiler-0.1.0/cascade_profiler/runner.py +407 -0
- cascade_profiler-0.1.0/cascade_profiler/timeline.py +220 -0
- cascade_profiler-0.1.0/cascade_profiler/tracing.py +16 -0
- cascade_profiler-0.1.0/cascade_profiler/utils.py +66 -0
- cascade_profiler-0.1.0/cascade_profiler/workload.py +103 -0
- cascade_profiler-0.1.0/cascade_profiler.egg-info/PKG-INFO +69 -0
- cascade_profiler-0.1.0/cascade_profiler.egg-info/SOURCES.txt +25 -0
- cascade_profiler-0.1.0/cascade_profiler.egg-info/dependency_links.txt +1 -0
- cascade_profiler-0.1.0/cascade_profiler.egg-info/requires.txt +8 -0
- cascade_profiler-0.1.0/cascade_profiler.egg-info/top_level.txt +1 -0
- cascade_profiler-0.1.0/pyproject.toml +33 -0
- cascade_profiler-0.1.0/setup.cfg +4 -0
- cascade_profiler-0.1.0/tests/test_modal_support.py +52 -0
- cascade_profiler-0.1.0/tests/test_phases.py +554 -0
- cascade_profiler-0.1.0/tests/test_pipeline.py +210 -0
- cascade_profiler-0.1.0/tests/test_planning.py +17 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cascade-profiler
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: GPU workload tracing and baseline/profiled timeline reconciliation
|
|
5
|
+
Project-URL: Repository, https://github.com/atoniolo76/cascade
|
|
6
|
+
Project-URL: Documentation, https://github.com/atoniolo76/cascade/blob/profiler/PROFILER_RUNS.md
|
|
7
|
+
Project-URL: Issues, https://github.com/atoniolo76/cascade/issues
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Topic :: System :: Benchmark
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Provides-Extra: modal
|
|
19
|
+
Requires-Dist: modal; extra == "modal"
|
|
20
|
+
Requires-Dist: boto3>=1.36; extra == "modal"
|
|
21
|
+
Provides-Extra: vllm
|
|
22
|
+
Requires-Dist: vllm==0.21.0; extra == "vllm"
|
|
23
|
+
Requires-Dist: nvtx; extra == "vllm"
|
|
24
|
+
|
|
25
|
+
# Cascade Profiler
|
|
26
|
+
|
|
27
|
+
GPU workload profiling, Nsight Systems timing reconciliation, and NVBit instruction
|
|
28
|
+
tracing for vLLM. Python 3.10+ can import and inspect runs; GPU workloads require
|
|
29
|
+
CUDA, vLLM, and the tracing tools. Modal images use Python 3.12.
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
pip install 'cascade-profiler[modal]'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from cascade_profiler import Workload, profile
|
|
37
|
+
|
|
38
|
+
workload = Workload("facebook/opt-125m", ["Hello, my name is"], max_tokens=32, repeats=4)
|
|
39
|
+
result = profile(workload, "/data/phases", trace_steps="plan", trace_repeat=1)
|
|
40
|
+
print(result.run_dir, result.stage_wall_s)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`profile` needs a GPU environment with vLLM, Nsight Systems and the NVBit tracer
|
|
44
|
+
installed. `cascade_profiler.image()` supplies the optional Modal GPU image;
|
|
45
|
+
`register(app, volume, gpu="H100!")` registers only the GPU `.profile` function.
|
|
46
|
+
See [PROFILER_RUNS.md](PROFILER_RUNS.md) and [examples/modal_app.py](examples/modal_app.py).
|
|
47
|
+
|
|
48
|
+
## Profile, then simulate
|
|
49
|
+
|
|
50
|
+
From this checkout:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
modal run accelsim_trace.py::e2e_main --run-name my-run --trace-steps plan
|
|
54
|
+
# Timing only (no CPU simulator image is built):
|
|
55
|
+
modal run accelsim_trace.py::e2e_main --run-name timing-run --timing-only
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Accel-Sim CPU execution lives in the separate `cascade.sim` simulator package on
|
|
59
|
+
the `simulator` branch of [atoniolo76/cascade](https://github.com/atoniolo76/cascade).
|
|
60
|
+
From that checkout, using the same Modal volume:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
modal run scripts/simulate_accelsim.py --run-name my-run --gpu-config SM90_H100 --propagate interp
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The run directory is the interface: `run.json`, `corrected_timeline.json`,
|
|
67
|
+
`trace_alignment.json`, and `traced/traces/` are produced by the profiler. The
|
|
68
|
+
simulator consumes them and writes `sim_durations.json` and `sim/`.
|
|
69
|
+
The profiler does not build or execute the CPU simulator.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Cascade Profiler
|
|
2
|
+
|
|
3
|
+
GPU workload profiling, Nsight Systems timing reconciliation, and NVBit instruction
|
|
4
|
+
tracing for vLLM. Python 3.10+ can import and inspect runs; GPU workloads require
|
|
5
|
+
CUDA, vLLM, and the tracing tools. Modal images use Python 3.12.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pip install 'cascade-profiler[modal]'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from cascade_profiler import Workload, profile
|
|
13
|
+
|
|
14
|
+
workload = Workload("facebook/opt-125m", ["Hello, my name is"], max_tokens=32, repeats=4)
|
|
15
|
+
result = profile(workload, "/data/phases", trace_steps="plan", trace_repeat=1)
|
|
16
|
+
print(result.run_dir, result.stage_wall_s)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`profile` needs a GPU environment with vLLM, Nsight Systems and the NVBit tracer
|
|
20
|
+
installed. `cascade_profiler.image()` supplies the optional Modal GPU image;
|
|
21
|
+
`register(app, volume, gpu="H100!")` registers only the GPU `.profile` function.
|
|
22
|
+
See [PROFILER_RUNS.md](PROFILER_RUNS.md) and [examples/modal_app.py](examples/modal_app.py).
|
|
23
|
+
|
|
24
|
+
## Profile, then simulate
|
|
25
|
+
|
|
26
|
+
From this checkout:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
modal run accelsim_trace.py::e2e_main --run-name my-run --trace-steps plan
|
|
30
|
+
# Timing only (no CPU simulator image is built):
|
|
31
|
+
modal run accelsim_trace.py::e2e_main --run-name timing-run --timing-only
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Accel-Sim CPU execution lives in the separate `cascade.sim` simulator package on
|
|
35
|
+
the `simulator` branch of [atoniolo76/cascade](https://github.com/atoniolo76/cascade).
|
|
36
|
+
From that checkout, using the same Modal volume:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
modal run scripts/simulate_accelsim.py --run-name my-run --gpu-config SM90_H100 --propagate interp
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The run directory is the interface: `run.json`, `corrected_timeline.json`,
|
|
43
|
+
`trace_alignment.json`, and `traced/traces/` are produced by the profiler. The
|
|
44
|
+
simulator consumes them and writes `sim_durations.json` and `sim/`.
|
|
45
|
+
The profiler does not build or execute the CPU simulator.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Profiler orchestration; GPU and Modal dependencies are optional."""
|
|
2
|
+
|
|
3
|
+
from cascade_profiler.pipeline import RunResult, profile
|
|
4
|
+
from cascade_profiler.planning import trace_plan
|
|
5
|
+
from cascade_profiler.workload import Workload
|
|
6
|
+
|
|
7
|
+
__all__ = ["RunResult", "Workload", "image", "profile", "register", "trace_plan"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def __getattr__(name):
|
|
11
|
+
if name in {"image", "register"}:
|
|
12
|
+
from cascade_profiler import modal_support
|
|
13
|
+
return getattr(modal_support, name)
|
|
14
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Ordered, step-local instruction-trace to timing alignment (no GPU dependencies)."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from difflib import SequenceMatcher
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def kernel_key(kernel: dict) -> tuple:
|
|
11
|
+
def dims(value):
|
|
12
|
+
return tuple(map(int, value.split("x") if isinstance(value, str) else value))
|
|
13
|
+
return (kernel.get("mangled_name") or kernel["name"], dims(kernel["grid"]), dims(kernel["block"]))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def read_launches(traces: Path) -> list[dict]:
|
|
17
|
+
launches = []
|
|
18
|
+
for path in sorted(traces.glob("stats_ctx_*")):
|
|
19
|
+
with path.open() as stream:
|
|
20
|
+
for row in csv.reader(stream):
|
|
21
|
+
if not row or not re.fullmatch(r"kernel-(\d+)-ctx_.*\.trace(?:\.xz)?", row[0].strip()):
|
|
22
|
+
continue
|
|
23
|
+
if len(row) < 12:
|
|
24
|
+
raise ValueError(f"malformed tracer stats row in {path}: {row}")
|
|
25
|
+
filename = row[0].strip()
|
|
26
|
+
raw = traces / filename
|
|
27
|
+
processed = traces / (filename.removesuffix(".xz") + "z")
|
|
28
|
+
launches.append({"tracer_uid": int(re.match(r"kernel-(\d+)-", filename)[1]),
|
|
29
|
+
"trace_file": filename, "name": row[1].strip(),
|
|
30
|
+
"grid": list(map(int, row[2:5])), "block": list(map(int, row[6:9])),
|
|
31
|
+
"traced": int(row[10]) > 0 or raw.is_file() or processed.is_file()})
|
|
32
|
+
ids = [r["tracer_uid"] for r in launches]
|
|
33
|
+
if len(set(ids)) != len(ids):
|
|
34
|
+
raise ValueError("duplicate tracer UIDs across contexts; cannot identify kernels unambiguously")
|
|
35
|
+
return sorted(launches, key=lambda r: r["tracer_uid"])
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def indexed_steps(steps: list[dict]) -> dict:
|
|
39
|
+
counts, result = {}, {}
|
|
40
|
+
for step in sorted(steps, key=lambda s: s["step"]):
|
|
41
|
+
repeat = step["repeat"]
|
|
42
|
+
local = step.get("step_in_repeat", counts.get(repeat, 0))
|
|
43
|
+
identity = repeat, local
|
|
44
|
+
if identity in result:
|
|
45
|
+
raise ValueError(f"duplicate repeat-local step {identity}")
|
|
46
|
+
result[identity] = step
|
|
47
|
+
counts[repeat] = local + 1
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def align_step(tracer: list[dict], timeline: list[dict]) -> list[tuple[int, int]]:
|
|
52
|
+
"""Dimension-exact anchors first; name-only fallback within the remaining ordered gaps."""
|
|
53
|
+
a, b = [kernel_key(k) for k in tracer], [kernel_key(k) for k in timeline]
|
|
54
|
+
matches = []
|
|
55
|
+
for tag, i, end_i, j, end_j in SequenceMatcher(a=a, b=b, autojunk=False).get_opcodes():
|
|
56
|
+
if tag == "equal":
|
|
57
|
+
matches.extend(zip(range(i, end_i), range(j, end_j)))
|
|
58
|
+
elif tag == "replace":
|
|
59
|
+
fallback = SequenceMatcher(a=[k[0] for k in a[i:end_i]],
|
|
60
|
+
b=[k[0] for k in b[j:end_j]], autojunk=False)
|
|
61
|
+
for block in fallback.get_matching_blocks():
|
|
62
|
+
matches.extend((i + block.a + n, j + block.b + n) for n in range(block.size))
|
|
63
|
+
return matches
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def build_alignment(timeline: dict, trace_steps: list[dict], launches: list[dict],
|
|
67
|
+
provenance: dict | None = None) -> dict:
|
|
68
|
+
targets = indexed_steps(timeline["steps"])
|
|
69
|
+
steps, kernels, missing_tracer, missing_timeline = [], [], [], []
|
|
70
|
+
used = set()
|
|
71
|
+
for identity, trace_step in indexed_steps(trace_steps).items():
|
|
72
|
+
if not trace_step.get("traced"):
|
|
73
|
+
continue
|
|
74
|
+
first, end = trace_step.get("first_kernel_id"), trace_step.get("end_kernel_id")
|
|
75
|
+
if first is None or end is None:
|
|
76
|
+
raise ValueError(f"missing tracer kernel-id range for step {identity}")
|
|
77
|
+
source = [k for k in launches if k["traced"] and first <= k["tracer_uid"] < end]
|
|
78
|
+
target = targets.get(identity)
|
|
79
|
+
expected = target["kernels"] if target else []
|
|
80
|
+
pairs = align_step(source, expected)
|
|
81
|
+
matched_a, matched_b = {a for a, _ in pairs}, {b for _, b in pairs}
|
|
82
|
+
for a, b in pairs:
|
|
83
|
+
launch, kernel = source[a], expected[b]
|
|
84
|
+
if launch["tracer_uid"] in used:
|
|
85
|
+
raise ValueError("overlapping traced step kernel-id ranges")
|
|
86
|
+
used.add(launch["tracer_uid"])
|
|
87
|
+
kernels.append({"tracer_uid": launch["tracer_uid"], "trace_file": launch["trace_file"],
|
|
88
|
+
"repeat": identity[0], "step": target["step"], "seq": kernel["seq"],
|
|
89
|
+
"grid": launch["grid"], "block": launch["block"],
|
|
90
|
+
"name": kernel_key(kernel)[0], "dims_match": kernel_key(launch) == kernel_key(kernel)})
|
|
91
|
+
missing_tracer.extend({**k, "repeat": identity[0], "step_in_repeat": identity[1]}
|
|
92
|
+
for n, k in enumerate(source) if n not in matched_a)
|
|
93
|
+
missing_timeline.extend({"repeat": identity[0], "step": target["step"], "seq": k["seq"], "name": k["name"]}
|
|
94
|
+
for n, k in enumerate(expected) if n not in matched_b)
|
|
95
|
+
steps.append({"repeat": identity[0], "step": target["step"] if target else None,
|
|
96
|
+
"step_in_repeat": identity[1], "n_tracer": len(source), "n_timeline": len(expected),
|
|
97
|
+
"matched": len(pairs), "unmatched_tracer": len(source) - len(pairs),
|
|
98
|
+
"unmatched_timeline": len(expected) - len(pairs),
|
|
99
|
+
"exact": target is not None and [kernel_key(k) for k in source] ==
|
|
100
|
+
[kernel_key(k) for k in expected]})
|
|
101
|
+
return {"provenance": {**(provenance or {}), "run_name": timeline["meta"].get("run_name"),
|
|
102
|
+
"traced_steps": [[s["repeat"], s["step_in_repeat"]] for s in steps],
|
|
103
|
+
"traced_timing_used": False}, "steps": steps, "kernels": kernels,
|
|
104
|
+
"unmatched": {"n_tracer": len(missing_tracer), "n_timeline": len(missing_timeline),
|
|
105
|
+
"tracer": missing_tracer, "timeline": missing_timeline}}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def write_alignment(run_dir: str | Path) -> dict:
|
|
109
|
+
run = Path(run_dir)
|
|
110
|
+
manifest = json.loads((run / "run.json").read_text())
|
|
111
|
+
result = build_alignment(json.loads((run / "corrected_timeline.json").read_text()),
|
|
112
|
+
json.loads((run / "traced/trace_steps.json").read_text()),
|
|
113
|
+
read_launches(run / "traced/traces"),
|
|
114
|
+
{"trace_repeat": manifest["trace_repeat"]})
|
|
115
|
+
(run / "trace_alignment.json").write_text(json.dumps(result, indent=2))
|
|
116
|
+
return result
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _write_clock_snapshot(path: Path):
|
|
8
|
+
"""nvidia-smi clock/throttle snapshot (supported, current, max, application clocks)."""
|
|
9
|
+
out = subprocess.run(["nvidia-smi", "-q", "-d", "CLOCK,PERFORMANCE,POWER"],
|
|
10
|
+
capture_output=True, text=True)
|
|
11
|
+
path.write_text(out.stdout + out.stderr)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ClockSampler(threading.Thread):
|
|
15
|
+
"""Samples SM/mem clocks + throttle reasons via NVML every `period_s` into a CSV.
|
|
16
|
+
|
|
17
|
+
Each row carries wall_ns (time.time_ns, same clock as vllm_trace.py's step log) so samples
|
|
18
|
+
can be mapped onto the nsys timeline through the step NVTX ranges.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
COLS = ["wall_ns", "sm_mhz", "mem_mhz", "gr_mhz", "power_w", "temp_c", "throttle_reasons_hex",
|
|
22
|
+
"throttle_reasons"]
|
|
23
|
+
|
|
24
|
+
def __init__(self, path: Path, period_s: float = 0.05):
|
|
25
|
+
super().__init__(daemon=True, name="clock-sampler")
|
|
26
|
+
self.path, self.period_s = path, period_s
|
|
27
|
+
self._stop_event = threading.Event()
|
|
28
|
+
self.rows: list[dict] = []
|
|
29
|
+
self.error: str | None = None
|
|
30
|
+
|
|
31
|
+
def run(self):
|
|
32
|
+
try:
|
|
33
|
+
import pynvml as nv # nvidia-ml-py, a vLLM dependency
|
|
34
|
+
nv.nvmlInit()
|
|
35
|
+
h = nv.nvmlDeviceGetHandleByIndex(0)
|
|
36
|
+
get_reasons = getattr(nv, "nvmlDeviceGetCurrentClocksEventReasons", None) or \
|
|
37
|
+
nv.nvmlDeviceGetCurrentClocksThrottleReasons
|
|
38
|
+
names = {getattr(nv, a): a.split("Reason", 1)[1] for a in dir(nv)
|
|
39
|
+
if a.startswith(("nvmlClocksThrottleReason", "nvmlClocksEventReason"))
|
|
40
|
+
and "All" not in a and "None" not in a and isinstance(getattr(nv, a), int)}
|
|
41
|
+
while not self._stop_event.is_set():
|
|
42
|
+
r = get_reasons(h)
|
|
43
|
+
self.rows.append({
|
|
44
|
+
"wall_ns": time.time_ns(),
|
|
45
|
+
"sm_mhz": nv.nvmlDeviceGetClockInfo(h, nv.NVML_CLOCK_SM),
|
|
46
|
+
"mem_mhz": nv.nvmlDeviceGetClockInfo(h, nv.NVML_CLOCK_MEM),
|
|
47
|
+
"gr_mhz": nv.nvmlDeviceGetClockInfo(h, nv.NVML_CLOCK_GRAPHICS),
|
|
48
|
+
"power_w": nv.nvmlDeviceGetPowerUsage(h) / 1000,
|
|
49
|
+
"temp_c": nv.nvmlDeviceGetTemperature(h, nv.NVML_TEMPERATURE_GPU),
|
|
50
|
+
"throttle_reasons_hex": hex(r),
|
|
51
|
+
"throttle_reasons": "|".join(sorted({n for b, n in names.items() if r & b})),
|
|
52
|
+
})
|
|
53
|
+
self._stop_event.wait(self.period_s)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
self.error = repr(e)
|
|
56
|
+
print(f"[clocks] NVML sampling failed: {e!r}", flush=True)
|
|
57
|
+
|
|
58
|
+
def stop(self):
|
|
59
|
+
self._stop_event.set()
|
|
60
|
+
self.join(10)
|
|
61
|
+
import csv
|
|
62
|
+
with open(self.path, "w", newline="") as f:
|
|
63
|
+
w = csv.DictWriter(f, fieldnames=self.COLS)
|
|
64
|
+
w.writeheader()
|
|
65
|
+
w.writerows(self.rows)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _lock_clocks(mhz: int) -> dict:
|
|
69
|
+
"""Try to lock SM clocks (needs root on the host; usually refused in containers)."""
|
|
70
|
+
res = {"requested_sm_mhz": mhz}
|
|
71
|
+
for key, cmd in (("lgc", ["nvidia-smi", "-lgc", f"{mhz},{mhz}"]),):
|
|
72
|
+
r = subprocess.run(cmd, capture_output=True, text=True)
|
|
73
|
+
res[key] = {"returncode": r.returncode, "output": (r.stdout + r.stderr).strip()}
|
|
74
|
+
res["locked"] = res["lgc"]["returncode"] == 0
|
|
75
|
+
return res
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _clock_stats(rows: list[dict], t0_ns: int | None, t1_ns: int | None) -> dict:
|
|
79
|
+
import statistics
|
|
80
|
+
sel = [r for r in rows if t0_ns is None or t0_ns <= r["wall_ns"] <= t1_ns] or rows
|
|
81
|
+
if not sel:
|
|
82
|
+
return {}
|
|
83
|
+
out = {"samples": len(sel)}
|
|
84
|
+
for k in ("sm_mhz", "mem_mhz"):
|
|
85
|
+
v = [r[k] for r in sel]
|
|
86
|
+
out[k] = {"min": min(v), "median": statistics.median(v), "max": max(v)}
|
|
87
|
+
out["throttle_reasons_seen"] = sorted({x for r in sel for x in r["throttle_reasons"].split("|") if x})
|
|
88
|
+
return out
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _gpu_name() -> str:
|
|
92
|
+
return subprocess.run(["nvidia-smi", "--query-gpu=name,driver_version", "--format=csv,noheader"],
|
|
93
|
+
capture_output=True, text=True).stdout.strip()
|