arborist/tests/test_watt_cogs.py
russell@unturf.com 892d9ed037
feat(#000057): bench/watt_calibrate.py — separate prefill vs decode energy
fox 2026-05-21: account wattage for input and output separately. Prefill
(process all prompt tokens, parallel/compute-bound) and decode (generate
output, autoregressive/bandwidth-bound) are different GPU ops with
different J/token — a single per-token number can't represent both.

Slope calibration (no sub-request power alignment): sweep prompt length
at tiny max_tokens -> prefill J/input-tok (fixed overhead cancels in the
slope); fix a tiny prompt and sweep forced output length (ignore_eos) ->
decode J/output-tok. Prefill kept COLD (unique filler so cached_tokens=0).
Reuses watt_bench probes. Bad points (context overflow) skip, not abort.

Measured qwen-nothink/4090 @$0.33/kWh: prefill 0.175 J/tok
($0.016/M-input-tok), decode 6.16 J/tok ($0.564/M-output-tok) — decode
35x dearer per token. Predicts measured substrate J/q within ~5%. 14
tests (+ slope). Validated live.
2026-05-21 11:48:36 -04:00

131 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Energy-COGS math + power-state classification (#000057).
Pure functions, no GPU / no nvidia-smi. fox 2026-05-21: a card occupies
DISTINCT power states (idle / middle-idle / generation) that differ per
card×model×server — measure them, never hardcode, never collapse them
into one blended "gen W". These tests pin the joules→kWh→$ arithmetic,
the serving-floor marginal, and the data-derived band split.
See docs/calculator-test-patterns.md.
"""
from bench.watt_bench import energy_cogs
from bench.watt_calibrate import _slope
from bench.watt_probe import classify_power_bands
def test_slope_recovers_known_rate():
# y = 0.175*x + 60 (decode-like: per-token slope + fixed overhead).
xs = [32.0, 128.0, 512.0]
ys = [0.175 * x + 60 for x in xs]
assert round(_slope(xs, ys), 4) == 0.175 # slope ignores intercept
def test_slope_two_points():
assert _slope([100.0, 200.0], [10.0, 30.0]) == 0.2
# ---- energy_cogs -------------------------------------------------------
def test_gross_cogs_one_kwh():
c = energy_cogs(gpu_joules=3.6e6, window_s=100.0, completion_tok=1000,
idle_mean_w=30.0, price_per_kwh=0.33)
assert c["available"] is True
assert c["gross_usd"] == 0.33 # 1 kWh * $0.33
assert c["gross_usd_per_1k_total_tok"] == 0.33
assert c["gross_usd_per_1k_completion_tok"] == 0.33
assert c["window_mean_w"] == 36000.0 # blend, honestly labelled
def test_prompt_context_makes_per_total_cheap_per_completion_dear():
# The fox correction: substrate prefills a big CONTEXT (prompt_tok).
# Per TOTAL token processed it is cheap; charging it all to the few
# completion tokens makes it look expensive — that's the artifact.
c = energy_cogs(gpu_joules=3.6e6, window_s=100.0, completion_tok=1000,
idle_mean_w=30.0, price_per_kwh=0.33, prompt_tok=9000)
assert c["prompt_tokens"] == 9000
assert c["total_tokens"] == 10000
assert c["gross_usd_per_1k_total_tok"] == 0.033 # 0.33 / 10000 *1k
assert c["gross_usd_per_1k_completion_tok"] == 0.33 # 10x dearer
assert (c["gross_usd_per_1k_completion_tok"]
> c["gross_usd_per_1k_total_tok"])
def test_marginal_uses_serving_floor_when_present():
c = energy_cogs(gpu_joules=3.6e6, window_s=100.0, completion_tok=1000,
idle_mean_w=30.0, price_per_kwh=0.33,
serving_floor_w=60.0, gen_draw_w=400.0, gen_duty=0.5)
assert c["marginal_floor"] == "serving"
assert c["marginal_floor_w"] == 60.0
assert c["marginal_joules"] == 3.6e6 - 60.0 * 100.0
assert c["gen_draw_w_measured"] == 400.0
assert c["gen_duty_cycle"] == 0.5
def test_marginal_falls_back_to_idle_without_serving_floor():
c = energy_cogs(gpu_joules=3.6e6, window_s=100.0, completion_tok=1000,
idle_mean_w=30.0, price_per_kwh=0.33)
assert c["marginal_floor"] == "idle"
assert c["marginal_joules"] == 3.6e6 - 30.0 * 100.0
assert (c["marginal_usd_per_1k_total_tok"]
< c["gross_usd_per_1k_total_tok"])
def test_marginal_clamps_to_zero_when_floor_exceeds_load():
c = energy_cogs(gpu_joules=5000.0, window_s=100.0, completion_tok=10,
idle_mean_w=30.0, price_per_kwh=0.33,
serving_floor_w=100.0)
assert c["marginal_joules"] == 0.0
assert c["marginal_usd"] == 0.0
def test_no_floor_gives_gross_only():
c = energy_cogs(gpu_joules=3.6e6, window_s=100.0, completion_tok=1000,
idle_mean_w=None, price_per_kwh=0.33)
assert c["available"] is True
assert "marginal_usd_per_1k_total_tok" not in c
def test_unavailable_when_inputs_missing():
assert energy_cogs(None, 100.0, 1000, 30.0, 0.33)["available"] is False
assert energy_cogs(3.6e6, None, 1000, 30.0, 0.33)["available"] is False
# zero completion AND zero prompt -> no tokens at all
assert energy_cogs(3.6e6, 100.0, 0, 30.0, 0.33,
prompt_tok=0)["available"] is False
def test_price_is_the_only_lever():
a = energy_cogs(3.6e6, 100.0, 1000, 30.0, 0.33, serving_floor_w=60.0)
b = energy_cogs(3.6e6, 100.0, 1000, 30.0, 0.66, serving_floor_w=60.0)
assert round(b["gross_usd"] / a["gross_usd"], 3) == 2.0
assert round(b["marginal_usd"] / a["marginal_usd"], 3) == 2.0
# ---- classify_power_bands ---------------------------------------------
def test_band_split_is_bimodal_on_idle_vs_gen():
# 60W middle-idle gaps interleaved with 400W generation bursts.
samples = [60, 62, 58, 405, 398, 410, 61, 400, 59, 402, 63, 395]
b = classify_power_bands(samples)
assert b["available"] and b["bimodal"]
assert 55 <= b["low_band_w"] <= 70 # serving floor
assert 390 <= b["high_band_w"] <= 415 # generation draw
assert b["peak_w"] == 410
assert 0.0 < b["duty_cycle"] < 1.0
def test_band_unimodal_when_idle_only():
# A quiet idle window: no distinct generation state.
b = classify_power_bands([30, 31, 29, 30, 32, 28, 31])
assert b["available"] is True
assert b["bimodal"] is False
def test_band_unavailable_too_few_samples():
assert classify_power_bands([400, 60])["available"] is False
def test_band_thresholds_not_hardcoded_scale_with_data():
# Same shape at a different absolute scale (a different card) still
# splits — proves the split is data-derived, not a fixed watt cut.
b = classify_power_bands([12, 13, 11, 95, 98, 92, 12, 96])
assert b["bimodal"] is True
assert b["low_band_w"] < 20 and b["high_band_w"] > 90