Opens a larger view. Escape closes it.

hardware-counters

parse_craypat.py

#!/usr/bin/env python3
"""
Parse CrayPat experiment dirs from the ARCHER2 single-node core sweep
into a tidy feature matrix for performance prediction.
"""
import os, re, subprocess, sys, csv
from pathlib import Path

# Data and output roots. Overridable so the pipeline can be relocated
# (a different account, a scratch copy, another site) without editing code;
# the default keeps existing invocations working unchanged.
D    = os.environ.get("DISS_ROOT", "/work/project/project/user")
BASE = Path(f"{D}/runs/sweep")
OUT  = Path(f"{D}/data")
OUT.mkdir(parents=True, exist_ok=True)

# Value lines have EXACTLY 2 leading spaces; legend lines have 3+ and are prose.
VAL_RE = re.compile(
    r"^  (?P<name>[A-Z][A-Za-z0-9_:]*)\s+"
    r"(?:(?:[\d.]+)\s*(?:[GMK]/sec|W)\s+)?"          # optional rate column
    r"(?P<val>\d[\d,]*(?:\.\d+)?)"                   # raw total
    r"\s*(?:ops|instr|cycles|J|refs|misses|hits)?\s*$"
)

PAT_REPORT = "/opt/cray/pe/perftools/23.09.0/bin/pat_report"

def pat_report(expdir, mode):
    """Invoke pat_report directly; module env is not inherited by `bash -lc`."""
    try:
        r = subprocess.run([PAT_REPORT, "-O", mode, str(expdir)],
                           capture_output=True, text=True, timeout=600)
        return r.stdout
    except Exception:
        return ""

def parse_hwpc(text):
    vals = {}
    for line in text.splitlines():
        if "PAT_RT_PERFCTR" in line:
            break                       # legend section follows
        m = VAL_RE.match(line)
        if m:
            try:
                vals[m.group("name")] = float(m.group("val").replace(",", ""))
            except ValueError:
                pass
    return vals

def parse_meta(outfile):
    meta, walls = {}, {}
    if not outfile or not outfile.exists():
        return meta, walls
    for line in outfile.read_text(errors="replace").splitlines():
        if line.startswith("META "):
            for kv in line.split()[1:]:
                if "=" in kv:
                    k, v = kv.split("=", 1); meta[k] = v
        m = re.match(r"=====END_SET (\w+) rc=(\d+) wall=([\d.]+)=====", line)
        if m:
            walls[m.group(1)] = {"rc": int(m.group(2)), "wall": float(m.group(3))}
    return meta, walls

FUNC_RE = re.compile(r"^\|+\s+([\d.]+)%\s+\|\s+([\d.]+)\s+\|.*\|\s+([\d,.]+)\s+\|\s+(\S.*)$")
def parse_funcs(text):
    out = []
    for line in text.splitlines():
        m = FUNC_RE.match(line)
        if m:
            out.append({"time_pct": float(m.group(1)), "time_s": float(m.group(2)),
                        "calls": float(m.group(3).replace(",", "")),
                        "function": m.group(4).strip()})
    return out

def main():
    runs, funcs = [], []
    for d in sorted(BASE.glob("*_c*")):
        if not d.is_dir():
            continue
        m = re.match(r"(\w+)_c(\d+)$", d.name)
        if not m:
            continue
        app, nc = m.group(1), int(m.group(2))
        # slurm job names differ from dir names for some apps (openfoam -> of_)
        prefixes = [app] + (["of"] if app == "openfoam" else [])
        cand = []
        for pre in prefixes:
            cand += sorted(BASE.glob(f"{pre}_c{nc}-*.out"))
        # newest file that actually recorded a completed sweep
        chosen = None
        for f in sorted(cand, key=lambda p: p.stat().st_mtime, reverse=True):
            if "SWEEP_DONE" in f.read_text(errors="replace"):
                chosen = f
                break
        meta, walls = parse_meta(chosen or (cand[-1] if cand else None))

        row = {"app": app, "ncore": nc}
        row.update({f"wall_{k}": v["wall"] for k, v in walls.items()})
        ok = sorted(v["wall"] for v in walls.values() if v["rc"] == 0)
        if ok:
            row["runtime_s"] = ok[len(ok)//2]          # median across sets
            row["runtime_min_s"] = ok[0]
            row["nsets_ok"] = len(ok)

        for s in "ABCDE":
            exp = d / f"exp_{s}"
            if exp.exists():
                row.update(parse_hwpc(pat_report(str(exp), "hwpc")))
                if s == "A":
                    for f in parse_funcs(pat_report(str(exp), "profile")):
                        funcs.append({"app": app, "ncore": nc, **f})
        runs.append(row)
        ncnt = sum(1 for k in row if k[0].isupper())
        print(f"parsed {app:8s} c{nc:<4d} counters={ncnt}", file=sys.stderr)

    if runs:
        lead = ["app","ncore","runtime_s","runtime_min_s","nsets_ok"]
        keys = lead + sorted({k for r in runs for k in r} - set(lead))
        with open(OUT/"runs.csv","w",newline="") as fh:
            w = csv.DictWriter(fh, fieldnames=keys); w.writeheader(); w.writerows(runs)
        print(f"wrote runs.csv ({len(runs)} rows, {len(keys)} cols)")
    if funcs:
        with open(OUT/"functions.csv","w",newline="") as fh:
            w = csv.DictWriter(fh, fieldnames=["app","ncore","function","time_pct","time_s","calls"])
            w.writeheader(); w.writerows(funcs)
        print(f"wrote functions.csv ({len(funcs)} rows)")

if __name__ == "__main__":
    main()