diff --git a/benchmarks/microbenchmarks/README.md b/benchmarks/microbenchmarks/README.md index ba868b0f0..35dffa652 100644 --- a/benchmarks/microbenchmarks/README.md +++ b/benchmarks/microbenchmarks/README.md @@ -75,3 +75,64 @@ python compare_results.py baseline.csv candidate.csv --bench-name GEMM The script auto-detects metric columns, computes speedups for overlapping rows, and reports rows that exist only in the baseline or only in the candidate. + +## Visualizing results + +`visualize.py` turns the produced CSVs into interactive +[Plotly](https://plotly.com/python/) charts and writes a single, self-contained +HTML file (no server needed — convenient over SSH or as a PR attachment). +`dashboard.py` is a live [Panel](https://panel.holoviz.org/) companion for +ad-hoc exploration. Install the extra dependencies first: + +```bash +pip install -r requirements-viz.txt +``` + +Both consume the aggregate (`--csv`) and per-sample (`--csv-samples`) CSVs and +auto-detect which format they were given. Parameter columns are detected +generically, so every benchmark in this directory is supported (dense GEMM, +FP8 GEMM, grouped GEMM, normalization, casting). When a benchmark has a +dimension the default axes don't show — e.g. grouped GEMM's expert count `B` — +the tool prints a `[note]` that medians are pooling across it; add `--color`, +`--facet`, or `--pass` (or use a dashboard filter) to separate the series. + +### Static HTML report + +```bash +# All applicable views for one CSV -> benchmark_gemm_samples.html +python visualize.py benchmark_gemm_samples.csv + +# A single view, choosing the metric +python visualize.py benchmark_gemm_samples.csv --kind scaling --value throughput + +# Baseline vs candidate speedup (visual complement to compare_results.py) +python visualize.py bench_gemm_candidate.csv --baseline benchmark_gemm_samples.csv +``` + +Plot kinds (`--kind`, default `report` = all applicable): + +- `distribution`: box plus every raw sample point per group, with percentile + trimming (`--trim-upper` / `--trim-lower`). The honest distribution view for + the suite's small (~12) sample counts, where a violin/KDE would over-smooth. + Requires a samples CSV. +- `scaling`: median throughput (or time) vs token count `M` per case, with a + shaded min–max band. +- `bars`: grouped median-throughput bars per case with IQR error bars. +- `comparison`: baseline-vs-candidate speedup bars, one per benchmark group + (needs `--baseline`). + +Axes default sensibly per kind and can be overridden with `--x`, `--color`, +`--facet`, `--pass`, and `--value`. Pass `--cdn` to load plotly.js from a CDN +instead of inlining it (much smaller file, needs internet to open). + +### Interactive dashboard + +```bash +panel serve dashboard.py --show --args benchmark_gemm_samples.csv +``` + +The CSV path may also be set via the `BENCH_CSV` environment variable. The +sidebar exposes the plot kind, metric, independent variable, hue, facet, a +percentile-trim slider, and a per-attribute filter for every parameter column. +Figure builders are shared with `visualize.py`, so the static and interactive +views stay in sync. diff --git a/benchmarks/microbenchmarks/dashboard.py b/benchmarks/microbenchmarks/dashboard.py new file mode 100644 index 000000000..d87352961 --- /dev/null +++ b/benchmarks/microbenchmarks/dashboard.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python +############################################################################### +# Copyright (c) 2026, Advanced Micro Devices, Inc. All rights reserved. +# +# See LICENSE for license information. +############################################################################### +"""Interactive Panel dashboard for microbenchmark CSVs. + +A live-exploration companion to ``visualize.py`` (which emits static, +self-contained HTML). It mirrors the JAX fused-attention dashboard +(``benchmarks/attention/panel_app.py``): pick the independent variable, the +hue/series, the metric, per-attribute filters, and a percentile trim, and the +Plotly figure re-renders. Figure builders are shared with ``visualize.py`` so +the static and interactive views never diverge. + +Usage +----- + panel serve dashboard.py --show --args benchmark_gemm_samples.csv + +The CSV path may also be given via the ``BENCH_CSV`` environment variable. If +neither is provided, the first ``*_samples.csv`` in the working directory is +used. +""" + +import glob +import os +import sys + +import panel as pn + +import viz_data as vd +import visualize as viz + +pn.extension("plotly", design="material", sizing_mode="stretch_width") + + +def _resolve_csv_path(): + """Find the CSV to load from --args, $BENCH_CSV, or the cwd.""" + for arg in sys.argv[1:]: + if arg.endswith(".csv"): + return arg + if os.environ.get("BENCH_CSV"): + return os.environ["BENCH_CSV"] + candidates = sorted(glob.glob("*_samples.csv")) or sorted(glob.glob("*.csv")) + if not candidates: + raise SystemExit( + "No CSV given. Pass one via '--args FILE' or set BENCH_CSV." + ) + return candidates[0] + + +CSV_PATH = _resolve_csv_path() +DF = vd.load_any(CSV_PATH) +IS_SAMPLES = DF.attrs["source"] == "samples" +PARAMS = list(DF.attrs["params"]) +AXIS_COLS = PARAMS + ["pass"] + +KINDS = (["distribution", "scaling", "bars"] if IS_SAMPLES else ["scaling", "bars"]) + +# --------------------------------------------------------------------------- +# Controls +# --------------------------------------------------------------------------- +kind_w = pn.widgets.Select(name="Plot", options=KINDS, value=KINDS[0]) +value_w = pn.widgets.Select( + name="Metric", options=["auto", "time_ms", "throughput"], value="auto", +) +x_w = pn.widgets.Select( + name="Independent variable (x)", options=AXIS_COLS, + value="M" if "M" in AXIS_COLS else AXIS_COLS[0], +) +color_w = pn.widgets.Select( + name="Hue / series", options=AXIS_COLS, + value="Case" if "Case" in AXIS_COLS else AXIS_COLS[-1], +) +facet_w = pn.widgets.Select( + name="Facet", options=["none"] + AXIS_COLS, + value="pass" if "pass" in AXIS_COLS else "none", +) +trim_w = pn.widgets.FloatSlider( + name="Percentile trim (upper)", start=0.5, end=1.0, step=0.01, value=0.95, + disabled=not IS_SAMPLES, +) + +# One filter per parameter column (+ pass). Empty selection means "all". +filter_ws = { + col: pn.widgets.MultiChoice( + name=col, options=sorted(DF[col].astype(str).unique(), key=str), value=[], + ) + for col in AXIS_COLS +} + + +def _resolve_value(kind, value): + if value != "auto": + return value + if kind == "distribution": + return "time_ms" + return vd.default_value_column(DF, "Forward") + + +def _apply_filters(df): + for col, widget in filter_ws.items(): + if widget.value: + df = df[df[col].astype(str).isin(widget.value)] + return df + + +@pn.depends( + kind=kind_w, value=value_w, x=x_w, color=color_w, facet=facet_w, trim=trim_w, + **{f"f_{c}": w for c, w in filter_ws.items()}, +) +def make_plot(kind, value, x, color, facet, trim, **_filters): + df = _apply_filters(DF) + if df.empty: + return pn.pane.Alert("No rows match the current filters.", alert_type="warning") + + value = _resolve_value(kind, value) + facet_arg = None if facet == "none" else facet + try: + if kind == "distribution": + fig = viz.fig_distribution(df, x=x, value=value, color=color, + facet=facet_arg, trim_upper=trim) + elif kind == "scaling": + fig = viz.fig_scaling(vd.trim_percentile(df, value=value, upper=trim), + x=x, value=value, color=color, facet=facet_arg) + else: + fig = viz.fig_throughput_bars(vd.trim_percentile(df, value=value, upper=trim), + x=x, value=value, color=color, facet=facet_arg) + except Exception as exc: # surface builder errors in the UI instead of 500s + return pn.pane.Alert(f"Could not build plot: {exc}", alert_type="danger") + return pn.pane.Plotly(fig, sizing_mode="stretch_width", height=640) + + +template = pn.template.BootstrapTemplate( + title=f"Microbenchmark Explorer — {os.path.basename(CSV_PATH)}", + sidebar=[ + pn.pane.Markdown(f"**Source:** `{CSV_PATH}` \n**Format:** {DF.attrs['source']}"), + kind_w, value_w, x_w, color_w, facet_w, trim_w, + pn.pane.Markdown("### Filters (empty = all)"), + *filter_ws.values(), + ], +) +template.main.append(pn.Column(make_plot)) +template.servable() diff --git a/benchmarks/microbenchmarks/requirements-viz.txt b/benchmarks/microbenchmarks/requirements-viz.txt new file mode 100644 index 000000000..20dcb954b --- /dev/null +++ b/benchmarks/microbenchmarks/requirements-viz.txt @@ -0,0 +1,5 @@ +# Dependencies for the visualization tooling (visualize.py, dashboard.py). +# Not required to run the benchmarks themselves. +pandas +plotly +panel diff --git a/benchmarks/microbenchmarks/visualize.py b/benchmarks/microbenchmarks/visualize.py new file mode 100644 index 000000000..cb2354bcf --- /dev/null +++ b/benchmarks/microbenchmarks/visualize.py @@ -0,0 +1,424 @@ +#!/usr/bin/env python +############################################################################### +# Copyright (c) 2026, Advanced Micro Devices, Inc. All rights reserved. +# +# See LICENSE for license information. +############################################################################### +"""Generate interactive Plotly visualizations from microbenchmark CSVs. + +Consumes the CSVs produced by the benchmarks in this directory (both the +per-sample ``--csv-samples`` output and the aggregate ``--csv`` output) and +writes self-contained, interactive HTML. No server is required to view the +result, which suits remote / SSH workflows and PR attachments. + +Plot kinds +---------- +* ``distribution`` box + all raw sample points per group, with percentile + trimming. The honest distribution view for the suite's small (~12) sample + counts, where a violin/KDE would over-smooth. +* ``scaling`` median throughput (or time) vs token count ``M`` per case, with + a shaded min--max band. +* ``bars`` grouped median-throughput bars per case with IQR error bars. +* ``comparison`` baseline-vs-candidate speedup bars, one per benchmark group + (visual complement to ``compare_results.py``). +* ``report`` (default) all applicable kinds in one HTML file. + +Examples +-------- + python visualize.py benchmark_gemm_samples.csv + python visualize.py benchmark_gemm_samples.csv --kind scaling --value throughput + python visualize.py bench_gemm_candidate.csv --baseline benchmark_gemm_samples.csv +""" + +import argparse +from pathlib import Path + +import plotly.express as px +import plotly.graph_objects as go +from plotly.subplots import make_subplots + +import viz_data as vd + +_PALETTE = px.colors.qualitative.Plotly +_FACET_WRAP = 4 + + +# --------------------------------------------------------------------------- +# Small helpers +# --------------------------------------------------------------------------- + +def _rgba(color, alpha): + """Convert a Plotly ``#RRGGBB`` color to an ``rgba(...)`` string.""" + color = color.lstrip("#") + r, g, b = (int(color[i:i + 2], 16) for i in (0, 2, 4)) + return f"rgba({r},{g},{b},{alpha})" + + +def _color_map(values): + ordered = sorted((str(v) for v in values), key=str) + return {v: _PALETTE[i % len(_PALETTE)] for i, v in enumerate(ordered)} + + +def _usable_facet(df, name, *, exclude): + """Return *name* if it is a sensible facet column, else ``None``.""" + if not name or name in exclude or name not in df.columns: + return None + return name if df[name].nunique(dropna=True) > 1 else None + + +def _key_series(df, cols): + """Join a few identifying columns into one ``a / b / c`` label per row.""" + present = [c for c in cols if c in df.columns] + return df[present].astype(str).agg(" / ".join, axis=1) + + +# --------------------------------------------------------------------------- +# Figure builders +# --------------------------------------------------------------------------- + +def fig_distribution(df, x="M", value="time_ms", color="pass", facet=None, + trim_upper=0.95, trim_lower=0.0, title=None): + """Box + overlaid raw points per group (the JAX-dashboard-style view).""" + work = df.dropna(subset=[value]).copy() + work = vd.trim_percentile(work, value=value, upper=trim_upper, lower=trim_lower) + facet = _usable_facet(work, facet, exclude={x, color, value}) + work[x] = work[x].astype(str) + + n_facets = work[facet].nunique() if facet else 1 + height = 480 if not facet else 300 * ((n_facets + _FACET_WRAP - 1) // _FACET_WRAP) + + fig = px.box( + work, x=x, y=value, color=color, points="all", + facet_col=facet, facet_col_wrap=_FACET_WRAP if facet else None, + title=title or "Per-sample distribution", + labels={value: vd.value_label(work, value)}, + category_orders={x: sorted(work[x].unique(), key=lambda v: float(v) + if str(v).replace('.', '', 1).isdigit() else v)}, + ) + fig.update_traces(boxmean=True, jitter=0.4, marker=dict(size=4, opacity=0.6)) + fig.update_layout(height=height, boxmode="group", legend_title_text=color) + fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1])) + return fig + + +def fig_scaling(df, x="M", value="throughput", color="Case", facet="pass", + band="minmax", title=None): + """Median *value* vs *x* per series, with a shaded spread band.""" + work = df.dropna(subset=[value]).copy() + facet = _usable_facet(work, facet, exclude={x, color, value}) + facets = (sorted(work[facet].dropna().unique(), key=str) if facet else [None]) + cmap = _color_map(work[color].unique()) + upper_col, lower_col = ("vmax", "vmin") if band == "minmax" else ("q75", "q25") + + fig = make_subplots( + rows=1, cols=len(facets), shared_yaxes=True, + subplot_titles=[f"{facet}={f}" for f in facets] if facet else None, + horizontal_spacing=0.04, + ) + + for col_idx, fval in enumerate(facets, start=1): + sub = work if fval is None else work[work[facet] == fval] + for cval in sorted(sub[color].dropna().unique(), key=str): + series = sub[sub[color] == cval] + stats = vd.aggregate_stats(series, value=value, group_cols=[x]).sort_values(x) + if stats.empty: + continue + xs = stats[x].tolist() + line_color = cmap[str(cval)] + if band and len(xs) > 1: + up, lo = stats[upper_col].tolist(), stats[lower_col].tolist() + fig.add_trace(go.Scatter( + x=xs + xs[::-1], y=up + lo[::-1], fill="toself", + fillcolor=_rgba(line_color, 0.15), line=dict(width=0), + hoverinfo="skip", legendgroup=str(cval), showlegend=False, + ), row=1, col=col_idx) + fig.add_trace(go.Scatter( + x=xs, y=stats["median"].tolist(), mode="lines+markers", + name=str(cval), legendgroup=str(cval), + line=dict(color=line_color), marker=dict(size=6), + showlegend=(col_idx == 1), + hovertemplate=f"{color}={cval}
{x}=%{{x}}
median=%{{y:.3g}}", + ), row=1, col=col_idx) + fig.update_xaxes(title_text=x, row=1, col=col_idx) + + fig.update_yaxes(title_text=vd.value_label(work, value), row=1, col=1) + band_desc = "min-max" if band == "minmax" else "IQR" + fig.update_layout( + title=title or f"Scaling vs {x} (line = median, band = {band_desc})", + height=480, legend_title_text=color, + ) + return fig + + +def fig_throughput_bars(df, x="Case", value="throughput", color="pass", + facet="M", title=None): + """Grouped median bars per *x* with IQR error bars.""" + work = df.dropna(subset=[value]).copy() + facet = _usable_facet(work, facet, exclude={x, color, value}) + group_cols = [c for c in dict.fromkeys([x, color, facet]) if c] + stats = vd.aggregate_stats(work, value=value, group_cols=group_cols) + stats["err_plus"] = stats["q75"] - stats["median"] + stats["err_minus"] = stats["median"] - stats["q25"] + + horizontal = work[x].nunique() > 12 + order = list(dict.fromkeys(work[x].astype(str))) + common = dict( + color=color, barmode="group", + facet_col=facet, facet_col_wrap=_FACET_WRAP if facet else None, + title=title or f"Median {vd.value_label(work, value).lower()} by {x}", + ) + if horizontal: + stats[x] = stats[x].astype(str) + fig = px.bar( + stats, x="median", y=x, orientation="h", + error_x="err_plus", error_x_minus="err_minus", + category_orders={x: order[::-1]}, + labels={"median": vd.value_label(work, value)}, **common, + ) + fig.update_layout(height=max(420, 26 * len(order))) + else: + stats[x] = stats[x].astype(str) + fig = px.bar( + stats, x=x, y="median", + error_y="err_plus", error_y_minus="err_minus", + category_orders={x: order}, + labels={"median": vd.value_label(work, value)}, **common, + ) + fig.update_layout(height=480) + fig.update_layout(legend_title_text=color) + fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1])) + return fig + + +def fig_comparison(base_df, cand_df, value="throughput", + base_name="baseline", cand_name="candidate", title=None): + """Baseline-vs-candidate speedup bars (one bar per benchmark group).""" + keys = vd.group_columns(base_df, value) + keys = [c for c in keys if c in cand_df.columns] + + bm = vd.aggregate_stats(base_df.dropna(subset=[value]), value, keys) + cm = vd.aggregate_stats(cand_df.dropna(subset=[value]), value, keys) + merged = bm[keys + ["median"]].merge( + cm[keys + ["median"]], on=keys, suffixes=("_base", "_cand"), + ) + merged = merged[(merged["median_base"] > 0) & (merged["median_cand"] > 0)] + if merged.empty: + raise ValueError("No overlapping benchmark groups between the two CSVs.") + + higher_better = value != "time_ms" + merged["speedup"] = (merged["median_cand"] / merged["median_base"] + if higher_better else merged["median_base"] / merged["median_cand"]) + merged["key"] = _key_series(merged, ["Case", "M", "pass", "dtype_short"]) + merged = merged.sort_values("speedup") + color_key = "pass" if "pass" in merged.columns else None + cmap = _color_map(merged[color_key].unique()) if color_key else None + + fig = go.Figure() + # Speedup bars (horizontal; one bar per group). + if color_key: + for cval in sorted(merged[color_key].dropna().unique(), key=str): + s = merged[merged[color_key] == cval] + fig.add_trace(go.Bar( + x=s["speedup"], y=s["key"], orientation="h", name=str(cval), + legendgroup=str(cval), marker_color=cmap[str(cval)], + hovertemplate="%{y}
speedup=%{x:.3f}x", + )) + else: + fig.add_trace(go.Bar( + x=merged["speedup"], y=merged["key"], orientation="h", + showlegend=False, + hovertemplate="%{y}
speedup=%{x:.3f}x", + )) + fig.add_vline(x=1.0, line=dict(color="black", dash="dash")) + fig.update_xaxes(title_text="speedup (x)") + + median_sp = float(merged["speedup"].median()) + fig.update_layout( + title=title or ( + f"Speedup: {cand_name} vs {base_name} " + f"(>1 = {cand_name} faster) — median {median_sp:.3f}x" + ), + height=max(480, 22 * len(merged)), legend_title_text=color_key or "", + ) + return fig + + +# --------------------------------------------------------------------------- +# HTML output +# --------------------------------------------------------------------------- + +_REPORT_CSS = """ +body { font-family: system-ui, -apple-system, Arial, sans-serif; margin: 24px; + color: #1a1a1a; } +h1 { margin-bottom: 4px; } +nav { margin: 12px 0 28px; padding-bottom: 8px; border-bottom: 1px solid #ddd; } +nav a { margin-right: 16px; text-decoration: none; color: #1565c0; } +section { margin-bottom: 48px; } +section h2 { border-left: 4px solid #1565c0; padding-left: 8px; } +.meta { color: #666; font-size: 0.9em; } +""" + + +def _slug(text): + return "".join(c if c.isalnum() else "-" for c in text.lower()).strip("-") + + +def write_report(sections, path, title, subtitle=None, include_plotlyjs="inline"): + """Write ``[(heading, fig), ...]`` to a single self-contained HTML file.""" + nav = " ".join( + f'{h}' for h, _ in sections + ) + blocks = [] + for i, (heading, fig) in enumerate(sections): + include = include_plotlyjs if i == 0 else False + div = fig.to_html(full_html=False, include_plotlyjs=include, + default_width="100%") + blocks.append(f'

{heading}

{div}
') + sub = f'

{subtitle}

' if subtitle else "" + html = ( + f"" + f"{title}" + f"

{title}

{sub}{''.join(blocks)}" + ) + Path(path).write_text(html, encoding="utf-8") + return path + + +# --------------------------------------------------------------------------- +# Defaults + CLI +# --------------------------------------------------------------------------- + +def _pick_x(df): + return "M" if "M" in df.columns else (df.attrs["params"][0] if df.attrs["params"] else "Case") + + +def _resolve(df, kind, args): + """Resolve x/color/facet/value defaults for *kind*, honoring CLI overrides.""" + x = args.x or ("Case" if kind == "bars" else _pick_x(df)) + if kind == "distribution": + color = args.color or "pass" + facet = args.facet if args.facet is not None else "Case" + value = args.value if args.value != "auto" else "time_ms" + elif kind == "scaling": + color = args.color or "Case" + facet = args.facet if args.facet is not None else "pass" + value = args.value if args.value != "auto" else vd.default_value_column(df, "Forward") + else: # bars + color = args.color or "pass" + facet = args.facet if args.facet is not None else "M" + value = args.value if args.value != "auto" else vd.default_value_column(df, "Forward") + facet = None if facet in ("", "none") else facet + # Drop a color that carries no information (e.g. the empty 'pass' of the + # single-metric casting benchmark) unless the user asked for it explicitly. + if (kind in ("distribution", "bars") and not args.color + and color in df.columns and df[color].nunique(dropna=True) <= 1): + color = None + return x, color, facet, value + + +def _collapsed_columns(df, shown): + """Param columns that vary *within* a (shown-axis) group and are silently + pooled together. e.g. grouped GEMM medians span expert counts ``B`` when + ``B`` is on no axis. Columns fully determined by a shown column (a model's + fixed ``hidden_size`` under ``Case``) are not flagged. + """ + shown = [c for c in shown if c and c in df.columns] + if not shown: + return [] + candidates = [c for c in (list(df.attrs.get("params", [])) + ["pass"]) + if c in df.columns and c not in shown] + collapsed = [] + for col in candidates: + if df[col].nunique(dropna=True) <= 1: + continue + if df.groupby(shown, dropna=False)[col].nunique(dropna=False).max() > 1: + collapsed.append(col) + return collapsed + + +def _build_kind(df, kind, args): + if args.pass_filter: + df = df[df["pass"] == args.pass_filter] + x, color, facet, value = _resolve(df, kind, args) + collapsed = _collapsed_columns(df, [x, color, facet]) + if collapsed: + print(f" [note] {kind}: each group pools multiple {', '.join(collapsed)} " + f"value(s); add --facet/--color/--pass to separate them.") + if kind == "distribution": + return fig_distribution(df, x=x, value=value, color=color, facet=facet, + trim_upper=args.trim_upper, trim_lower=args.trim_lower) + if kind == "scaling": + return fig_scaling(df, x=x, value=value, color=color, facet=facet) + return fig_throughput_bars(df, x=x, value=value, color=color, facet=facet) + + +def main(): + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("csv", help="Benchmark CSV (samples or aggregate format)") + parser.add_argument("--baseline", metavar="FILE", + help="Baseline CSV; switches to / adds the comparison view") + parser.add_argument("--kind", default="report", + choices=["distribution", "scaling", "bars", "comparison", "report"], + help="Visualization to produce (default: report = all)") + parser.add_argument("--value", default="auto", choices=["auto", "time_ms", "throughput"], + help="Metric to plot (default: auto)") + parser.add_argument("--x", help="Override the x-axis column") + parser.add_argument("--color", help="Override the color/series column") + parser.add_argument("--facet", help="Override the facet column ('none' to disable)") + parser.add_argument("--pass", dest="pass_filter", metavar="PASS", + help="Restrict to one pass, e.g. 'Forward'") + parser.add_argument("--trim-upper", type=float, default=0.95, + help="Upper percentile for per-group outlier trimming (default 0.95)") + parser.add_argument("--trim-lower", type=float, default=0.0, + help="Lower percentile for per-group outlier trimming (default 0.0)") + parser.add_argument("--cdn", action="store_true", + help="Load plotly.js from CDN instead of inlining it (smaller file)") + parser.add_argument("-o", "--output", help="Output HTML path") + args = parser.parse_args() + + df = vd.load_any(args.csv) + base_df = vd.load_any(args.baseline) if args.baseline else None + include_js = "cdn" if args.cdn else "inline" + stem = Path(args.csv).stem + + sections = [] + if args.kind == "comparison" or (args.baseline and args.kind == "report"): + if base_df is None: + parser.error("--baseline is required for the comparison view") + value = args.value if args.value != "auto" else vd.default_value_column(df, "Forward") + sections.append(( + "Comparison", + fig_comparison(base_df, df, value=value, + base_name=Path(args.baseline).stem, cand_name=stem), + )) + + if args.kind != "comparison": + kinds = (["distribution", "scaling", "bars"] if args.kind == "report" + else [args.kind]) + for kind in kinds: + if kind == "distribution" and df.attrs["source"] != "samples": + continue # distribution needs per-sample data + try: + sections.append((kind.capitalize(), _build_kind(df, kind, args))) + except Exception as exc: # keep the report alive if one view fails + print(f" [skip] {kind}: {exc}") + + if not sections: + parser.error("Nothing to plot for the given CSV/kind combination.") + + out = args.output or f"{stem}.html" if args.kind == "report" else ( + args.output or f"{stem}_{args.kind}.html" + ) + title = f"Microbenchmark: {stem}" + subtitle = f"source: {Path(args.csv).name}" + if args.baseline: + subtitle += f" — baseline: {Path(args.baseline).name}" + write_report(sections, out, title, subtitle=subtitle, include_plotlyjs=include_js) + print(f"Wrote {out} ({len(sections)} view(s))") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/benchmarks/microbenchmarks/viz_data.py b/benchmarks/microbenchmarks/viz_data.py new file mode 100644 index 000000000..1233a941f --- /dev/null +++ b/benchmarks/microbenchmarks/viz_data.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python +############################################################################### +# Copyright (c) 2026, Advanced Micro Devices, Inc. All rights reserved. +# +# See LICENSE for license information. +############################################################################### +"""Shared data layer for microbenchmark visualization. + +Loads the two CSV shapes the microbenchmark suite produces and normalizes +both into a single tidy ("long") DataFrame so the plotting code does not need +to know which file it came from. + +Schemas +------- +* **samples** (from ``--csv-samples``): one row per timing sample. + ``, label, sample_idx, time_ms[, throughput, unit]``. +* **aggregate** (from ``--csv``): one row per case with paired metric columns + ``