Files
manifoldbt/benchmarks/gpu/report.py
T
Jimmy7892 923559d68a bench: time only the GPU on Modal, against the CPU job's published numbers
The CPU reference for a sweep is the CPU job's, published from its
GitHub runner. Timing the CPU again in the Modal container added a second
CPU number from another machine, most of the run's billed time, and a
ratio that measured the container's CPU as much as the card.

The CPU path now runs once per point, untimed, only so that every GPU
cell is still checked against it. The report reads the CPU column from
vs_vectorbt/results/latest.json, matched on bars, combinations and data
digest, and gives a ratio only when both runs measured the same version.

Validated on a Modal L4 with manifoldbt 0.28.1: 32 ms, 107 ms and 223 ms
against 228 ms, 902 ms and 3.53 s in the CPU job's run of the same
version (7.1x, 8.4x, 15.9x), every cell bit-identical, 32 s from start to
finish instead of 120 s. A local run on a different numpy produced other
data digests and was, as intended, not matched with the CPU job.
2026-10-25 11:25:42 +01:00

147 lines
6.0 KiB
Python

"""Render a GPU sweep result as Markdown, and into the GitHub job summary.
python report.py results-gpu.json [--cpu-results ../vs_vectorbt/results/latest.json]
The CPU column is not measured here. It is the CPU sweeps job's published number
for the same grid, read from its results file and matched on bars, combinations
and the digest of the data, so both columns are about the same work. The two
come from different machines (a GitHub runner and a Modal GPU), which is what
the ratio says: what the sweep costs on the runner the CPU numbers are published
from, against what it costs on the card. It is only computed when both runs
measured the same manifoldbt version.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from typing import Any, Dict, List, Optional, Tuple
HERE = os.path.dirname(os.path.abspath(__file__))
CPU_RESULTS = os.path.join(HERE, "..", "vs_vectorbt", "results", "latest.json")
METHOD_LINK = (
"https://github.com/manifoldbt/manifoldbt/blob/master/benchmarks/gpu/README.md"
)
Key = Tuple[int, int, str]
def cpu_reference(published: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""The CPU sweeps job's points, keyed by (bars, combos, data digest)."""
payload = (published or {}).get("sweeps") or {}
env = payload.get("environment") or {}
points: Dict[Key, float] = {}
for row in payload.get("sweeps") or []:
seconds = ((row.get("timings") or {}).get("seconds") or {}).get("manifoldbt")
if seconds is not None:
points[(row["bars"], row["combos"], row.get("data_digest", ""))] = seconds
return {
"points": points,
"version": (env.get("versions") or {}).get("manifoldbt"),
"cores": env.get("logical_cores"),
"run_url": (published or {}).get("run_url", ""),
}
def _card(env: Dict[str, Any]) -> str:
gpus = env.get("gpus") or []
if not gpus:
return "no GPU visible"
g = gpus[0]
extra = ", ".join(x for x in (g.get("memory.total"), "driver " + g["driver_version"]
if g.get("driver_version") else "") if x)
return "{} ({})".format(g.get("name", "?"), extra) if extra else g.get("name", "?")
def _seconds(value: float) -> str:
return "{:.3f} s".format(value) if value >= 0.1 else "{:.1f} ms".format(value * 1e3)
def render(payload: Dict[str, Any], cpu: Optional[Dict[str, Any]] = None) -> str:
cpu = cpu or cpu_reference(None)
env = payload.get("environment") or {}
version = (env.get("versions") or {}).get("manifoldbt", "?")
same_version = cpu["version"] == version
lines: List[str] = ["## GPU parameter sweeps", "",
"manifoldbt {} on {}. {}".format(
version, _card(env), env.get("platform", "")).rstrip()]
if cpu["points"]:
lines.append(
"CPU column: the CPU sweeps job's published run{}, manifoldbt {} on a "
"{}-vCPU GitHub runner.{}".format(
" ({})".format(cpu["run_url"]) if cpu["run_url"] else "",
cpu["version"] or "?", cpu["cores"] or "?",
"" if same_version else
" Not the version measured here, so no ratio is given."))
lines.append("")
if payload.get("status") != "exact" and payload.get("reason"):
lines += ["**Failed: {}**".format(payload["reason"]), ""]
rows = payload.get("sweeps") or []
if rows:
lines += [
"| bars | combos | GPU | per combo | first GPU call | CPU (runner) "
"| GPU vs runner CPU | parity |",
"|---:|---:|---:|---:|---:|---:|---:|:---|",
]
for row in rows:
if "timings" not in row:
lines.append("| {:,} | {:,} | | | | | | failed: {} |".format(
row["bars"], row.get("requested_combos", 0), row.get("reason", "")))
continue
t = row["timings"]
cpu_s = cpu["points"].get((row["bars"], row["combos"], row.get("data_digest", "")))
ratio = "x{:.1f}".format(cpu_s / t["seconds"]) if cpu_s and same_version else ""
parity = row["parity"]
verdict = "{} ({:,}/{:,} bit-identical)".format(
row["status"], parity.get("identical_cells", 0), row["combos"])
if row.get("reason"):
verdict += ": " + row["reason"]
lines.append("| {:,} | {:,} | {}{} | {:.2f} us | {} | {} | {} | {} |".format(
row["bars"], row["combos"], _seconds(t["seconds"]),
" ~" if t.get("noisy") else "", t["per_combo_us"],
_seconds(t["first_call_s"]), _seconds(cpu_s) if cpu_s else "",
ratio, verdict))
lines.append("")
lines += [
"---",
"",
"GPU: medians of {} repetitions, after an untimed first call (reported apart: "
"it compiles the kernels) and a calibration of the card. `~` = IQR above 15% of "
"the median, indicative only. Parity: every cell of the GPU grid against the "
"same grid run once on the CPU of the GPU machine, keyed by parameter pair. "
"Method: {}".format(payload.get("reps", "?"), METHOD_LINK),
"",
]
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(description="render a GPU sweep result")
parser.add_argument("results", nargs="?", default="results-gpu.json")
parser.add_argument("--cpu-results", default=CPU_RESULTS,
help="the CPU benchmark's published results file")
args = parser.parse_args()
with open(args.results, encoding="utf-8") as fh:
payload = json.load(fh)
published = None
if os.path.exists(args.cpu_results):
with open(args.cpu_results, encoding="utf-8") as fh:
published = json.load(fh)
text = render(payload, cpu_reference(published))
sys.stdout.write(text + "\n")
summary = os.environ.get("GITHUB_STEP_SUMMARY")
if summary:
with open(summary, "a", encoding="utf-8") as fh:
fh.write(text + "\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())