|
| 1 | +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| 2 | +# SPDX-License-Identifier: Apache-2.0 |
| 3 | +# /// script |
| 4 | +# requires-python = ">=3.10" |
| 5 | +# dependencies = [ |
| 6 | +# "matplotlib==3.9.4", |
| 7 | +# ] |
| 8 | +# /// |
| 9 | +"""Regenerate the "Top Model Usage" telemetry figure. |
| 10 | +
|
| 11 | +Renders the ranked input-vs-output token breakdown shown in the README's |
| 12 | +"Top models (YTD)" section, styled to match the Data Designer devnote charts |
| 13 | +(near-black canvas, NVIDIA-green duotone). The same PNG is written to both |
| 14 | +tracked copies so the README and Fern docs site stay in sync: |
| 15 | +
|
| 16 | + docs/images/top-models.png (rendered by the README) |
| 17 | + fern/images/top-models.png (Fern's /images/* mirror) |
| 18 | +
|
| 19 | +The source telemetry export lives at docs/scripts/top-model-usage.csv with |
| 20 | +columns: model name, input (context) tokens, output (generated) tokens, plus a |
| 21 | +trailing "Other" aggregate row. Drop in a fresh export to refresh the figure. |
| 22 | +
|
| 23 | +Run: |
| 24 | + # Regenerate from the committed CSV (zero args) |
| 25 | + uv run docs/scripts/generate_top_models_figure.py |
| 26 | +
|
| 27 | + # Refresh from a new telemetry export |
| 28 | + uv run docs/scripts/generate_top_models_figure.py --csv ~/Downloads/new-export.csv |
| 29 | +
|
| 30 | + # Options |
| 31 | + uv run docs/scripts/generate_top_models_figure.py --help |
| 32 | +""" |
| 33 | + |
| 34 | +from __future__ import annotations |
| 35 | + |
| 36 | +import argparse |
| 37 | +import csv |
| 38 | +import shutil |
| 39 | +from pathlib import Path |
| 40 | + |
| 41 | +import matplotlib.pyplot as plt |
| 42 | +from matplotlib import rcParams |
| 43 | +from matplotlib.ticker import FuncFormatter, MaxNLocator |
| 44 | + |
| 45 | +# Repo root is two levels up from docs/scripts/. |
| 46 | +REPO_ROOT = Path(__file__).resolve().parents[2] |
| 47 | +DEFAULT_CSV = REPO_ROOT / "docs" / "scripts" / "top-model-usage.csv" |
| 48 | +# Tracked copies of the figure; first entry is the canonical render target. |
| 49 | +# docs/images/ is what the README renders; fern/images/ is Fern's mirror for |
| 50 | +# /images/* references. |
| 51 | +TARGETS = ( |
| 52 | + REPO_ROOT / "docs" / "images" / "top-models.png", |
| 53 | + REPO_ROOT / "fern" / "images" / "top-models.png", |
| 54 | +) |
| 55 | + |
| 56 | +# ---------------------------------------------------------------- palette ---- |
| 57 | +BG = "#0E0E0E" # near-black canvas (matches DD devnote charts) |
| 58 | +GREEN = "#76B900" # NVIDIA green -> input (context) tokens |
| 59 | +LIME = "#C5E86C" # light NVIDIA-tint green -> output (generated) tokens |
| 60 | +WHITE = "#FFFFFF" |
| 61 | +SUBTLE = "#9A9A9A" |
| 62 | +AXIS = "#B8B8B8" |
| 63 | +MODELNAME = "#ECECEC" |
| 64 | +GRID = "#FFFFFF" |
| 65 | +SPINE = "#4A4A4A" |
| 66 | +INK = "#0E0E0E" # dark ink for labels sitting on bright bars |
| 67 | + |
| 68 | +B = 1e9 # render token counts in billions |
| 69 | + |
| 70 | + |
| 71 | +def load_rows(csv_path: Path) -> list[tuple[str, float, float]]: |
| 72 | + """Parse the telemetry CSV into (name, input_tokens, output_tokens) rows.""" |
| 73 | + rows: list[tuple[str, float, float]] = [] |
| 74 | + with csv_path.open(newline="", encoding="utf-8-sig") as fh: |
| 75 | + reader = csv.reader(fh) |
| 76 | + next(reader) # header |
| 77 | + for name, inp, out in reader: |
| 78 | + rows.append((name, float(inp.replace(",", "")), float(out.replace(",", "")))) |
| 79 | + return rows |
| 80 | + |
| 81 | + |
| 82 | +def configure_matplotlib() -> None: |
| 83 | + """Pin rendering to deterministic settings so the asset is reproducible. |
| 84 | +
|
| 85 | + Forces the Agg backend and matplotlib's bundled DejaVu Sans face rather than |
| 86 | + opportunistically selecting a system Helvetica/Arial. Combined with the |
| 87 | + pinned matplotlib version in the script metadata, this keeps the checked-in |
| 88 | + PNG byte-reproducible across machines and CI. |
| 89 | + """ |
| 90 | + plt.switch_backend("Agg") |
| 91 | + rcParams["font.family"] = "DejaVu Sans" |
| 92 | + rcParams["font.size"] = 13 |
| 93 | + |
| 94 | + |
| 95 | +def fmt(v: float) -> str: |
| 96 | + """Compact billions/trillions label.""" |
| 97 | + if v >= 1e12: |
| 98 | + return f"{v / 1e12:.2f}T" |
| 99 | + return f"{v / 1e9:.0f}B" |
| 100 | + |
| 101 | + |
| 102 | +def render(rows: list[tuple[str, float, float]], out_path: Path) -> None: |
| 103 | + """Render the ranked stacked-bar figure to out_path.""" |
| 104 | + # Split the "Other" aggregate out; sort named models by total descending. |
| 105 | + other = next((r for r in rows if r[0].lower() == "other"), None) |
| 106 | + models = [r for r in rows if r[0].lower() != "other"] |
| 107 | + models.sort(key=lambda r: r[1] + r[2], reverse=True) |
| 108 | + |
| 109 | + n = len(models) |
| 110 | + ypos = list(range(n, 0, -1)) # n, n-1, ... 1 (top -> down) |
| 111 | + labels = [m[0] for m in models] |
| 112 | + inputs = [m[1] for m in models] |
| 113 | + outputs = [m[2] for m in models] |
| 114 | + |
| 115 | + if other is not None: |
| 116 | + ypos.append(-0.6) # gap below the named models |
| 117 | + labels.append("Other models") |
| 118 | + inputs.append(other[1]) |
| 119 | + outputs.append(other[2]) |
| 120 | + |
| 121 | + fig, ax = plt.subplots(figsize=(14.5, 9.2), dpi=200) |
| 122 | + fig.patch.set_facecolor(BG) |
| 123 | + ax.set_facecolor(BG) |
| 124 | + |
| 125 | + xmax = max(i + o for i, o in zip(inputs, outputs)) / B |
| 126 | + bar_h = 0.62 |
| 127 | + |
| 128 | + for idx, (y, inp, out) in enumerate(zip(ypos, inputs, outputs)): |
| 129 | + is_other = other is not None and idx == len(ypos) - 1 |
| 130 | + a = 0.45 if is_other else 1.0 |
| 131 | + |
| 132 | + ax.barh(y, inp / B, height=bar_h, color=GREEN, alpha=a, zorder=3, edgecolor=BG, linewidth=1.2) |
| 133 | + ax.barh(y, out / B, height=bar_h, left=inp / B, color=LIME, alpha=a, zorder=3, edgecolor=BG, linewidth=1.2) |
| 134 | + |
| 135 | + total = (inp + out) / B |
| 136 | + ax.text( |
| 137 | + total + xmax * 0.008, |
| 138 | + y, |
| 139 | + fmt(inp + out), |
| 140 | + va="center", |
| 141 | + ha="left", |
| 142 | + color=SUBTLE if is_other else WHITE, |
| 143 | + fontweight="bold", |
| 144 | + fontsize=13.5, |
| 145 | + zorder=5, |
| 146 | + ) |
| 147 | + |
| 148 | + # In-segment value labels only where the segment is wide enough. |
| 149 | + if inp / B > xmax * 0.085: |
| 150 | + ax.text( |
| 151 | + (inp / B) / 2, |
| 152 | + y, |
| 153 | + fmt(inp), |
| 154 | + va="center", |
| 155 | + ha="center", |
| 156 | + color=INK, |
| 157 | + fontweight="bold", |
| 158 | + fontsize=11.5, |
| 159 | + alpha=a, |
| 160 | + zorder=5, |
| 161 | + ) |
| 162 | + if out / B > xmax * 0.085: |
| 163 | + ax.text( |
| 164 | + inp / B + (out / B) / 2, |
| 165 | + y, |
| 166 | + fmt(out), |
| 167 | + va="center", |
| 168 | + ha="center", |
| 169 | + color=INK, |
| 170 | + fontweight="bold", |
| 171 | + fontsize=11.5, |
| 172 | + alpha=a, |
| 173 | + zorder=5, |
| 174 | + ) |
| 175 | + |
| 176 | + # ------------------------------------------------------------- axes ----- |
| 177 | + ax.set_yticks(ypos) |
| 178 | + ax.set_yticklabels(labels, fontsize=12.5) |
| 179 | + is_other_flags = [False] * n + ([True] if other else []) |
| 180 | + for tick, is_other in zip(ax.get_yticklabels(), is_other_flags): |
| 181 | + tick.set_color(SUBTLE if is_other else MODELNAME) |
| 182 | + if is_other: |
| 183 | + tick.set_fontstyle("italic") |
| 184 | + |
| 185 | + ax.set_xlim(0, xmax * 1.13) |
| 186 | + ax.set_ylim(-1.3, n + 0.8) |
| 187 | + |
| 188 | + # Derive ticks from the data so the axis stays sane as totals grow; fmt() |
| 189 | + # promotes B -> T automatically, so the labels never need hand-editing. |
| 190 | + ax.xaxis.set_major_locator(MaxNLocator(nbins=8, steps=[1, 2, 2.5, 5, 10])) |
| 191 | + ax.xaxis.set_major_formatter(FuncFormatter(lambda v, _pos: "0" if v <= 0 else fmt(v * B))) |
| 192 | + ax.tick_params(axis="y", length=0, pad=10) |
| 193 | + ax.tick_params(axis="x", colors=AXIS, length=0, pad=8, labelsize=11) |
| 194 | + ax.set_xlabel("Tokens processed", color=AXIS, fontsize=12.5, labelpad=12) |
| 195 | + |
| 196 | + ax.xaxis.grid(True, color=GRID, alpha=0.07, linewidth=1, zorder=0) |
| 197 | + ax.set_axisbelow(True) |
| 198 | + for s in ("top", "right"): |
| 199 | + ax.spines[s].set_visible(False) |
| 200 | + for s in ("bottom", "left"): |
| 201 | + ax.spines[s].set_color(SPINE) |
| 202 | + ax.spines[s].set_linewidth(1.0) |
| 203 | + |
| 204 | + # ---------------------------------------------------------- titling ----- |
| 205 | + fig.subplots_adjust(left=0.235, right=0.965, top=0.83, bottom=0.085) |
| 206 | + # Signature DD green left-accent rule (mirrors the .devnote-dek element). |
| 207 | + ax.add_patch( |
| 208 | + plt.Rectangle( |
| 209 | + (-0.018, 1.045), |
| 210 | + 0.006, |
| 211 | + 0.135, |
| 212 | + transform=ax.transAxes, |
| 213 | + facecolor=GREEN, |
| 214 | + edgecolor="none", |
| 215 | + clip_on=False, |
| 216 | + zorder=6, |
| 217 | + ) |
| 218 | + ) |
| 219 | + ax.text( |
| 220 | + 0.012, |
| 221 | + 1.145, |
| 222 | + "Top Model Usage", |
| 223 | + transform=ax.transAxes, |
| 224 | + color=WHITE, |
| 225 | + fontweight="bold", |
| 226 | + fontsize=26, |
| 227 | + ha="left", |
| 228 | + va="bottom", |
| 229 | + ) |
| 230 | + ax.text( |
| 231 | + 0.012, |
| 232 | + 1.07, |
| 233 | + "Context vs. generated tokens across the most-used models", |
| 234 | + transform=ax.transAxes, |
| 235 | + color=SUBTLE, |
| 236 | + fontsize=13.5, |
| 237 | + ha="left", |
| 238 | + va="bottom", |
| 239 | + ) |
| 240 | + |
| 241 | + # Manual legend, top-right of the plotting area. |
| 242 | + leg_x, leg_y = 0.99, 1.115 |
| 243 | + legend = [(GREEN, "Input · context tokens"), (LIME, "Output · generated tokens")] |
| 244 | + for i, (c, lbl) in enumerate(legend): |
| 245 | + yy = leg_y - i * 0.052 |
| 246 | + ax.add_patch( |
| 247 | + plt.Rectangle( |
| 248 | + (leg_x - 0.205, yy - 0.012), |
| 249 | + 0.022, |
| 250 | + 0.026, |
| 251 | + transform=ax.transAxes, |
| 252 | + facecolor=c, |
| 253 | + edgecolor="none", |
| 254 | + clip_on=False, |
| 255 | + zorder=6, |
| 256 | + ) |
| 257 | + ) |
| 258 | + ax.text(leg_x - 0.172, yy, lbl, transform=ax.transAxes, color=MODELNAME, fontsize=12, ha="left", va="center") |
| 259 | + |
| 260 | + out_path.parent.mkdir(parents=True, exist_ok=True) |
| 261 | + fig.savefig(out_path, facecolor=BG, dpi=200, bbox_inches="tight", pad_inches=0.25) |
| 262 | + plt.close(fig) |
| 263 | + |
| 264 | + |
| 265 | +def main() -> None: |
| 266 | + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) |
| 267 | + parser.add_argument("--csv", type=Path, default=DEFAULT_CSV, help=f"Telemetry export CSV (default: {DEFAULT_CSV})") |
| 268 | + args = parser.parse_args() |
| 269 | + |
| 270 | + configure_matplotlib() |
| 271 | + rows = load_rows(args.csv) |
| 272 | + |
| 273 | + primary, *mirrors = TARGETS |
| 274 | + render(rows, primary) |
| 275 | + for mirror in mirrors: |
| 276 | + mirror.parent.mkdir(parents=True, exist_ok=True) |
| 277 | + shutil.copyfile(primary, mirror) |
| 278 | + |
| 279 | + for target in TARGETS: |
| 280 | + print(f"wrote {target.relative_to(REPO_ROOT)}") |
| 281 | + |
| 282 | + |
| 283 | +if __name__ == "__main__": |
| 284 | + main() |
0 commit comments