{
  "nbformat": 4,
  "nbformat_minor": 5,
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "name": "python",
      "version": "3.12.14"
    },
    "gavel_analysis": {
      "data_files": [
        "gavel-fresh-trace.json",
        "gavel-weight-summary.json",
        "gavel-source.json"
      ],
      "input_mount": "/work",
      "execution": "saved-data analysis"
    }
  },
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# Gavel: inspect the saved allocation trace\n",
        "\n",
        "This notebook recomputes the two **1:2:3** policy means in [How should AI jobs share different GPUs?](https://aryanputta.com/blog/gavel-trace), then lets you choose a timestep interval and a job. The inputs are the exact values saved in that article at commit `9df3f608a3e7b2e95312504f9d1cbf22e190091f`.\n",
        "\n",
        "Each score is effective throughput divided by that same job's expected rate at one-third time on each of K80, P100, and V100. The saved run used published policy code and measured throughput tables for an allocation-only numerical simulation. This notebook analyzes its outputs; it does not run GPU training or the round-based scheduler.\n",
        "\n",
        "The source and saved outputs are visible before execution. Run all cells to check the numbers, or change the interval and job in the query cell. The article's existing charts remain directly explorable without running Python.\n"
      ],
      "id": "ec6cafa815b7"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 1. Read the three inputs\n",
        "\n",
        "The site's runner mounts these files under `/work`: `gavel-fresh-trace.json`, `gavel-weight-summary.json`, and `gavel-source.json`. Locally, place them beside this notebook and run from that directory. The analysis uses the Python standard library; `IPython.display` renders the SVG outputs. There are no package installs or network calls in the notebook.\n"
      ],
      "id": "d4fe435b39f2"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "from pathlib import Path\n",
        "from collections import defaultdict\n",
        "from statistics import fmean\n",
        "from math import isclose, isfinite, ceil\n",
        "from html import escape\n",
        "import csv\n",
        "import hashlib\n",
        "import json\n",
        "\n",
        "POLICIES = (\"with_perf\", \"agnostic\")\n",
        "LABELS = {\"with_perf\": \"Throughput-aware\", \"agnostic\": \"Performance-agnostic\"}\n",
        "COLORS = {\"with_perf\": \"#7d292e\", \"agnostic\": \"#246a73\"}\n",
        "INPUT_FILES = (\"gavel-fresh-trace.json\", \"gavel-weight-summary.json\", \"gavel-source.json\")\n",
        "\n",
        "from IPython.display import SVG, display\n",
        "\n",
        "def load_inputs(folder):\n",
        "    \"\"\"Read the three mounted inputs and check their recorded content hashes.\"\"\"\n",
        "    folder = Path(folder)\n",
        "    trace, weights, source = [json.loads((folder / name).read_text()) for name in INPUT_FILES]\n",
        "    for name, expected in source[\"data_sha256\"].items():\n",
        "        actual = hashlib.sha256((folder / name).read_bytes()).hexdigest()\n",
        "        assert actual == expected, f\"Input hash changed: {name}\"\n",
        "    return trace, weights, source\n",
        "\n",
        "DATA_DIR = Path('/work') if (Path('/work') / INPUT_FILES[0]).is_file() else Path.cwd()\n",
        "trace, weights, source = load_inputs(DATA_DIR)\n",
        "print('Source article commit:', source['commit'])\n",
        "print('Verified input hashes:', len(source['data_sha256']))\n",
        "print('Saved job values:', sum(len(v['points']) for v in trace['policies'].values()))\n"
      ],
      "execution_count": 1,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Source article commit: 9df3f608a3e7b2e95312504f9d1cbf22e190091f\n",
            "Verified input hashes: 2\n",
            "Saved job values: 1368\n"
          ]
        }
      ],
      "id": "242916de1a94"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 2. Define the aggregation and checks\n",
        "\n",
        "At each timestep, sum the scores of jobs that have arrived. The published bar is the arithmetic mean of those 72 totals, so every timestep receives equal weight. Averaging all 684 job records directly would answer a different question because the number of active jobs changes.\n",
        "\n",
        "The checks verify unique job/timestep pairs, all 18 arrivals, both policy traces, finite nonnegative scores, and the two published means to an absolute tolerance of `1e-10`.\n"
      ],
      "id": "d6ce763dd99f"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "def timestep_totals(trace, policy):\n",
        "    \"\"\"Sum the normalized scores of jobs that have arrived at each timestep.\"\"\"\n",
        "    assert policy in POLICIES\n",
        "    by_step = defaultdict(list)\n",
        "    for p in trace[\"policies\"][policy][\"points\"]:\n",
        "        by_step[p[\"t\"]].append(p[\"fresh\"])\n",
        "    return {t: sum(by_step[t]) for t in range(1, 73)}\n",
        "\n",
        "\n",
        "def interval_summary(trace, start=1, end=72, job_id=None):\n",
        "    \"\"\"Aggregate every selected step; job means include only steps after arrival.\"\"\"\n",
        "    if type(start) is not int or type(end) is not int or not 1 <= start <= end <= 72:\n",
        "        raise ValueError(\"Choose integer timesteps with 1 <= start <= end <= 72.\")\n",
        "    if job_id is not None and (type(job_id) is not int or not 0 <= job_id < 18):\n",
        "        raise ValueError(\"Choose a job ID from 0 to 17, or None for all jobs.\")\n",
        "    result = []\n",
        "    for policy in POLICIES:\n",
        "        if job_id is None:\n",
        "            values = [v for t, v in timestep_totals(trace, policy).items() if start <= t <= end]\n",
        "        else:\n",
        "            values = [p[\"fresh\"] for p in trace[\"policies\"][policy][\"points\"]\n",
        "                      if start <= p[\"t\"] <= end and p[\"job\"] == job_id]\n",
        "        result.append({\"policy\": policy, \"steps_with_values\": len(values),\n",
        "                       \"mean_score\": fmean(values) if values else None,\n",
        "                       \"min_score\": min(values) if values else None,\n",
        "                       \"max_score\": max(values) if values else None})\n",
        "    return result\n",
        "\n",
        "\n",
        "def print_summary(rows):\n",
        "    print(f\"{'Policy':<23} {'Steps':>5} {'Mean':>12} {'Min':>12} {'Max':>12}\")\n",
        "    for r in rows:\n",
        "        nums = [\"not arrived\" if r[k] is None else f\"{r[k]:.6f}\"\n",
        "                for k in (\"mean_score\", \"min_score\", \"max_score\")]\n",
        "        print(f\"{LABELS[r['policy']]:<23} {r['steps_with_values']:>5} \"\n",
        "              + \" \".join(f\"{n:>12}\" for n in nums))\n",
        "\n",
        "\n",
        "def check_trace(trace, weights):\n",
        "    \"\"\"Check the saved trace's dimensions, arrivals, and published baseline means.\"\"\"\n",
        "    assert set(trace[\"policies\"]) == set(POLICIES)\n",
        "    for policy in POLICIES:\n",
        "        points = trace[\"policies\"][policy][\"points\"]\n",
        "        assert len(points) == 684\n",
        "        seen = set()\n",
        "        for p in points:\n",
        "            assert type(p[\"t\"]) is int and 1 <= p[\"t\"] <= 72\n",
        "            assert type(p[\"job\"]) is int and 0 <= p[\"job\"] < 18\n",
        "            assert isinstance(p[\"fresh\"], (float, int)) and isfinite(p[\"fresh\"]) and p[\"fresh\"] >= 0\n",
        "            key = (p[\"t\"], p[\"job\"])\n",
        "            assert key not in seen\n",
        "            seen.add(key)\n",
        "        expected_keys = {(t, job) for job in range(18) for t in range(1 + 4 * job, 73)}\n",
        "        assert seen == expected_keys, \"Missing, duplicate, or pre-arrival job values\"\n",
        "        totals = timestep_totals(trace, policy)\n",
        "        saved = next(r[\"mean_score\"] for r in weights\n",
        "                     if r[\"scenario\"] == \"baseline_1_2_3\" and r[\"policy\"] == policy)\n",
        "        assert isclose(fmean(totals.values()), saved, rel_tol=0, abs_tol=1e-10)\n",
        "    expected_means = {\"with_perf\": 7.750821275892416, \"agnostic\": 6.441239770549949}\n",
        "    for policy, expected in expected_means.items():\n",
        "        assert isclose(fmean(timestep_totals(trace, policy).values()), expected,\n",
        "                       rel_tol=0, abs_tol=1e-10)\n",
        "    assert len(weights) == 4\n",
        "    assert {(r[\"weights\"], r[\"policy\"]) for r in weights} == {\n",
        "        (w, p) for w in (\"1:2:3\", \"1:1:1\") for p in POLICIES}\n",
        "    return True\n",
        "\n",
        "assert check_trace(trace, weights)\n",
        "assert interval_summary(trace, 1, 48, 12)[0]['mean_score'] is None\n",
        "assert interval_summary(trace, 49, 72, 12)[0]['steps_with_values'] == 24\n",
        "print('All numerical and trace-coverage assertions passed.')\n"
      ],
      "execution_count": 2,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "All numerical and trace-coverage assertions passed.\n"
          ]
        }
      ],
      "id": "f684004f3efe"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 3. Recompute the published baseline\n",
        "\n",
        "These are normalized progress scores, not raw tokens per second or GPU utilization. The comparison below describes one saved job sequence and one set of weights.\n"
      ],
      "id": "3057a0298def"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "baseline = interval_summary(trace)\n",
        "print('Weights 1:2:3; all 72 timesteps')\n",
        "print_summary(baseline)\n",
        "means = {row['policy']: row['mean_score'] for row in baseline}\n",
        "aware_gain = 100 * (means['with_perf'] / means['agnostic'] - 1)\n",
        "agnostic_drop = 100 * (1 - means['agnostic'] / means['with_perf'])\n",
        "print(f'\\nThroughput-aware mean is {aware_gain:.3f}% above the agnostic mean.')\n",
        "print(f'Agnostic mean is {agnostic_drop:.3f}% below the throughput-aware mean.')\n"
      ],
      "execution_count": 3,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Weights 1:2:3; all 72 timesteps\n",
            "Policy                  Steps         Mean          Min          Max\n",
            "Throughput-aware           72     7.750821     1.719601    10.459325\n",
            "Performance-agnostic       72     6.441240     1.000000     9.000000\n",
            "\n",
            "Throughput-aware mean is 20.331% above the agnostic mean.\n",
            "Agnostic mean is 16.896% below the throughput-aware mean.\n"
          ]
        }
      ],
      "id": "3f9de44ce09a"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 4. Draw the checked values\n",
        "\n",
        "Both lines use the saved 1:2:3 trace. Each plotted total is computed from its active jobs; no points are interpolated to create missing records. SVG point titles retain the timestep and value.\n"
      ],
      "id": "1a5e8fc0634e"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "def line_svg(series, title, subtitle, start=1, end=72, reference=None):\n",
        "    \"\"\"Create a self-contained SVG from checked (timestep, value) pairs.\"\"\"\n",
        "    width, height, left, right, top, bottom = 900, 430, 76, 28, 98, 74\n",
        "    plot_w, plot_h = width-left-right, height-top-bottom\n",
        "    all_values = [v for pairs in series.values() for _, v in pairs]\n",
        "    ymax = max(1.0, ceil(max(all_values, default=1.0) * 1.12 * 2) / 2)\n",
        "    x = lambda t: left + ((t-start) / (end-start) if end > start else 0.5) * plot_w\n",
        "    y = lambda v: top + (1-v/ymax) * plot_h\n",
        "    chart_id = \"gavel-\" + hashlib.sha256(\n",
        "        json.dumps([title, subtitle, series], sort_keys=True).encode()\n",
        "    ).hexdigest()[:12]\n",
        "    parts = [f'<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"900\" height=\"430\" '\n",
        "             f'viewBox=\"0 0 900 430\" role=\"img\" aria-labelledby=\"{chart_id}-title\" '\n",
        "             f'aria-describedby=\"{chart_id}-description\">',\n",
        "             f'<title id=\"{chart_id}-title\">{escape(title)}</title>',\n",
        "             f'<desc id=\"{chart_id}-description\">{escape(subtitle)}. '\n",
        "             'Scores use each job\\'s equal-time K80, P100, V100 reference. Missing jobs are not drawn.</desc>',\n",
        "             '<rect width=\"900\" height=\"430\" fill=\"white\"/>',\n",
        "             '<g font-family=\"system-ui,Arial,sans-serif\" font-size=\"12\" fill=\"#42484b\">',\n",
        "             f'<text x=\"24\" y=\"30\" font-size=\"20\" font-weight=\"600\">{escape(title)}</text>',\n",
        "             f'<text x=\"24\" y=\"53\" fill=\"#636d72\">{escape(subtitle)}</text>']\n",
        "    for i, policy in enumerate(series):\n",
        "        lx = 24 + 230*i\n",
        "        dash = ' stroke-dasharray=\"6 4\"' if policy == \"agnostic\" else \"\"\n",
        "        parts.append(f'<line x1=\"{lx}\" y1=\"76\" x2=\"{lx+24}\" y2=\"76\" '\n",
        "                     f'stroke=\"{COLORS[policy]}\" stroke-width=\"3\"{dash}/>')\n",
        "        parts.append(f'<text x=\"{lx+32}\" y=\"80\">{LABELS[policy]}</text>')\n",
        "    for i in range(5):\n",
        "        val = ymax*i/4\n",
        "        parts.append(f'<line x1=\"{left}\" y1=\"{y(val):.2f}\" x2=\"{width-right}\" '\n",
        "                     f'y2=\"{y(val):.2f}\" stroke=\"#e5e8e9\"/>')\n",
        "        parts.append(f'<text x=\"{left-12}\" y=\"{y(val)+4:.2f}\" text-anchor=\"end\">{val:.2f}</text>')\n",
        "    ticks = sorted({round(start + (end-start)*i/4) for i in range(5)})\n",
        "    for t in ticks:\n",
        "        parts.append(f'<text x=\"{x(t):.2f}\" y=\"{height-bottom+22}\" text-anchor=\"middle\">{t}</text>')\n",
        "    parts.append(f'<text x=\"{left+plot_w/2}\" y=\"{height-20}\" text-anchor=\"middle\">Simulated timestep</text>')\n",
        "    if reference is not None:\n",
        "        parts.append(f'<line x1=\"{left}\" y1=\"{y(reference):.2f}\" x2=\"{width-right}\" '\n",
        "                     f'y2=\"{y(reference):.2f}\" stroke=\"#72797d\" stroke-dasharray=\"3 4\"/>')\n",
        "    for policy, pairs in series.items():\n",
        "        coords = \" \".join(f\"{x(t):.3f},{y(v):.3f}\" for t, v in pairs)\n",
        "        dash = ' stroke-dasharray=\"6 4\"' if policy == \"agnostic\" else \"\"\n",
        "        parts.append(f'<polyline points=\"{coords}\" fill=\"none\" stroke=\"{COLORS[policy]}\" '\n",
        "                     f'stroke-width=\"2.8\"{dash}/>')\n",
        "        for t, v in pairs:\n",
        "            parts.append(f'<circle cx=\"{x(t):.3f}\" cy=\"{y(v):.3f}\" r=\"2.3\" fill=\"{COLORS[policy]}\">'\n",
        "                         f'<title>{LABELS[policy]}, step {t}: {v:.12g}</title></circle>')\n",
        "    if not all_values:\n",
        "        parts.append(f'<text x=\"450\" y=\"230\" text-anchor=\"middle\">The selected job has not arrived in this interval.</text>')\n",
        "    parts.extend(['</g>', '</svg>'])\n",
        "    return \"\\n\".join(parts)\n",
        "\n",
        "\n",
        "def aggregate_svg(trace, start=1, end=72):\n",
        "    interval_summary(trace, start, end)  # Validate the requested interval.\n",
        "    series = {p: [(t, v) for t, v in timestep_totals(trace, p).items() if start <= t <= end]\n",
        "              for p in POLICIES}\n",
        "    return line_svg(series, \"Aggregate normalized progress\", \n",
        "                    f\"Weights 1:2:3 · sum of active jobs' scores · steps {start}–{end}\", start, end)\n",
        "\n",
        "\n",
        "def job_svg(trace, job_id, start=1, end=72):\n",
        "    interval_summary(trace, start, end, job_id)\n",
        "    series = {policy: [(p[\"t\"], p[\"fresh\"]) for p in trace[\"policies\"][policy][\"points\"]\n",
        "                       if p[\"job\"] == job_id and start <= p[\"t\"] <= end] for policy in POLICIES}\n",
        "    return line_svg(series, f\"Job {job_id}: normalized effective throughput\",\n",
        "                    f\"Weights 1:2:3 · score relative to equal-time reference · steps {start}–{end}\",\n",
        "                    start, end, reference=1.0)\n",
        "\n",
        "display(SVG(aggregate_svg(trace)))"
      ],
      "execution_count": 4,
      "outputs": [
        {
          "output_type": "display_data",
          "data": {
            "text/plain": "<IPython.core.display.SVG object>",
            "image/svg+xml": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"900\" height=\"430\" viewBox=\"0 0 900 430\" role=\"img\" aria-labelledby=\"gavel-72573125b617-title\" aria-describedby=\"gavel-72573125b617-description\">\n<title id=\"gavel-72573125b617-title\">Aggregate normalized progress</title>\n<desc id=\"gavel-72573125b617-description\">Weights 1:2:3 · sum of active jobs' scores · steps 1–72. Scores use each job's equal-time K80, P100, V100 reference. Missing jobs are not drawn.</desc>\n<rect width=\"900\" height=\"430\" fill=\"white\"/>\n<g font-family=\"system-ui,Arial,sans-serif\" font-size=\"12\" fill=\"#42484b\">\n<text x=\"24\" y=\"30\" font-size=\"20\" font-weight=\"600\">Aggregate normalized progress</text>\n<text x=\"24\" y=\"53\" fill=\"#636d72\">Weights 1:2:3 · sum of active jobs' scores · steps 1–72</text>\n<line x1=\"24\" y1=\"76\" x2=\"48\" y2=\"76\" stroke=\"#7d292e\" stroke-width=\"3\"/>\n<text x=\"56\" y=\"80\">Throughput-aware</text>\n<line x1=\"254\" y1=\"76\" x2=\"278\" y2=\"76\" stroke=\"#246a73\" stroke-width=\"3\" stroke-dasharray=\"6 4\"/>\n<text x=\"286\" y=\"80\">Performance-agnostic</text>\n<line x1=\"76\" y1=\"356.00\" x2=\"872\" y2=\"356.00\" stroke=\"#e5e8e9\"/>\n<text x=\"64\" y=\"360.00\" text-anchor=\"end\">0.00</text>\n<line x1=\"76\" y1=\"291.50\" x2=\"872\" y2=\"291.50\" stroke=\"#e5e8e9\"/>\n<text x=\"64\" y=\"295.50\" text-anchor=\"end\">3.00</text>\n<line x1=\"76\" y1=\"227.00\" x2=\"872\" y2=\"227.00\" stroke=\"#e5e8e9\"/>\n<text x=\"64\" y=\"231.00\" text-anchor=\"end\">6.00</text>\n<line x1=\"76\" y1=\"162.50\" x2=\"872\" y2=\"162.50\" stroke=\"#e5e8e9\"/>\n<text x=\"64\" y=\"166.50\" text-anchor=\"end\">9.00</text>\n<line x1=\"76\" y1=\"98.00\" x2=\"872\" y2=\"98.00\" stroke=\"#e5e8e9\"/>\n<text x=\"64\" y=\"102.00\" text-anchor=\"end\">12.00</text>\n<text x=\"76.00\" y=\"378\" text-anchor=\"middle\">1</text>\n<text x=\"277.80\" y=\"378\" text-anchor=\"middle\">19</text>\n<text x=\"468.39\" y=\"378\" text-anchor=\"middle\">36</text>\n<text x=\"670.20\" y=\"378\" text-anchor=\"middle\">54</text>\n<text x=\"872.00\" y=\"378\" text-anchor=\"middle\">72</text>\n<text x=\"474.0\" y=\"410\" text-anchor=\"middle\">Simulated timestep</text>\n<polyline points=\"76.000,319.029 87.211,319.029 98.423,319.029 109.634,319.029 120.845,290.626 132.056,290.626 143.268,290.626 154.479,290.626 165.690,261.624 176.901,261.624 188.113,261.624 199.324,261.624 210.535,229.948 221.746,229.948 232.958,229.948 244.169,229.948 255.380,208.958 266.592,208.958 277.803,208.958 289.014,208.958 300.225,180.920 311.437,180.920 322.648,180.920 333.859,180.920 345.070,247.072 356.282,247.072 367.493,247.072 378.704,247.072 389.915,203.630 401.127,203.630 412.338,203.630 423.549,203.630 434.761,156.803 445.972,156.803 457.183,156.803 468.394,156.803 479.606,157.175 490.817,157.175 502.028,157.175 513.239,157.175 524.451,143.419 535.662,143.419 546.873,143.419 558.085,143.419 569.296,147.259 580.507,147.259 591.718,147.259 602.930,147.259 614.141,160.418 625.352,160.418 636.563,160.418 647.775,160.418 658.986,144.975 670.197,144.975 681.408,144.975 692.620,144.975 703.831,154.795 715.042,154.795 726.254,154.795 737.465,154.795 748.676,138.010 759.887,138.010 771.099,138.010 782.310,138.010 793.521,132.645 804.732,132.645 815.944,132.645 827.155,132.645 838.366,131.125 849.577,131.125 860.789,131.125 872.000,131.125\" fill=\"none\" stroke=\"#7d292e\" stroke-width=\"2.8\"/>\n<circle cx=\"76.000\" cy=\"319.029\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 1: 1.71960132826</title></circle>\n<circle cx=\"87.211\" cy=\"319.029\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 2: 1.71960132826</title></circle>\n<circle cx=\"98.423\" cy=\"319.029\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 3: 1.71960132826</title></circle>\n<circle cx=\"109.634\" cy=\"319.029\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 4: 1.71960132826</title></circle>\n<circle cx=\"120.845\" cy=\"290.626\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 5: 3.04062848724</title></circle>\n<circle cx=\"132.056\" cy=\"290.626\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 6: 3.04062848724</title></circle>\n<circle cx=\"143.268\" cy=\"290.626\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 7: 3.04062848724</title></circle>\n<circle cx=\"154.479\" cy=\"290.626\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 8: 3.04062848724</title></circle>\n<circle cx=\"165.690\" cy=\"261.624\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 9: 4.38955975793</title></circle>\n<circle cx=\"176.901\" cy=\"261.624\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 10: 4.38955975793</title></circle>\n<circle cx=\"188.113\" cy=\"261.624\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 11: 4.38955975793</title></circle>\n<circle cx=\"199.324\" cy=\"261.624\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 12: 4.38955975793</title></circle>\n<circle cx=\"210.535\" cy=\"229.948\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 13: 5.86287735885</title></circle>\n<circle cx=\"221.746\" cy=\"229.948\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 14: 5.86287735885</title></circle>\n<circle cx=\"232.958\" cy=\"229.948\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 15: 5.86287735885</title></circle>\n<circle cx=\"244.169\" cy=\"229.948\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 16: 5.86287735885</title></circle>\n<circle cx=\"255.380\" cy=\"208.958\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 17: 6.83918281288</title></circle>\n<circle cx=\"266.592\" cy=\"208.958\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 18: 6.83918281288</title></circle>\n<circle cx=\"277.803\" cy=\"208.958\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 19: 6.83918281288</title></circle>\n<circle cx=\"289.014\" cy=\"208.958\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 20: 6.83918281288</title></circle>\n<circle cx=\"300.225\" cy=\"180.920\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 21: 8.14323362451</title></circle>\n<circle cx=\"311.437\" cy=\"180.920\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 22: 8.14323362451</title></circle>\n<circle cx=\"322.648\" cy=\"180.920\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 23: 8.14323362451</title></circle>\n<circle cx=\"333.859\" cy=\"180.920\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 24: 8.14323362451</title></circle>\n<circle cx=\"345.070\" cy=\"247.072\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 25: 5.06639714855</title></circle>\n<circle cx=\"356.282\" cy=\"247.072\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 26: 5.06639714855</title></circle>\n<circle cx=\"367.493\" cy=\"247.072\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 27: 5.06639714855</title></circle>\n<circle cx=\"378.704\" cy=\"247.072\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 28: 5.06639714855</title></circle>\n<circle cx=\"389.915\" cy=\"203.630\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 29: 7.08695766918</title></circle>\n<circle cx=\"401.127\" cy=\"203.630\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 30: 7.08695766918</title></circle>\n<circle cx=\"412.338\" cy=\"203.630\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 31: 7.08695766918</title></circle>\n<circle cx=\"423.549\" cy=\"203.630\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 32: 7.08695766918</title></circle>\n<circle cx=\"434.761\" cy=\"156.803\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 33: 9.26496942651</title></circle>\n<circle cx=\"445.972\" cy=\"156.803\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 34: 9.26496942651</title></circle>\n<circle cx=\"457.183\" cy=\"156.803\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 35: 9.26496942651</title></circle>\n<circle cx=\"468.394\" cy=\"156.803\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 36: 9.26496942651</title></circle>\n<circle cx=\"479.606\" cy=\"157.175\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 37: 9.24767309325</title></circle>\n<circle cx=\"490.817\" cy=\"157.175\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 38: 9.24767309325</title></circle>\n<circle cx=\"502.028\" cy=\"157.175\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 39: 9.24767309325</title></circle>\n<circle cx=\"513.239\" cy=\"157.175\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 40: 9.24767309325</title></circle>\n<circle cx=\"524.451\" cy=\"143.419\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 41: 9.88746719265</title></circle>\n<circle cx=\"535.662\" cy=\"143.419\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 42: 9.88746719265</title></circle>\n<circle cx=\"546.873\" cy=\"143.419\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 43: 9.88746719265</title></circle>\n<circle cx=\"558.085\" cy=\"143.419\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 44: 9.88746719265</title></circle>\n<circle cx=\"569.296\" cy=\"147.259\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 45: 9.70889019572</title></circle>\n<circle cx=\"580.507\" cy=\"147.259\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 46: 9.70889019572</title></circle>\n<circle cx=\"591.718\" cy=\"147.259\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 47: 9.70889019572</title></circle>\n<circle cx=\"602.930\" cy=\"147.259\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 48: 9.70889019572</title></circle>\n<circle cx=\"614.141\" cy=\"160.418\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 49: 9.09683758832</title></circle>\n<circle cx=\"625.352\" cy=\"160.418\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 50: 9.09683758832</title></circle>\n<circle cx=\"636.563\" cy=\"160.418\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 51: 9.09683758832</title></circle>\n<circle cx=\"647.775\" cy=\"160.418\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 52: 9.09683758832</title></circle>\n<circle cx=\"658.986\" cy=\"144.975\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 53: 9.81513070055</title></circle>\n<circle cx=\"670.197\" cy=\"144.975\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 54: 9.81513070055</title></circle>\n<circle cx=\"681.408\" cy=\"144.975\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 55: 9.81513070055</title></circle>\n<circle cx=\"692.620\" cy=\"144.975\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 56: 9.81513070055</title></circle>\n<circle cx=\"703.831\" cy=\"154.795\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 57: 9.35839273685</title></circle>\n<circle cx=\"715.042\" cy=\"154.795\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 58: 9.35839273685</title></circle>\n<circle cx=\"726.254\" cy=\"154.795\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 59: 9.35839273685</title></circle>\n<circle cx=\"737.465\" cy=\"154.795\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 60: 9.35839273685</title></circle>\n<circle cx=\"748.676\" cy=\"138.010\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 61: 10.1390529893</title></circle>\n<circle cx=\"759.887\" cy=\"138.010\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 62: 10.1390529893</title></circle>\n<circle cx=\"771.099\" cy=\"138.010\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 63: 10.1390529893</title></circle>\n<circle cx=\"782.310\" cy=\"138.010\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 64: 10.1390529893</title></circle>\n<circle cx=\"793.521\" cy=\"132.645\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 65: 10.3886058344</title></circle>\n<circle cx=\"804.732\" cy=\"132.645\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 66: 10.3886058344</title></circle>\n<circle cx=\"815.944\" cy=\"132.645\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 67: 10.3886058344</title></circle>\n<circle cx=\"827.155\" cy=\"132.645\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 68: 10.3886058344</title></circle>\n<circle cx=\"838.366\" cy=\"131.125\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 69: 10.4593250211</title></circle>\n<circle cx=\"849.577\" cy=\"131.125\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 70: 10.4593250211</title></circle>\n<circle cx=\"860.789\" cy=\"131.125\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 71: 10.4593250211</title></circle>\n<circle cx=\"872.000\" cy=\"131.125\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 72: 10.4593250211</title></circle>\n<polyline points=\"76.000,334.500 87.211,334.500 98.423,334.500 109.634,334.500 120.845,313.000 132.056,313.000 143.268,313.000 154.479,313.000 165.690,291.500 176.901,291.500 188.113,291.500 199.324,291.500 210.535,270.000 221.746,270.000 232.958,270.000 244.169,270.000 255.380,254.586 266.592,254.586 277.803,254.586 289.014,254.586 300.225,233.225 311.437,233.225 322.648,233.225 333.859,233.225 345.070,251.996 356.282,251.996 367.493,251.996 378.704,251.996 389.915,231.864 401.127,231.864 412.338,231.864 423.549,231.864 434.761,209.555 445.972,209.555 457.183,209.555 468.394,209.555 479.606,186.109 490.817,186.109 502.028,186.109 513.239,186.109 524.451,166.348 535.662,166.348 546.873,166.348 558.085,166.348 569.296,162.500 580.507,162.500 591.718,162.500 602.930,162.500 614.141,187.083 625.352,187.083 636.563,187.083 647.775,187.083 658.986,172.974 670.197,172.974 681.408,172.974 692.620,172.974 703.831,162.500 715.042,162.500 726.254,162.500 737.465,162.500 748.676,162.500 759.887,162.500 771.099,162.500 782.310,162.500 793.521,162.500 804.732,162.500 815.944,162.500 827.155,162.500 838.366,162.500 849.577,162.500 860.789,162.500 872.000,162.500\" fill=\"none\" stroke=\"#246a73\" stroke-width=\"2.8\" stroke-dasharray=\"6 4\"/>\n<circle cx=\"76.000\" cy=\"334.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 1: 0.999999999105</title></circle>\n<circle cx=\"87.211\" cy=\"334.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 2: 0.999999999105</title></circle>\n<circle cx=\"98.423\" cy=\"334.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 3: 0.999999999105</title></circle>\n<circle cx=\"109.634\" cy=\"334.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 4: 0.999999999105</title></circle>\n<circle cx=\"120.845\" cy=\"313.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 5: 1.99999999951</title></circle>\n<circle cx=\"132.056\" cy=\"313.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 6: 1.99999999951</title></circle>\n<circle cx=\"143.268\" cy=\"313.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 7: 1.99999999951</title></circle>\n<circle cx=\"154.479\" cy=\"313.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 8: 1.99999999951</title></circle>\n<circle cx=\"165.690\" cy=\"291.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 9: 2.99999999999</title></circle>\n<circle cx=\"176.901\" cy=\"291.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 10: 2.99999999999</title></circle>\n<circle cx=\"188.113\" cy=\"291.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 11: 2.99999999999</title></circle>\n<circle cx=\"199.324\" cy=\"291.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 12: 2.99999999999</title></circle>\n<circle cx=\"210.535\" cy=\"270.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 13: 3.99999999999</title></circle>\n<circle cx=\"221.746\" cy=\"270.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 14: 3.99999999999</title></circle>\n<circle cx=\"232.958\" cy=\"270.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 15: 3.99999999999</title></circle>\n<circle cx=\"244.169\" cy=\"270.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 16: 3.99999999999</title></circle>\n<circle cx=\"255.380\" cy=\"254.586\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 17: 4.71694785323</title></circle>\n<circle cx=\"266.592\" cy=\"254.586\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 18: 4.71694785323</title></circle>\n<circle cx=\"277.803\" cy=\"254.586\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 19: 4.71694785323</title></circle>\n<circle cx=\"289.014\" cy=\"254.586\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 20: 4.71694785323</title></circle>\n<circle cx=\"300.225\" cy=\"233.225\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 21: 5.71046134838</title></circle>\n<circle cx=\"311.437\" cy=\"233.225\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 22: 5.71046134838</title></circle>\n<circle cx=\"322.648\" cy=\"233.225\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 23: 5.71046134838</title></circle>\n<circle cx=\"333.859\" cy=\"233.225\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 24: 5.71046134838</title></circle>\n<circle cx=\"345.070\" cy=\"251.996\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 25: 4.83741298679</title></circle>\n<circle cx=\"356.282\" cy=\"251.996\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 26: 4.83741298679</title></circle>\n<circle cx=\"367.493\" cy=\"251.996\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 27: 4.83741298679</title></circle>\n<circle cx=\"378.704\" cy=\"251.996\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 28: 4.83741298679</title></circle>\n<circle cx=\"389.915\" cy=\"231.864\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 29: 5.77377515404</title></circle>\n<circle cx=\"401.127\" cy=\"231.864\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 30: 5.77377515404</title></circle>\n<circle cx=\"412.338\" cy=\"231.864\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 31: 5.77377515404</title></circle>\n<circle cx=\"423.549\" cy=\"231.864\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 32: 5.77377515404</title></circle>\n<circle cx=\"434.761\" cy=\"209.555\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 33: 6.81137268192</title></circle>\n<circle cx=\"445.972\" cy=\"209.555\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 34: 6.81137268192</title></circle>\n<circle cx=\"457.183\" cy=\"209.555\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 35: 6.81137268192</title></circle>\n<circle cx=\"468.394\" cy=\"209.555\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 36: 6.81137268192</title></circle>\n<circle cx=\"479.606\" cy=\"186.109\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 37: 7.90188712294</title></circle>\n<circle cx=\"490.817\" cy=\"186.109\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 38: 7.90188712294</title></circle>\n<circle cx=\"502.028\" cy=\"186.109\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 39: 7.90188712294</title></circle>\n<circle cx=\"513.239\" cy=\"186.109\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 40: 7.90188712294</title></circle>\n<circle cx=\"524.451\" cy=\"166.348\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 41: 8.82103164347</title></circle>\n<circle cx=\"535.662\" cy=\"166.348\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 42: 8.82103164347</title></circle>\n<circle cx=\"546.873\" cy=\"166.348\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 43: 8.82103164347</title></circle>\n<circle cx=\"558.085\" cy=\"166.348\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 44: 8.82103164347</title></circle>\n<circle cx=\"569.296\" cy=\"162.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 45: 8.99999999996</title></circle>\n<circle cx=\"580.507\" cy=\"162.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 46: 8.99999999996</title></circle>\n<circle cx=\"591.718\" cy=\"162.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 47: 8.99999999996</title></circle>\n<circle cx=\"602.930\" cy=\"162.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 48: 8.99999999996</title></circle>\n<circle cx=\"614.141\" cy=\"187.083\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 49: 7.85658788236</title></circle>\n<circle cx=\"625.352\" cy=\"187.083\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 50: 7.85658788236</title></circle>\n<circle cx=\"636.563\" cy=\"187.083\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 51: 7.85658788236</title></circle>\n<circle cx=\"647.775\" cy=\"187.083\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 52: 7.85658788236</title></circle>\n<circle cx=\"658.986\" cy=\"172.974\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 53: 8.51283919801</title></circle>\n<circle cx=\"670.197\" cy=\"172.974\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 54: 8.51283919801</title></circle>\n<circle cx=\"681.408\" cy=\"172.974\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 55: 8.51283919801</title></circle>\n<circle cx=\"692.620\" cy=\"172.974\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 56: 8.51283919801</title></circle>\n<circle cx=\"703.831\" cy=\"162.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 57: 9.00000000003</title></circle>\n<circle cx=\"715.042\" cy=\"162.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 58: 9.00000000003</title></circle>\n<circle cx=\"726.254\" cy=\"162.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 59: 9.00000000003</title></circle>\n<circle cx=\"737.465\" cy=\"162.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 60: 9.00000000003</title></circle>\n<circle cx=\"748.676\" cy=\"162.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 61: 9.00000000043</title></circle>\n<circle cx=\"759.887\" cy=\"162.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 62: 9.00000000043</title></circle>\n<circle cx=\"771.099\" cy=\"162.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 63: 9.00000000043</title></circle>\n<circle cx=\"782.310\" cy=\"162.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 64: 9.00000000043</title></circle>\n<circle cx=\"793.521\" cy=\"162.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 65: 8.99999999985</title></circle>\n<circle cx=\"804.732\" cy=\"162.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 66: 8.99999999985</title></circle>\n<circle cx=\"815.944\" cy=\"162.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 67: 8.99999999985</title></circle>\n<circle cx=\"827.155\" cy=\"162.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 68: 8.99999999985</title></circle>\n<circle cx=\"838.366\" cy=\"162.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 69: 8.99999999989</title></circle>\n<circle cx=\"849.577\" cy=\"162.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 70: 8.99999999989</title></circle>\n<circle cx=\"860.789\" cy=\"162.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 71: 8.99999999989</title></circle>\n<circle cx=\"872.000\" cy=\"162.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 72: 8.99999999989</title></circle>\n</g>\n</svg>"
          },
          "metadata": {}
        }
      ],
      "id": "ac3ecd55c98b"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 5. Query a different interval or job\n",
        "\n",
        "Edit `START_STEP`, `END_STEP`, or `JOB_ID`, then run this cell. Job IDs run from 0 to 17. A job's mean includes only the selected steps after its arrival; a pre-arrival interval reports “not arrived” instead of treating an absent job as a zero-throughput job. The aggregate mean still includes every selected timestep.\n"
      ],
      "id": "962e9a48d2b6"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "START_STEP = 49\n",
        "END_STEP = 72\n",
        "JOB_ID = 12\n",
        "\n",
        "print(f'All active jobs, steps {START_STEP}–{END_STEP}:')\n",
        "print_summary(interval_summary(trace, START_STEP, END_STEP))\n",
        "print(f'\\nJob {JOB_ID}, selected steps after arrival only:')\n",
        "print_summary(interval_summary(trace, START_STEP, END_STEP, JOB_ID))\n",
        "display(SVG(job_svg(trace, JOB_ID, START_STEP, END_STEP)))\n"
      ],
      "execution_count": 5,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "All active jobs, steps 49–72:\n",
            "Policy                  Steps         Mean          Min          Max\n",
            "Throughput-aware           24     9.876224     9.096838    10.459325\n",
            "Performance-agnostic       24     8.728238     7.856588     9.000000\n",
            "\n",
            "Job 12, selected steps after arrival only:\n",
            "Policy                  Steps         Mean          Min          Max\n",
            "Throughput-aware           24     1.130766     0.581074     1.527814\n",
            "Performance-agnostic       24     0.888889     0.500000     1.000000\n"
          ]
        },
        {
          "output_type": "display_data",
          "data": {
            "text/plain": "<IPython.core.display.SVG object>",
            "image/svg+xml": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"900\" height=\"430\" viewBox=\"0 0 900 430\" role=\"img\" aria-labelledby=\"gavel-cdf1b45db753-title\" aria-describedby=\"gavel-cdf1b45db753-description\">\n<title id=\"gavel-cdf1b45db753-title\">Job 12: normalized effective throughput</title>\n<desc id=\"gavel-cdf1b45db753-description\">Weights 1:2:3 · score relative to equal-time reference · steps 49–72. Scores use each job's equal-time K80, P100, V100 reference. Missing jobs are not drawn.</desc>\n<rect width=\"900\" height=\"430\" fill=\"white\"/>\n<g font-family=\"system-ui,Arial,sans-serif\" font-size=\"12\" fill=\"#42484b\">\n<text x=\"24\" y=\"30\" font-size=\"20\" font-weight=\"600\">Job 12: normalized effective throughput</text>\n<text x=\"24\" y=\"53\" fill=\"#636d72\">Weights 1:2:3 · score relative to equal-time reference · steps 49–72</text>\n<line x1=\"24\" y1=\"76\" x2=\"48\" y2=\"76\" stroke=\"#7d292e\" stroke-width=\"3\"/>\n<text x=\"56\" y=\"80\">Throughput-aware</text>\n<line x1=\"254\" y1=\"76\" x2=\"278\" y2=\"76\" stroke=\"#246a73\" stroke-width=\"3\" stroke-dasharray=\"6 4\"/>\n<text x=\"286\" y=\"80\">Performance-agnostic</text>\n<line x1=\"76\" y1=\"356.00\" x2=\"872\" y2=\"356.00\" stroke=\"#e5e8e9\"/>\n<text x=\"64\" y=\"360.00\" text-anchor=\"end\">0.00</text>\n<line x1=\"76\" y1=\"291.50\" x2=\"872\" y2=\"291.50\" stroke=\"#e5e8e9\"/>\n<text x=\"64\" y=\"295.50\" text-anchor=\"end\">0.50</text>\n<line x1=\"76\" y1=\"227.00\" x2=\"872\" y2=\"227.00\" stroke=\"#e5e8e9\"/>\n<text x=\"64\" y=\"231.00\" text-anchor=\"end\">1.00</text>\n<line x1=\"76\" y1=\"162.50\" x2=\"872\" y2=\"162.50\" stroke=\"#e5e8e9\"/>\n<text x=\"64\" y=\"166.50\" text-anchor=\"end\">1.50</text>\n<line x1=\"76\" y1=\"98.00\" x2=\"872\" y2=\"98.00\" stroke=\"#e5e8e9\"/>\n<text x=\"64\" y=\"102.00\" text-anchor=\"end\">2.00</text>\n<text x=\"76.00\" y=\"378\" text-anchor=\"middle\">49</text>\n<text x=\"283.65\" y=\"378\" text-anchor=\"middle\">55</text>\n<text x=\"456.70\" y=\"378\" text-anchor=\"middle\">60</text>\n<text x=\"664.35\" y=\"378\" text-anchor=\"middle\">66</text>\n<text x=\"872.00\" y=\"378\" text-anchor=\"middle\">72</text>\n<text x=\"474.0\" y=\"410\" text-anchor=\"middle\">Simulated timestep</text>\n<line x1=\"76\" y1=\"227.00\" x2=\"872\" y2=\"227.00\" stroke=\"#72797d\" stroke-dasharray=\"3 4\"/>\n<polyline points=\"76.000,158.912 110.609,158.912 145.217,158.912 179.826,158.912 214.435,158.912 249.043,158.912 283.652,158.912 318.261,158.912 352.870,158.912 387.478,158.912 422.087,158.912 456.696,158.912 491.304,242.733 525.913,242.733 560.522,242.733 595.130,242.733 629.739,260.276 664.348,260.276 698.957,260.276 733.565,260.276 768.174,281.042 802.783,281.042 837.391,281.042 872.000,281.042\" fill=\"none\" stroke=\"#7d292e\" stroke-width=\"2.8\"/>\n<circle cx=\"76.000\" cy=\"158.912\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 49: 1.52781402795</title></circle>\n<circle cx=\"110.609\" cy=\"158.912\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 50: 1.52781402795</title></circle>\n<circle cx=\"145.217\" cy=\"158.912\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 51: 1.52781402795</title></circle>\n<circle cx=\"179.826\" cy=\"158.912\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 52: 1.52781402795</title></circle>\n<circle cx=\"214.435\" cy=\"158.912\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 53: 1.52781402795</title></circle>\n<circle cx=\"249.043\" cy=\"158.912\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 54: 1.52781402795</title></circle>\n<circle cx=\"283.652\" cy=\"158.912\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 55: 1.52781402795</title></circle>\n<circle cx=\"318.261\" cy=\"158.912\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 56: 1.52781402795</title></circle>\n<circle cx=\"352.870\" cy=\"158.912\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 57: 1.52781402795</title></circle>\n<circle cx=\"387.478\" cy=\"158.912\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 58: 1.52781402795</title></circle>\n<circle cx=\"422.087\" cy=\"158.912\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 59: 1.52781402795</title></circle>\n<circle cx=\"456.696\" cy=\"158.912\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 60: 1.52781402795</title></circle>\n<circle cx=\"491.304\" cy=\"242.733\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 61: 0.878037322916</title></circle>\n<circle cx=\"525.913\" cy=\"242.733\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 62: 0.878037322916</title></circle>\n<circle cx=\"560.522\" cy=\"242.733\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 63: 0.878037322916</title></circle>\n<circle cx=\"595.130\" cy=\"242.733\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 64: 0.878037322916</title></circle>\n<circle cx=\"629.739\" cy=\"260.276\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 65: 0.742043273885</title></circle>\n<circle cx=\"664.348\" cy=\"260.276\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 66: 0.742043273885</title></circle>\n<circle cx=\"698.957\" cy=\"260.276\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 67: 0.742043273885</title></circle>\n<circle cx=\"733.565\" cy=\"260.276\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 68: 0.742043273885</title></circle>\n<circle cx=\"768.174\" cy=\"281.042\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 69: 0.581073612282</title></circle>\n<circle cx=\"802.783\" cy=\"281.042\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 70: 0.581073612282</title></circle>\n<circle cx=\"837.391\" cy=\"281.042\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 71: 0.581073612282</title></circle>\n<circle cx=\"872.000\" cy=\"281.042\" r=\"2.3\" fill=\"#7d292e\"><title>Throughput-aware, step 72: 0.581073612282</title></circle>\n<polyline points=\"76.000,227.000 110.609,227.000 145.217,227.000 179.826,227.000 214.435,227.000 249.043,227.000 283.652,227.000 318.261,227.000 352.870,227.000 387.478,227.000 422.087,227.000 456.696,227.000 491.304,227.000 525.913,227.000 560.522,227.000 595.130,227.000 629.739,248.500 664.348,248.500 698.957,248.500 733.565,248.500 768.174,291.500 802.783,291.500 837.391,291.500 872.000,291.500\" fill=\"none\" stroke=\"#246a73\" stroke-width=\"2.8\" stroke-dasharray=\"6 4\"/>\n<circle cx=\"76.000\" cy=\"227.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 49: 0.999999999922</title></circle>\n<circle cx=\"110.609\" cy=\"227.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 50: 0.999999999922</title></circle>\n<circle cx=\"145.217\" cy=\"227.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 51: 0.999999999922</title></circle>\n<circle cx=\"179.826\" cy=\"227.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 52: 0.999999999922</title></circle>\n<circle cx=\"214.435\" cy=\"227.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 53: 1.00000000013</title></circle>\n<circle cx=\"249.043\" cy=\"227.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 54: 1.00000000013</title></circle>\n<circle cx=\"283.652\" cy=\"227.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 55: 1.00000000013</title></circle>\n<circle cx=\"318.261\" cy=\"227.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 56: 1.00000000013</title></circle>\n<circle cx=\"352.870\" cy=\"227.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 57: 0.999999999674</title></circle>\n<circle cx=\"387.478\" cy=\"227.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 58: 0.999999999674</title></circle>\n<circle cx=\"422.087\" cy=\"227.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 59: 0.999999999674</title></circle>\n<circle cx=\"456.696\" cy=\"227.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 60: 0.999999999674</title></circle>\n<circle cx=\"491.304\" cy=\"227.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 61: 1.00000000019</title></circle>\n<circle cx=\"525.913\" cy=\"227.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 62: 1.00000000019</title></circle>\n<circle cx=\"560.522\" cy=\"227.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 63: 1.00000000019</title></circle>\n<circle cx=\"595.130\" cy=\"227.000\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 64: 1.00000000019</title></circle>\n<circle cx=\"629.739\" cy=\"248.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 65: 0.833333333341</title></circle>\n<circle cx=\"664.348\" cy=\"248.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 66: 0.833333333341</title></circle>\n<circle cx=\"698.957\" cy=\"248.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 67: 0.833333333341</title></circle>\n<circle cx=\"733.565\" cy=\"248.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 68: 0.833333333341</title></circle>\n<circle cx=\"768.174\" cy=\"291.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 69: 0.499999999968</title></circle>\n<circle cx=\"802.783\" cy=\"291.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 70: 0.499999999968</title></circle>\n<circle cx=\"837.391\" cy=\"291.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 71: 0.499999999968</title></circle>\n<circle cx=\"872.000\" cy=\"291.500\" r=\"2.3\" fill=\"#246a73\"><title>Performance-agnostic, step 72: 0.499999999968</title></circle>\n</g>\n</svg>"
          },
          "metadata": {}
        }
      ],
      "id": "089f9c5b29f6"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 6. Compare the two saved weight scenarios\n",
        "\n",
        "The article also saves four means for weights **1:2:3** and **1:1:1**. The 1:2:3 means were independently recomputed above. The article contains only summary values for 1:1:1, so this cell compares those saved means without claiming to rerun that trace or solve new weights.\n",
        "\n",
        "Changing weights changes the allocation objective. These differences describe this workload response; they do not establish a general policy or fairness ranking. Target-share distances and raw GPU allocations are not inputs to this notebook.\n"
      ],
      "id": "8b95cf00beac"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "def weight_comparison(weights):\n",
        "    \"\"\"Compare only the two scenarios with published saved summary values.\"\"\"\n",
        "    result = []\n",
        "    for policy in POLICIES:\n",
        "        values = {r[\"weights\"]: r[\"mean_score\"] for r in weights if r[\"policy\"] == policy}\n",
        "        result.append({\"policy\": policy, \"baseline_mean\": values[\"1:2:3\"],\n",
        "                       \"uniform_saved_mean\": values[\"1:1:1\"],\n",
        "                       \"change_percent\": 100*(values[\"1:1:1\"]/values[\"1:2:3\"]-1)})\n",
        "    return result\n",
        "\n",
        "print(f\"{'Policy':<23} {'1:2:3 recomputed':>17} {'1:1:1 saved':>15} {'Change':>10}\")\n",
        "for row in weight_comparison(weights):\n",
        "    assert isclose(row['baseline_mean'], means[row['policy']], rel_tol=0, abs_tol=1e-10)\n",
        "    print(f\"{LABELS[row['policy']]:<23} {row['baseline_mean']:17.9f} \"\n",
        "          f\"{row['uniform_saved_mean']:15.9f} {row['change_percent']:9.3f}%\")\n"
      ],
      "execution_count": 6,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Policy                   1:2:3 recomputed     1:1:1 saved     Change\n",
            "Throughput-aware              7.750821276     7.921449236     2.201%\n",
            "Performance-agnostic          6.441239771     6.601999472     2.496%\n"
          ]
        }
      ],
      "id": "33c337cdfb8d"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Source trail\n",
        "\n",
        "- **Public references:** [Live article](https://aryanputta.com/blog/gavel-trace), [Gavel paper, OSDI 2020](https://www.usenix.org/system/files/osdi20-narayanan_deepak.pdf), and [Gavel source code](https://github.com/stanford-futuredata/gavel).\n",
        "- **Exact input source:** `script#gavel-data` in the saved `source/gavel-trace.html`. Its Git blob SHA-1 is `572f09dc5a6399e5f80254791f0acbc1744798ba`.\n",
        "- **Trace coverage:** 684 active-job records per policy, 1,368 total; 72 timesteps; 18 jobs. The full 1:2:3 series supports interval and job selection. The 1:1:1 series is not present in the article's embedded data.\n",
        "- **Preserved reproduction:** [Download the original source, allocation data, and diagnostics](https://aryanputta.com/blog/assets/gavel-reproducibility.zip). This small analysis package supplements that download.\n",
        "- **Portable checks:** `analysis.py` regenerates the CSV tables and SVG figures with Python's standard library. `validate.py` checks the exact extraction, source hashes, numerical assertions, figures, and executed notebook. `manifest.json` records the inputs and artifact hashes.\n"
      ],
      "id": "1a4004190887"
    }
  ]
}
