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.
131 lines
5.4 KiB
Python
131 lines
5.4 KiB
Python
"""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
|