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
inferencekit/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""inferencekit: measure-first LLM inference optimisation.
|
|
2
|
+
|
|
3
|
+
Every operation produces an *evidence record*: a small JSON document that says what
|
|
4
|
+
was measured, on which GPU / engine / model / workload, with which tool, and what
|
|
5
|
+
the result does NOT show (``limitations``). Records are content-addressed, so a
|
|
6
|
+
report can cite exactly the numbers it used.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
inferencekit/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Pure parsers and analyses (no subprocesses, no GPU): easy to unit-test on synthetic inputs."""
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Parse Nsight Compute CSV exports into one summary per profiled kernel launch.
|
|
2
|
+
|
|
3
|
+
Two CSV layouts, both from ``ncu --import report.ncu-rep --csv``:
|
|
4
|
+
|
|
5
|
+
--page raw wide: row 1 = metric names, row 2 = units, then one row per launch.
|
|
6
|
+
Exact metric names (dram__bytes_read.sum, sm__throughput...).
|
|
7
|
+
--page details long: one row per (launch, section, metric) with human names
|
|
8
|
+
("DRAM Throughput", "Achieved Occupancy").
|
|
9
|
+
|
|
10
|
+
Raw is preferred; details fills whatever raw lacks. Values are converted to base
|
|
11
|
+
units (ns, bytes, bytes/s) using the unit row, because ncu picks units per value
|
|
12
|
+
(usecond for one kernel, msecond for another) and its spelling changes between
|
|
13
|
+
releases ("Gbyte/second" in older ones, "Gbyte/s" and "us" in newer ones). Units are
|
|
14
|
+
parsed by structure (SI prefix + byte + optional per-second), and every field says
|
|
15
|
+
which kind of quantity it must be. A value whose unit is unknown, or of the wrong kind
|
|
16
|
+
(a "%" where a byte rate is expected), becomes None and is listed in
|
|
17
|
+
``unparsed_units``; it is never silently read as base units.
|
|
18
|
+
|
|
19
|
+
The "bound" label is a speed-of-light rule of thumb: ncu reports each unit's
|
|
20
|
+
throughput as % of its peak. If DRAM or SM is >= 60% of peak, that unit is the
|
|
21
|
+
limiter. If both are < 40%, neither is busy: the kernel is waiting (latency-bound:
|
|
22
|
+
too little parallelism, dependencies, or launch/tail effects). In between: mixed.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import csv
|
|
28
|
+
import io
|
|
29
|
+
import re
|
|
30
|
+
|
|
31
|
+
# Time units -> nanoseconds.
|
|
32
|
+
TIME_NS = {"nsecond": 1.0, "ns": 1.0, "usecond": 1e3, "us": 1e3, "msecond": 1e6, "ms": 1e6, "second": 1e9, "s": 1e9}
|
|
33
|
+
SI_PREFIX = {"": 1.0, "K": 1e3, "M": 1e6, "G": 1e9, "T": 1e12}
|
|
34
|
+
# Byte amounts and byte rates: "byte", "Kbyte", "GB", "Gbyte/second", "Gbyte/s", "TB/s"...
|
|
35
|
+
BYTES_RX = re.compile(r"^(?P<prefix>[KMGT]?)(?:byte|B)(?P<rate>/(?:second|s))?$")
|
|
36
|
+
# Units that carry a physical dimension we do not convert (cycle/nsecond, Gbyte/ms, Hz...).
|
|
37
|
+
UNKNOWN_DIMENSION_RX = re.compile(r"byte|second|^[KMGT]?B(/|$)|hz|watt|/(?:[num]?s)$", re.IGNORECASE)
|
|
38
|
+
|
|
39
|
+
# What each field must be. Fields not listed are counts (registers, grid size, waves...).
|
|
40
|
+
FIELD_KIND = {"duration_ns": "time", "dram_read_bytes": "bytes", "dram_write_bytes": "bytes",
|
|
41
|
+
"dram_bytes_per_s": "byte_rate", "dram_pct": "percent", "sm_pct": "percent", "mem_pct": "percent",
|
|
42
|
+
"occupancy_achieved_pct": "percent", "occupancy_theoretical_pct": "percent", "l2_hit_pct": "percent"}
|
|
43
|
+
|
|
44
|
+
# field -> raw-page metric names, first present wins
|
|
45
|
+
RAW_FIELDS = {
|
|
46
|
+
"duration_ns": ["gpu__time_duration.sum"],
|
|
47
|
+
# Metric names differ across Nsight Compute releases (2025.x uses the *_op_* spelling).
|
|
48
|
+
"dram_read_bytes": ["dram__bytes_read.sum", "dram__bytes_op_read.sum"],
|
|
49
|
+
"dram_write_bytes": ["dram__bytes_write.sum", "dram__bytes_op_write.sum"],
|
|
50
|
+
"dram_pct": ["gpu__dram_throughput.avg.pct_of_peak_sustained_elapsed",
|
|
51
|
+
"dram__throughput.avg.pct_of_peak_sustained_elapsed"],
|
|
52
|
+
"sm_pct": ["sm__throughput.avg.pct_of_peak_sustained_elapsed"],
|
|
53
|
+
"mem_pct": ["gpu__compute_memory_throughput.avg.pct_of_peak_sustained_elapsed"],
|
|
54
|
+
"occupancy_achieved_pct": ["sm__warps_active.avg.pct_of_peak_sustained_active"],
|
|
55
|
+
"occupancy_theoretical_pct": ["sm__maximum_warps_per_active_cycle_pct"],
|
|
56
|
+
"registers": ["launch__registers_per_thread"],
|
|
57
|
+
"waves": ["launch__waves_per_multiprocessor"],
|
|
58
|
+
"grid": ["launch__grid_size"],
|
|
59
|
+
"block": ["launch__block_size"],
|
|
60
|
+
"limit_registers": ["launch__occupancy_limit_registers"],
|
|
61
|
+
"limit_shared_mem": ["launch__occupancy_limit_shared_mem"],
|
|
62
|
+
"limit_warps": ["launch__occupancy_limit_warps"],
|
|
63
|
+
"limit_blocks": ["launch__occupancy_limit_blocks"],
|
|
64
|
+
"l2_hit_pct": ["lts__t_sector_hit_rate.pct"],
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# field -> (section, metric) in the details page; section None = any section
|
|
68
|
+
DETAIL_FIELDS = {
|
|
69
|
+
"duration_ns": [("GPU Speed Of Light Throughput", "Duration")],
|
|
70
|
+
"dram_pct": [("GPU Speed Of Light Throughput", "DRAM Throughput")],
|
|
71
|
+
"sm_pct": [("GPU Speed Of Light Throughput", "Compute (SM) Throughput")],
|
|
72
|
+
"mem_pct": [("GPU Speed Of Light Throughput", "Memory Throughput")],
|
|
73
|
+
"dram_bytes_per_s": [("Memory Workload Analysis", "Memory Throughput")],
|
|
74
|
+
"occupancy_achieved_pct": [("Occupancy", "Achieved Occupancy")],
|
|
75
|
+
"occupancy_theoretical_pct": [("Occupancy", "Theoretical Occupancy")],
|
|
76
|
+
"registers": [("Launch Statistics", "Registers Per Thread")],
|
|
77
|
+
"waves": [("Launch Statistics", "Waves Per SM")],
|
|
78
|
+
"grid": [("Launch Statistics", "Grid Size")],
|
|
79
|
+
"block": [("Launch Statistics", "Block Size")],
|
|
80
|
+
"limit_registers": [("Occupancy", "Block Limit Registers")],
|
|
81
|
+
"limit_shared_mem": [("Occupancy", "Block Limit Shared Mem")],
|
|
82
|
+
"limit_warps": [("Occupancy", "Block Limit Warps")],
|
|
83
|
+
"limit_blocks": [("Occupancy", "Block Limit SM")],
|
|
84
|
+
"l2_hit_pct": [("Memory Workload Analysis", "L2 Hit Rate")],
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Warp stall reasons: cycles stalled per issued instruction, or PC-sampling counts.
|
|
88
|
+
STALL_RATIO_RX = re.compile(r"^smsp__average_warps_issue_stalled_(.+)_per_issue_active\.ratio$")
|
|
89
|
+
STALL_PCSAMP_RX = re.compile(r"^smsp__pcsamp_warps_issue_stalled_(.+?)(?:\.sum)?$")
|
|
90
|
+
NOT_A_STALL = {"selected", "not_selected"} # "selected" = the warp issued; not a stall
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def unit_kind(unit: str) -> tuple[str, float] | None:
|
|
94
|
+
"""(kind, factor to base units) for an ncu unit string, or None when it cannot be converted.
|
|
95
|
+
|
|
96
|
+
'usecond' -> ('time', 1e3) 'Gbyte/s' -> ('byte_rate', 1e9) 'Kbyte' -> ('bytes', 1e3)
|
|
97
|
+
'%' -> ('percent', 1.0) 'block', 'register/thread', '' -> ('count', 1.0)
|
|
98
|
+
'cycle/nsecond', 'Gbyte/ms' -> None (a dimension this parser does not handle)
|
|
99
|
+
"""
|
|
100
|
+
u = (unit or "").strip()
|
|
101
|
+
if u == "%":
|
|
102
|
+
return "percent", 1.0
|
|
103
|
+
if u in TIME_NS:
|
|
104
|
+
return "time", TIME_NS[u]
|
|
105
|
+
m = BYTES_RX.match(u)
|
|
106
|
+
if m:
|
|
107
|
+
return ("byte_rate" if m["rate"] else "bytes"), SI_PREFIX[m["prefix"]]
|
|
108
|
+
if UNKNOWN_DIMENSION_RX.search(u):
|
|
109
|
+
return None
|
|
110
|
+
return "count", 1.0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def to_number(text, unit: str = "", kind: str | None = None) -> float | None:
|
|
114
|
+
"""'1,234.5' + 'usecond' -> 1234500.0 (ns). Non-numeric -> None.
|
|
115
|
+
|
|
116
|
+
``kind`` is what the caller needs ('time', 'bytes', 'byte_rate', 'percent', 'count').
|
|
117
|
+
Returns None when the unit is unknown or of another kind: 763 'Gbyte/s' must never be
|
|
118
|
+
taken as 763 bytes/s, and a '%' must never be taken as a byte rate. A percent field
|
|
119
|
+
also accepts a bare number (no unit).
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
x = float(str(text).replace(",", "").strip())
|
|
123
|
+
except ValueError:
|
|
124
|
+
return None
|
|
125
|
+
found = unit_kind(unit)
|
|
126
|
+
if found is None:
|
|
127
|
+
return None
|
|
128
|
+
got, factor = found
|
|
129
|
+
if kind is not None and got != kind and not (kind == "percent" and got == "count" and not unit.strip()):
|
|
130
|
+
return None
|
|
131
|
+
return x * factor
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _field_value(field: str, text: str, unit: str, problems: list[str]) -> float | None:
|
|
135
|
+
"""Convert one field's value; note it in ``problems`` if it was there but could not be read."""
|
|
136
|
+
value = to_number(text, unit, FIELD_KIND.get(field, "count"))
|
|
137
|
+
if value is None and str(text).replace(",", "").strip() not in ("", "n/a", "N/A"):
|
|
138
|
+
problems.append(f"{field}: {text!r} {unit!r}")
|
|
139
|
+
return value
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _csv_rows(text: str) -> list[list[str]]:
|
|
143
|
+
# ncu may print "==PROF==" or warning lines around the CSV; keep the quoted CSV lines only.
|
|
144
|
+
lines = [ln for ln in text.splitlines() if ln.startswith('"')]
|
|
145
|
+
return list(csv.reader(io.StringIO("\n".join(lines))))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def parse_raw(text: str) -> list[dict]:
|
|
149
|
+
"""--page raw --csv -> [{id, kernel, fields..., stalls}] in profiling order."""
|
|
150
|
+
rows = _csv_rows(text)
|
|
151
|
+
if len(rows) < 2:
|
|
152
|
+
return []
|
|
153
|
+
header, units, data = rows[0], rows[1], rows[2:]
|
|
154
|
+
col = {h: i for i, h in enumerate(header)}
|
|
155
|
+
out = []
|
|
156
|
+
for k, row in enumerate(data):
|
|
157
|
+
if len(row) < len(header):
|
|
158
|
+
continue
|
|
159
|
+
rec = {"index": k, "id": row[col["ID"]] if "ID" in col else str(k),
|
|
160
|
+
"kernel": row[col["Kernel Name"]] if "Kernel Name" in col else "", "_problems": []}
|
|
161
|
+
for field, names in RAW_FIELDS.items():
|
|
162
|
+
name = next((n for n in names if n in col), None)
|
|
163
|
+
rec[field] = _field_value(field, row[col[name]], units[col[name]], rec["_problems"]) if name else None
|
|
164
|
+
stalls = {}
|
|
165
|
+
for name, i in col.items():
|
|
166
|
+
m = STALL_PCSAMP_RX.match(name)
|
|
167
|
+
# each reason also has a "_not_issued" twin (a subset): skip it to avoid double counting
|
|
168
|
+
if m and m.group(1) not in NOT_A_STALL and not m.group(1).endswith("not_issued"):
|
|
169
|
+
stalls[m.group(1)] = ("pcsamp", to_number(row[i]))
|
|
170
|
+
if not stalls:
|
|
171
|
+
for name, i in col.items():
|
|
172
|
+
m = STALL_RATIO_RX.match(name)
|
|
173
|
+
if m and m.group(1) not in NOT_A_STALL:
|
|
174
|
+
stalls[m.group(1)] = ("ratio", to_number(row[i]))
|
|
175
|
+
rec["_stalls"] = stalls
|
|
176
|
+
out.append(rec)
|
|
177
|
+
return out
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def parse_details(text: str) -> dict[str, dict]:
|
|
181
|
+
"""--page details --csv -> {launch id: {kernel, fields...}}."""
|
|
182
|
+
rows = _csv_rows(text)
|
|
183
|
+
if len(rows) < 2:
|
|
184
|
+
return {}
|
|
185
|
+
header = rows[0]
|
|
186
|
+
col = {h: i for i, h in enumerate(header)}
|
|
187
|
+
need = ("ID", "Metric Name", "Metric Unit", "Metric Value")
|
|
188
|
+
if any(n not in col for n in need):
|
|
189
|
+
return {}
|
|
190
|
+
values: dict[str, dict] = {}
|
|
191
|
+
for row in rows[1:]:
|
|
192
|
+
if len(row) <= col["Metric Value"]:
|
|
193
|
+
continue
|
|
194
|
+
kid = row[col["ID"]]
|
|
195
|
+
d = values.setdefault(kid, {"kernel": row[col["Kernel Name"]] if "Kernel Name" in col else "", "_m": {}})
|
|
196
|
+
section = row[col["Section Name"]] if "Section Name" in col else ""
|
|
197
|
+
d["_m"][(section, row[col["Metric Name"]])] = (row[col["Metric Value"]], row[col["Metric Unit"]])
|
|
198
|
+
out = {}
|
|
199
|
+
for kid, d in values.items():
|
|
200
|
+
rec = {"kernel": d["kernel"], "_problems": []}
|
|
201
|
+
for field, keys in DETAIL_FIELDS.items():
|
|
202
|
+
rec[field] = None
|
|
203
|
+
for section, name in keys:
|
|
204
|
+
hit = d["_m"].get((section, name))
|
|
205
|
+
if hit is None: # section renamed in this ncu version: accept the name if it is unambiguous
|
|
206
|
+
same = [v for (_s, n), v in d["_m"].items() if n == name]
|
|
207
|
+
hit = same[0] if len(same) == 1 else None
|
|
208
|
+
if hit is not None:
|
|
209
|
+
# With --set basic there is no Memory Workload Analysis section, and the only
|
|
210
|
+
# "Memory Throughput" left is the Speed Of Light one, in %. The kind check
|
|
211
|
+
# rejects it for the byte-rate field instead of reading 43% as 43 bytes/s.
|
|
212
|
+
rec[field] = _field_value(field, *hit, rec["_problems"])
|
|
213
|
+
break
|
|
214
|
+
out[kid] = rec
|
|
215
|
+
return out
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def bound_label(dram_pct: float | None, sm_pct: float | None) -> str:
|
|
219
|
+
if dram_pct is None and sm_pct is None:
|
|
220
|
+
return "unknown"
|
|
221
|
+
d, s = dram_pct or 0.0, sm_pct or 0.0
|
|
222
|
+
if max(d, s) >= 60:
|
|
223
|
+
return "memory-bound" if d >= s else "compute-bound"
|
|
224
|
+
if d < 40 and s < 40:
|
|
225
|
+
return "latency-bound"
|
|
226
|
+
return "mixed"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def occupancy_limiter(rec: dict) -> str | None:
|
|
230
|
+
"""Which resource caps blocks per SM: the smallest of the per-resource block limits."""
|
|
231
|
+
limits = {"registers": rec.get("limit_registers"), "shared memory": rec.get("limit_shared_mem"),
|
|
232
|
+
"warps": rec.get("limit_warps"), "blocks per SM": rec.get("limit_blocks")}
|
|
233
|
+
limits = {k: v for k, v in limits.items() if v is not None}
|
|
234
|
+
return min(limits, key=limits.get) if limits else None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def summarize(raw_text: str = "", details_text: str = "", peak_gbs: float | None = None) -> list[dict]:
|
|
238
|
+
"""Merge both pages and derive GB/s, % of measured peak, limiter, stalls, bound label."""
|
|
239
|
+
raw = parse_raw(raw_text) if raw_text else []
|
|
240
|
+
details = parse_details(details_text) if details_text else {}
|
|
241
|
+
if not raw: # details only: build records from it, in id order
|
|
242
|
+
raw = [{"index": i, "id": kid, **rec, "_stalls": {}}
|
|
243
|
+
for i, (kid, rec) in enumerate(sorted(details.items(), key=lambda kv: _int_or(kv[0])))]
|
|
244
|
+
details = {} # every field is in raw now; nothing left to merge
|
|
245
|
+
out = []
|
|
246
|
+
for rec in raw:
|
|
247
|
+
extra = details.get(rec["id"], {})
|
|
248
|
+
problems = rec.pop("_problems", []) + extra.get("_problems", [])
|
|
249
|
+
for k, v in extra.items():
|
|
250
|
+
if k != "_problems" and rec.get(k) is None and v is not None:
|
|
251
|
+
rec[k] = v
|
|
252
|
+
dur = rec.get("duration_ns")
|
|
253
|
+
moved = (rec.get("dram_read_bytes") or 0) + (rec.get("dram_write_bytes") or 0)
|
|
254
|
+
gbs = moved / dur if dur and moved else None # bytes per ns == GB/s
|
|
255
|
+
if gbs is None and rec.get("dram_bytes_per_s") is not None:
|
|
256
|
+
gbs = rec["dram_bytes_per_s"] / 1e9
|
|
257
|
+
stalls = rec.pop("_stalls", {})
|
|
258
|
+
total = sum(v for _, v in stalls.values() if v) or 0.0
|
|
259
|
+
top = sorted(((v or 0.0, name, how) for name, (how, v) in stalls.items()), reverse=True)[:4]
|
|
260
|
+
out.append({
|
|
261
|
+
"index": rec["index"], "id": rec["id"], "kernel": rec.get("kernel", "")[:200],
|
|
262
|
+
"duration_us": round(dur / 1e3, 3) if dur else None,
|
|
263
|
+
"dram_gbs": round(gbs, 1) if gbs is not None else None,
|
|
264
|
+
"dram_pct_of_measured_peak": round(100 * gbs / peak_gbs, 1) if gbs is not None and peak_gbs else None,
|
|
265
|
+
"dram_pct": rec.get("dram_pct"), "sm_pct": rec.get("sm_pct"), "mem_pct": rec.get("mem_pct"),
|
|
266
|
+
"occupancy_achieved_pct": rec.get("occupancy_achieved_pct"),
|
|
267
|
+
"occupancy_theoretical_pct": rec.get("occupancy_theoretical_pct"),
|
|
268
|
+
"occupancy_limiter": occupancy_limiter(rec),
|
|
269
|
+
"registers": rec.get("registers"), "waves": rec.get("waves"), "grid": rec.get("grid"),
|
|
270
|
+
"block": rec.get("block"), "l2_hit_pct": rec.get("l2_hit_pct"),
|
|
271
|
+
"top_stalls": [{"reason": name, "value": round(v, 3), "share_pct": round(100 * v / total, 1) if total
|
|
272
|
+
else None, "source": how} for v, name, how in top if v > 0],
|
|
273
|
+
"bound": bound_label(rec.get("dram_pct") if rec.get("dram_pct") is not None else rec.get("mem_pct"),
|
|
274
|
+
rec.get("sm_pct")),
|
|
275
|
+
"unparsed_units": problems,
|
|
276
|
+
})
|
|
277
|
+
return out
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _int_or(text: str):
|
|
281
|
+
try:
|
|
282
|
+
return (0, int(text))
|
|
283
|
+
except ValueError:
|
|
284
|
+
return (1, text)
|