Skip to content

Commit 8bd8c8c

Browse files
committed
dashboard: switch emissions chart from instantaneous rate to cumulative total
The chart was titled "emissions over time" but plotted an instantaneous rate (kg CO2/hr) -- tiny and nearly flat for nimbus, so with beginAtZero it visually read as empty. Now integrates the rate client-side (rectangle rule) into a running total that climbs steadily even while idle.
1 parent d530656 commit 8bd8c8c

2 files changed

Lines changed: 52 additions & 46 deletions

File tree

cmd/static/dashboard.html

Lines changed: 39 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ <h2>Models</h2>
170170
let currentRange = '7d';
171171
let barMode = 'co2';
172172
let lastData = null;
173+
let lastEmissionsTotalGrams = 0;
173174

174175
function chartColors() {
175176
const light = document.body.classList.contains('light');
@@ -490,7 +491,19 @@ <h2>Models</h2>
490491
}).join('');
491492
}
492493

493-
// CO₂/token over time — only points where tokens were being generated.
494+
// The backend always emits step as one of "5m0s", "1h0m0s", "6h0m0s"
495+
// (Go's time.Duration.String()) -- parse the h/m/s components present.
496+
function parseStepSeconds(stepStr) {
497+
const h = /(\d+)h/.exec(stepStr);
498+
const m = /(\d+)m/.exec(stepStr);
499+
const s = /(\d+)s/.exec(stepStr);
500+
return (h?+h[1]*3600:0) + (m?+m[1]*60:0) + (s?+s[1]:0);
501+
}
502+
503+
// Cumulative CO₂ emissions over the selected window — integrates the
504+
// co2_grams_per_hour rate series using the rectangle rule (rate × step
505+
// duration), running-summed. Always populated as long as the range has any
506+
// points, even during idle periods (the total simply grows slowly).
494507
function renderTimeSeries(data) {
495508
const pts = data.points || [];
496509
if (!pts.length) return;
@@ -504,59 +517,49 @@ <h2>Models</h2>
504517
return d.toLocaleDateString([],{month:'short',day:'numeric'});
505518
};
506519

507-
// Use CO₂/token where available; show null (gap) during idle periods
508-
const tokenPts = pts.filter(p => p.co2_mg_per_token > 0);
509-
const hasTokens = tokenPts.length > 0;
510-
511-
const datasets = hasTokens ? [
512-
{
513-
label: 'Cluster avg CO₂/token (mg)',
514-
data: pts.map(p => p.co2_mg_per_token > 0 ? +p.co2_mg_per_token.toFixed(3) : null),
515-
borderColor: '#22c55e',
516-
backgroundColor: 'rgba(34,197,94,0.08)',
517-
fill: true, tension: 0.3, pointRadius: 0, spanGaps: false,
518-
},
519-
] : [
520-
// Fallback: show CO₂/hr when no token data in range
521-
{
522-
label: 'CO₂ rate (kg/hr)',
523-
data: pts.map(p => +(p.co2_grams_per_hour/1000).toFixed(4)),
524-
borderColor: '#6366f1', backgroundColor: 'rgba(99,102,241,0.1)',
520+
const stepHours = parseStepSeconds(data.step) / 3600;
521+
let running = 0;
522+
const cumulativeGrams = pts.map(p => {
523+
running += p.co2_grams_per_hour * stepHours;
524+
return +running.toFixed(2);
525+
});
526+
lastEmissionsTotalGrams = running;
527+
528+
const totalLabel = running >= 1000
529+
? (running/1000).toFixed(2) + ' kg CO₂e'
530+
: running.toFixed(1) + ' g CO₂e';
531+
532+
const chartData = {
533+
labels: pts.map(mkLabel),
534+
datasets: [{
535+
label: 'Cumulative CO₂ (g)',
536+
data: cumulativeGrams,
537+
borderColor: '#6366f1',
538+
backgroundColor: 'rgba(99,102,241,0.1)',
525539
fill: true, tension: 0.3, pointRadius: 0,
526-
}
527-
];
528-
529-
const avgToken = hasTokens
530-
? (tokenPts.reduce((s,p)=>s+p.co2_mg_per_token,0)/tokenPts.length).toFixed(2) + ' mg CO₂/token avg'
531-
: 'no active generation in range — showing CO₂/hr';
532-
533-
const yTitle = hasTokens ? 'mg CO₂ / token' : 'kg CO₂ / hr';
534-
const tooltipFmt = hasTokens
535-
? c => ` ${c.dataset.label}: ${c.parsed.y != null ? c.parsed.y.toFixed(3) : '—'} mg/token`
536-
: c => ` ${c.parsed.y.toFixed(3)} kg CO₂/hr`;
537-
538-
const chartData = { labels: pts.map(mkLabel), datasets };
540+
}],
541+
};
539542
const lc = chartColors();
540543
const options = {
541544
responsive: true,
542545
interaction: { mode:'index', intersect:false },
543546
plugins: {
544547
legend: { labels:{ color:lc.legend, font:{size:10}, boxWidth:12 } },
545-
title: { display:true, text:`${rangeLabel}${avgToken}`,
548+
title: { display:true, text:`${rangeLabel}Total: ${totalLabel}`,
546549
color:lc.tick, font:{size:11, weight:'normal'}, padding:{bottom:8} },
547-
tooltip: { callbacks: { label: tooltipFmt } }
550+
tooltip: { callbacks: { label: c => ` ${c.parsed.y.toFixed(2)} g CO₂ cumulative` } }
548551
},
549552
scales: {
550553
x: { ticks:{color:lc.tick,font:{size:10},maxTicksLimit:10}, grid:{color:lc.grid} },
551554
y: { ticks:{color:lc.tick,font:{size:10}}, grid:{color:lc.grid},
552-
title:{display:true, text:yTitle, color:lc.tick, font:{size:10}},
555+
title:{display:true, text:'g CO₂ (cumulative)', color:lc.tick, font:{size:10}},
553556
beginAtZero:true }
554557
}
555558
};
556559

557560
if (lineChart) {
558561
lineChart.data = chartData;
559-
lineChart.options.plugins.title.text = `${rangeLabel}${avgToken}`;
562+
lineChart.options.plugins.title.text = `${rangeLabel}Total: ${totalLabel}`;
560563
lineChart.update('none');
561564
} else {
562565
lineChart = new Chart(document.getElementById('lineChart'), { type:'line', data:chartData, options });

cmd/static/methodology.html

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -348,18 +348,21 @@ <h3>Energy per token across models (J/token view)</h3>
348348
watts, tok/s, and GPU hardware for context.</p>
349349
</div>
350350

351-
<h3>Cumulative CO₂ and cluster CO₂/token (time-series view)</h3>
351+
<h3>Cumulative CO₂ emissions (time-series view)</h3>
352352
<p>
353-
For the time-series charts, we query the Prometheus range API at adaptive resolution
354-
(5-minute steps for 24 h, hourly for 7 d, 6-hourly for 30 d),
355-
apply the same per-node intensity to each sample, and integrate:
353+
The backend's timeseries endpoint queries the Prometheus range API at
354+
adaptive resolution (5-minute steps for 24 h, hourly for 7 d, 6-hourly for
355+
30 d) and returns <code>co2_grams_per_hour</code> at each step (already
356+
computed server-side as <code>power_watts × 0.198</code>, nimbus's fixed
357+
grid intensity). The dashboard then integrates that rate client-side using
358+
the rectangle rule, running-summed over the selected window:
359+
</p>
360+
<pre><code>total_g = Σ (co2_grams_per_hour_i × step_hours)</code></pre>
361+
<p>
362+
This total only grows — it never resets within a window — so the chart
363+
climbs steadily even while nimbus is mostly idle, rather than looking flat
364+
or empty the way a tiny, nearly-constant rate does.
356365
</p>
357-
<pre><code>CO₂_kg_cumulative = Σ (P_watts_i × intensity_i × Δt_hrs / 1000)
358-
359-
-- Total token rate used as denominator for cluster-wide CO₂/token:
360-
sum by (namespace, container) (
361-
rate(vllm:generation_tokens_total[5m]) + rate(vllm:prompt_tokens_total[5m])
362-
)</code></pre>
363366

364367
<h2>Limitations and Future Work</h2>
365368
<ul style="color:#cbd5e1; padding-left:1.5rem; margin:0.5rem 0 1rem;">

0 commit comments

Comments
 (0)