Files
Raptor/validation/tools/pim/pimcomp/compare/generate_plots.py
T
NiccoloN a77edafcbf
Validate Operations / validate-operations (push) Has been cancelled
even better more structured throughput scheduling
plot adjustments
validation adjustments
2026-10-27 17:46:46 +01:00

720 lines
37 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/, svg/, and pdf/ 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.
A compact dot-plot summary combines latency, throughput, and both energy cases.
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.text import Text
from matplotlib.ticker import FuncFormatter, MultipleLocator
from matplotlib.transforms import Bbox
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from plot_style import (ARCH_NAMES, ARCH_STYLES, BAND_COLOR, COMPACT_MODE_NAMES, COMPILER_NAMES, COMPILER_STYLES,
DAGGER_COLOR, ERROR_COLOR, LINE_COLOR, MATRIX_TEXT_COLOR,
MODE_NAMES, MODEL_NAMES, NETWORK_STYLES,
PALETTE, TEXT_COLOR, add_legend, configure_style, fit_value_labels,
fit_bar_widths, fit_x_tick_labels, save_figure, value_label)
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))
VALIDATION_STYLES = {
"PASS": PALETTE["sage"],
"FAIL": PALETTE["red"],
"SKIPPED": PALETTE["pale_sand"],
"NA": PALETTE["pale_blue"],
}
@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)
@property
def speedup(self) -> float:
return self.latency_speedup if self.mode == "latency" else self.throughput_speedup
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:
fill, border = COMPILER_STYLES[compiler]
handles.append(Patch(facecolor=fill, edgecolor=border,
label=COMPILER_NAMES[compiler] + ("" if absolute else " advantage")))
handles += [Patch(facecolor="white", edgecolor=TEXT_COLOR, hatch=STAGE_PATTERNS[depth],
label=("1 stage (latency)" if absolute else "Single-inference")
if depth == 1 else f"{depth} stages")
for depth in sorted(stages) if stages != {1}]
styles = []
if len(available) < len(measurements):
styles.append(Patch(facecolor="white", edgecolor=LINE_COLOR,
label="N/A: missing measurement"))
handles += styles
if handles:
add_legend(fig, handles)
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_NAMES[model])
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=BAND_COLOR, zorder=0)
if index > 0:
ax.axvline(left, color=LINE_COLOR, linewidth=0.7)
start = end
ax.set_xticks(centers, labels, fontsize=9.5)
def grouped_positions(results: list[Result]) -> list[float]:
positions = []
for index, row in enumerate(results):
if index == 0:
positions.append(0.0)
continue
previous = results[index - 1]
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)
return positions
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, compact: bool = False) -> None:
cells = {}
for row, compiler in absolute_columns(results):
stage = row.pipeline if compiler == "raptor" else row.pimcomp_schedule
key = (row.arch, row.model, row.mode, None if compact else stage, compiler)
cells.setdefault(key, set()).add(getattr(row, f"{compiler}_validation"))
configurations = list(dict.fromkeys(key[2:] for key in cells))
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"{COMPILER_NAMES['raptor']}\n{stage} stages" if mode == "throughput" and compiler == "raptor" and not compact
else "PIMCOMP\nbatch" if mode == "throughput" and compiler == "pimcomp" and not compact
else COMPILER_NAMES[compiler]
for mode, stage, compiler in configurations]
fig, axes = plt.subplots(1, 2, figsize=(8.5 if compact else 12, 3.3), 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(COMPACT_MODE_NAMES[mode] if compact else
"Latency" if mode == "latency" else MODE_NAMES[mode])
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_NAMES[model] for model in MODELS], fontsize=9)
ax.tick_params(axis="both", length=0, pad=6)
ax.tick_params(axis="y", labelleft=arch == ARCHITECTURES[0])
architecture = ARCH_NAMES[arch]
ax.set_xlabel(architecture, fontsize=11, weight="bold", labelpad=8)
for (cell_arch, model, mode, stage, compiler), statuses in cells.items():
if cell_arch != arch:
continue
x, y = configuration_x[(mode, stage, compiler)], MODELS.index(model)
status = next(status for status in ("FAIL", "SKIPPED", "NA", "PASS") if status in statuses)
color, border = VALIDATION_STYLES[status]
ax.add_patch(Rectangle((x - 0.5, y - 0.42), 1, 0.84,
facecolor=color, edgecolor=border, linewidth=0.7))
ax.text(x, y, "N/A" if status == "NA" else status,
color=MATRIX_TEXT_COLOR, ha="center", va="center", fontsize=12, weight="normal")
fit_x_tick_labels(fig)
save_figure(fig, "validation_status_compact" if compact else "validation_status", output_dir)
def select_compact_speedups(results: list[Result]) -> list[Result]:
selected = []
for _, group in groupby(results, key=lambda row: (row.arch, row.model, row.mode)):
rows = list(group)
selected.append(max(rows, key=lambda row: row.speedup if math.isfinite(row.speedup) else -math.inf))
return selected
def draw_compact_summary(results: list[Result], output_dir: Path, *, marker_borders: bool = True) -> None:
selected = select_compact_speedups([row for row in results if row.model != "yolo11n"])
lookup = {(row.model, row.arch, row.mode): row for row in selected}
keys = [(model, arch) for model in MODELS if model != "yolo11n" for arch in ARCHITECTURES]
panels = ((f"{COMPACT_MODE_NAMES['latency']}\nspeedup", "latency", "latency_speedup"),
(f"{COMPACT_MODE_NAMES['throughput']}\nspeedup", "throughput", "throughput_speedup"),
(f"{COMPACT_MODE_NAMES['latency']}\nenergy reduction", "latency", "energy"),
(f"{COMPACT_MODE_NAMES['throughput']}\nenergy reduction", "throughput", "energy"))
measurements = []
for _, mode, field in panels:
points = []
for model, arch in keys:
row = lookup[(model, arch, mode)]
ratio = getattr(row, field)
if not math.isfinite(ratio) or ratio <= 0:
raise ValueError(f"Compact summary requires a positive {field} ratio for {arch}/{model}")
pimcomp_invalid = row.pimcomp_schedule == "batch" or row.pimcomp_validation == "FAIL"
status = ("P. INVALID" if pimcomp_invalid else
f"{COMPILER_NAMES['raptor'][0]}. INVALID" if row.raptor_validation == "FAIL" else
"NOT VALIDATED" if not row.comparison_passed else "")
points.append((row, ratio, status))
measurements.append(points)
ratios = [ratio for points in measurements for _, ratio, _ in points]
padding = max(max(ratios) - min(ratios), 1) * 0.04
limits = (0.25 if min(ratios) < 0.5 else 0.5, max(1, *ratios) + padding)
fig, axes = plt.subplots(4, 1, sharex=True, figsize=(6.8, 7.6), layout="constrained")
markers = dict(zip((model for model in MODELS if model != "yolo11n"), ("o", "s", "^", "D")))
lanes = {arch: index * 2 for index, arch in enumerate(ARCHITECTURES)}
daggers = {}
for ax, (title, _, _), points in zip(axes, panels, measurements):
ax.set_xlim(*limits)
ax.set_ylim(4, -2)
ax.set_ylabel(title, rotation=90, ha="center", va="bottom", multialignment="center",
fontsize=8.5, labelpad=10)
ax.set_yticks(list(lanes.values()), [ARCH_NAMES[arch] for arch in lanes], fontsize=8)
ax.set_xticks([limits[0], *range(1, math.floor(limits[1]) + 1)])
ax.xaxis.set_major_formatter(FuncFormatter(lambda value, _: f"{value:g}"))
ax.tick_params(axis="both", length=0, pad=4, labelsize=8.5)
ax.tick_params(axis="x", bottom=False, labelbottom=False,
top=ax is axes[0], labeltop=ax is axes[0])
ax.grid(axis="x", linewidth=0.6)
ax.axvline(1, color=TEXT_COLOR, linewidth=0.9, linestyle="--", zorder=1)
for lane in lanes.values():
ax.axhline(lane, color=LINE_COLOR, linewidth=0.6)
ax.spines["left"].set_visible(False)
ax.spines["bottom"].set_visible(False)
ax.spines["top"].set_visible(ax is axes[0])
for row, ratio, status in points:
fill, border = NETWORK_STYLES[row.model]
lane = lanes[row.arch]
ax.plot(ratio, lane, marker=markers[row.model], markersize=6.5, linestyle="none",
markerfacecolor=fill if marker_borders else border, markeredgecolor=border,
markeredgewidth=1.1 if marker_borders else 0)
suffix = f" [{status}]" if status and status != "P. INVALID" else ""
label = f"{ratio:.2f}×{suffix}"
annotation = ax.annotate(label, (ratio, lane), xytext=(0, -5), textcoords="offset points",
rotation=90, rotation_mode="anchor", ha="right", va="center",
fontsize=7.5, color=TEXT_COLOR, math_fontfamily="cm")
annotation.set_in_layout(False)
dagger = ax.annotate(r"$\boldsymbol{\dagger}$", (0.5, 1), xycoords=annotation,
xytext=(0, 1), textcoords="offset points", rotation=90,
rotation_mode="anchor", ha="left", va="center", fontsize=9,
color=DAGGER_COLOR, math_fontfamily="cm",
alpha=1 if status == "P. INVALID" else 0)
dagger.set_in_layout(False)
daggers[annotation] = dagger
handles = [Line2D([], [], marker=marker, color=NETWORK_STYLES[model][1],
markerfacecolor=NETWORK_STYLES[model][0 if marker_borders else 1], markersize=6.5,
markeredgewidth=1 if marker_borders else 0,
linestyle="none", label=MODEL_NAMES[model])
for model, marker in markers.items()]
handles.append(Text(0, 0, r"$\boldsymbol{\dagger}$", color=DAGGER_COLOR, fontsize=9,
math_fontfamily="cm", label="PIMCOMP invalid"))
add_legend(fig, handles)
axes[0].xaxis.set_label_position("top")
axes[0].set_xlabel("Ratio (x)", fontsize=9, labelpad=7, va="bottom")
axes[0].xaxis.set_label_coords(0.5, 1.18)
axes[0].text(0, 1.18, "← PIMCOMP better", transform=axes[0].transAxes, ha="left", va="bottom", fontsize=8)
axes[0].text(1, 1.18, f"{COMPILER_NAMES['raptor']} better →", transform=axes[0].transAxes,
ha="right", va="bottom", fontsize=8)
fig.canvas.draw()
renderer = fig.canvas.get_renderer()
def label_bounds(text):
return Bbox.union([text.get_window_extent(renderer), daggers[text].get_window_extent(renderer)])
def position_label(text, dx, dy):
text.set_ha("left" if dy > 0 else "right")
extra = (daggers[text].get_window_extent(renderer).height + renderer.points_to_pixels(1)
if dy < 0 else 0)
text.set_position((dx, dy - extra * 72 / fig.dpi))
labels_by_axis = [[text for text in ax.texts if getattr(text, "xycoords", None) == "data"] for ax in axes]
label_height = max(label_bounds(text).height for labels in labels_by_axis for text in labels)
lane_gap = min(abs(ax.transData.transform((0, 0))[1] - ax.transData.transform((0, 2))[1])
for ax in axes)
height_scale = (label_height + renderer.points_to_pixels(5)) / lane_gap
if height_scale > 1:
fig.set_size_inches(fig.get_figwidth(), fig.get_figheight() * height_scale)
fig.canvas.draw()
renderer = fig.canvas.get_renderer()
for ax, labels in zip(axes, labels_by_axis):
placed = []
for line in ax.lines:
placed.append(line.get_window_extent(renderer).padded(1))
bounds = ax.get_window_extent(renderer)
choices = []
for text in labels:
preferred_dy = 5 if text.xy[1] == lanes[ARCHITECTURES[0]] else -5
candidates = []
for dx in (0, -3, 3, -6, 6, -8, 8):
for dy in (-5, 5):
position_label(text, dx, dy)
box = label_bounds(text)
overlap = any(box.overlaps(other) for other in placed)
outside = not (bounds.x0 <= box.x0 <= box.x1 <= bounds.x1 and
bounds.y0 <= box.y0 <= box.y1 <= bounds.y1)
if not outside and not overlap:
candidates.append(((dy != preferred_dy, abs(dx) + abs(dy)), dx, dy, box))
choices.append((text, candidates))
layouts = [((0, 0), [])]
for text, candidates in sorted(choices, key=lambda choice: len(choice[1])):
layouts = sorted(
(((cost[0] + distance[0], cost[1] + distance[1]), assignments + [(text, dx, dy, box)])
for cost, assignments in layouts for distance, dx, dy, box in candidates
if not any(box.overlaps(other[3]) for other in assignments)),
key=lambda layout: layout[0])[:32]
if not layouts:
raise ValueError("Dot labels do not fit without overlap; increase lane spacing or figure width")
for text, dx, dy, _ in layouts[0][1]:
position_label(text, dx, dy)
save_figure(fig, "performance_energy_compact", output_dir)
def draw_metric(results: list[Result], field: str, ylabel: str,
basename: str, output_dir: Path, diverging: bool = False,
compact: 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.25 if min(available, default=1) < 0.5 else 0.5)
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 lower
y_interval = 1.0 if y_max - y_min > 4.5 else 0.5
fig, axes = plt.subplots(1, 2, sharey=True, figsize=(8.5 if compact else 12, 4.4),
layout="constrained")
if compact:
metric_suffix = "" if field == "speedup" else f" {field}"
panel_labels = {mode: label + metric_suffix for mode, label in COMPACT_MODE_NAMES.items()}
handles = [Patch(facecolor=fill, edgecolor=border, hatch=pattern, label=ARCH_NAMES[arch])
for arch, (fill, border, pattern) in ARCH_STYLES.items()]
panels = ("latency", "throughput") if compact else ARCHITECTURES
for panel_index, (ax, panel) in enumerate(zip(axes, panels)):
selected = [row for row in results if getattr(row, "mode" if compact else "arch") == panel]
if compact:
selected.sort(key=lambda row: (MODELS.index(row.model), ARCHITECTURES.index(row.arch)))
positions = [index + index // 2 for index in range(len(selected))] if compact else grouped_positions(selected)
ax.set_xlabel(panel_labels[panel] if compact else ARCH_NAMES[panel],
fontsize=11, weight="bold", labelpad=8)
ax.set_axisbelow(True)
ax.set_ylim(y_min, y_max)
ax.set_xlim(-0.9, positions[-1] + 0.9 if positions else 0.9)
label_groups(ax, selected, positions)
ax.tick_params(axis="y", labelleft=panel_index == 0, 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}"))
else:
ax.set_yticks([y_min], minor=True)
ax.yaxis.set_minor_formatter(FuncFormatter(lambda value, _: f"{value:g}"))
ax.tick_params(axis="y", which="minor", labelleft=panel_index == 0, pad=5, length=0)
ax.grid(axis="y")
ax.axhline(0 if diverging else 1, color=TEXT_COLOR, linewidth=1.0,
linestyle="--", zorder=3)
if panel_index == 0:
ax.set_ylabel(ylabel, fontsize=10.5, labelpad=6)
for position, row in zip(positions, selected):
ratio = getattr(row, field)
if not math.isfinite(ratio):
ax.text(position, (0 if diverging else y_min) + (y_max - y_min) * 0.02, "N/A", ha="center", va="bottom",
fontsize=9.5, color=TEXT_COLOR, 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]
color, border = COMPILER_STYLES[compiler] if compiler else ("white", TEXT_COLOR)
if compact:
color, border, pattern = ARCH_STYLES[row.arch]
measurement = diverging_ratio(ratio) if diverging else ratio
ax.bar(position, measurement, width=1.0,
color=color,
hatch=pattern,
edgecolor=border, linewidth=0.7, 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, invalid_label="P. INVALID")
fit_bar_widths(fig)
if compact:
add_legend(fig, handles)
else:
figure_legend(fig, results, field)
fit_x_tick_labels(fig)
fit_value_labels(fig, lower_limit=None if diverging else y_min)
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=(12, 4.4),
layout="constrained")
for ax, arch in zip(axes, ARCHITECTURES):
selected = [(row, compiler) for row, compiler in columns if row.arch == arch]
positions = grouped_positions([row for row, _ in selected])
ax.set_xlabel(ARCH_NAMES[arch],
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("Latency" if mode == "latency" else MODE_NAMES[mode])
ax.set_xticks(centers, labels, minor=True)
ax.tick_params(axis="x", which="minor", length=0, pad=4, labelsize=8)
if arch == ARCHITECTURES[0]:
ax.set_ylabel(ylabel, fontsize=10.5)
if field == "throughput":
ax.ticklabel_format(axis="y", style="plain", useOffset=False)
ax.grid(axis="y")
ax.tick_params(axis="y", labelleft=arch == ARCHITECTURES[0], 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=TEXT_COLOR)
continue
status = getattr(row, f"{compiler}_validation")
pattern = STAGE_PATTERNS[row.pipeline] if compiler == "raptor" else ""
fill, border = COMPILER_STYLES[compiler]
ax.bar(x, measurement, width=1.0,
color=fill,
hatch=pattern,
edgecolor=border, linewidth=0.7, 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, invalid_label="P. INVALID")
fit_bar_widths(fig)
figure_legend(fig, results, field, absolute=True, raptor_only=raptor_only)
fit_x_tick_labels(fig)
fit_value_labels(fig)
save_figure(fig, basename, output_dir)
def draw_pipeline_effect(results: list[Result], output_dir: Path, *, marker_borders: bool = True) -> 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 for model in MODELS 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=(2.6 * len(panels), 4.3), squeeze=False, layout="constrained")
fig.supylabel("Throughput (inferences/s)", fontsize=10.5)
for column, model in enumerate(panels):
fill, border = NETWORK_STYLES[model]
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]
ax.plot(stages, values, color=border, linewidth=1.8)
for stage, value in zip(stages, values):
if math.isfinite(value):
ax.plot(stage, value, color=border,
markerfacecolor=fill if marker_borders else border, marker="s" if stage == 1 else "o",
markeredgewidth=1 if marker_borders else 0, markersize=7, linestyle="none")
ax.set_xticks(stages)
if index == 0:
ax.set_title(MODEL_NAMES[model], fontsize=10.5, weight="normal", pad=8)
ax.grid(axis="y")
ax.tick_params(axis="both", length=0, pad=6,
labelbottom=index == len(ARCHITECTURES) - 1, labelleft=True)
ax.ticklabel_format(axis="y", style="plain", useOffset=False)
if column == len(panels) - 1:
ax.yaxis.set_label_position("right")
ax.set_ylabel(ARCH_NAMES[arch], fontsize=10.5, weight="bold", rotation=270, labelpad=24)
for ax in axes.flat:
ax.margins(y=0.08)
handles = [Line2D([], [], color=TEXT_COLOR, markerfacecolor="white" if marker_borders else TEXT_COLOR,
markeredgewidth=1 if marker_borders else 0,
marker=marker, markersize=7, linestyle="none", label=label)
for marker, label in (("s", "Derived as 1 / latency"),
("o", "Throughput measurement"))]
add_legend(fig, handles)
fig.supxlabel(f"{COMPILER_NAMES['raptor']} pipeline stages", fontsize=11)
fit_x_tick_labels(fig)
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/default")
parser.add_argument("--colors", action=argparse.BooleanOptionalAction, default=True,
help="Use the pastel palette (default); --no-colors exports grayscale charts")
parser.add_argument("--anonymize", action=argparse.BooleanOptionalAction, default=False,
help="Replace the displayed Raptor name with Anonimized")
parser.add_argument("--marker-borders", action=argparse.BooleanOptionalAction, default=True,
help="Outline markers (default); --no-marker-borders uses solid markers in dot and line charts")
args = parser.parse_args()
configure_style(colors=args.colors, anonymize=args.anonymize)
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_validation(results, args.output_dir, compact=True)
draw_pipeline_effect(results, args.output_dir, marker_borders=args.marker_borders)
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]
compact_speedups = select_compact_speedups(comparisons)
draw_compact_summary(comparisons, args.output_dir, marker_borders=args.marker_borders)
for metric, label in (("speedup", "Speedup (x)"),
("energy", "Energy reduction (x)"),
("power", "Average power reduction (x)")):
for suffix, diverging in (("comparison", False), ("diverging", True)):
draw_metric(compact_speedups, metric, label,
f"{metric}_compact_{suffix}", args.output_dir, diverging=diverging, compact=True)
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 reduction (x)", None),
("energy", "energy", "Energy reduction (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()