Download code/scripts/profile_summary.py from changh95/frnet-p150: direct link, hf CLI and curl.
- Browser
- Download file 12.9 kB
-
https://huggingface.co/changh95/frnet-p150/resolve/main/code/scripts/profile_summary.py
- Command line
-
hf download hf://changh95/frnet-p150/code/scripts/profile_summary.py
-
curl -L -o profile_summary.py https://huggingface.co/changh95/frnet-p150/resolve/main/code/scripts/profile_summary.py
12.9 kB
| #!/usr/bin/env python3 | |
| # SPDX-License-Identifier: Apache-2.0 | |
| """Summary of a device profile of ``profile_ops.py`` (CPU only, standard library): the OPT_BASELINE.md tables. | |
| python code/scripts/profile_summary.py <ops_perf_results_*.csv> <out.json> [--md out.md] | |
| Reads the Tracy ops CSV (``python -m tracy -r ...``) and its sections ``eager`` .. ``eager_end`` and ``trace`` .. | |
| ``trace_end``. The eager section carries per-module (``m:<module>``) and per-conv (``c:<layer>``) signposts; the | |
| replay has the same op sequence (checked op code by op code), so every replayed op inherits the module / layer of | |
| the eager op at the same position. Per section: program count, device kernel sum, FW sum, op-to-op gap sum and | |
| median, span (first FW start -> last FW end at the CSV's chip frequency), cores per op, math fidelities; tables per | |
| category, module, op code and layer; the top single ops; the largest gaps. If the trace section holds several | |
| replays (``profile_ops.py --replays N``), the last replay session is summarised. Input shapes of the CSV are those of | |
| the first op that built a cached program (``--no-op-info-cache`` in tracy shows every op's own), so read shapes of | |
| later ops of the same program with care; durations are per op. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import csv | |
| import json | |
| import re | |
| import statistics | |
| from collections import OrderedDict, defaultdict | |
| from pathlib import Path | |
| from typing import Any, Dict, List, Optional, Tuple | |
| KERNEL = "DEVICE KERNEL DURATION [ns]" | |
| FW = "DEVICE FW DURATION [ns]" | |
| GAP = "OP TO OP LATENCY [ns]" | |
| START = "DEVICE FW START CYCLE" | |
| END = "DEVICE FW END CYCLE" | |
| CATEGORIES = [ # (category, regex on the op code), first match wins | |
| ("segment_reduce (K1 generic_op)", r"GenericOp|generic_op|Generic"), | |
| ("conv2d", r"Conv2d|conv2d|Conv2D"), | |
| ("halo", r"Halo|halo"), | |
| ("matmul (point linears, 1x1 convs, resize matmuls)", r"Matmul|matmul|Linear"), | |
| ("slice / DRAM-slicing copies", r"Slice|slice"), | |
| ("ternary (addcmul)", r"Ternary|ternary|Addcmul|addcmul"), | |
| ("binary (adds, two-term / K-split sums)", r"Binary|binary"), | |
| ("softmax", r"Softmax|softmax"), | |
| ("reduction", r"Reduce|reduce|Max\b|MaxDevice"), | |
| ("embedding (gathers)", r"Embedding|embedding"), | |
| ("tilize / untilize / layout", r"Tilize|tilize|Untilize|untilize|ToLayout|Layout"), | |
| ("reshape / transpose / permute / concat", r"Reshape|reshape|Transpose|transpose|Permute|permute|View|Concat|concat"), | |
| ("move / copy / reshard", r"Move|move|Copy|copy|Shard|shard|Interleaved|interleaved|Reallocate"), | |
| ("pad / fill", r"Pad|pad|Fill|fill"), | |
| ("unary / typecast (hardswish, relu, sigmoid)", r"Unary|unary|Typecast|typecast"), | |
| ] | |
| COMPUTE = ("Conv2dDeviceOperation", "MatmulDeviceOperation") | |
| def category(op: str) -> str: | |
| for name, pat in CATEGORIES: | |
| if re.search(pat, op): | |
| return name | |
| return "other" | |
| def num(row: Dict[str, str], key: str) -> float: | |
| v = (row.get(key) or "").strip() | |
| try: | |
| return float(v) if v else 0.0 | |
| except ValueError: | |
| return 0.0 | |
| def logical(v: str) -> str: | |
| """'32[16]' (padded[logical]) -> '16'; '7' -> '7'.""" | |
| m = re.match(r"\s*([0-9]+)(?:\[([0-9]+)\])?", v or "") | |
| return (m.group(2) or m.group(1)) if m else "?" | |
| def chip_freq_mhz(csv_path: Path) -> Optional[float]: | |
| for cand in [csv_path.parent / "profile_log_device.csv", csv_path.parent.parent / "profile_log_device.csv", | |
| csv_path.parent / ".logs" / "profile_log_device.csv", | |
| csv_path.parent.parent / ".logs" / "profile_log_device.csv"]: | |
| try: | |
| head = cand.read_text(errors="ignore").splitlines()[0] | |
| except (OSError, IndexError): | |
| continue | |
| m = re.search(r"CHIP_FREQ\[MHz\]:\s*([0-9.]+)", head) | |
| if m: | |
| return float(m.group(1)) | |
| return None | |
| def sections(rows: List[Dict[str, str]], name_key: str) -> Dict[str, List[Tuple[Dict[str, str], str, str]]]: | |
| """{"eager": [(row, module, layer)], "trace": [...]} from the signposts.""" | |
| out: Dict[str, List[Tuple[Dict[str, str], str, str]]] = {"eager": [], "trace": []} | |
| cur: Optional[str] = None | |
| module, layer = "", "" | |
| for r in rows: | |
| if (r.get("OP TYPE") or "") == "signpost": | |
| code = r.get(name_key, "") | |
| if code in ("eager", "trace"): | |
| cur, module, layer = code, "", "" | |
| elif code in ("eager_end", "trace_end"): | |
| cur = None | |
| elif code.startswith("m:"): | |
| module, layer = code[2:], "" | |
| elif code.startswith("c:"): | |
| layer = code[2:] | |
| continue | |
| if cur is not None: | |
| out[cur].append((r, module, layer)) | |
| return out | |
| def last_replay(sec: List[Tuple[Dict[str, str], str, str]]) -> List[Tuple[Dict[str, str], str, str]]: | |
| """The ops of the last trace replay session (all of them when the CSV has no session ids).""" | |
| ids = [(r.get("METAL TRACE REPLAY SESSION ID") or "").strip() for r, _, _ in sec] | |
| if not any(ids): | |
| return sec | |
| last = [i for i in ids if i][-1] | |
| return [x for x, i in zip(sec, ids) if i == last] | |
| def summarize(sec: List[Tuple[Dict[str, str], str, str]], name_key: str, freq: float, top: int = 10) -> Dict[str, Any]: | |
| rows = [r for r, _, _ in sec] | |
| kern = [num(r, KERNEL) / 1e3 for r in rows] | |
| fw = [num(r, FW) / 1e3 for r in rows] | |
| gaps = [num(r, GAP) / 1e3 for r in rows] | |
| if gaps: | |
| gaps[0] = 0.0 # the first op's gap reaches back across the section boundary | |
| starts = [num(r, START) for r in rows if num(r, START) > 0] | |
| ends = [num(r, END) for r in rows if num(r, END) > 0] | |
| span = (max(ends) - min(starts)) / freq if starts and ends else None | |
| total = sum(kern) or 1.0 | |
| def group(keyfn) -> List[Dict[str, Any]]: | |
| agg: Dict[str, List[float]] = OrderedDict() | |
| for (r, m, l), k, g in zip(sec, kern, gaps): | |
| key = keyfn(r, m, l) | |
| a = agg.setdefault(key, [0, 0.0, 0.0, 0.0]) | |
| a[0] += 1 | |
| a[1] += k | |
| a[2] += g | |
| if r.get(name_key, "") in COMPUTE: | |
| a[3] += k | |
| return [{"key": key, "ops": int(c), "kernel_us": round(t, 1), "share": round(t / total, 4), | |
| "gap_us": round(gs, 1), "compute_us": round(cu, 1)} | |
| for key, (c, t, gs, cu) in sorted(agg.items(), key=lambda kv: -kv[1][1])] | |
| fid: Dict[str, int] = defaultdict(int) | |
| cores = [] | |
| for r in rows: | |
| f = (r.get("MATH FIDELITY") or "").strip() | |
| if f: | |
| fid[f] += 1 | |
| cores.append(num(r, "CORE COUNT")) | |
| order = sorted(range(len(rows)), key=lambda i: -kern[i])[:top] | |
| top_ops = [] | |
| for i in order: | |
| r, m, l = sec[i] | |
| shape = "x".join(logical(r.get(f"INPUT_0_{d}_PAD[LOGICAL]") or r.get(f"INPUT_0_{d}") or "") for d in "WZYX") | |
| oshape = "x".join(logical(r.get(f"OUTPUT_0_{d}_PAD[LOGICAL]") or r.get(f"OUTPUT_0_{d}") or "") for d in "WZYX") | |
| top_ops.append({"index": i, "op": r.get(name_key), "module": m, "layer": l, "kernel_us": round(kern[i], 1), | |
| "fw_us": round(fw[i], 1), "cores": int(num(r, "CORE COUNT")), | |
| "fidelity": (r.get("MATH FIDELITY") or "").strip(), "input0": shape, "output0": oshape, | |
| "in0_dtype": (r.get("INPUT_0_DATATYPE") or "").strip(), | |
| "in0_mem": (r.get("INPUT_0_MEMORY") or "").strip(), | |
| "out0_mem": (r.get("OUTPUT_0_MEMORY") or "").strip()}) | |
| gap_order = sorted(range(1, len(rows)), key=lambda i: -gaps[i])[:top] | |
| top_gaps = [{"index": i, "op": rows[i].get(name_key), "after": rows[i - 1].get(name_key), | |
| "gap_us": round(gaps[i], 1), "module": sec[i][1], "layer": sec[i][2]} for i in gap_order] | |
| nz_gaps = [g for g in gaps[1:] if g > 0] | |
| ops_list = [{"index": i, "op": r.get(name_key), "module": m, "layer": l, "kernel_us": round(k, 2), | |
| "gap_us": round(g, 2), "cores": int(num(r, "CORE COUNT")), | |
| "fidelity": (r.get("MATH FIDELITY") or "").strip(), | |
| "input0": "x".join(logical(r.get(f"INPUT_0_{d}_PAD[LOGICAL]") or r.get(f"INPUT_0_{d}") or "") | |
| for d in "WZYX"), | |
| "in0_mem": (r.get("INPUT_0_MEMORY") or "").strip(), | |
| "out0_mem": (r.get("OUTPUT_0_MEMORY") or "").strip()} | |
| for i, ((r, m, l), k, g) in enumerate(zip(sec, kern, gaps))] | |
| return { | |
| "ops": len(rows), "kernel_sum_us": round(sum(kern), 1), "fw_sum_us": round(sum(fw), 1), | |
| "op2op_sum_us": round(sum(gaps), 1), | |
| "op2op_median_us": round(statistics.median(nz_gaps), 2) if nz_gaps else None, | |
| "span_us": round(span, 1) if span else None, "freq_mhz": freq, | |
| "cores_median": statistics.median(cores) if cores else None, | |
| "ops_below_120_cores": sum(1 for c in cores if 0 < c < 120), | |
| "compute_us": round(sum(k for r, k in zip(rows, kern) if r.get(name_key, "") in COMPUTE), 1), | |
| "fidelity": dict(fid), | |
| "by_op": group(lambda r, m, l: r.get(name_key, "?")), | |
| "by_category": group(lambda r, m, l: category(r.get(name_key, "?"))), | |
| "by_module": group(lambda r, m, l: m or "?"), | |
| "by_layer": group(lambda r, m, l: l or f"[{m or '?'}]"), | |
| "top_ops": top_ops, "top_gaps": top_gaps, "ops_list": ops_list, | |
| } | |
| def md_table(rows: List[Dict[str, Any]], n: int, label: str) -> str: | |
| out = [f"| {label} | ops | kernel us | share | compute us | op-to-op us |", "|---|---:|---:|---:|---:|---:|"] | |
| for r in rows[:n]: | |
| out.append(f"| {r['key']} | {r['ops']} | {r['kernel_us']:.1f} | {100 * r['share']:.1f} % | " | |
| f"{r['compute_us']:.1f} | {r['gap_us']:.1f} |") | |
| return "\n".join(out) | |
| def main() -> None: | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("csv") | |
| ap.add_argument("out_json") | |
| ap.add_argument("--md") | |
| ap.add_argument("--freq-mhz", type=float, default=None) | |
| a = ap.parse_args() | |
| path = Path(a.csv) | |
| with open(path, newline="") as f: | |
| rows = list(csv.DictReader(f)) | |
| name_key = "OP CODE" if rows and "OP CODE" in rows[0] else "OP NAME" | |
| freq = a.freq_mhz or chip_freq_mhz(path) or 1350.0 | |
| secs = sections(rows, name_key) | |
| eager, trace_all = secs["eager"], secs["trace"] | |
| trace = last_replay(trace_all) | |
| # the replay inherits the module / layer of the eager op at the same position (identical op sequence) | |
| same = len(eager) == len(trace) and all(e[0].get(name_key) == t[0].get(name_key) for e, t in zip(eager, trace)) | |
| if same: | |
| trace = [(r, e[1], e[2]) for (r, _, _), e in zip(trace, eager)] | |
| res = {"csv": str(path), "columns": list(rows[0].keys()) if rows else [], "freq_mhz": freq, | |
| "trace_matches_eager_op_sequence": same, "trace_section_ops": len(trace_all), | |
| "trace_ids": sorted({(r.get("METAL TRACE ID") or "").strip() for r, _, _ in trace_all}), | |
| "replay_sessions": sorted({(r.get("METAL TRACE REPLAY SESSION ID") or "").strip() for r, _, _ in trace_all}), | |
| "eager": summarize(eager, name_key, freq), "trace": summarize(trace, name_key, freq)} | |
| Path(a.out_json).write_text(json.dumps(res, indent=1) + "\n") | |
| t = res["trace"] | |
| lines = [f"csv: {path}", f"freq {freq} MHz; trace op sequence == eager: {same}; trace ids {res['trace_ids']}; " | |
| f"replay sessions {res['replay_sessions']} ({len(trace_all)} ops in the trace section)"] | |
| for s in ("eager", "trace"): | |
| x = res[s] | |
| lines.append(f"{s}: ops {x['ops']} kernel {x['kernel_sum_us']:.1f} us (conv/matmul {x['compute_us']:.1f}) " | |
| f"fw {x['fw_sum_us']:.1f} us op2op {x['op2op_sum_us']:.1f} us (median {x['op2op_median_us']}) " | |
| f"span {x['span_us']} us cores median {x['cores_median']} <120 cores: " | |
| f"{x['ops_below_120_cores']} fidelity {x['fidelity']}") | |
| lines += ["", md_table(t["by_category"], 20, "category (trace)"), "", md_table(t["by_module"], 20, "module (trace)"), | |
| "", md_table(t["by_op"], 25, "op code (trace)"), "", md_table(t["by_layer"], 40, "layer (trace)"), "", | |
| "| # | op | module | layer | kernel us | cores | fidelity | input0 | output0 | in mem | out mem |", | |
| "|---:|---|---|---|---:|---:|---|---|---|---|---|"] | |
| for k, o in enumerate(t["top_ops"], 1): | |
| lines.append(f"| {k} | {o['op']} | {o['module']} | {o['layer']} | {o['kernel_us']:.1f} | {o['cores']} | " | |
| f"{o['fidelity']} | {o['input0']} | {o['output0']} | {o['in0_mem']} | {o['out0_mem']} |") | |
| lines += ["", "largest op-to-op gaps (trace): " + json.dumps(t["top_gaps"])] | |
| text = "\n".join(lines) | |
| print(text) | |
| if a.md: | |
| Path(a.md).write_text(text + "\n") | |
| if __name__ == "__main__": | |
| main() | |