SIMPLE / index.html
AndreP-git
refactor
1981953
Raw History Blame Contribute Delete
19.5 kB
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SIMPLE Law — Energy-per-Token Demo</title>
<script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js"></script>
<style>
/* Fluid base size: every rem below scales with the viewport width
(14px floor on phones, 18px cap on large screens) */
html {
font-size: clamp(14px, 0.75rem + 0.35vw, 18px);
}
/* Content column: grows with the screen but keeps empty space on both
sides (at least 6% of the width per side, and never wider than 1280px) */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
color: #1d1d1b;
background: white;
max-width: 1280px;
margin: 0 auto;
padding: 24px clamp(16px, 6vw, 120px);
overflow-wrap: break-word;
}
:root {
--accent: #1a9850;
--accent-soft: #e8f5ec;
--border: #e2e2e0;
--muted: #6b6b68;
--surface: #fafaf9;
}
.card {
background: white;
border: 1px solid white;
border-radius: 14px;
padding: clamp(18px, 2.2vw, 32px) clamp(20px, 3vw, 44px);
margin-top: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}
.cardHeader {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.cardHeader h3 {
margin: 0;
font-size: 1.0625rem;
}
.status {
font-size: 0.8125rem;
padding: 4px 12px;
border-radius: 999px;
border: 1px solid transparent;
overflow-wrap: anywhere;
}
.status:empty {
display: none;
}
.status.ok {
background: var(--accent-soft);
color: #146c3a;
border-color: #bfe3cb;
}
.status.loading,
.status.custom {
background: #f1f1ef;
color: var(--muted);
border-color: var(--border);
}
.status.error {
background: #fdecea;
color: #a52a1f;
border-color: #f5c6c0;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
}
.field label,
.sliderHeader label {
font-size: 0.8125rem;
font-weight: 600;
color: #333;
}
.hint {
font-weight: 400;
color: var(--muted);
margin-left: 4px;
}
.card select,
.card input[type="text"],
.card input[type="number"] {
font: inherit;
font-size: 0.875rem;
height: 38px;
padding: 0 12px;
border: 1px solid #cfcfcc;
border-radius: 8px;
background: white;
width: 100%;
box-sizing: border-box;
transition: border-color 0.15s, box-shadow 0.15s;
}
.card select:focus,
.card input[type="text"]:focus,
.card input[type="number"]:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(26, 152, 80, 0.15);
}
.sources {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1.4fr);
gap: 16px;
align-items: end;
margin-top: 18px;
}
.divider {
color: var(--muted);
font-size: 0.8125rem;
height: 38px;
display: flex;
align-items: center;
}
.inputGroup {
display: flex;
}
.inputGroup input[type="text"] {
border-radius: 8px 0 0 8px;
}
.inputGroup button {
font: inherit;
font-size: 0.875rem;
font-weight: 600;
height: 38px;
padding: 0 18px;
border: 1px solid var(--accent);
border-radius: 0 8px 8px 0;
background: var(--accent);
color: white;
cursor: pointer;
margin-left: -1px;
}
.inputGroup button:hover {
background: #137a40;
}
.sectionLabel {
margin: 22px 0 10px;
font-size: 0.75rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
}
.archGrid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px;
}
#archSummary {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.chip {
font-size: 0.75rem;
padding: 3px 10px;
border-radius: 999px;
background: white;
border: 1px solid var(--border);
color: #444;
}
@media (max-width: 700px) {
.sources {
grid-template-columns: 1fr;
}
.divider {
height: auto;
}
.archGrid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
/* Input-token slider */
#ninWrapper {
width: 100%;
margin-bottom: 22px;
padding-bottom: 18px;
border-bottom: 1px solid var(--border);
}
.sliderHeader {
display: flex;
align-items: center;
gap: 10px;
}
.badge {
font-size: 0.8125rem;
font-weight: 600;
font-variant-numeric: tabular-nums;
padding: 2px 10px;
border-radius: 6px;
background: var(--accent-soft);
color: #146c3a;
}
#ninSlider {
width: 100%;
display: block;
margin-top: 10px;
accent-color: var(--accent);
}
#ninLabels {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
color: var(--muted);
margin-top: 3px;
}
/* Plots side by side; the component plot is the narrower one */
#plots {
display: flex;
gap: 20px;
flex-wrap: wrap;
margin-top: 20px;
}
#plotPanel {
flex: 2 1 380px;
min-width: 0;
}
#contourPanel {
flex: 3 1 520px;
min-width: 0;
}
/* ---------- Header ---------- */
.hero {
margin: 8px 0 24px;
}
.eyebrow {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--accent);
}
.hero h1 {
font-size: 1.875rem;
line-height: 1.2;
margin: 6px 0 8px;
letter-spacing: -0.01em;
}
.lede {
margin: 0;
color: var(--muted);
font-size: 0.9375rem;
line-height: 1.5;
}
/* ---------- Formula + thetas ---------- */
:root {
--c-prefill: #55A868;
--c-kvcache: #E2C044;
--c-nexttok: #DD8452;
}
.modelGrid {
display: grid;
/* Theta card as wide as its one-line rows; the formula takes the rest */
grid-template-columns: minmax(0, 1fr) auto;
gap: 20px;
align-items: stretch;
}
.modelGrid .card {
margin-top: 0;
display: flex;
flex-direction: column;
}
.card h3 {
margin: 0 0 16px;
font-size: 1.0625rem;
}
/* Always a single line: everything inside is sized in em, and
fitFormula() in app.js lowers font-size when the line would overflow */
.formula {
/* Centered vertically in the card, but only as tall as its content,
so the term boxes do not stretch to the card height */
margin: auto 0;
display: flex;
align-items: center;
flex-wrap: nowrap;
gap: 0.55em;
font-size: 1.125rem;
white-space: nowrap;
overflow: hidden;
}
.formula > * {
flex-shrink: 0;
}
.plus {
font-size: 1.11em;
color: var(--muted);
}
.term {
align-self: stretch;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
gap: 0.33em;
padding: 0.55em 0.78em 0.45em;
border-radius: 0.55em;
border: 1.5px solid var(--c);
background-color: color-mix(in srgb, var(--c) 16%, white);
}
/* Same colors and hatches as the stacked plot (x, /, -) */
.prefill { --c: var(--c-prefill); }
.kvcache { --c: var(--c-kvcache); }
.nexttok { --c: var(--c-nexttok); }
.term.prefill {
background-image:
repeating-linear-gradient(45deg, rgba(0,0,0,0.05) 0 1px, transparent 1px 9px),
repeating-linear-gradient(-45deg, rgba(0,0,0,0.05) 0 1px, transparent 1px 9px);
}
.term.kvcache {
background-image:
repeating-linear-gradient(-45deg, rgba(0,0,0,0.06) 0 1px, transparent 1px 9px);
}
.term.nexttok {
background-image:
repeating-linear-gradient(0deg, rgba(0,0,0,0.06) 0 1px, transparent 1px 9px);
}
.termLabel {
font-size: 0.61em;
font-weight: 600;
letter-spacing: 0.02em;
color: color-mix(in srgb, var(--c) 55%, black);
text-align: center;
}
.symbols {
display: flex;
flex-wrap: wrap;
gap: 8px 20px;
margin: 18px 0 0;
padding-top: 14px;
border-top: 1px solid var(--border);
font-size: 0.8125rem;
color: var(--muted);
}
.symbols span .katex {
color: #1d1d1b;
}
#thetaTable {
width: 100%;
border-collapse: collapse;
}
#thetaTable td {
padding: 9px 6px;
border-bottom: 1px solid var(--border);
white-space: nowrap;
}
#thetaTable tr:last-child td {
border-bottom: none;
}
#thetaTable td:nth-child(2) {
font-size: 0.9375rem;
}
#thetaTable .termName {
font-size: 0.75rem;
color: color-mix(in srgb, var(--c) 55%, black);
text-align: right;
padding-left: 16px;
}
.dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 3px;
background: var(--c);
margin-right: 8px;
vertical-align: middle;
}
.note {
margin: 12px 0 0;
font-size: 0.75rem;
color: var(--muted);
}
/* ---------- About ---------- */
.about {
margin: 32px 0 12px;
}
.about p {
line-height: 1.6;
}
.about a {
color: #146c3a;
}
.authors {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 8px 28px;
font-size: 0.9375rem;
font-weight: 600;
}
.authors sup {
font-weight: 400;
color: var(--muted);
margin-left: 1px;
}
.affiliations {
list-style: none;
padding: 0;
margin: 10px 0 0;
font-size: 0.8125rem;
line-height: 1.6;
color: var(--muted);
}
.bibtex {
position: relative;
}
.bibtex pre {
margin: 0;
padding: 14px 16px;
padding-right: 80px;
background: white;
border: 1px solid var(--border);
border-radius: 8px;
font-size: 0.8125rem;
line-height: 1.5;
overflow-x: auto;
}
.copyBtn {
position: absolute;
top: 8px;
right: 8px;
font: inherit;
font-size: 0.75rem;
font-weight: 600;
padding: 4px 12px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface);
color: #333;
cursor: pointer;
}
.copyBtn:hover {
border-color: var(--accent);
color: var(--accent);
}
/* Model shown under each plot title, kept in sync by setStatus() */
#plots h4 {
margin-bottom: 4px;
}
.plotModel {
font-size: 0.8125rem;
color: var(--muted);
}
.plotModel b {
color: #1d1d1b;
}
@media (max-width: 900px) {
.modelGrid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<header class="hero">
<div class="eyebrow">Interactive demo</div>
<h1>The SIMPLE Law for Energy Cost of LLM Inference</h1>
<p class="lede">
An architecture-aware analytical model of the energy spent on each
generated token, fitted on an NVIDIA H100 with TensorRT-LLM.
</p>
</header>
<div class="modelGrid">
<section class="card">
<h3>SIMPLE Law</h3>
<div class="formula">
<span data-tex="E_{\mathrm{tok}}(\tau_{\mathrm{in}}, \tau_{\mathrm{out}}) ="></span>
<div class="term prefill">
<span data-tex="\theta_1\, d^2 L\, \dfrac{\tau_{\mathrm{in}}}{\tau_{\mathrm{out}}}"></span>
<span class="termLabel">Prefill Amortization</span>
</div>
<span class="plus">+</span>
<div class="term kvcache">
<span data-tex="\theta_2\, d L\, R \left(\tau_{\mathrm{in}} + \dfrac{\tau_{\mathrm{out}}}{2}\right)"></span>
<span class="termLabel">Decode Accum. KV-cache</span>
</div>
<span class="plus">+</span>
<div class="term nexttok">
<span data-tex="\theta_0 + \theta_3\, d L"></span>
<span class="termLabel">Decode Accum. Next-token</span>
</div>
</div>
<div class="symbols">
<span><span data-tex="d"></span> hidden size</span>
<span><span data-tex="L"></span> layers</span>
<span><span data-tex="R = n_{kv} / n_q"></span> KV-head ratio</span>
<span><span data-tex="\tau_{\mathrm{in}}, \tau_{\mathrm{out}}"></span> input / output tokens</span>
</div>
</section>
<section class="card">
<h3>Fitted parameters</h3>
<table id="thetaTable">
<tbody id="thetaBody"></tbody>
</table>
<p class="note">E<sub>tok</sub> in J/token.</p>
</section>
</div>
<div id="plots">
<div id="plotPanel">
<h4>Component breakdown at the selected τ<sub>in</sub></h4>
<div class="plotModel"></div>
<div id="plot"></div>
</div>
<div id="contourPanel">
<h4>E<sub>tok</sub> over input and output tokens</h4>
<div class="plotModel"></div>
<div id="contour"></div>
</div>
</div>
<p>
The contour map shows the total energy per token for the current architecture;
the dashed line marks the selected τ<sub>in</sub>.
</p>
<section class="card" id="controls">
<div id="ninWrapper">
<div class="sliderHeader">
<label for="ninSlider">Input tokens τ<sub>in</sub></label>
<span id="ninValue" class="badge">64</span>
</div>
<input
id="ninSlider"
type="range"
min="4"
max="16"
step="0.01"
value="6"
oninput="updateNin(this.value)"
>
<div id="ninLabels">
<span>16</span>
<span>65536</span>
</div>
<!-- Actual token count used by app.js -->
<input id="nin" type="hidden" value="64">
</div>
<div class="cardHeader">
<h3>Model</h3>
<span id="status" class="status"></span>
</div>
<div class="sources">
<div class="field">
<label for="preset">Validated models</label>
<select id="preset" onchange="loadPreset()"></select>
</div>
<div class="divider"><span>or</span></div>
<div class="field">
<label for="repo">Any public Hugging Face model <span class="hint">dense, &lt; 20B</span></label>
<div class="inputGroup">
<input id="repo" type="text" placeholder="e.g. deepseek-ai/DeepSeek-R1-Distill-Llama-8B" spellcheck="false">
<button onclick="loadModel()">Load</button>
</div>
</div>
</div>
<h4 class="sectionLabel">Architecture</h4>
<div class="archGrid">
<div class="field">
<label for="d">Hidden size <i>d</i></label>
<input id="d" type="number" min="1" value="4096">
</div>
<div class="field">
<label for="L">Layers <i>L</i></label>
<input id="L" type="number" min="1" value="32">
</div>
<div class="field">
<label for="nq">Query heads <i>n</i><sub>q</sub></label>
<input id="nq" type="number" min="1" value="32">
</div>
<div class="field">
<label for="nkv">KV heads <i>n</i><sub>kv</sub></label>
<input id="nkv" type="number" min="1" value="8">
</div>
</div>
<div id="archSummary"></div>
</section>
<script>
const MIN_TOKENS = 16;
const MAX_TOKENS = 65536;
function updateNin(exponent) {
const value = Math.round(Math.pow(2, parseFloat(exponent)));
const clamped = Math.min(MAX_TOKENS, Math.max(MIN_TOKENS, value));
document.getElementById('nin').value = clamped;
document.getElementById('ninValue').textContent = clamped;
draw();
}
</script>
<section class="card about">
<h3>About the SIMPLE Law</h3>
<p>
The <b>SIMPLE Law</b> is an architecture-aware analytical model that predicts
the <b>energy consumed per generated token</b> (E<sub>tok</sub>) during LLM
inference from a model's architecture (<i>d</i>, <i>L</i>, <i>n</i><sub>q</sub>,
<i>n</i><sub>kv</sub>) and its input and output token counts
(τ<sub>in</sub>, τ<sub>out</sub>).
This demo plots the SIMPLE Law's prediction and the share of each of its terms.
Select one of the models validated in the paper, or load the architecture of any
<b>dense decoder-only Transformer</b> (up to approximately 20B parameters) from
Hugging Face.
</p>
<p>
The SIMPLE Law was fitted using
<a href="https://zenodo.org/records/18714476" target="_blank">this dataset</a>.
</p>
<p>
For the complete methodology, experimental validation, and citation
information, see the paper:
<a href="https://arxiv.org/abs/2611.03330" target="_blank">
The SIMPLE Law for Energy Cost of LLM Inference</a>.
</p>
<h4 class="sectionLabel">Authors</h4>
<ul class="authors">
<li>Hiari Pizzini Cavagna<sup>a</sup></li>
<li>Andrea Proia<sup>a</sup></li>
<li>Daniele Cesarini<sup>b</sup></li>
<li>Zeynep Kiziltan<sup>a</sup></li>
<li>Andrea Bartolini<sup>a</sup></li>
</ul>
<ul class="affiliations">
<li><sup>a</sup> University of Bologna, Bologna, Italy</li>
<li><sup>b</sup> CINECA, Casalecchio di Reno, Italy</li>
</ul>
<h4 class="sectionLabel">Citation</h4>
<!-- TODO: fill in venue, year, pages and DOI once published -->
<div class="bibtex">
<button class="copyBtn" onclick="copyBibtex(this)">Copy</button>
<pre id="bibtex">@misc{cavagna2026simplelawenergycost,
title={The SIMPLE Law for Energy Cost of LLM Inference},
author={Hiari Pizzini Cavagna and Andrea Proia and Daniele Cesarini and Zeynep Kiziltan and Andrea Bartolini},
year={2026},
eprint={2611.03330},
archivePrefix={arXiv},
primaryClass={cs.AI},
url={https://arxiv.org/abs/2611.03330},
}</pre>
</div>
<script>
function copyBibtex(button) {
navigator.clipboard.writeText(document.getElementById('bibtex').textContent);
button.textContent = 'Copied';
setTimeout(() => button.textContent = 'Copy', 1500);
}
</script>
<p class="note">
Web demo developed with the assistance of Claude (Anthropic), an AI assistant.
</p>
</section>
<script src="app.js"></script>
</body>
</html>