cascade-profiler 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.
@@ -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()