even better more structured throughput scheduling
Validate Operations / validate-operations (push) Has been cancelled

plot adjustments
validation adjustments
This commit is contained in:
NiccoloN
2026-10-27 17:46:46 +01:00
parent 49d6fb42cf
commit a77edafcbf
46 changed files with 3265 additions and 1043 deletions
+64 -16
View File
@@ -6,10 +6,14 @@ from math import ceil
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.colors import to_rgba
from matplotlib.legend_handler import HandlerBase
from matplotlib.lines import Line2D
from matplotlib.patches import Patch
from matplotlib.path import Path as PlotPath
from matplotlib.patheffects import AbstractPathEffect
from matplotlib.transforms import Affine2D, IdentityTransform, TransformedPath
from matplotlib.text import Text
from matplotlib.transforms import Affine2D, Bbox, IdentityTransform, TransformedPath
PALETTE = {
@@ -25,11 +29,12 @@ PALETTE = {
TEXT_COLOR = "#222222"
MATRIX_TEXT_COLOR = "#1D2530"
ERROR_COLOR = PALETTE["red"][1]
DAGGER_COLOR = "#AA1C1C"
GRID_COLOR = "#E6E9EC"
LINE_COLOR = "#C9CED3"
BAND_COLOR = "#F7F9FA"
COMPILER_STYLES = {"raptor": PALETTE["sage"], "pimcomp": PALETTE["sand"]}
METRIC_STYLES = {"latency": PALETTE["blue"], "throughput": PALETTE["sage"]}
ARCH_STYLES = {"arch-a": (*PALETTE["blue"], ""), "arch-b": (*PALETTE["sage"], "//")}
NETWORK_STYLES = {"vgg8": PALETTE["blue"], "resnet18": PALETTE["sage"],
"resnet34": PALETTE["sand"], "googlenet": PALETTE["purple"],
"yolo11n": PALETTE["red"]}
@@ -38,6 +43,8 @@ ARCH_NAMES = {"arch-a": "Arch-A", "arch-b": "Arch-B"}
MODEL_NAMES = {"vgg8": "VGG-8", "resnet18": "ResNet-18", "resnet34": "ResNet-34",
"googlenet": "GoogLeNet", "yolo11n": "YOLO11n"}
MODE_NAMES = {"latency": "Single-inference", "throughput": "Throughput"}
COMPACT_MODE_NAMES = {"latency": "Latency-mode", "throughput": "Throughput-mode"}
_USE_COLORS = True
class CenteredHatch(AbstractPathEffect):
@@ -47,11 +54,14 @@ class CenteredHatch(AbstractPathEffect):
hatch = gc.get_hatch() or ""
base_gc = renderer.new_gc()
base_gc.copy_properties(gc)
base_gc.set_hatch("".join(symbol for symbol in hatch if symbol not in "./\\"))
base_gc.set_hatch("".join(symbol for symbol in hatch if symbol not in "./\\") or None)
renderer.draw_path(base_gc, tpath, affine, rgbFace)
base_gc.restore()
bounds = affine.transform_path(tpath).get_extents()
clip_bounds = bounds if gc.get_clip_rectangle() is None else Bbox.intersection(bounds, gc.get_clip_rectangle())
if clip_bounds is None:
return
center_x, center_y = (bounds.x0 + bounds.x1) / 2, (bounds.y0 + bounds.y1) / 2
vertices = []
diagonal = 2 ** -0.5
@@ -61,13 +71,16 @@ class CenteredHatch(AbstractPathEffect):
continue
spacing = renderer.points_to_pixels(12 / count)
half_span = (abs(dy) * bounds.width + abs(dx) * bounds.height) / 2
half_length = (abs(dx) * bounds.width + abs(dy) * bounds.height) / 2
extent = ceil(half_span / spacing)
for step in range(-extent, extent + 1):
x = center_x - dy * step * spacing
y = center_y + dx * step * spacing
vertices.extend(((x - dx * half_length, y - dy * half_length),
(x + dx * half_length, y + dy * half_length)))
y_limits = sorted(((clip_bounds.y0 - y) / dy, (clip_bounds.y1 - y) / dy))
start = max((clip_bounds.x0 - x) / dx, y_limits[0])
end = min((clip_bounds.x1 - x) / dx, y_limits[1])
if start < end:
vertices.extend(((x + dx * start, y + dy * start),
(x + dx * end, y + dy * end)))
line_gc = renderer.new_gc()
line_gc.copy_properties(gc)
line_gc.set_hatch(None)
@@ -81,17 +94,22 @@ class CenteredHatch(AbstractPathEffect):
spacing = renderer.points_to_pixels(12 / hatch.count("."))
x_extent = ceil(bounds.width / (2 * spacing))
y_extent = ceil(bounds.height / (2 * spacing))
centers = [(center_x + x * spacing, center_y + y * spacing)
for x in range(-x_extent, x_extent + 1)
for y in range(-y_extent, y_extent + 1)]
# Trim geometry too: SVG importers can discard clip paths.
dots = [PlotPath.unit_circle().transformed(
Affine2D().scale(renderer.points_to_pixels(0.9)).translate(
center_x + x * spacing, center_y + y * spacing)).clip_to_bbox(clip_bounds)
for x in range(-x_extent, x_extent + 1)
for y in range(-y_extent, y_extent + 1)]
line_gc.set_linewidth(0)
renderer.draw_markers(line_gc, PlotPath.unit_circle(),
Affine2D().scale(renderer.points_to_pixels(0.9)),
PlotPath(centers), IdentityTransform(), gc.get_hatch_color())
renderer.draw_path(line_gc, PlotPath.make_compound_path(*dots),
IdentityTransform(), gc.get_hatch_color())
line_gc.restore()
def configure_style() -> None:
def configure_style(*, colors: bool = True, anonymize: bool = False) -> None:
global _USE_COLORS
_USE_COLORS = colors
COMPILER_NAMES["raptor"] = "Anonimized" if anonymize else "Raptor"
plt.rcParams.update({
"font.family": "DejaVu Sans",
"axes.spines.top": False,
@@ -109,6 +127,14 @@ def configure_style() -> None:
})
class TextLegendHandler(HandlerBase):
def create_artists(self, legend, original, xdescent, ydescent, width, height, fontsize, transform):
return [Text(width / 2 - xdescent, height / 2 - ydescent, original.get_text(),
color=original.get_color(), fontproperties=original.get_fontproperties(),
math_fontfamily=original.get_math_fontfamily(), ha="center", va="center",
transform=transform)]
def add_legend(fig, handles) -> None:
"""Use the fewest balanced legend rows that fit the figure width."""
renderer = fig.canvas.get_renderer()
@@ -123,6 +149,7 @@ def add_legend(fig, handles) -> None:
ordered = [handle for column in zip(*grid) for handle in column]
legend = fig.legend(handles=[handle if handle is not None else Patch(visible=False) for handle in ordered],
labels=[handle.get_label() if handle is not None else "" for handle in ordered],
handler_map={Text: TextLegendHandler()},
loc="outside upper center", ncols=columns, frameon=False, fontsize=9.5,
handlelength=1.7, columnspacing=1.5, handletextpad=0.6, labelspacing=0.4)
for handle, patch, text in zip(ordered, legend.legend_handles, legend.get_texts()):
@@ -197,7 +224,7 @@ def fit_bar_widths(fig) -> None:
fig.set_size_inches(fig.get_figwidth() * scale, fig.get_figheight())
def fit_value_labels(fig) -> None:
def fit_value_labels(fig, *, lower_limit: float | None = None) -> None:
"""Fit common vertical limits to the data and rendered value labels."""
for ax in fig.axes:
for text in ax.texts:
@@ -215,18 +242,39 @@ def fit_value_labels(fig) -> None:
labels.append((y, max(0, anchor - extent.y0 + 3) / bounds.height,
max(0, extent.y1 - anchor + 3) / bounds.height))
limits.append(ax.dataLim.intervaly if ax.has_data() else ax.get_ylim())
lower = min(0, *(lower for lower, _ in limits))
lower = min(0, *(lower for lower, _ in limits)) if lower_limit is None else lower_limit
upper = max(0, *(upper for _, upper in limits))
if lower == upper:
upper = lower + 1
for _ in range(12):
lower = min([lower, *((y - below * upper) / (1 - below) for y, below, _ in labels)])
if lower_limit is None:
lower = min([lower, *((y - below * upper) / (1 - below) for y, below, _ in labels)])
upper = max([upper, *((y - above * lower) / (1 - above) for y, _, above in labels)])
for ax in fig.axes:
ax.set_ylim(lower, upper)
def grayscale_figure(fig) -> None:
def gray(color):
red, green, blue, alpha = to_rgba(color)
level = 0.2126 * red + 0.7152 * green + 0.0722 * blue
return level, level, level, alpha
for artist in fig.findobj():
if isinstance(artist, Patch):
artist.set_facecolor(gray(artist.get_facecolor()))
artist.set_edgecolor(gray(artist.get_edgecolor()))
elif isinstance(artist, Line2D):
artist.set_color(gray(artist.get_color()))
artist.set_markerfacecolor(gray(artist.get_markerfacecolor()))
artist.set_markeredgecolor(gray(artist.get_markeredgecolor()))
elif isinstance(artist, Text):
artist.set_color(gray(artist.get_color()))
def save_figure(fig, basename: str, output_dir: Path) -> None:
if not _USE_COLORS:
grayscale_figure(fig)
for patch in fig.findobj(Patch):
hatch = patch.get_hatch() or ""
if any(symbol in hatch for symbol in "./\\"):