-Your one-stop shop for static memory allocation.
+
State-of-the-art static memory allocation for neural networks.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+OmniMalloc is a Python library for **static memory allocation**: given buffers
+with known sizes and lifetimes, assign offsets so that **peak memory is minimized**.
+This is the memory-planning step at the heart of **ML compilers**, embedded
+runtimes, and accelerator toolchains.
+
+It ships a collection of allocators and allocation algorithms behind one API,
+implemented with an efficient C++ backend. This includes **SuperMalloc**, a new
+allocator that **outperforms the best open-source alternatives** (see benchmarks
+below). OmniMalloc also provides a rich benchmark harness and visualization
+tools to develop and evaluate new allocation strategies.
## Installation
@@ -10,7 +34,48 @@ pip install omnimalloc
## Usage
-Please refer to [examples](examples/), in particular [examples/01_basic.py](examples/01_basic.py).
+```python
+from omnimalloc import Allocation, Pool, run_allocation
+
+pool = Pool(id="pool", allocations=(
+ Allocation(id=0, size=64, start=0, end=10),
+ Allocation(id=1, size=64, start=12, end=20),
+ Allocation(id=2, size=32, start=5, end=15),
+))
+
+pool = run_allocation(pool, allocator="supermalloc_allocator", validate=True)
+
+print(pool.size) # 96
+print([alloc.offset for alloc in pool.allocations]) # [0, 0, 64]
+```
+
+On a real problem, the result looks like this: 308 buffers of an ML workload
+packed with zero wasted memory.
+
+
+
+
+
+
+See [examples](examples/) for allocator selection, visualization, custom
+allocation sources, and benchmarking.
+
+## Benchmarks
+
+
+
+
+
+
+
+
+
+
+
+
+Every figure on this page is generated from a deterministic benchmark run by
+[`scripts/generate_readme_assets.py`](scripts/generate_readme_assets.py).
+Run your own campaigns with the [benchmark harness](examples/05_benchmark.py).
## Development
diff --git a/assets/allocation_dark.svg b/assets/allocation_dark.svg
new file mode 100644
index 0000000..601ef49
--- /dev/null
+++ b/assets/allocation_dark.svg
@@ -0,0 +1,4453 @@
+
+
+
diff --git a/assets/allocation_light.svg b/assets/allocation_light.svg
new file mode 100644
index 0000000..f813bd9
--- /dev/null
+++ b/assets/allocation_light.svg
@@ -0,0 +1,4453 @@
+
+
+
diff --git a/assets/hero_dark.svg b/assets/hero_dark.svg
new file mode 100644
index 0000000..bb61313
--- /dev/null
+++ b/assets/hero_dark.svg
@@ -0,0 +1,2541 @@
+
+
+
diff --git a/assets/hero_light.svg b/assets/hero_light.svg
new file mode 100644
index 0000000..e879ef4
--- /dev/null
+++ b/assets/hero_light.svg
@@ -0,0 +1,2541 @@
+
+
+
diff --git a/assets/quality_dark.svg b/assets/quality_dark.svg
new file mode 100644
index 0000000..75f4962
--- /dev/null
+++ b/assets/quality_dark.svg
@@ -0,0 +1,2433 @@
+
+
+
diff --git a/assets/quality_light.svg b/assets/quality_light.svg
new file mode 100644
index 0000000..0b95b57
--- /dev/null
+++ b/assets/quality_light.svg
@@ -0,0 +1,2433 @@
+
+
+
diff --git a/assets/scaling_dark.svg b/assets/scaling_dark.svg
new file mode 100644
index 0000000..f127dbd
--- /dev/null
+++ b/assets/scaling_dark.svg
@@ -0,0 +1,2024 @@
+
+
+
diff --git a/assets/scaling_light.svg b/assets/scaling_light.svg
new file mode 100644
index 0000000..22f51b0
--- /dev/null
+++ b/assets/scaling_light.svg
@@ -0,0 +1,2024 @@
+
+
+
diff --git a/scripts/generate_readme_assets.py b/scripts/generate_readme_assets.py
new file mode 100644
index 0000000..e313846
--- /dev/null
+++ b/scripts/generate_readme_assets.py
@@ -0,0 +1,679 @@
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+"""Generate the figures embedded in the README.
+
+Runs a deterministic benchmark suite and renders four figures, each in a light
+and a dark variant with transparent backgrounds (GitHub picks the right one via
+a ```` element):
+
+- ``hero``: packing efficiency vs. solve time across allocators (Pareto view)
+- ``quality``: per-problem packing efficiency for three allocators
+- ``scaling``: solve time vs. problem size
+- ``allocation``: a solved allocation rendered as offset/time rectangles
+
+Regenerate the committed assets with:
+
+ uv run python scripts/generate_readme_assets.py
+
+The benchmark portion takes a few minutes (genetic search and branch-and-bound
+timeouts dominate). Use ``--dump data.json`` once and ``--data data.json`` to
+iterate on rendering without re-running it. ``--preview DIR`` additionally
+writes PNG previews.
+"""
+
+from __future__ import annotations
+
+import argparse
+import importlib
+import json
+import random
+import shutil
+import subprocess
+import sys
+import time
+from dataclasses import dataclass
+from pathlib import Path
+from typing import TYPE_CHECKING, Any
+
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+from matplotlib.colors import LinearSegmentedColormap
+from matplotlib.patches import Rectangle
+from matplotlib.ticker import FuncFormatter, MultipleLocator
+from omnimalloc import run_allocation, validate_allocation
+from omnimalloc.allocators import BaseAllocator
+from omnimalloc.allocators.supermalloc import SupermallocAllocator, SupermallocConfig
+from omnimalloc.benchmark.sources import BaseSource
+from omnimalloc.common.units import MB
+
+if TYPE_CHECKING:
+ from matplotlib.axes import Axes
+ from matplotlib.figure import Figure
+ from omnimalloc.primitives import Pool
+
+SEED = 0
+SUPERMALLOC_TIMEOUT = 10.0
+SCALING_TIMEOUT = 3.0
+MINIMALLOC_URL = "git+https://github.com/google/minimalloc.git"
+SCALING_SIZES = (10, 32, 100, 316, 1000, 3162, 10000)
+SCALING_SIZES_SLOW = SCALING_SIZES[:-1] # hill climbing needs minutes at 10k
+
+ASSETS_DIR = Path(__file__).resolve().parent.parent / "assets"
+
+# Hero points: allocator -> (display label, palette role).
+HERO_ALLOCATORS: dict[str, tuple[str, str]] = {
+ "random_allocator": ("random search", "baseline"),
+ "greedy_by_area_allocator_cpp": ("greedy (area)", "greedy"),
+ "greedy_by_size_allocator_cpp": ("greedy (size)", "greedy"),
+ "greedy_by_all_allocator_cpp": ("greedy (all)", "greedy"),
+ "hill_climb_allocator": ("hill climbing", "search"),
+ "genetic_allocator": ("genetic", "search"),
+ "minimalloc": ("minimalloc", "minimalloc"),
+ "supermalloc": ("supermalloc", "exact"),
+}
+
+QUALITY_ALLOCATORS = (
+ "greedy_by_size_allocator_cpp",
+ "greedy_by_all_allocator_cpp",
+ "minimalloc",
+ "supermalloc",
+)
+QUALITY_PROBLEMS = ("mm-A", "mm-C", "mm-H", "mm-K", "pinwheel", "tiling", "random")
+
+SCALING_ALLOCATORS: dict[str, tuple[str, str]] = {
+ "naive_allocator": ("naive", "baseline"),
+ "greedy_by_size_allocator_cpp": ("greedy (size)", "greedy"),
+ "hill_climb_allocator": ("hill climbing", "search"),
+ "minimalloc": ("minimalloc", "minimalloc"),
+ "supermalloc": ("supermalloc", "exact"),
+}
+
+
+@dataclass(frozen=True)
+class Theme:
+ name: str
+ ink: str
+ muted: str
+ faint: str
+ grid: str
+ optimal: str
+ role: dict[str, str] # baseline / greedy / search / exact
+
+
+LIGHT = Theme(
+ name="light",
+ ink="#24292f",
+ muted="#59626d",
+ faint="#818b98",
+ grid="#d0d7de",
+ optimal="#1a7f37",
+ role={
+ "baseline": "#848d97",
+ "greedy": "#0969da",
+ "greedy_alt": "#54aeff",
+ "search": "#bc4c00",
+ "minimalloc": "#bf3989",
+ "exact": "#8250df",
+ },
+)
+DARK = Theme(
+ name="dark",
+ ink="#e6edf3",
+ muted="#9198a1",
+ faint="#767e88",
+ grid="#30363d",
+ optimal="#3fb950",
+ role={
+ "baseline": "#768390",
+ "greedy": "#4493f8",
+ "greedy_alt": "#79c0ff",
+ "search": "#f0883e",
+ "minimalloc": "#db61a2",
+ "exact": "#ab7df8",
+ },
+)
+
+
+def _pip_install(spec: str) -> None:
+ """Install a package into the running interpreter, preferring uv over pip."""
+ for cmd in (
+ ["uv", "pip", "install", "--python", sys.executable, spec],
+ [sys.executable, "-m", "pip", "install", spec],
+ ):
+ if shutil.which(cmd[0]) and subprocess.run(cmd, check=False).returncode == 0:
+ return
+ raise RuntimeError(f"Could not install {spec!r}; install it manually")
+
+
+def _ensure_minimalloc() -> None:
+ """Install Google's minimalloc on demand (no PyPI wheel) and refresh the wrapper."""
+ try:
+ import minimalloc # type: ignore # noqa: F401
+ except ImportError:
+ pass
+ else:
+ return
+ print(f"minimalloc not installed — installing from {MINIMALLOC_URL} ...")
+ _pip_install(MINIMALLOC_URL)
+ importlib.invalidate_caches()
+ # The wrapper decides availability at import time; reload it post-install.
+ importlib.reload(importlib.import_module("omnimalloc.allocators.minimalloc"))
+
+
+def _allocator(name: str, timeout: float = SUPERMALLOC_TIMEOUT) -> BaseAllocator:
+ if name == "supermalloc":
+ return SupermallocAllocator(config=SupermallocConfig(timeout=timeout))
+ if name == "minimalloc":
+ from omnimalloc.allocators.minimalloc import MinimallocAllocator
+
+ return MinimallocAllocator(timeout=int(timeout))
+ return BaseAllocator.get(name)()
+
+
+def _solve(pool: Pool, allocator: BaseAllocator) -> tuple[float, float, Pool]:
+ """Time the solve alone; validation is quadratic and would skew timings."""
+ start = time.perf_counter()
+ solved = run_allocation(pool, allocator=allocator)
+ seconds = time.perf_counter() - start
+ validate_allocation(solved)
+ return seconds, solved.efficiency, solved
+
+
+def _hard_suite() -> dict[str, Pool]:
+ """Real minimalloc benchmarks plus adversarial synthetic patterns."""
+ suite: dict[str, Pool] = {}
+ minimalloc = BaseSource.get("minimalloc_source")()
+ for variant in minimalloc.get_available_variants():
+ suite[f"mm-{variant.split('.')[0]}"] = minimalloc.get_variant(variant)
+ suite["pinwheel"] = BaseSource.get("pinwheel_source")().get_variant(101)
+ suite["tiling"] = BaseSource.get("tiling_source")().get_variant(100)
+ suite["random"] = BaseSource.get("random_source")().get_variant(250)
+ return suite
+
+
+def collect_data() -> dict[str, Any]:
+ _ensure_minimalloc()
+ random.seed(SEED)
+ suite = _hard_suite()
+ hard = {k: v for k, v in suite.items() if k != "random"}
+
+ # Hero + quality: every allocator over every problem.
+ runs: dict[str, dict[str, tuple[float, float]]] = {}
+ names = set(HERO_ALLOCATORS) | set(QUALITY_ALLOCATORS)
+ for name in sorted(names):
+ allocator = _allocator(name)
+ runs[name] = {}
+ for problem, pool in suite.items():
+ seconds, efficiency, _ = _solve(pool, allocator)
+ runs[name][problem] = (seconds, efficiency)
+ print(f"{name:38s} {problem:10s} {seconds:8.3f}s {efficiency:7.2%}")
+
+ hero = {
+ name: {
+ "seconds": sum(runs[name][p][0] for p in hard) / len(hard),
+ "efficiency": sum(runs[name][p][1] for p in hard) / len(hard) * 100,
+ }
+ for name in HERO_ALLOCATORS
+ }
+ quality = {
+ name: {p: runs[name][p][1] * 100 for p in QUALITY_PROBLEMS}
+ for name in QUALITY_ALLOCATORS
+ }
+
+ # Scaling: solve time vs. problem size on the random source.
+ source = BaseSource.get("random_source")()
+ scaling: dict[str, dict[str, float]] = {}
+ for name in SCALING_ALLOCATORS:
+ allocator = _allocator(name, SCALING_TIMEOUT)
+ # Hill climbing needs minutes at 10k; minimalloc can't solve 10k in budget.
+ capped = name in ("hill_climb_allocator", "minimalloc")
+ sizes = SCALING_SIZES_SLOW if capped else SCALING_SIZES
+ scaling[name] = {}
+ for size in sizes:
+ seconds, _, _ = _solve(source.get_variant(size), allocator)
+ scaling[name][str(size)] = seconds
+ print(f"{name:38s} n={size:<6d} {seconds:8.3f}s")
+
+ # Allocation rendering: one real problem solved to proven optimality.
+ problem = "mm-G"
+ _, efficiency, solved = _solve(suite[problem], _allocator("supermalloc"))
+ allocation = {
+ "problem": QUALITY_ROW_LABELS.get(problem, problem),
+ "efficiency": efficiency,
+ "size": solved.size,
+ "rects": [[a.start, a.duration, a.offset, a.size] for a in solved.allocations],
+ }
+
+ return {
+ "hero": hero,
+ "quality": quality,
+ "scaling": scaling,
+ "allocation": allocation,
+ }
+
+
+def _style(theme: Theme) -> dict[str, Any]:
+ return {
+ "font.family": ["Lato", "DejaVu Sans"],
+ "font.size": 9.0,
+ "svg.fonttype": "path",
+ "figure.facecolor": "none",
+ "axes.facecolor": "none",
+ "savefig.facecolor": "none",
+ "savefig.transparent": True,
+ "axes.edgecolor": theme.grid,
+ "axes.labelcolor": theme.muted,
+ "axes.linewidth": 0.8,
+ "xtick.color": theme.grid,
+ "ytick.color": theme.grid,
+ "xtick.labelcolor": theme.muted,
+ "ytick.labelcolor": theme.muted,
+ "xtick.labelsize": 8.0,
+ "ytick.labelsize": 8.0,
+ "axes.labelsize": 8.5,
+ }
+
+
+def _despine(ax: Axes, keep: tuple[str, ...] = ("left", "bottom")) -> None:
+ for side, spine in ax.spines.items():
+ spine.set_visible(side in keep)
+
+
+def _title(fig: Figure, theme: Theme, title: str, subtitle: str) -> None:
+ fig.text(
+ 0.01,
+ 0.99,
+ title,
+ ha="left",
+ va="top",
+ fontsize=11.5,
+ fontweight="bold",
+ color=theme.ink,
+ )
+ fig.text(
+ 0.01, 0.905, subtitle, ha="left", va="top", fontsize=8.5, color=theme.muted
+ )
+
+
+def _series_line(fig: Figure, series: list[tuple[str, str]], y: float) -> None:
+ """Draw an inline legend: colored '● label' entries on one figure line."""
+ x = 0.012
+ for label, color in series:
+ fig.text(x, y, f"● {label}", ha="left", va="top", fontsize=8, color=color)
+ x += 0.033 + len(label) * 0.0148
+
+
+def _format_seconds(value: float) -> str:
+ if value < 1e-3:
+ return f"{value * 1e6:.0f} µs"
+ if value < 1:
+ return f"{value * 1e3:.0f} ms"
+ return f"{value:.0f} s"
+
+
+def _format_steps(value: float, _pos: int) -> str:
+ if value >= 1e6:
+ return f"{value / 1e6:g}M"
+ if value >= 1e3:
+ return f"{value / 1e3:g}k"
+ return f"{value:g}"
+
+
+def _save(fig: Figure, name: str, theme: Theme, preview: Path | None) -> None:
+ ASSETS_DIR.mkdir(parents=True, exist_ok=True)
+ svg = ASSETS_DIR / f"{name}_{theme.name}.svg"
+ fig.savefig(svg, bbox_inches="tight", pad_inches=0.02)
+ # matplotlib emits trailing whitespace on some lines; strip it so the
+ # committed assets stay clean under the trailing-whitespace pre-commit hook.
+ svg.write_text(
+ "\n".join(line.rstrip() for line in svg.read_text().splitlines()) + "\n"
+ )
+ if preview is not None:
+ preview.mkdir(parents=True, exist_ok=True)
+ fig.savefig(
+ preview / f"{name}_{theme.name}.png",
+ dpi=220,
+ bbox_inches="tight",
+ pad_inches=0.02,
+ facecolor="#ffffff" if theme.name == "light" else "#0d1117",
+ transparent=False,
+ )
+ plt.close(fig)
+
+
+# Direct-label offsets in points, tuned per hero point: (dx, dy, ha).
+HERO_LABEL_OFFSETS: dict[str, tuple[float, float, str]] = {
+ "random search": (0, -11, "center"),
+ "greedy (area)": (0, 8, "center"),
+ "greedy (size)": (-8, 0, "right"),
+ "greedy (all)": (0, -11, "center"),
+ "hill climbing": (0, 8, "center"),
+ "genetic": (0, -11, "center"),
+ "minimalloc": (0, 8, "center"),
+ "supermalloc": (-10, 0, "right"),
+}
+
+
+def render_hero(data: dict[str, Any], theme: Theme, preview: Path | None) -> None:
+ fig, ax = plt.subplots(figsize=(7.4, 3.5))
+
+ points = {
+ label: (data[name]["seconds"], data[name]["efficiency"], role)
+ for name, (label, role) in HERO_ALLOCATORS.items()
+ }
+
+ ax.axhline(
+ 100,
+ color=theme.optimal,
+ linewidth=0.8,
+ linestyle=(0, (4, 4)),
+ alpha=0.8,
+ zorder=1,
+ )
+ ax.annotate(
+ "optimal",
+ xy=(1.0, 100),
+ xycoords=("axes fraction", "data"),
+ xytext=(-2, 3),
+ textcoords="offset points",
+ ha="right",
+ fontsize=7.5,
+ color=theme.optimal,
+ )
+
+ # Pareto frontier: lower time and higher efficiency dominate.
+ ordered = sorted(points.values(), key=lambda p: (p[0], -p[1]))
+ front, best = [], float("-inf")
+ for seconds, efficiency, _ in ordered:
+ if efficiency > best:
+ front.append((seconds, efficiency))
+ best = efficiency
+ ax.plot(
+ *zip(*front, strict=False),
+ color=theme.faint,
+ linewidth=0.9,
+ linestyle=(0, (1, 2)),
+ zorder=2,
+ )
+
+ for label, (seconds, efficiency, role) in points.items():
+ color = theme.role[role]
+ emphasis = label == "supermalloc"
+ ax.scatter(
+ seconds,
+ efficiency,
+ s=110 if emphasis else 52,
+ color=color,
+ linewidths=1.4 if emphasis else 0,
+ edgecolors=theme.ink if emphasis else "none",
+ zorder=4,
+ )
+ dx, dy, ha = HERO_LABEL_OFFSETS[label]
+ ax.annotate(
+ label,
+ (seconds, efficiency),
+ xytext=(dx, dy),
+ textcoords="offset points",
+ ha=ha,
+ va="center",
+ fontsize=8.5 if emphasis else 8,
+ color=color,
+ fontweight="bold" if emphasis else "normal",
+ zorder=5,
+ )
+
+ ax.set_xscale("log")
+ ax.set_xlim(1.2e-3, 22)
+ ax.set_ylim(52, 104)
+ ticks = (1e-3, 1e-2, 1e-1, 1, 10)
+ ax.set_xticks(ticks)
+ ax.set_xticklabels([_format_seconds(t) for t in ticks])
+ ax.grid(visible=True, axis="both", color=theme.grid, linewidth=0.5, alpha=0.45)
+ _despine(ax, keep=())
+ ax.tick_params(length=0, which="both")
+ ax.minorticks_off()
+ ax.set_xlabel("mean solve time (log scale)")
+ ax.set_ylabel("mean packing efficiency (%)")
+
+ _title(
+ fig,
+ theme,
+ "Solution quality vs. solve time",
+ "13 hard problems: 11 real-world minimalloc benchmarks "
+ "and 2 adversarial patterns · 100% = proven lower bound",
+ )
+ fig.subplots_adjust(top=0.80, bottom=0.13, left=0.075, right=0.985)
+ _save(fig, "hero", theme, preview)
+
+
+QUALITY_LABELS = {
+ "greedy_by_size_allocator_cpp": ("greedy (size)", "greedy_alt"),
+ "greedy_by_all_allocator_cpp": ("greedy (all)", "greedy"),
+ "minimalloc": ("minimalloc", "minimalloc"),
+ "supermalloc": ("supermalloc", "exact"),
+}
+QUALITY_ROW_LABELS = {
+ "mm-A": "minimalloc A",
+ "mm-C": "minimalloc C",
+ "mm-G": "minimalloc G",
+ "mm-H": "minimalloc H",
+ "mm-K": "minimalloc K",
+ "pinwheel": "pinwheel",
+ "tiling": "tiling",
+ "random": "random (easy)",
+}
+
+
+def render_quality(data: dict[str, Any], theme: Theme, preview: Path | None) -> None:
+ fig, ax = plt.subplots(figsize=(3.8, 3.35))
+
+ rows = list(QUALITY_PROBLEMS)
+ ys = range(len(rows), 0, -1)
+
+ ax.axvline(
+ 100,
+ color=theme.optimal,
+ linewidth=0.8,
+ linestyle=(0, (4, 4)),
+ alpha=0.8,
+ zorder=1,
+ )
+
+ for row, y in zip(rows, ys, strict=True):
+ values = [data[name][row] for name in QUALITY_ALLOCATORS]
+ ax.plot(
+ [min(values), max(values)],
+ [y, y],
+ color=theme.grid,
+ linewidth=1.1,
+ zorder=2,
+ solid_capstyle="round",
+ )
+ for name, value in zip(QUALITY_ALLOCATORS, values, strict=True):
+ _, role = QUALITY_LABELS[name]
+ emphasis = name == "supermalloc"
+ ax.scatter(
+ value,
+ y,
+ s=52 if emphasis else 34,
+ color=theme.role[role],
+ zorder=4,
+ linewidths=1.2 if emphasis else 0,
+ edgecolors=theme.ink if emphasis else "none",
+ )
+
+ ax.set_yticks(list(ys))
+ ax.set_yticklabels([QUALITY_ROW_LABELS[r] for r in rows], fontsize=8.5)
+ ax.set_ylim(0.4, len(rows) + 0.6)
+ ax.set_xlim(60, 103)
+ ax.set_xticks((60, 70, 80, 90, 100))
+ ax.grid(visible=True, axis="x", color=theme.grid, linewidth=0.5, alpha=0.45)
+ _despine(ax, keep=())
+ ax.tick_params(length=0)
+ ax.set_xlabel("packing efficiency (%)")
+
+ _title(fig, theme, "Quality per problem", "100% = proven lower bound")
+ series = [(label, theme.role[role]) for label, role in QUALITY_LABELS.values()]
+ _series_line(fig, series, y=0.842)
+ fig.subplots_adjust(top=0.775, bottom=0.125, left=0.265, right=0.97)
+ _save(fig, "quality", theme, preview)
+
+
+# Direct-label offsets in points: (dx, dy, ha). minimalloc's line ends early
+# (no 10k point), so its label anchors left, away from the 10k label cluster.
+SCALING_LABEL_OFFSETS = {
+ "naive": (4, -2, "left"),
+ "greedy (size)": (4, -4, "left"),
+ "hill climbing": (4, 2, "left"),
+ "minimalloc": (0, 9, "center"),
+ "supermalloc": (4, 4, "left"),
+}
+
+
+def render_scaling(data: dict[str, Any], theme: Theme, preview: Path | None) -> None:
+ fig, ax = plt.subplots(figsize=(3.8, 3.35))
+
+ for name, (label, role) in SCALING_ALLOCATORS.items():
+ series = data[name]
+ sizes = sorted(int(k) for k in series)
+ seconds = [series[str(n)] for n in sizes]
+ color = theme.role[role]
+ emphasis = name == "supermalloc"
+ ax.plot(
+ sizes,
+ seconds,
+ color=color,
+ linewidth=1.8 if emphasis else 1.4,
+ marker="o",
+ markersize=3.4,
+ markeredgewidth=0,
+ zorder=4,
+ )
+ dx, dy, ha = SCALING_LABEL_OFFSETS[label]
+ ax.annotate(
+ label,
+ (sizes[-1], seconds[-1]),
+ xytext=(dx, dy),
+ textcoords="offset points",
+ ha=ha,
+ va="center",
+ fontsize=8,
+ color=color,
+ fontweight="bold" if emphasis else "normal",
+ )
+
+ ax.set_xscale("log")
+ ax.set_yscale("log")
+ ax.set_xlim(8, 1.5e5)
+ ax.set_xticks((10, 100, 1000, 10000))
+ ax.set_xticklabels(["10", "100", "1k", "10k"])
+ yticks = (1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 1, 10, 100)
+ ax.set_ylim(3e-6, 400)
+ ax.set_yticks(yticks)
+ ax.set_yticklabels([_format_seconds(t) for t in yticks])
+ ax.grid(visible=True, axis="both", color=theme.grid, linewidth=0.5, alpha=0.45)
+ _despine(ax, keep=())
+ ax.tick_params(length=0, which="both")
+ ax.minorticks_off()
+ ax.set_xlabel("number of allocations")
+ ax.set_ylabel("solve time")
+
+ _title(
+ fig,
+ theme,
+ "Scaling",
+ f"random problems · supermalloc budget {SCALING_TIMEOUT:.0f} s",
+ )
+ fig.subplots_adjust(top=0.845, bottom=0.125, left=0.16, right=0.97)
+ _save(fig, "scaling", theme, preview)
+
+
+def render_allocation(data: dict[str, Any], theme: Theme, preview: Path | None) -> None:
+ fig, ax = plt.subplots(figsize=(7.4, 2.9))
+
+ rects = data["rects"]
+ size = data["size"]
+ max_end = max(start + duration for start, duration, _, _ in rects)
+
+ # Color each buffer by size quantile within the greedy-to-exact hue range.
+ cmap = LinearSegmentedColormap.from_list(
+ "buffers", [theme.role["greedy"], theme.role["exact"]]
+ )
+ ordered_sizes = sorted(r[3] for r in rects)
+ for start, duration, offset, height in rects:
+ quantile = ordered_sizes.index(height) / max(len(ordered_sizes) - 1, 1)
+ ax.add_patch(
+ Rectangle(
+ (start, offset),
+ duration,
+ height,
+ facecolor=cmap(quantile),
+ alpha=0.85,
+ edgecolor=theme.ink,
+ linewidth=0.2,
+ )
+ )
+
+ ax.axhline(
+ size, color=theme.optimal, linewidth=0.9, linestyle=(0, (4, 4)), zorder=5
+ )
+ ax.annotate(
+ f"peak {size / MB:.2f} MB = lower bound (proven optimal)",
+ xy=(0.995, size),
+ xycoords=("axes fraction", "data"),
+ xytext=(0, 4),
+ textcoords="offset points",
+ ha="right",
+ fontsize=8,
+ color=theme.optimal,
+ zorder=6,
+ )
+
+ ax.set_xlim(0, max_end)
+ ax.set_ylim(0, size * 1.14)
+ ax.yaxis.set_major_locator(MultipleLocator(0.25 * MB))
+ ax.yaxis.set_major_formatter(FuncFormatter(lambda v, _: f"{v / MB:g}"))
+ ax.xaxis.set_major_formatter(FuncFormatter(_format_steps))
+ ax.grid(visible=True, axis="y", color=theme.grid, linewidth=0.5, alpha=0.45)
+ _despine(ax, keep=())
+ ax.tick_params(length=0)
+ ax.set_xlabel("time step")
+ ax.set_ylabel("offset (MB)")
+
+ _title(
+ fig,
+ theme,
+ "A solved problem",
+ f"{data['problem']} · {len(rects)} buffers packed at "
+ f"{data['efficiency']:.0%} efficiency by supermalloc",
+ )
+ fig.subplots_adjust(top=0.775, bottom=0.155, left=0.055, right=0.985)
+ _save(fig, "allocation", theme, preview)
+
+
+def render_all(data: dict[str, Any], preview: Path | None) -> None:
+ for theme in (LIGHT, DARK):
+ with mpl.rc_context(_style(theme)):
+ render_hero(data["hero"], theme, preview)
+ render_quality(data["quality"], theme, preview)
+ render_scaling(data["scaling"], theme, preview)
+ render_allocation(data["allocation"], theme, preview)
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--data", type=Path, help="load benchmark data from JSON")
+ parser.add_argument("--dump", type=Path, help="write benchmark data to JSON")
+ parser.add_argument("--preview", type=Path, help="also write PNG previews")
+ args = parser.parse_args()
+
+ data = json.loads(args.data.read_text()) if args.data else collect_data()
+ if args.dump:
+ args.dump.write_text(json.dumps(data, indent=2))
+
+ render_all(data, args.preview)
+
+
+if __name__ == "__main__":
+ main()