hotkernel 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.
hotkernel/server.py ADDED
@@ -0,0 +1,975 @@
1
+ """hotkernel MCP server.
2
+
3
+ Tools (all return JSON-serialisable dicts; errors come back as {"error": ...} so the agent
4
+ can read them instead of crashing):
5
+
6
+ gpu_info what card is in this machine, driver, memory, power limit, compute capability
7
+ roofline ridge point for this card; optionally where a kernel sits given flops/bytes
8
+ bench run a script n times, collect the JSON timing line it prints, return mean/std/min
9
+ torch_trace run a script under torch.profiler, return the top CUDA kernels by GPU time
10
+ profile run a command under Nsight Systems or Nsight Compute, return a parsed report
11
+ top_kernels read a saved profile report and return the n heaviest kernels
12
+ disasm PTX/SASS for a cubin/ptx file or a Triton-cache kernel via cuobjdump/nvdisasm
13
+ verify baseline vs candidate, interleaved, n runs each: speedup + verdict
14
+ (faster / slower / inconclusive / incorrect / invalid)
15
+ record write a verify result to results/<target>/ and append the LEADERBOARD row
16
+
17
+ Design rules:
18
+ * hotkernel's own environment stays light (no torch). Anything that needs the user's
19
+ PyTorch/Triton runs in the user's interpreter (HOTKERNEL_PYTHON or `python` on PATH).
20
+ * every timing result carries n, mean, std, min and the exact command, so the agent can
21
+ write an honest LEADERBOARD row ("measured, not claimed").
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import csv
27
+ import functools
28
+ import glob
29
+ import io
30
+ import json
31
+ import math
32
+ import os
33
+ import re
34
+ import shutil
35
+ import statistics
36
+ import subprocess
37
+ import sys
38
+ import tempfile
39
+ import time
40
+ from pathlib import Path
41
+
42
+ try: # mcp >= 2.0 renamed FastMCP to MCPServer; the decorator API is the same.
43
+ from mcp.server.mcpserver import MCPServer as _Server
44
+ except ImportError: # mcp 1.x
45
+ from mcp.server.fastmcp import FastMCP as _Server
46
+
47
+ from . import __version__, gpus
48
+
49
+ _INSTRUCTIONS = (
50
+ "GPU kernel profiling tools for the card in this machine. Start with gpu_info, then "
51
+ "bench(baseline, n>=5) for the number to beat, then torch_trace to find the dominant "
52
+ "kernel, roofline to classify it, disasm('triton-cache', kernel=...) to read its SASS. "
53
+ "bench and torch_trace run the script with HOTKERNEL_PYTHON (your torch/Triton "
54
+ "interpreter). profile needs nsys/ncu on PATH; without them use torch_trace. Every "
55
+ "timing carries n/mean/std; report speedups only against the repo baseline."
56
+ )
57
+ try:
58
+ mcp = _Server("hotkernel", version=__version__, instructions=_INSTRUCTIONS)
59
+ except TypeError: # older mcp 1.x FastMCP without a `version` argument
60
+ mcp = _Server("hotkernel", instructions=_INSTRUCTIONS)
61
+
62
+ _DEFAULT_TIMEOUT = 600
63
+
64
+
65
+ # --------------------------------------------------------------------------- helpers
66
+
67
+ def _python() -> str:
68
+ return os.environ.get("HOTKERNEL_PYTHON") or shutil.which("python") or sys.executable
69
+
70
+
71
+ def _local_path(p: str | None) -> str | None:
72
+ """An agent on Windows may talk to a server that runs inside WSL (claude mcp add ... wsl.exe)
73
+ and pass C:\\... paths. Map them to /mnt/<drive>/... when that mount exists; everything
74
+ else passes through untouched."""
75
+ if not p or sys.platform != "linux":
76
+ return p
77
+ m = re.match(r"^([A-Za-z]):[\\/](.*)$", p)
78
+ if m and Path("/mnt", m.group(1).lower()).is_dir():
79
+ return str(Path("/mnt", m.group(1).lower(), m.group(2).replace("\\", "/")))
80
+ return p
81
+
82
+
83
+ def _run(cmd: list[str], cwd: str | None = None, timeout: int = _DEFAULT_TIMEOUT,
84
+ env: dict | None = None, max_out: int = 20000) -> dict:
85
+ """Run a command and return its tail of stdout/stderr (max_out chars each, so a chatty
86
+ script cannot blow up the MCP response; tools that need whole files write them to disk)."""
87
+ t0 = time.perf_counter()
88
+ cmd = [_local_path(c) if isinstance(c, str) else c for c in cmd]
89
+ cwd = _local_path(cwd)
90
+ try:
91
+ p = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout,
92
+ env={**os.environ, **(env or {})}, check=False)
93
+ except FileNotFoundError:
94
+ return {"error": f"executable not found: {cmd[0]}", "cmd": cmd}
95
+ except subprocess.TimeoutExpired:
96
+ return {"error": f"timed out after {timeout}s", "cmd": cmd}
97
+ return {
98
+ "cmd": cmd, "returncode": p.returncode,
99
+ "stdout": p.stdout[-max_out:], "stderr": p.stderr[-max_out:],
100
+ "wall_s": round(time.perf_counter() - t0, 3),
101
+ }
102
+
103
+
104
+ def _last_json_line(text: str) -> dict | None:
105
+ for line in reversed(text.splitlines()):
106
+ line = line.strip()
107
+ if line.startswith("{") and line.endswith("}"):
108
+ try:
109
+ return json.loads(line)
110
+ except json.JSONDecodeError:
111
+ continue
112
+ return None
113
+
114
+
115
+ @functools.lru_cache(maxsize=4)
116
+ def _triton_bin_dir(py: str) -> Path | None:
117
+ """Triton wheels ship ptxas/cuobjdump/nvdisasm under site-packages/triton/backends/nvidia/bin,
118
+ so a machine with torch+Triton can read SASS without a CUDA toolkit."""
119
+ r = _run([py, "-c", "import sysconfig; print(sysconfig.get_paths()['purelib'])"], timeout=60)
120
+ if "error" in r or r["returncode"] != 0:
121
+ return None
122
+ d = Path(r["stdout"].strip()) / "triton" / "backends" / "nvidia" / "bin"
123
+ return d if d.is_dir() else None
124
+
125
+
126
+ _TOOL_GLOBS: dict[str, list[str]] = {
127
+ # Linux / WSL2: apt installs under /opt/nvidia; `dpkg-deb -x` into ~/opt/nvidia keeps the
128
+ # same layout one level down. Windows: standalone Nsight installers and the CUDA toolkit.
129
+ "nsys": ["/opt/nvidia/nsight-systems/*/bin", "/opt/nvidia/nsight-systems-cli/*/bin",
130
+ "~/opt/nvidia/opt/nvidia/nsight-systems/*/bin",
131
+ "~/opt/nvidia/opt/nvidia/nsight-systems-cli/*/bin",
132
+ "/usr/local/cuda/bin", "/usr/local/cuda-*/bin",
133
+ "C:/Program Files/NVIDIA Corporation/Nsight Systems */target-windows-x64",
134
+ "C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v*/bin"],
135
+ "ncu": ["/opt/nvidia/nsight-compute/*", "~/opt/nvidia/opt/nvidia/nsight-compute/*",
136
+ "/usr/local/cuda/bin", "/usr/local/cuda-*/bin",
137
+ "C:/Program Files/NVIDIA Corporation/Nsight Compute *",
138
+ "C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v*/bin"],
139
+ "cuobjdump": ["/usr/local/cuda/bin", "/usr/local/cuda-*/bin",
140
+ "C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v*/bin"],
141
+ }
142
+ _TOOL_GLOBS["nvdisasm"] = _TOOL_GLOBS["cuobjdump"]
143
+
144
+
145
+ def _find_tool(name: str) -> str | None:
146
+ """PATH first, then HOTKERNEL_TOOL_DIRS, then the Triton wheel (for cuobjdump/nvdisasm),
147
+ then the usual Nsight/CUDA install locations. Newest version wins when several exist."""
148
+ path = shutil.which(name)
149
+ if path:
150
+ return path
151
+ dirs: list[Path] = [Path(d) for d in os.environ.get("HOTKERNEL_TOOL_DIRS", "").split(os.pathsep) if d]
152
+ if name in ("cuobjdump", "nvdisasm", "ptxas"):
153
+ tb = _triton_bin_dir(_python())
154
+ if tb:
155
+ dirs.append(tb)
156
+ for pattern in _TOOL_GLOBS.get(name, []):
157
+ dirs += [Path(p) for p in sorted(glob.glob(os.path.expanduser(pattern)), reverse=True)]
158
+ for d in dirs:
159
+ for exe in (name, name + ".exe"):
160
+ p = d / exe
161
+ if p.is_file():
162
+ return str(p)
163
+ return None
164
+
165
+
166
+ def _which_or_error(name: str) -> str | dict:
167
+ path = _find_tool(name)
168
+ if path:
169
+ return path
170
+ hint = {
171
+ "nsys": "Nsight Systems: Linux/WSL2 `apt install nsight-systems-<ver>` from NVIDIA's CUDA apt repo "
172
+ "(or `dpkg-deb -x` the .deb under ~/opt/nvidia); Windows: developer.nvidia.com/nsight-systems.",
173
+ "ncu": "Nsight Compute: Linux/WSL2 `apt install nsight-compute-<ver>` from NVIDIA's CUDA apt repo "
174
+ "(or `dpkg-deb -x` the .deb under ~/opt/nvidia); Windows: developer.nvidia.com/nsight-compute.",
175
+ }.get(name, "It ships with the CUDA toolkit; Triton wheels also bundle cuobjdump/nvdisasm "
176
+ "(set HOTKERNEL_PYTHON to the interpreter that has Triton).")
177
+ return {"error": f"{name} not found on PATH, HOTKERNEL_TOOL_DIRS, or the usual install dirs. {hint} "
178
+ f"Or set HOTKERNEL_TOOL_DIRS to the directory that holds {name}.",
179
+ "fallback": "torch_trace works without Nsight" if name in ("nsys", "ncu") else None}
180
+
181
+
182
+ # --------------------------------------------------------------------------- tools
183
+
184
+ @mcp.tool()
185
+ def gpu_info() -> dict:
186
+ """Identify the GPU(s) in this machine via nvidia-smi: name, driver, memory, power limit,
187
+ compute capability, max SM clock. Also returns the matching roofline table entry if known."""
188
+ q = "name,driver_version,memory.total,power.limit,compute_cap,clocks.max.sm"
189
+ r = _run(["nvidia-smi", f"--query-gpu={q}", "--format=csv,noheader,nounits"])
190
+ if "error" in r or r["returncode"] != 0:
191
+ return {"error": "nvidia-smi failed", "detail": r}
192
+ gpus_out = []
193
+ for row in csv.reader(io.StringIO(r["stdout"].strip())):
194
+ if len(row) < 6:
195
+ continue
196
+ name = row[0].strip()
197
+ spec = gpus.lookup(name)
198
+ gpus_out.append({
199
+ "name": name, "driver": row[1].strip(), "memory_mib": float(row[2]),
200
+ "power_limit_w": float(row[3]) if row[3].strip() not in ("", "[N/A]") else None,
201
+ "compute_cap": row[4].strip(), "max_sm_clock_mhz": float(row[5]),
202
+ "roofline_entry": spec.as_dict() if spec else None,
203
+ })
204
+ return {"gpus": gpus_out, "platform": sys.platform,
205
+ "wsl": "microsoft" in Path("/proc/version").read_text().lower()
206
+ if Path("/proc/version").exists() else False}
207
+
208
+
209
+ @mcp.tool()
210
+ def roofline(gpu: str = "", flops: float | None = None, bytes: float | None = None,
211
+ dtype: str = "bf16", measured_mem_gbs: float | None = None) -> dict:
212
+ """Roofline for a GPU (default: the first GPU in this machine). Pass the kernel's flops and
213
+ bytes moved (bench()/verify() return them as roofline_inputs) to get the bound, the
214
+ attainable TFLOPS and attainable_ms from the spec sheet. Pass measured_mem_gbs (the
215
+ bandwidth a streaming copy reaches on this card under the repo's timer, see
216
+ docs/platform-matrix.md; 1501 on the reference RTX 5090) to also get attainable_ms_measured,
217
+ the floor a kernel can realistically get within 10% of. dtype: bf16 | fp16 | fp8 | fp32."""
218
+ if not gpu:
219
+ info = gpu_info()
220
+ if "error" in info or not info["gpus"]:
221
+ return {"error": "could not detect GPU; pass gpu='rtx5090' etc.", "detail": info}
222
+ gpu = info["gpus"][0]["name"]
223
+ spec = gpus.lookup(gpu)
224
+ if spec is None:
225
+ return {"error": f"no roofline entry for '{gpu}'", "known": sorted(gpus.SPECS)}
226
+ try:
227
+ return gpus.roofline(spec, flops, bytes, dtype, measured_mem_gbs=measured_mem_gbs)
228
+ except ValueError as e:
229
+ return {"error": str(e)}
230
+
231
+
232
+ @mcp.tool()
233
+ def bench(script: str, n: int = 5, args: list[str] | None = None, cwd: str | None = None,
234
+ timeout: int = _DEFAULT_TIMEOUT, warmup: int = 1) -> dict:
235
+ """Run a Python script n times in the user's interpreter and aggregate its timing.
236
+ Contract: the script prints one JSON line to stdout containing at least {"ms": <float>}
237
+ and optionally {"ok": <bool>} for a correctness check. Returns mean/std/min over the n
238
+ timed runs plus every raw sample, so a LEADERBOARD row can cite n and seed."""
239
+ py = _python()
240
+ cmd = [py, script, *(args or [])]
241
+ samples: list[float] = []
242
+ oks: list[bool] = []
243
+ raw: list[dict] = []
244
+ for i in range(warmup + n):
245
+ r = _run(cmd, cwd=cwd, timeout=timeout)
246
+ if "error" in r:
247
+ return r
248
+ if r["returncode"] != 0:
249
+ return {"error": f"script failed on run {i}", "detail": r}
250
+ j = _last_json_line(r["stdout"])
251
+ if not j or "ms" not in j:
252
+ return {"error": "script did not print a JSON line with an 'ms' field",
253
+ "stdout_tail": r["stdout"][-2000:]}
254
+ if i < warmup:
255
+ continue
256
+ samples.append(float(j["ms"]))
257
+ oks.append(bool(j.get("ok", True)))
258
+ raw.append(j)
259
+ return {
260
+ "cmd": cmd, "n": n, "warmup": warmup,
261
+ "mean_ms": round(statistics.fmean(samples), 4),
262
+ "std_ms": round(statistics.pstdev(samples), 4) if n > 1 else 0.0,
263
+ "min_ms": round(min(samples), 4), "max_ms": round(max(samples), 4),
264
+ "all_ok": all(oks), "samples": raw,
265
+ "roofline_inputs": {k: raw[0].get(k) for k in ("min_bytes", "flops") if k in raw[0]},
266
+ }
267
+
268
+
269
+ _TORCH_PROF_RUNNER = r'''
270
+ import json, os, runpy, sys
271
+ import torch
272
+ from torch.profiler import profile, ProfilerActivity
273
+ script, top = sys.argv[1], int(sys.argv[2])
274
+ sys.argv = [script] + sys.argv[3:]
275
+ # `python script.py` puts the script's directory first on sys.path; runpy does not. Targets
276
+ # import their sibling baseline / _timing module, so do the same here.
277
+ sys.path.insert(0, os.path.dirname(os.path.abspath(script)))
278
+ exit_code = 0
279
+ with profile(activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA], record_shapes=True) as prof:
280
+ try:
281
+ runpy.run_path(script, run_name="__main__")
282
+ except SystemExit as e: # targets end with sys.exit(0 if ok else 1); keep profiling output
283
+ exit_code = int(e.code or 0) if isinstance(e.code, int) or e.code is None else 1
284
+ torch.cuda.synchronize()
285
+ def _dev_us(e):
286
+ v = getattr(e, "device_time_total", None) # torch >= 2.4
287
+ if v is None:
288
+ v = getattr(e, "cuda_time_total", 0.0) # older torch
289
+ return float(v or 0.0)
290
+
291
+ # torch 2.11: key_averages() mixes real GPU kernels (device_type CUDA) with CPU-side ops
292
+ # (aten::*, device_type CPU) whose device time is the inclusive time of the kernels they
293
+ # launched. Report the two separately so percentages are over kernel time only.
294
+ _RUNTIME_ROWS = ("cudaLaunchKernel", "cudaDeviceSynchronize", "cudaStreamSynchronize",
295
+ "cudaEventRecord", "cudaEventSynchronize", "cudaMemcpy", "cudaMemset",
296
+ "Lazy Function Loading", "Runtime Triggered Module Loading", "cudaFuncGetAttributes",
297
+ "cudaModuleGetLoadingMode", "cudaGetDevice", "cudaMalloc", "cudaFree")
298
+ kernels, ops = [], []
299
+ for e in prof.key_averages():
300
+ us = _dev_us(e)
301
+ if us <= 0:
302
+ continue
303
+ row = {"name": e.key, "device_time_us": round(us, 1), "calls": int(e.count),
304
+ "avg_us": round(us / max(int(e.count), 1), 2)}
305
+ if str(getattr(e, "device_type", "")).endswith("CUDA"):
306
+ kernels.append(row)
307
+ elif not e.key.startswith(_RUNTIME_ROWS):
308
+ shapes = getattr(e, "input_shapes", None)
309
+ if shapes:
310
+ row["input_shapes"] = str(shapes)[:200]
311
+ ops.append(row)
312
+ kernels.sort(key=lambda r: -r["device_time_us"])
313
+ ops.sort(key=lambda r: -r["device_time_us"])
314
+ total = sum(r["device_time_us"] for r in kernels) or 1.0
315
+ for r in kernels:
316
+ r["pct"] = round(100.0 * r["device_time_us"] / total, 2)
317
+ print("HOTKERNEL_JSON " + json.dumps({
318
+ "kernels": kernels[:top], "total_kernel_time_us": round(total, 1), "n_kernels": len(kernels),
319
+ "ops": ops[:top],
320
+ "ops_note": "inclusive GPU time per aten op (nested ops overlap; use kernels for shares)",
321
+ "script_exit_code": exit_code, "torch": torch.__version__}))
322
+ '''
323
+
324
+
325
+ @mcp.tool()
326
+ def torch_trace(script: str, top: int = 15, args: list[str] | None = None,
327
+ cwd: str | None = None, timeout: int = _DEFAULT_TIMEOUT) -> dict:
328
+ """Run a Python script under torch.profiler in the user's interpreter. Returns `kernels`
329
+ (real GPU kernels by device time, pct over total kernel time) and `ops` (aten ops with
330
+ input shapes, inclusive time). Works without Nsight installed; use this first."""
331
+ py = _python()
332
+ with tempfile.NamedTemporaryFile("w", suffix="_hotkernel_torchprof.py", delete=False) as f:
333
+ f.write(_TORCH_PROF_RUNNER)
334
+ runner = f.name
335
+ try:
336
+ # Targets skip their L2-flush scaffolding under this variable so it does not show up
337
+ # as the top kernel; timing tools (bench, verify) never set it.
338
+ r = _run([py, runner, script, str(top), *(args or [])], cwd=cwd, timeout=timeout,
339
+ env={"HOTKERNEL_NO_FLUSH": "1"})
340
+ finally:
341
+ try:
342
+ os.unlink(runner)
343
+ except OSError:
344
+ pass
345
+ if "error" in r:
346
+ return r
347
+ if r["returncode"] != 0:
348
+ return {"error": "script failed under torch.profiler", "detail": r}
349
+ for line in reversed(r["stdout"].splitlines()):
350
+ if line.startswith("HOTKERNEL_JSON "):
351
+ out = json.loads(line[len("HOTKERNEL_JSON "):])
352
+ for k in out.get("kernels", []):
353
+ k["short_name"] = _short_kernel_name(k["name"])
354
+ k["name"] = k["name"][:160]
355
+ out["cmd"] = r["cmd"]
356
+ out["note"] = ("whole-script profile (setup, reference, correctness check, warmup "
357
+ "included); the target's L2 flush is disabled here, so use bench()/"
358
+ "verify() for timing")
359
+ return out
360
+ return {"error": "no profiler output found", "stdout_tail": r["stdout"][-2000:]}
361
+
362
+
363
+ @mcp.tool()
364
+ def profile(cmd: list[str], tool: str = "nsys", out_dir: str = "results/profiles",
365
+ cwd: str | None = None, timeout: int = _DEFAULT_TIMEOUT,
366
+ ncu_kernel_regex: str = "", ncu_launch_count: int = 20) -> dict:
367
+ """Run a command under Nsight Systems (tool='nsys') or Nsight Compute (tool='ncu') and
368
+ return the path of the report plus a parsed kernel summary. nsys gives the per-kernel time
369
+ breakdown; ncu gives per-kernel metrics (achieved occupancy, DRAM throughput, SM throughput)
370
+ and is much slower, so use ncu_kernel_regex to target one kernel."""
371
+ # out_dir is relative to cwd (the directory the command runs in); report paths come back
372
+ # absolute so the agent can pass them straight to top_kernels() or read them.
373
+ cwd, out_dir = _local_path(cwd), _local_path(out_dir) or out_dir
374
+ if cmd and cmd[0] in ("python", "python3", "py"):
375
+ cmd = [_python(), *cmd[1:]] # the interpreter that has the user's torch, not PATH's
376
+ base = Path(cwd) if cwd else Path.cwd()
377
+ out = Path(out_dir) if Path(out_dir).is_absolute() else base / out_dir
378
+ out.mkdir(parents=True, exist_ok=True)
379
+ stamp = time.strftime("%Y%m%d-%H%M%S")
380
+ if tool == "nsys":
381
+ exe = _which_or_error("nsys")
382
+ if isinstance(exe, dict):
383
+ return exe
384
+ rep = str(out / f"nsys-{stamp}")
385
+ r = _run([exe, "profile", "--trace=cuda,nvtx", "--sample=none", "--cpuctxsw=none",
386
+ "-o", rep, "--force-overwrite", "true", *cmd], cwd=cwd, timeout=timeout,
387
+ env={"HOTKERNEL_NO_FLUSH": "1"}) # see torch_trace
388
+ if "error" in r or r["returncode"] != 0:
389
+ return {"error": "nsys profile failed", "detail": r}
390
+ # --output <base> makes nsys write <base>_cuda_gpu_kern_sum.csv itself, so a long kernel
391
+ # list is never cut by the stdout tail we keep.
392
+ stats = _run([exe, "stats", "--report", "cuda_gpu_kern_sum", "--format", "csv",
393
+ "--force-export", "true", "--output", rep, rep + ".nsys-rep"],
394
+ cwd=cwd, timeout=timeout)
395
+ if "error" in stats or stats["returncode"] != 0:
396
+ return {"error": "nsys stats failed", "detail": stats, "report": rep + ".nsys-rep"}
397
+ csv_path = rep + ".kern_sum.csv"
398
+ written = Path(rep + "_cuda_gpu_kern_sum.csv")
399
+ if written.exists():
400
+ written.replace(csv_path)
401
+ else: # older nsys that ignored --output: fall back to captured stdout
402
+ Path(csv_path).write_text(stats["stdout"])
403
+ parsed = top_kernels(csv_path, 10)
404
+ return {"tool": "nsys", "nsys": exe, "report": rep + ".nsys-rep", "summary_csv": csv_path,
405
+ "profile_wall_s": r["wall_s"], "top": parsed.get("kernels"),
406
+ "n_kernels_total": parsed.get("n_kernels_total"),
407
+ "total_gpu_time_ns": parsed.get("total_gpu_time_ns"),
408
+ "note": "kernel times include the whole script (setup, reference, correctness "
409
+ "check, warmup) and the target ran with HOTKERNEL_NO_FLUSH=1, so per-call "
410
+ "durations are warm-L2 numbers, shorter than bench()/verify() which flush "
411
+ "L2 before every call and are the numbers that count"}
412
+ if tool == "ncu":
413
+ exe = _which_or_error("ncu")
414
+ if isinstance(exe, dict):
415
+ return exe
416
+ rep = str(out / f"ncu-{stamp}")
417
+ ncu_cmd = [exe, "--set", "basic", "--launch-count", str(ncu_launch_count),
418
+ "-o", rep, "--force-overwrite"]
419
+ if ncu_kernel_regex:
420
+ ncu_cmd += ["--kernel-name", f"regex:{ncu_kernel_regex}"]
421
+ r = _run([*ncu_cmd, *cmd], cwd=cwd, timeout=timeout, env={"HOTKERNEL_NO_FLUSH": "1"})
422
+ if "error" in r:
423
+ return r
424
+ if "ERR_NVGPUCTRPERM" in (r["stdout"] + r["stderr"]):
425
+ return {
426
+ "error": "ncu ran but the driver refused GPU performance-counter access "
427
+ "(ERR_NVGPUCTRPERM). This is a one-time driver setting, not a hotkernel bug.",
428
+ "fix": {
429
+ "windows_or_wsl2": "On Windows (also for WSL2 guests): NVIDIA App > System > "
430
+ "Advanced > Developer > Manage GPU Performance Counters > allow "
431
+ "all users; or NVIDIA Control Panel (run as administrator) > "
432
+ "Desktop > Enable Developer Settings, then Developer > Manage GPU "
433
+ "Performance Counters > 'Allow access to the GPU performance "
434
+ "counters to all users'. No reboot needed.",
435
+ "linux": "/etc/modprobe.d/nvidia.conf: options nvidia "
436
+ "NVreg_RestrictProfilingToAdminUsers=0, then reboot (or run ncu as root).",
437
+ "docs": "https://developer.nvidia.com/ERR_NVGPUCTRPERM",
438
+ },
439
+ "fallback": "profile(tool='nsys') and torch_trace work without this permission.",
440
+ "detail": r,
441
+ }
442
+ if r["returncode"] != 0 or not Path(rep + ".ncu-rep").exists():
443
+ return {"error": "ncu failed", "detail": r}
444
+ # --log-file makes ncu write the CSV itself; a details page is one row per metric per
445
+ # launch (hundreds of KB for a few launches) and would not survive the stdout tail.
446
+ csv_path = rep + ".details.csv"
447
+ imp = _run([exe, "--import", rep + ".ncu-rep", "--csv", "--page", "details",
448
+ "--log-file", csv_path], cwd=cwd, timeout=timeout)
449
+ if "error" in imp or imp["returncode"] != 0 or not Path(csv_path).exists():
450
+ return {"error": "ncu --import failed", "detail": imp, "report": rep + ".ncu-rep"}
451
+ parsed = top_kernels(csv_path, 10, tool="ncu")
452
+ return {"tool": "ncu", "report": rep + ".ncu-rep", "details_csv": csv_path,
453
+ "top": parsed.get("kernels"), "parse_error": parsed.get("error"),
454
+ "note": "the target ran with HOTKERNEL_NO_FLUSH=1 and ncu replays each kernel "
455
+ "with its own cache control, so duration_us here is not comparable with "
456
+ "bench()/verify() (which flush L2 before every call); use ncu for "
457
+ "throughput %, occupancy and registers, verify() for time"}
458
+ return {"error": f"unknown tool '{tool}', use 'nsys' or 'ncu'"}
459
+
460
+
461
+ def _num(s: str | None) -> float | None:
462
+ if s is None:
463
+ return None
464
+ s = s.strip().replace(",", "")
465
+ if s in ("", "n/a", "N/A", "-"):
466
+ return None
467
+ try:
468
+ return float(s)
469
+ except ValueError:
470
+ return None
471
+
472
+
473
+ def _short_kernel_name(name: str) -> str:
474
+ """'void at::native::elementwise_kernel<(int)128, ...>(int, T3)' -> 'elementwise_kernel';
475
+ Triton names ('rmsnorm_fwd_kernel') come back unchanged."""
476
+ junk = {"void", "std", "type", "enable_if", "internal", "anonymous", "namespace"}
477
+ # identifiers (with namespaces) that open a template or call: the first non-junk one is
478
+ # the kernel; cuBLAS names start with `std::enable_if<...>::type internal::gemvx::kernel<`.
479
+ for cand in re.findall(r"((?:[A-Za-z_]\w*::)*[A-Za-z_]\w*)\s*[<(]", name):
480
+ parts = cand.split("::")
481
+ if parts[-1] in junk or parts[0] in ("std",):
482
+ continue
483
+ return "::".join(parts[-2:]) if parts[-1] == "kernel" and len(parts) > 1 else parts[-1]
484
+ return name.strip()[:60]
485
+
486
+
487
+ def _top_kernels_nsys(p: Path, n: int) -> dict:
488
+ """nsys stats --report cuda_gpu_kern_sum --format csv. Verified against nsys 2026.1.3:
489
+ header 'Time (%),Total Time (ns),Instances,Avg (ns),Med (ns),Min (ns),Max (ns),StdDev (ns),Name'
490
+ preceded by free-text 'Generating SQLite...' / 'Processing ...' lines."""
491
+ lines = p.read_text().splitlines()
492
+ start = next((i for i, l in enumerate(lines)
493
+ if l.startswith(("Time (%)", "\"Time (%)\""))), None)
494
+ if start is None:
495
+ return {"error": "could not find the nsys cuda_gpu_kern_sum header row", "head": lines[:5]}
496
+ rows = list(csv.DictReader(io.StringIO("\n".join(lines[start:]))))
497
+ out = []
498
+ for r in rows[:n]:
499
+ full = r.get("Name") or ""
500
+ row = {
501
+ "name": full[:160],
502
+ "short_name": _short_kernel_name(full),
503
+ "time_pct": _num(r.get("Time (%)")) or 0.0,
504
+ "total_ns": _num(r.get("Total Time (ns)")) or 0.0,
505
+ "calls": int(_num(r.get("Instances")) or 0),
506
+ "avg_ns": _num(r.get("Avg (ns)")) or 0.0,
507
+ }
508
+ for key, col in (("med_ns", "Med (ns)"), ("min_ns", "Min (ns)"), ("max_ns", "Max (ns)"),
509
+ ("std_ns", "StdDev (ns)")):
510
+ v = _num(r.get(col))
511
+ if v is not None:
512
+ row[key] = v
513
+ out.append(row)
514
+ total_ns = sum(_num(r.get("Total Time (ns)")) or 0.0 for r in rows)
515
+ return {"tool": "nsys", "source": str(p), "n_kernels_total": len(rows),
516
+ "total_gpu_time_ns": total_ns, "kernels": out}
517
+
518
+
519
+ # Metric names as printed by `ncu --import <rep> --csv --page details` for --set basic.
520
+ _NCU_METRICS = {
521
+ "Duration": "duration",
522
+ "Achieved Occupancy": "achieved_occupancy_pct",
523
+ "Theoretical Occupancy": "theoretical_occupancy_pct",
524
+ "DRAM Throughput": "dram_throughput_pct",
525
+ "Memory Throughput": "memory_throughput_pct",
526
+ "Compute (SM) Throughput": "sm_throughput_pct",
527
+ "L1/TEX Cache Throughput": "l1_throughput_pct",
528
+ "L2 Cache Throughput": "l2_throughput_pct",
529
+ "Registers Per Thread": "registers_per_thread",
530
+ "Static Shared Memory Per Block": "static_smem_per_block",
531
+ "Dynamic Shared Memory Per Block": "dynamic_smem_per_block",
532
+ "Waves Per SM": "waves_per_sm",
533
+ "SM Frequency": "sm_frequency",
534
+ "DRAM Frequency": "dram_frequency",
535
+ "Elapsed Cycles": "elapsed_cycles",
536
+ }
537
+ _DURATION_TO_US = {"nsecond": 1e-3, "ns": 1e-3, "usecond": 1.0, "us": 1.0,
538
+ "msecond": 1e3, "ms": 1e3, "second": 1e6, "s": 1e6}
539
+
540
+
541
+ def _top_kernels_ncu(p: Path, n: int) -> dict:
542
+ """Parse the long-format CSV that `ncu --import <rep> --csv --page details` prints: one row
543
+ per (kernel launch, metric) with columns ID, Kernel Name, Block Size, Grid Size, Section Name,
544
+ Metric Name, Metric Unit, Metric Value. Written against the documented format and a synthetic
545
+ fixture (tests/); not yet verified on hardware because ncu needs the performance-counter
546
+ permission (ERR_NVGPUCTRPERM) that profile() reports."""
547
+ lines = p.read_text().splitlines()
548
+ start = next((i for i, l in enumerate(lines) if "\"Metric Name\"" in l or "Metric Name" in l), None)
549
+ if start is None:
550
+ return {"error": "could not find the ncu details CSV header (no 'Metric Name' column)",
551
+ "head": lines[:5]}
552
+ rows = list(csv.DictReader(io.StringIO("\n".join(lines[start:]))))
553
+ launches: dict[str, dict] = {}
554
+ for r in rows:
555
+ lid = r.get("ID") or r.get("ID") or ""
556
+ full = r.get("Kernel Name") or ""
557
+ k = launches.setdefault(lid, {
558
+ "id": lid, "name": full[:160], "short_name": _short_kernel_name(full),
559
+ "block_size": r.get("Block Size"), "grid_size": r.get("Grid Size"),
560
+ "device": r.get("Device"), "cc": r.get("CC"), "metrics": {},
561
+ })
562
+ mname = (r.get("Metric Name") or "").strip()
563
+ unit = (r.get("Metric Unit") or "").strip()
564
+ val = _num(r.get("Metric Value"))
565
+ rule = (r.get("Rule Name") or "").strip()
566
+ if rule and not mname: # ncu's own advice rows: keep them, agents find them useful
567
+ k.setdefault("hints", []).append({
568
+ "rule": rule, "type": r.get("Rule Type"),
569
+ "estimated_speedup_pct": _num(r.get("Estimated Speedup")),
570
+ "text": (r.get("Rule Description") or "").strip()[:400]})
571
+ continue
572
+ if not mname or val is None:
573
+ continue
574
+ key = _NCU_METRICS.get(mname)
575
+ if key == "duration":
576
+ k["duration_us"] = round(val * _DURATION_TO_US.get(unit, 1.0), 4)
577
+ elif key:
578
+ k["metrics"][key] = val
579
+ if unit and key not in ("dram_throughput_pct", "memory_throughput_pct",
580
+ "sm_throughput_pct", "achieved_occupancy_pct",
581
+ "theoretical_occupancy_pct", "l1_throughput_pct",
582
+ "l2_throughput_pct"):
583
+ k["metrics"][key + "_unit"] = unit
584
+ out = sorted(launches.values(), key=lambda k: -(k.get("duration_us") or 0.0))[:n]
585
+ for k in out:
586
+ m = k["metrics"]
587
+ dram, sm = m.get("dram_throughput_pct"), m.get("sm_throughput_pct")
588
+ if dram is not None and sm is not None:
589
+ k["bound_hint"] = ("memory" if dram >= sm else "compute") if max(dram, sm) >= 60 \
590
+ else "latency (neither DRAM nor SM above 60% of peak)"
591
+ return {"tool": "ncu", "source": str(p), "n_launches": len(launches), "kernels": out,
592
+ "hints_note": "`hints` are ncu's own rules with its estimated speedups; the estimates "
593
+ "assume the named limiter is the only one and run high for "
594
+ "memory-bound kernels (a 'partial last wave' rule can claim 30% on a "
595
+ "kernel already at 85% of DRAM bandwidth). Treat them as leads, verify() "
596
+ "decides."}
597
+
598
+
599
+ @mcp.tool()
600
+ def top_kernels(summary_csv: str, n: int = 10, tool: str = "nsys") -> dict:
601
+ """Read a saved profile CSV (from profile()) and return the n heaviest kernels.
602
+ tool='nsys': cuda_gpu_kern_sum CSV -> share of GPU time, calls, avg/med/min/max/std ns.
603
+ tool='ncu': details CSV -> per-launch duration, achieved occupancy, DRAM / SM throughput %,
604
+ registers per thread, and a memory/compute/latency hint."""
605
+ p = Path(_local_path(summary_csv) or summary_csv)
606
+ if not p.exists():
607
+ return {"error": f"no such file: {summary_csv}"}
608
+ if tool == "nsys":
609
+ return _top_kernels_nsys(p, n)
610
+ if tool == "ncu":
611
+ return _top_kernels_ncu(p, n)
612
+ return {"error": f"unknown tool '{tool}', use 'nsys' or 'ncu'"}
613
+
614
+
615
+ @mcp.tool()
616
+ def disasm(target: str, level: str = "sass", kernel: str = "", max_lines: int = 1500) -> dict:
617
+ """Disassemble a kernel. target is a .cubin/.ptx/.so/.exe path, or 'triton-cache' to search
618
+ ~/.triton/cache (and TRITON_CACHE_DIR) for the most recent kernel whose name contains
619
+ `kernel`. level: 'sass' (via cuobjdump -sass) or 'ptx' (cuobjdump -ptx / the .ptx file).
620
+ A real kernel is 500-3000 SASS lines; max_lines<=0 returns all of them."""
621
+ if target == "triton-cache":
622
+ roots = [Path(os.environ.get("TRITON_CACHE_DIR", Path.home() / ".triton" / "cache"))]
623
+ cands: list[Path] = []
624
+ for root in roots:
625
+ if root.exists():
626
+ ext = "*.cubin" if level == "sass" else "*.ptx"
627
+ cands += [p for p in root.rglob(ext) if kernel in p.stem]
628
+ if not cands:
629
+ return {"error": f"no cached Triton {level} found for kernel '{kernel}'",
630
+ "searched": [str(r) for r in roots]}
631
+ path = max(cands, key=lambda p: p.stat().st_mtime)
632
+ else:
633
+ path = Path(_local_path(target) or target)
634
+ if not path.exists():
635
+ return {"error": f"no such file: {target}"}
636
+ if level == "ptx" and path.suffix == ".ptx":
637
+ text = path.read_text()
638
+ else:
639
+ exe = _which_or_error("cuobjdump")
640
+ if isinstance(exe, dict):
641
+ return exe
642
+ flag = "-sass" if level == "sass" else "-ptx"
643
+ # SASS for a real kernel is hundreds of KB; keep all of it, max_lines trims below.
644
+ r = _run([exe, flag, str(path)], max_out=50_000_000)
645
+ if "error" in r or r["returncode"] != 0:
646
+ return {"error": "cuobjdump failed", "detail": r}
647
+ text = r["stdout"]
648
+ lines = text.splitlines()
649
+ keep = len(lines) if max_lines <= 0 else max_lines
650
+ return {"path": str(path), "level": level, "n_lines": len(lines),
651
+ "text": "\n".join(lines[:keep]),
652
+ "truncated": len(lines) > keep,
653
+ **({"note": f"showing {keep} of {len(lines)} lines; pass max_lines=0 for all"}
654
+ if len(lines) > keep else {})}
655
+
656
+
657
+ # --------------------------------------------------------------------------- verify / record
658
+
659
+ def _nvidia_smi_state() -> dict:
660
+ """What a LEADERBOARD row has to cite: card, driver, power limit, and the clock/temperature
661
+ at the moment of the measurement."""
662
+ q = "name,driver_version,power.limit,clocks.sm,temperature.gpu"
663
+ r = _run(["nvidia-smi", f"--query-gpu={q}", "--format=csv,noheader,nounits"], timeout=30)
664
+ if "error" in r or r["returncode"] != 0:
665
+ return {"gpu": None, "driver": None, "power_limit_w": None}
666
+ row = [c.strip() for c in next(csv.reader(io.StringIO(r["stdout"].strip())), [])] + [""] * 5
667
+ return {"gpu": row[0] or None, "driver": row[1] or None, "power_limit_w": _num(row[2]),
668
+ "sm_clock_mhz": _num(row[3]), "temperature_c": _num(row[4])}
669
+
670
+
671
+ def _run_target(cmd: list[str], cwd: str | None, timeout: int) -> dict:
672
+ """One run of a contract script. A JSON line with ok=false and a non-zero exit is a valid
673
+ 'incorrect' result, not a crash; no JSON line at all is an error."""
674
+ r = _run(cmd, cwd=cwd, timeout=timeout)
675
+ if "error" in r:
676
+ return r
677
+ j = _last_json_line(r["stdout"])
678
+ if not j or "ms" not in j:
679
+ return {"error": f"script exited {r['returncode']} without a JSON line carrying 'ms'",
680
+ "stdout_tail": r["stdout"][-2000:], "stderr_tail": r["stderr"][-2000:]}
681
+ return {"ms": float(j["ms"]), "ok": bool(j.get("ok", True)), "json": j, "wall_s": r["wall_s"]}
682
+
683
+
684
+ def _stats(xs: list[float]) -> dict:
685
+ n = len(xs)
686
+ return {"n": n, "mean_ms": round(statistics.fmean(xs), 5),
687
+ "std_ms": round(statistics.stdev(xs), 5) if n > 1 else 0.0,
688
+ "min_ms": round(min(xs), 5), "max_ms": round(max(xs), 5),
689
+ "samples_ms": [round(x, 5) for x in xs]}
690
+
691
+
692
+ def _decide(b: list[float], c: list[float], b_ok: list[bool], c_ok: list[bool],
693
+ mismatch: list[str]) -> dict:
694
+ """The verdict rules, kept free of I/O so they can be unit-tested:
695
+ invalid the two scripts do not measure the same thing, or the baseline fails itself
696
+ incorrect any candidate run printed ok=false (no speedup is reported)
697
+ faster/slower |mean_b - mean_c| > 2 * sqrt(std_b^2/n + std_c^2/n) (sample std)
698
+ inconclusive otherwise: the difference is inside the noise, run again or change more"""
699
+ out = {"baseline": _stats(b), "candidate": _stats(c),
700
+ "baseline_all_ok": all(b_ok), "candidate_all_ok": all(c_ok),
701
+ "contract_mismatch": mismatch}
702
+ if mismatch:
703
+ out.update(verdict="invalid", speedup=None,
704
+ reason="baseline and candidate disagree on " + ", ".join(mismatch)
705
+ + "; a candidate must keep the baseline's shape, dtype, iters and seed")
706
+ return out
707
+ if not all(b_ok):
708
+ out.update(verdict="invalid", speedup=None,
709
+ reason="the baseline failed its own correctness check; fix the target first")
710
+ return out
711
+ if not all(c_ok):
712
+ out.update(verdict="incorrect", speedup=None,
713
+ reason=f"candidate printed ok=false in {c_ok.count(False)} of {len(c_ok)} runs; "
714
+ "no speedup is reported for incorrect output")
715
+ return out
716
+ n = min(len(b), len(c))
717
+ sb, sc = out["baseline"]["std_ms"], out["candidate"]["std_ms"]
718
+ delta = out["baseline"]["mean_ms"] - out["candidate"]["mean_ms"]
719
+ threshold = 2.0 * math.sqrt(sb ** 2 / n + sc ** 2 / n)
720
+ significant = abs(delta) > threshold
721
+ speedup = out["baseline"]["mean_ms"] / out["candidate"]["mean_ms"]
722
+ verdict = ("faster" if delta > 0 else "slower") if significant else "inconclusive"
723
+ out.update(speedup=round(speedup, 4), delta_ms=round(delta, 5),
724
+ threshold_ms=round(threshold, 5), significant=significant, verdict=verdict,
725
+ reason=(f"candidate mean {out['candidate']['mean_ms']:.4f} ms vs baseline "
726
+ f"{out['baseline']['mean_ms']:.4f} ms; |delta| {abs(delta):.4f} ms "
727
+ f"{'>' if significant else '<='} 2*SE {threshold:.4f} ms (n={n})"))
728
+ return out
729
+
730
+
731
+ @mcp.tool()
732
+ def verify(candidate: str, baseline: str, n: int = 7, warmup: int = 2,
733
+ args: list[str] | None = None, cwd: str | None = None,
734
+ timeout: int = _DEFAULT_TIMEOUT) -> dict:
735
+ """Decide whether a candidate script beats the baseline script. Both follow the bench
736
+ contract (print one JSON line with "ms" and "ok"). Runs them interleaved (baseline,
737
+ candidate, baseline, candidate, ...) warmup+n times each, back to back on the same card,
738
+ discards the warmup pairs, and returns mean/std/min for both plus a verdict:
739
+ 'faster' or 'slower' when |mean_b - mean_c| > 2*sqrt(std_b^2/n + std_c^2/n),
740
+ 'inconclusive' otherwise, 'incorrect' if the candidate printed ok=false in any run (then no
741
+ speedup is reported), 'invalid' if the two scripts disagree on shape/dtype/iters/seed or the
742
+ baseline fails its own check. Also records gpu, driver, power limit, timestamp and the exact
743
+ commands. Pass the result to record()."""
744
+ py = _python()
745
+ b_cmd = [py, baseline, *(args or [])]
746
+ c_cmd = [py, candidate, *(args or [])]
747
+ t0 = time.perf_counter()
748
+ b_ms: list[float] = []
749
+ c_ms: list[float] = []
750
+ b_ok: list[bool] = []
751
+ c_ok: list[bool] = []
752
+ b_json: dict | None = None
753
+ c_json: dict | None = None
754
+ for i in range(warmup + n):
755
+ for label, cmd in (("baseline", b_cmd), ("candidate", c_cmd)):
756
+ r = _run_target(cmd, cwd, timeout)
757
+ if "error" in r:
758
+ return {"error": f"{label} failed on run {i}: {r['error']}", "cmd": cmd,
759
+ "detail": {k: v for k, v in r.items() if k != "error"}}
760
+ if i < warmup:
761
+ continue
762
+ if label == "baseline":
763
+ b_ms.append(r["ms"]); b_ok.append(r["ok"]); b_json = b_json or r["json"]
764
+ else:
765
+ c_ms.append(r["ms"]); c_ok.append(r["ok"]); c_json = c_json or r["json"]
766
+ assert b_json is not None and c_json is not None
767
+ # The candidate must measure the same thing the same way: shape, dtype, iteration count,
768
+ # seed, and the timing protocol (a candidate timed under an older protocol is not
769
+ # comparable, whatever its number says).
770
+ mismatch = [k for k in ("shape", "dtype", "iters", "seed", "protocol", "timing")
771
+ if k in b_json and b_json.get(k) != c_json.get(k)]
772
+ out = _decide(b_ms, c_ms, b_ok, c_ok, mismatch)
773
+ out.update({
774
+ "n": n, "warmup": warmup, "order": "interleaved baseline/candidate",
775
+ "cmds": {"baseline": b_cmd, "candidate": c_cmd}, "cwd": cwd,
776
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
777
+ "wall_s": round(time.perf_counter() - t0, 1),
778
+ "contract": {k: b_json.get(k) for k in ("shape", "dtype", "iters", "seed", "protocol",
779
+ "timing", "rotation_k", "rotation_bytes",
780
+ "l2_bytes", "torch")
781
+ if k in b_json},
782
+ # What to pass to roofline(): the target's own traffic floor and nominal flop count.
783
+ "roofline_inputs": {k: b_json.get(k) for k in ("min_bytes", "flops") if k in b_json},
784
+ "candidate_extra": {k: v for k, v in c_json.items()
785
+ if k not in b_json and k not in ("ms", "ok")},
786
+ "note": "speedup = this call's baseline mean / candidate mean and moves with the "
787
+ "baseline's run-to-run noise (a few %); to rank hypotheses against each other "
788
+ "compare candidate mean_ms, which is what record() uses for keep",
789
+ **_nvidia_smi_state(),
790
+ })
791
+ return out
792
+
793
+
794
+ _LEADERBOARD_INTRO = """# Leaderboard
795
+
796
+ Every row below is generated by the `record()` tool from the JSON logs in `results/<target>/`;
797
+ each call rebuilds the whole table from those files, so it can always be reproduced from the
798
+ raw data and nobody edits it by hand. Each log is one `verify()` result: baseline and candidate
799
+ run interleaved on the stated card, n runs each after warmup, same shape, same dtype, same
800
+ power limit, timed by the target's baseline (the current targets flush L2 before every timed
801
+ call; the `timing` field in each log says so). The Speedup cell is filled only when the verdict
802
+ was `faster` (the gap beat 2 * sqrt(std_b^2/n + std_c^2/n) and every candidate run passed the
803
+ correctness check); otherwise it says `no gain`, `incorrect` or `invalid`. Speedup is relative
804
+ to the baseline script in `examples/`, never to a vendor library. The log link holds every
805
+ sample, the exact commands, driver, clock and temperature.
806
+
807
+ """
808
+ _LEADERBOARD_COLUMNS = (
809
+ "| Target | GPU | Power | Baseline mean ms (n) | Candidate mean ms (n) | Speedup | "
810
+ "What changed | Date | Commit | Who |\n"
811
+ "|--------|-----|-------|----------------------|-----------------------|---------|"
812
+ "--------------|------|--------|-----|\n")
813
+ _SUPERSEDED_INTRO = """
814
+ ## Superseded, not counted
815
+
816
+ Logs kept under `results/<target>/superseded-<reason>/`. They were measured under an earlier
817
+ timing protocol and are not comparable with the table above; they stay so nothing disappears.
818
+
819
+ """
820
+
821
+
822
+ def _speedup_cell(v: dict) -> str:
823
+ verdict = v.get("verdict")
824
+ if verdict == "faster" and v.get("speedup"):
825
+ return f"{v['speedup']:.2f}x"
826
+ if verdict in ("slower", "inconclusive"):
827
+ return "no gain"
828
+ return str(verdict) # incorrect / invalid
829
+
830
+
831
+ def _leaderboard_row(payload: dict, log_path: Path, base: Path) -> str:
832
+ v = payload["verify"]
833
+ b, c = v["baseline"], v["candidate"]
834
+ power = f"{v['power_limit_w']:.0f} W" if v.get("power_limit_w") else ""
835
+ rel_log = log_path.relative_to(base).as_posix()
836
+ what = " ".join((payload.get("note") or "").replace("|", "/").split())
837
+ what = f"{what} ([log]({rel_log}))" if what else f"[log]({rel_log})"
838
+ stamp = payload.get("recorded_at") or ""
839
+ date = f"{stamp[:4]}-{stamp[4:6]}-{stamp[6:8]}" if len(stamp) >= 8 else ""
840
+ return (f"| {payload.get('target', '')} | {v.get('gpu') or ''} | {power} | "
841
+ f"{b['mean_ms']:.4f} ({b['n']}) | {c['mean_ms']:.4f} ({c['n']}) | {_speedup_cell(v)} | "
842
+ f"{what} | {date} | {payload.get('commit', '')} | {payload.get('who', '')} |\n")
843
+
844
+
845
+ def _read_record(p: Path) -> dict | None:
846
+ """A record() log, or None for any other JSON that happens to live under results/."""
847
+ try:
848
+ d = json.loads(p.read_text())
849
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError):
850
+ return None
851
+ v = d.get("verify") if isinstance(d, dict) else None
852
+ if isinstance(v, dict) and "target" in d and {"verdict", "baseline", "candidate"} <= set(v):
853
+ return d
854
+ return None
855
+
856
+
857
+ def _rebuild_leaderboard(base: Path) -> dict:
858
+ """Regenerate LEADERBOARD.md from results/<target>/*.json (current rows, sorted by target
859
+ then time) and results/<target>/superseded-*/*.json (listed separately, not counted)."""
860
+ current: list[tuple[str, str, str]] = []
861
+ superseded: list[tuple[str, str, str]] = []
862
+ results = base / "results"
863
+ if results.is_dir():
864
+ for tdir in sorted(p for p in results.iterdir() if p.is_dir()):
865
+ for p in sorted(tdir.glob("*.json")):
866
+ d = _read_record(p)
867
+ if d:
868
+ current.append((d.get("target", ""), d.get("recorded_at", ""),
869
+ _leaderboard_row(d, p, base)))
870
+ for sdir in sorted(p for p in tdir.glob("superseded*") if p.is_dir()):
871
+ reason = sdir.name.removeprefix("superseded").strip("-_ ") or "superseded"
872
+ for p in sorted(sdir.glob("*.json")):
873
+ d = _read_record(p)
874
+ if d:
875
+ superseded.append((d.get("target", ""), d.get("recorded_at", ""),
876
+ f"| {reason} " + _leaderboard_row(d, p, base)))
877
+ text = _LEADERBOARD_INTRO + _LEADERBOARD_COLUMNS + "".join(r for _, _, r in sorted(current))
878
+ if superseded:
879
+ text += (_SUPERSEDED_INTRO + "| Reason " + _LEADERBOARD_COLUMNS.replace("\n|--", "\n|--------|--", 1)
880
+ + "".join(r for _, _, r in sorted(superseded)))
881
+ (base / "LEADERBOARD.md").write_text(text)
882
+ return {"rows": len(current), "superseded_rows": len(superseded)}
883
+
884
+
885
+ @mcp.tool()
886
+ def record(target: str, verify_result: dict | str, note: str = "", who: str = "",
887
+ cwd: str | None = None) -> dict:
888
+ """Write a verify() result to results/<target>/<timestamp>.json, then regenerate
889
+ LEADERBOARD.md from every log under results/ (so the table is always reproducible from
890
+ the raw files). The Speedup cell is filled only when the verdict is 'faster'; otherwise it
891
+ says 'no gain', 'incorrect' or 'invalid'. Call it after every verify(), win or lose; never
892
+ edit LEADERBOARD.md by hand. `note` is the one-line "what changed"; `who` says honestly
893
+ who produced the candidate, e.g. 'agent:<model> running kernel-speedrun unattended' or
894
+ 'hand-written by <who> during tool bring-up'."""
895
+ if isinstance(verify_result, str):
896
+ try:
897
+ verify_result = json.loads(verify_result)
898
+ except json.JSONDecodeError as e:
899
+ return {"error": f"verify_result is not JSON: {e}"}
900
+ v = verify_result
901
+ if not isinstance(v, dict) or not {"verdict", "baseline", "candidate"} <= set(v):
902
+ return {"error": "verify_result must be the object returned by verify() "
903
+ "(it needs verdict, baseline and candidate)"}
904
+ cwd = _local_path(cwd)
905
+ base = Path(cwd) if cwd else Path.cwd()
906
+ safe = re.sub(r"[^A-Za-z0-9_.-]+", "_", target).strip("_") or "target"
907
+ stamp = time.strftime("%Y%m%d-%H%M%S")
908
+ log_dir = base / "results" / safe
909
+ log_dir.mkdir(parents=True, exist_ok=True)
910
+ log_path = log_dir / f"{stamp}.json"
911
+ for i in range(1, 1000): # two records in the same second must not overwrite each other
912
+ if not log_path.exists():
913
+ break
914
+ log_path = log_dir / f"{stamp}-{i}.json"
915
+ git = _run(["git", "rev-parse", "--short", "HEAD"], cwd=str(base), timeout=30)
916
+ commit = git["stdout"].strip() if git.get("returncode") == 0 else ""
917
+ # The verdict is relative to the baseline, so once a good candidate exists every later
918
+ # variant is still 'faster'. `keep` therefore compares the candidate mean with the best
919
+ # earlier 'faster' row that has the same `who` (the agent's own rows in this run; the
920
+ # prompt puts the run id in `who`). The all-time best for the target is reported too, but
921
+ # its code may belong to another run, so it never drives the advice.
922
+ best_same_who: tuple[float, str] | None = None
923
+ best_all: tuple[float, str] | None = None
924
+ for p in sorted(log_dir.glob("*.json")):
925
+ d = _read_record(p)
926
+ if not d or d["verify"].get("verdict") != "faster":
927
+ continue
928
+ c_ms, rel = d["verify"]["candidate"]["mean_ms"], p.relative_to(base).as_posix()
929
+ if best_all is None or c_ms < best_all[0]:
930
+ best_all = (c_ms, rel)
931
+ if d.get("who") == who and (best_same_who is None or c_ms < best_same_who[0]):
932
+ best_same_who = (c_ms, rel)
933
+ this_ms = v["candidate"]["mean_ms"]
934
+ improved = v["verdict"] == "faster" and (best_same_who is None or this_ms < best_same_who[0])
935
+ payload = {"target": target, "note": note, "who": who, "commit": commit,
936
+ "recorded_at": stamp, "verify": v}
937
+ log_path.write_text(json.dumps(payload, indent=1) + "\n")
938
+ counts = _rebuild_leaderboard(base)
939
+ noise = 2.0 * float(v["candidate"].get("std_ms") or 0.0)
940
+ equivalent = (not improved and v["verdict"] == "faster" and best_same_who is not None
941
+ and this_ms - best_same_who[0] <= noise)
942
+ if improved:
943
+ advice = ("keep this change: lowest candidate mean among your rows so far "
944
+ "(compare candidate mean_ms between hypotheses, not speedups; the speedup "
945
+ "moves with the baseline's noise)")
946
+ elif equivalent:
947
+ advice = (f"same as your best within noise ({this_ms:.4f} vs {best_same_who[0]:.4f} ms, "
948
+ f"2*std {noise:.4f} ms): a re-run or an equivalent change; nothing to revert, "
949
+ "but it does not count as an improvement")
950
+ elif v["verdict"] == "faster":
951
+ advice = (f"revert to your own best ({best_same_who[0]:.4f} ms, {best_same_who[1]}); "
952
+ "this row is faster than the baseline but not better than that")
953
+ else:
954
+ advice = "revert or fix the candidate; this row is not a win"
955
+ return {"log": str(log_path), "leaderboard": str(base / "LEADERBOARD.md"),
956
+ "row": _leaderboard_row(payload, log_path, base).strip(),
957
+ "verdict": v["verdict"], "speedup_cell": _speedup_cell(v),
958
+ "candidate_mean_ms": this_ms, "keep": improved, "improved_on_best": improved,
959
+ "equivalent_to_best": equivalent,
960
+ "previous_best_candidate_ms": best_same_who[0] if best_same_who else None,
961
+ "previous_best_log": best_same_who[1] if best_same_who else None,
962
+ "previous_best_scope": "rows with the same `who` (your own earlier rows)",
963
+ "all_time_best_candidate_ms": best_all[0] if best_all else None,
964
+ "all_time_best_log": best_all[1] if best_all else None,
965
+ "all_time_best_note": "may come from another run or author; its code is in that "
966
+ "log's candidate command, not necessarily in your file",
967
+ "advice": advice, **counts}
968
+
969
+
970
+ def main() -> None:
971
+ mcp.run()
972
+
973
+
974
+ if __name__ == "__main__":
975
+ main()