Files
Raptor/validation/tools/pim/pimcomp/compare/generate_plots.py
T
NiccoloN 034c197260
Validate Operations / validate-operations (push) Failing after 3h10m55s
rework documentation
add plot generation scripts
adjust validation tools
add up-to-date results
2026-10-19 16:34:01 +02:00

515 lines
25 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Generate publication-ready Raptor vs Pimcomp comparison charts.
Usage:
.venv/bin/python validation/tools/pim/pimcomp/compare/generate_plots.py
Dependencies: matplotlib (no pandas or seaborn required).
By default, reads results_comparison.csv beside this script and writes plots/
beside it, with outputs grouped in png/ and svg/ under the output directory.
YOLO models without Pimcomp measurements get separate Raptor-only charts.
Comparisons use vertical bars for ratios, diverging advantages, and absolute
values, with compiler colors and patterns for the Raptor pipeline stages.
Throughput-mode latency is the output interval, a bottleneck-stage proxy,
not end-to-end inference latency. Separate throughput plots use inferences/s.
Energy is reported per inference; power is averaged over the simulation.
Numerical results whose compiler did not pass functional validation have a red
status following their value label; missing Pimcomp
measurements appear as 'N/A'. The fixed compiler palette is shared by
all metrics. Relative plots use Pimcomp / Raptor for latency, power and energy,
and Raptor / Pimcomp for throughput, so >1 favors Raptor. Diverging plots put the winner's
factor above (Raptor) or below (Pimcomp) parity, with 1x at the zero line.
"""
from __future__ import annotations
import argparse
import csv
import math
import sys
from dataclasses import dataclass
from itertools import groupby
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Patch, Rectangle
from matplotlib.lines import Line2D
from matplotlib.ticker import FuncFormatter, MultipleLocator
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from plot_style import COLORS, HATCH_COLOR, TEXT_COLOR, configure_style, fit_value_labels, save_figure, value_label
COMPILER_COLORS = {"raptor": COLORS[0], "pimcomp": COLORS[2]}
STAGE_PATTERNS = {1: "", 2: "..", 4: "//", 8: "\\\\"}
ARCHITECTURES = ("arch-a", "arch-b")
MODELS = ("vgg8", "resnet18", "resnet34", "googlenet", "yolo11n")
CONFIGURATIONS = (("latency", 1), ("throughput", 2), ("throughput", 4), ("throughput", 8))
LINE_COLOR = "#A8B2C0"
VALIDATION_STYLES = {
"PASS": ("#168C8B", "white", ""),
"FAIL": ("#D18D32", HATCH_COLOR, ""),
"SKIPPED": ("#E7DFF0", TEXT_COLOR, ""),
"NA": ("#E8EDF1", TEXT_COLOR, ""),
}
@dataclass(frozen=True)
class Result:
arch: str
model: str
mode: str
pipeline: int
pimcomp_schedule: str
raptor_validation: str
pimcomp_validation: str
latency_speedup: float
power: float
energy: float
raptor_latency: float
pimcomp_latency: float
raptor_throughput: float
pimcomp_throughput: float
raptor_power: float
pimcomp_power: float
raptor_energy: float
pimcomp_energy: float
@property
def comparison_passed(self) -> bool:
return self.raptor_validation == self.pimcomp_validation == "PASS"
@property
def throughput_speedup(self) -> float:
return quotient(self.raptor_throughput, self.pimcomp_throughput)
def positive_float(text: str | None) -> float:
try:
value = float(text)
except (TypeError, ValueError):
return math.nan
return value if math.isfinite(value) and value > 0 else math.nan
def quotient(numerator: float, denominator: float) -> float:
if not (math.isfinite(numerator) and math.isfinite(denominator)):
return math.nan
return numerator / denominator
def validation_status(text: str) -> str:
status = text.strip().upper()
if status not in VALIDATION_STYLES:
raise ValueError(f"Unknown functional-validation status: {text!r}")
return status
def load_results(path: Path) -> list[Result]:
with path.open(newline="", encoding="utf-8-sig") as handle:
source = list(csv.DictReader(handle))
rows = []
expected = {(arch, model, mode, stage)
for arch in ARCHITECTURES for model in MODELS
for mode, stage in CONFIGURATIONS}
seen = {}
for line, row in enumerate(source, 2):
try:
arch, model, mode = (row[field].strip().lower() for field in ("arch", "model", "mode"))
stage = int(row["raptor_pipeline"])
except (KeyError, AttributeError, TypeError, ValueError) as exc:
raise ValueError(f"Invalid configuration key at CSV row {line}: {exc}") from exc
key = arch, model, mode, stage
if key not in expected:
raise ValueError(f"Unexpected configuration at CSV row {line}: {key}")
if key in seen:
raise ValueError(f"Duplicate configuration {key} at CSV rows {seen[key]} and {line}")
seen[key] = line
schedule = row["pimcomp_pipeline"].strip().lower()
if schedule != ("element" if mode == "latency" else "batch"):
raise ValueError(f"{key}: incompatible Pimcomp schedule {schedule!r}")
rows.append(Result(
arch=arch,
model=model,
mode=mode,
pipeline=stage,
pimcomp_schedule=schedule,
raptor_validation=validation_status(row["raptor_functional_validation"]),
pimcomp_validation=validation_status(row["pimcomp_functional_validation"]),
latency_speedup=quotient(positive_float(row["pimcomp_latency_ms"]),
positive_float(row["raptor_latency_ms"])),
power=quotient(positive_float(row["pimcomp_power_mw"]),
positive_float(row["raptor_power_mw"])),
energy=quotient(positive_float(row["pimcomp_energy_pj"]),
positive_float(row["raptor_energy_pj"])),
raptor_latency=positive_float(row["raptor_latency_ms"]),
pimcomp_latency=positive_float(row["pimcomp_latency_ms"]),
raptor_throughput=positive_float(row["raptor_throughput_samples_s"]),
pimcomp_throughput=positive_float(row["pimcomp_throughput_samples_s"]),
raptor_power=positive_float(row["raptor_power_mw"]),
pimcomp_power=positive_float(row["pimcomp_power_mw"]),
raptor_energy=positive_float(row["raptor_energy_pj"]),
pimcomp_energy=positive_float(row["pimcomp_energy_pj"]),
))
missing = expected - seen.keys()
if missing:
raise ValueError(f"Missing {len(missing)} expected configurations. Examples: {sorted(missing)[:5]}")
rows.sort(key=lambda r: (ARCHITECTURES.index(r.arch), MODELS.index(r.model), r.pipeline))
absolute_columns(rows)
return rows
def advantage_compiler(ratio: float) -> str | None:
if ratio > 1:
return "raptor"
if ratio < 1:
return "pimcomp"
return None
def figure_legend(fig, results: list[Result], field: str,
absolute=False, raptor_only=False):
compilers = ("raptor",) if raptor_only else ("raptor", "pimcomp")
if absolute:
measurements = [(row, compiler, getattr(row, f"{compiler}_{field}"))
for row, compiler in absolute_columns(results, raptor_only)]
else:
measurements = [(row, None, getattr(row, field)) for row in results]
available = [(row, compiler, value) for row, compiler, value in measurements
if math.isfinite(value)]
stages = {row.pipeline for row, compiler, _ in available if compiler != "pimcomp"}
handles = []
visible_compilers = {compiler if absolute else advantage_compiler(value)
for _, compiler, value in available} - {None}
if len(visible_compilers) > 1:
for compiler in compilers:
handles.append(Patch(facecolor=COMPILER_COLORS[compiler],
label=compiler.capitalize() + ("" if absolute else " advantage")))
handles += [Patch(facecolor="white", edgecolor=TEXT_COLOR, hatch=STAGE_PATTERNS[depth],
label=f"{depth} stage" + (" (latency)" if depth == 1 else "s"))
for depth in sorted(stages) if stages != {1}]
styles = []
if len(available) < len(measurements):
styles.append(Patch(facecolor="white", edgecolor="#B2BAC5",
label="N/A: missing measurement"))
handles += styles
if handles:
fig.legend(handles=handles, loc="outside upper center", ncol=(len(handles) + 1) // 2,
frameon=False, fontsize=9.5, handlelength=1.7, columnspacing=1.5,
handletextpad=0.6, labelspacing=0.4)
def absolute_columns(results: list[Result], raptor_only: bool = False) -> list[tuple[Result, str]]:
columns = []
for (arch, model), group in groupby(results, key=lambda row: (row.arch, row.model)):
rows = sorted(group, key=lambda row: row.pipeline)
for row in rows:
columns.append((row, "raptor"))
if row.mode == "latency" and not raptor_only:
columns.append((row, "pimcomp"))
throughput = [row for row in rows if row.mode == "throughput"]
if throughput and not raptor_only:
signatures = {
(row.pimcomp_schedule, row.pimcomp_validation,
*(None if math.isnan(value) else value for value in (
row.pimcomp_latency, row.pimcomp_throughput, row.pimcomp_power, row.pimcomp_energy)))
for row in throughput
}
if len(signatures) != 1:
raise ValueError(f"{arch}/{model}: inconsistent shared Pimcomp batch results across pipelines")
columns.append((throughput[0], "pimcomp"))
return columns
def label_groups(ax, results: list[Result], positions: list[float] | None = None) -> None:
if positions is None:
positions = list(range(len(results)))
centers, labels = [], []
start = 0
for index, (model, rows) in enumerate(groupby(results, key=lambda row: row.model)):
end = start + len(list(rows))
centers.append((positions[start] + positions[end - 1]) / 2)
labels.append(model.upper())
left = ax.get_xlim()[0] if start == 0 else (positions[start - 1] + positions[start]) / 2
right = ax.get_xlim()[1] if end == len(results) else (positions[end - 1] + positions[end]) / 2
if index % 2 == 0:
ax.axvspan(left, right, facecolor="#F7F9FC", zorder=0)
if index > 0:
ax.axvline(left, color="#DDE3EB", linewidth=0.9)
start = end
ax.set_xticks(centers, labels, fontsize=9.5)
def diverging_ratio(ratio: float) -> float:
return ratio - 1 if ratio >= 1 else 1 - 1 / ratio
def draw_validation(results: list[Result], output_dir: Path) -> None:
columns = absolute_columns(results)
configurations = list(dict.fromkeys((row.mode, row.pipeline if compiler == "raptor" else row.pimcomp_schedule,
compiler) for row, compiler in columns))
positions = []
for index, (mode, _, _) in enumerate(configurations):
gap = 0.4 if index and mode != configurations[index - 1][0] else 0
positions.append(positions[-1] + 1 + gap if index else 0)
configuration_x = dict(zip(configurations, positions))
headers = [f"Raptor\n{stage} stages" if mode == "throughput" and compiler == "raptor"
else "Pimcomp\nbatch" if mode == "throughput" else compiler.capitalize()
for mode, stage, compiler in configurations]
fig, axes = plt.subplots(1, 2, figsize=(14, 3.8), layout="constrained")
for ax, arch in zip(axes, ARCHITECTURES):
ax.set_xlim(positions[0] - 0.55, positions[-1] + 0.55)
ax.set_ylim(len(MODELS) - 0.5, -0.5)
centers, modes = [], []
for mode, group in groupby(zip(configurations, positions), key=lambda item: item[0][0]):
group_positions = [x for _, x in group]
centers.append((group_positions[0] + group_positions[-1]) / 2)
modes.append(mode.capitalize())
ax.set_xticks(centers, modes, fontsize=10)
ax.set_xticks(positions, headers, minor=True)
ax.tick_params(axis="x", which="minor", top=True, labeltop=True,
bottom=False, labelbottom=False, length=0, pad=6, labelsize=9)
ax.set_yticks(range(len(MODELS)), [model.upper() for model in MODELS], fontsize=9)
ax.tick_params(axis="both", length=0, pad=6)
architecture = "ARCHITECTURE A" if arch == "arch-a" else "ARCHITECTURE B"
ax.set_xlabel(architecture, fontsize=11, weight="bold", labelpad=8)
for row, compiler in columns:
if row.arch != arch:
continue
key = (row.mode, row.pipeline if compiler == "raptor" else row.pimcomp_schedule, compiler)
x, y = configuration_x[key], MODELS.index(row.model)
status = getattr(row, f"{compiler}_validation")
color, text_color, _ = VALIDATION_STYLES[status]
ax.add_patch(Rectangle((x - 0.5, y - 0.42), 1, 0.84,
facecolor=color, edgecolor="#657081", linewidth=0.5))
ax.text(x, y, "N/A" if status == "NA" else status,
color=text_color, ha="center", va="center", fontsize=9)
save_figure(fig, "validation_status", output_dir)
def draw_metric(results: list[Result], field: str, ylabel: str,
basename: str, output_dir: Path, diverging: bool = False) -> None:
data = [diverging_ratio(getattr(row, field)) if diverging else getattr(row, field)
for row in results]
available = [v for v in data if math.isfinite(v)]
upper = max(0, max(available, default=1.0))
lower = min(0, min(available, default=0)) if diverging else 0
padding = max(upper - lower, 1.0) * 0.3
y_max = math.ceil((upper + padding) * 2) / 2
y_min = math.floor((lower - padding) * 2) / 2 if diverging else 0
y_interval = 1.0 if y_max - y_min > 4.5 else 0.5
fig, axes = plt.subplots(1, 2, sharey=True, figsize=(14, 5.2),
layout="constrained")
figure_legend(fig, results, field)
for ax, arch in zip(axes, ARCHITECTURES):
selected = [row for row in results if row.arch == arch]
ax.set_xlabel("ARCHITECTURE A" if arch == "arch-a" else "ARCHITECTURE B",
fontsize=11, weight="bold", labelpad=8)
ax.set_axisbelow(True)
ax.set_ylim(y_min, y_max)
ax.set_xlim(-0.5, len(selected) - 0.5)
label_groups(ax, selected)
ax.tick_params(axis="y", labelleft=True, pad=5, length=0)
ax.tick_params(axis="x", labelsize=9.5, length=0, pad=6)
ax.yaxis.set_major_locator(MultipleLocator(y_interval))
if diverging:
ax.yaxis.set_major_formatter(FuncFormatter(lambda value, _: f"{abs(value) + 1:g}×"))
ax.grid(axis="y", color="#E6EAF0", linewidth=0.8)
ax.axhline(0 if diverging else 1, color="#536478", linewidth=1.2,
linestyle="--", zorder=3)
ax.set_ylabel(ylabel, fontsize=10.5, labelpad=6)
for position, row in enumerate(selected):
ratio = getattr(row, field)
if not math.isfinite(ratio):
ax.text(position, (y_max - y_min) * 0.02, "N/A", ha="center", va="bottom",
fontsize=9.5, color="#85909F", fontstyle="italic")
continue
compiler = advantage_compiler(ratio)
statuses = [status for status in (row.raptor_validation, row.pimcomp_validation) if status != "PASS"]
status = "FAIL" if "FAIL" in statuses else next(iter(statuses), "PASS")
pattern = STAGE_PATTERNS[row.pipeline]
measurement = diverging_ratio(ratio) if diverging else ratio
ax.bar(position, measurement, width=0.72,
color=COMPILER_COLORS.get(compiler, "#657081"),
hatch=pattern,
edgecolor=TEXT_COLOR if pattern else "none",
linewidth=0.7 if pattern else 0.0, zorder=2)
direction = -1 if measurement < 0 else 1
factor = max(ratio, 1 / ratio) if diverging else ratio
value_label(ax, position, measurement + direction * (y_max - y_min) * 0.012,
f"{factor:.2f}×", status, direction, fontsize=9.1)
fit_value_labels(fig)
save_figure(fig, basename, output_dir)
def draw_absolute(results: list[Result], field: str, ylabel: str,
basename: str, output_dir: Path, scale: float = 1.0,
raptor_only: bool = False) -> None:
columns = absolute_columns(results, raptor_only)
available = [getattr(row, f"{compiler}_{field}") * scale
for row, compiler in columns
if math.isfinite(getattr(row, f"{compiler}_{field}"))]
y_max = max(available, default=1.0) * 1.35
fig, axes = plt.subplots(1, 2, sharey=True, figsize=(14, 5.2),
layout="constrained")
figure_legend(fig, results, field, absolute=True, raptor_only=raptor_only)
for ax, arch in zip(axes, ARCHITECTURES):
selected = [(row, compiler) for row, compiler in columns if row.arch == arch]
positions = []
for index, (row, _) in enumerate(selected):
if index == 0:
positions.append(0.0)
continue
previous = selected[index - 1][0]
gap = 0.8 if row.model != previous.model else 0.4 if row.mode != previous.mode else 0.0
positions.append(positions[-1] + 1.0 + gap)
ax.set_xlabel("ARCHITECTURE A" if arch == "arch-a" else "ARCHITECTURE B",
fontsize=11, weight="bold", labelpad=8)
ax.set_axisbelow(True)
ax.set_ylim(0, y_max)
if positions:
ax.set_xlim(positions[0] - 0.9, positions[-1] + 0.9)
else:
ax.set_xlim(-0.9, 0.9)
label_groups(ax, [row for row, _ in selected], positions)
if {row.mode for row, _ in selected} == {"latency", "throughput"}:
centers, labels = [], []
for (_, mode), group in groupby(enumerate(selected), key=lambda item: (item[1][0].model, item[1][0].mode)):
group_positions = [positions[position] for position, _ in group]
centers.append((group_positions[0] + group_positions[-1]) / 2)
labels.append(mode.capitalize())
ax.set_xticks(centers, labels, minor=True)
ax.tick_params(axis="x", which="minor", length=0, pad=4, labelsize=8)
ax.set_ylabel(ylabel, fontsize=10.5)
if field == "throughput":
ax.ticklabel_format(axis="y", style="plain", useOffset=False)
ax.grid(axis="y", color="#E6EAF0", linewidth=0.8)
ax.tick_params(axis="y", length=0, pad=6)
ax.tick_params(axis="x", which="major", length=0,
pad=22 if {row.mode for row, _ in selected} == {"latency", "throughput"} else 6)
for x, (row, compiler) in zip(positions, selected):
measurement = getattr(row, f"{compiler}_{field}") * scale
if not math.isfinite(measurement):
ax.text(x, y_max * 0.02, "N/A", rotation=90, ha="center",
va="bottom", fontsize=8, color="#85909F")
continue
status = getattr(row, f"{compiler}_validation")
pattern = STAGE_PATTERNS[row.pipeline] if compiler == "raptor" else ""
ax.bar(x, measurement, width=1.0,
color=COMPILER_COLORS[compiler],
hatch=pattern,
edgecolor=TEXT_COLOR if pattern else "none",
linewidth=0.7 if pattern else 0.0, zorder=2)
label = (f"{measurement:,.2f}".rstrip("0").rstrip(".")
if field == "throughput" else f"{measurement:.3g}")
value_label(ax, x, measurement + y_max * 0.012, label, status)
fit_value_labels(fig)
save_figure(fig, basename, output_dir)
def draw_pipeline_effect(results: list[Result], output_dir: Path) -> None:
rows = [row for row in results if row.mode == "throughput" or row.pipeline == 1]
stages = sorted({row.pipeline for row in rows})
panels = [(model, color) for model, color in zip(MODELS, COLORS)
if any(row.model == model for row in rows)]
lookup = {(row.arch, row.model, row.pipeline): row for row in rows}
rates = {key: quotient(1000, row.raptor_latency) if row.mode == "latency" else row.raptor_throughput
for key, row in lookup.items()}
fig, axes = plt.subplots(len(ARCHITECTURES), len(panels), sharex=True, sharey="col",
figsize=(3 * len(panels), 7.2), squeeze=False, layout="constrained")
for column, (model, color) in enumerate(panels):
for index, arch in enumerate(ARCHITECTURES):
ax = axes[index, column]
keys = [(arch, model, stage) for stage in stages]
values = [rates.get(key, math.nan) for key in keys]
valid = [key in lookup and lookup[key].raptor_validation == "PASS" for key in keys]
ax.plot(stages, values, color=color, linewidth=1.8)
for stage, value, passed in zip(stages, values, valid):
if math.isfinite(value):
marker = ("s" if stage == 1 else "o") if passed else "x"
ax.plot(stage, value, color=color, marker=marker,
markersize=6, linestyle="none")
ax.set_xticks(stages)
ax.set_xlabel(model.upper(), fontsize=10.5, weight="bold", labelpad=8)
ax.grid(axis="y", color="#E6EAF0", linewidth=0.8)
ax.tick_params(axis="both", length=0, pad=6, labelbottom=True, labelleft=True)
ax.ticklabel_format(axis="y", style="plain", useOffset=False)
if column == 0:
ax.set_ylabel(f"{arch.replace('arch-', 'ARCHITECTURE ').upper()}\n"
"Throughput (inferences/s)", fontsize=10.5)
for ax in axes.flat:
ax.set_ylim(bottom=0)
handles = [Line2D([], [], color="#657081", marker=marker, linestyle="none", label=label)
for marker, label in (("s", "Derived as 1000ms / latency_ms"),
("o", "Throughput measurement"))]
if any(row.raptor_validation != "PASS" and math.isfinite(rates[key]) for key, row in lookup.items()):
handles.append(Line2D([], [], color="#424B58", marker="x", linestyle="none",
label="Raptor validation not PASS"))
fig.legend(handles=handles, loc="outside upper center", ncol=len(handles), frameon=False, fontsize=9.5)
fig.supxlabel("Raptor pipeline stages", fontsize=11)
save_figure(fig, "raptor_pipeline_throughput", output_dir)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
script_dir = Path(__file__).resolve().parent
parser.add_argument("--input", type=Path, default=script_dir / "results_comparison.csv")
parser.add_argument("--output-dir", type=Path, default=script_dir / "plots")
args = parser.parse_args()
configure_style()
results = load_results(args.input)
valid = sum(row.comparison_passed for row in results)
print(f"Results: {len(results)} configurations, {valid} valid comparisons, "
f"{len(results)-valid} comparisons without two PASS statuses")
draw_validation(results, args.output_dir)
draw_pipeline_effect(results, args.output_dir)
yolo_only_models = {
model for model in MODELS if model.startswith("yolo")
and not any(math.isfinite(getattr(row, f"pimcomp_{field}"))
for row in results if row.model == model
for field in ("latency", "power", "energy"))
}
comparisons = [row for row in results if row.model not in yolo_only_models]
yolo_only = [row for row in results if row.model in yolo_only_models]
for metric, name, label, mode in [
("latency_speedup", "longest_stage_latency", "Longest stage latency speedup (x)", None),
("latency_speedup", "latency", "Single-inference latency speedup (x)", "latency"),
("throughput_speedup", "throughput", "Throughput speedup (x)", "throughput"),
("power", "power", "Average power advantage (x)", None),
("energy", "energy", "Energy per inference advantage (x)", None),
]:
selected = [row for row in comparisons if mode is None or row.mode == mode]
draw_metric(selected, metric, label,
f"{name}_comparison", args.output_dir)
draw_metric(selected, metric, label,
f"{name}_diverging", args.output_dir, diverging=True)
for metric, name, label, scale, mode in [
("latency", "longest_stage_latency", "Longest stage latency (ms)", 1.0, None),
("latency", "latency", "Single-inference latency (ms)", 1.0, "latency"),
("power", "power", "Average power (mW)", 1.0, None),
("energy", "energy", "Energy per inference (mJ)", 1e-9, None),
("throughput", "throughput", "Throughput (inferences/s)", 1.0, "throughput"),
]:
for suffix, group, raptor_only in (
("absolute", comparisons, False), ("yolo", yolo_only, True)
):
selected = [row for row in group if mode is None or row.mode == mode]
if not selected:
continue
draw_absolute(selected, metric, label,
f"{name}_{suffix}", args.output_dir, scale,
raptor_only=raptor_only)
if __name__ == "__main__":
main()