distinct / distinct_server /comparators.py
User1342's picture
Measure GPU and CPU energy from hardware counters and sum them; drop the modelled fallback
26a7c18
Raw History Blame Contribute Delete
16.7 kB
"""What a number of joules actually amounts to, in things people have done.
WHY A SECOND TABLE, WHEN render.COMPARATORS ALREADY EXISTS
That one answers "how long would this run an appliance": *twelve seconds of a
10 W bulb*. It is exact and it is nearly useless at the small end, because
nobody has an intuition for twelve seconds of a light. This table answers the
other question: "how much of a whole, familiar thing is this": *a tenth of a
cup of tea*. Those are the units people already own.
HOW THE NUMBERS WERE GOT
Every entry is either derived here from physics, or is a rated figure for a
common appliance run for a stated time. The working is in the note beside it so
a reader can check it rather than trust it, which is the same standard the rest
of this project applies to the figures it republishes.
Water is the recurring one, so it is worth stating once. Heating water takes
``mass x 4186 J/kg/K x temperature rise``. A 250 ml mug from a 20 C tap to
100 C is ``0.25 x 4186 x 80 = 83.7 kJ`` of heat into the water. A domestic
kettle puts roughly 80% of what it draws into the water, the rest going into
the element, the body and the steam, so the wall figure is about 105 kJ. Where
an entry says "in a kettle" it is the wall figure, because that is the
electricity somebody actually paid for.
Anything expressed in kWh is converted at 3.6 MJ per kWh.
WHAT IS DELIBERATELY NOT HERE
No CO2, no water footprint, no land, no money. Those are conversions with
assumptions of their own -- grid mix, time of day, jurisdiction -- and this
project does not make assessor-created estimates. A joule compared to another
joule needs no assumption at all, which is why every entry on this list is an
energy and nothing else.
"""
from __future__ import annotations
from dataclasses import dataclass
KWH = 3_600_000.0
#: Specific heat capacity of water, J/(kg K).
WATER_SPECIFIC_HEAT = 4186.0
#: A domestic kettle delivers roughly this share of its draw into the water.
KETTLE_EFFICIENCY = 0.80
#: Standard gravity, m/s^2.
G = 9.81
def _heat_water(litres: float, rise_c: float, *, efficiency: float = 1.0) -> float:
return litres * WATER_SPECIFIC_HEAT * rise_c / efficiency
def _appliance(watts: float, seconds: float) -> float:
return watts * seconds
def _lift(kilograms: float, metres: float) -> float:
return kilograms * G * metres
@dataclass(frozen=True)
class Comparator:
"""One familiar thing, and what it costs."""
#: Reads after a number: "about 2.5 <label>".
label: str
#: Energy for exactly one of them, in joules.
joules: float
#: How that figure was arrived at.
note: str
#: Plural form where "s" will not do.
plural: str = ""
def name(self, count: float) -> str:
"""Singular only when the number in front of it will print as "1".
Matched to :func:`_round_count` rather than to the raw float, because
"about 0.98 minute" is what a tolerance around 1.0 produces and it
reads as a mistake.
"""
return self.label if _round_count(count) == "1" else (self.plural or self.label + "s")
#: Ordered by size. Roughly one entry every quarter-decade, so a picker can
#: nearly always find one that puts the figure between 0.1 and 100 of itself.
COMPARATORS: tuple[Comparator, ...] = (
# ---- sub-joule to 100 J: the scale of a short CPU-only run -----------
Comparator("heartbeat", 1.0, "about 1 J of mechanical work per beat at rest"),
Comparator("apple lifted one metre", 0.98, "0.1 kg x 9.81 m/s^2 x 1 m", "apples lifted one metre"),
Comparator("second of a smoke alarm", 1.0, "1 W standby draw for 1 s", "seconds of a smoke alarm"),
Comparator("phone screen-second", 2.0, "about 2 W for the panel, for 1 s", "phone screen-seconds"),
Comparator("teaspoon of water warmed by 1 C", 20.9, "0.005 kg x 4186 x 1", "teaspoons of water warmed by 1 C"),
Comparator("second of an LED bulb", 10.0, "10 W for 1 s", "seconds of an LED bulb"),
Comparator("second of a wifi router", 6.0, "6 W for 1 s", "seconds of a wifi router"),
Comparator("AA battery's worth of torch light", 15_120.0, "2.8 Ah x 1.5 V alkaline = 4.2 Wh"),
Comparator("stair step climbed", 132.0, "75 kg x 9.81 x 0.18 m riser", "stair steps climbed"),
Comparator("second of a laptop", 50.0, "50 W for 1 s", "seconds of a laptop"),
Comparator("press-up", 200.0, "about 200 J of work for an adult", "press-ups"),
Comparator("second of a desktop PC", 150.0, "150 W for 1 s", "seconds of a desktop PC"),
Comparator("minute of an LED bulb", 600.0, "10 W for 60 s", "minutes of an LED bulb"),
Comparator("second of a gaming GPU at full load", 350.0, "350 W for 1 s", "seconds of a gaming GPU at full load"),
Comparator("smartphone photo taken", 500.0, "about 0.5 kJ including capture and write", "smartphone photos taken"),
# ---- 1 kJ to 100 kJ: a normal LLM run --------------------------------
Comparator("second of a kettle", 3000.0, "3 kW element for 1 s", "seconds of a kettle"),
Comparator("minute of a wifi router", 360.0, "6 W for 60 s", "minutes of a wifi router"),
Comparator("minute of a laptop", 3000.0, "50 W for 60 s", "minutes of a laptop"),
Comparator("Google search", 1080.0, "0.3 Wh, Google's own published figure", "Google searches"),
Comparator("minute of a desktop PC", 9000.0, "150 W for 60 s", "minutes of a desktop PC"),
Comparator("litre of water warmed by 1 C", 4186.0, "1 kg x 4186 x 1", "litres of water warmed by 1 C"),
Comparator("minute of a 55-inch TV", 6000.0, "100 W for 60 s", "minutes of a 55-inch TV"),
Comparator("phone charged by 1%", 623.0, "4500 mAh x 3.85 V = 17.3 Wh, one hundredth", "% of a phone charge"),
Comparator("minute of a gaming GPU at full load", 21_000.0, "350 W for 60 s", "minutes of a gaming GPU at full load"),
Comparator("slice of toast", 96_000.0, "800 W toaster for 2 minutes", "slices of toast"),
Comparator("mug of tea boiled", 104_650.0, "0.25 l from 20 C to 100 C in a kettle at 80%", "mugs of tea boiled"),
Comparator("minute in a microwave", 60_000.0, "1 kW draw for 60 s", "minutes in a microwave"),
Comparator("egg boiled", 130_000.0, "hob heating 0.4 l to 100 C and holding it", "eggs boiled"),
Comparator("hour of a wifi router", 21_600.0, "6 W for 1 h", "hours of a wifi router"),
Comparator("minute of an electric shower", 510_000.0, "8.5 kW for 60 s", "minutes of an electric shower"),
Comparator("hour of an LED bulb", 36_000.0, "10 W for 1 h", "hours of an LED bulb"),
Comparator("hour of a laptop", 180_000.0, "50 W for 1 h", "hours of a laptop"),
Comparator("phone charged from flat", 62_300.0, "4500 mAh x 3.85 V = 17.3 Wh", "phones charged from flat"),
Comparator("pint of water boiled", 237_800.0, "0.568 l from 20 C to 100 C in a kettle at 80%", "pints of water boiled"),
Comparator("hour of a 55-inch TV", 360_000.0, "100 W for 1 h", "hours of a 55-inch TV"),
Comparator("piece of bread baked", 250_000.0, "oven share for one loaf's worth of a slice", "pieces of bread baked"),
# ---- 1 MJ to 100 MJ: a long session, or a machine left running -------
Comparator("hour of a desktop PC", 540_000.0, "150 W for 1 h", "hours of a desktop PC"),
Comparator("hour of a gaming GPU at full load", 1_260_000.0, "350 W for 1 h", "hours of a gaming GPU at full load"),
Comparator("hour of a fridge", 144_000.0, "40 W average duty for 1 h", "hours of a fridge"),
Comparator("load of washing", 2_520_000.0, "0.7 kWh, a 40 C cotton cycle", "loads of washing"),
Comparator("dishwasher cycle", 3_600_000.0, "1.0 kWh eco programme", "dishwasher cycles"),
Comparator("shower taken", 4_080_000.0, "8.5 kW electric shower for 8 minutes", "showers taken"),
Comparator("hour of an oven", 7_200_000.0, "2 kW for 1 h", "hours of an oven"),
Comparator("tumble dryer cycle", 9_000_000.0, "2.5 kWh, a condenser dryer", "tumble dryer cycles"),
Comparator("kilometre driven in an electric car", 648_000.0, "0.18 kWh/km", "kilometres driven in an electric car"),
Comparator("day of a fridge", 3_456_000.0, "40 W average duty for 24 h", "days of a fridge"),
Comparator("day of a laptop in constant use", 4_320_000.0, "50 W for 24 h", "days of a laptop in constant use"),
Comparator("bath run", 7_060_000.0, "150 l from 10 C to 40 C at 90% immersion efficiency", "baths run"),
Comparator("mile driven in an electric car", 1_043_000.0, "0.18 kWh/km over 1.609 km", "miles driven in an electric car"),
# ---- filling the band a run actually lands in ------------------------
Comparator("second of a kettle at half power", 1500.0, "1.5 kW for 1 s", "seconds of a kettle at half power"),
Comparator("second of a hair dryer", 1800.0, "1.8 kW for 1 s", "seconds of a hair dryer"),
Comparator("second of a microwave", 1000.0, "1 kW for 1 s", "seconds of a microwave"),
Comparator("second of a vacuum cleaner", 900.0, "900 W for 1 s", "seconds of a vacuum cleaner"),
Comparator("second of a 55-inch TV", 100.0, "100 W for 1 s", "seconds of a 55-inch TV"),
Comparator("second of a games console", 200.0, "200 W for 1 s", "seconds of a games console"),
Comparator("second of a fridge compressor", 120.0, "120 W while running, for 1 s", "seconds of a fridge compressor"),
Comparator("minute of a phone charging", 1038.0, "17.3 Wh over about 100 minutes", "minutes of a phone charging"),
Comparator("minute of a games console", 12_000.0, "200 W for 60 s", "minutes of a games console"),
Comparator("minute of a fridge compressor", 7200.0, "120 W for 60 s", "minutes of a fridge compressor"),
Comparator("minute of a hair dryer", 108_000.0, "1.8 kW for 60 s", "minutes of a hair dryer"),
Comparator("minute of a vacuum cleaner", 54_000.0, "900 W for 60 s", "minutes of a vacuum cleaner"),
Comparator("minute of an oven", 120_000.0, "2 kW for 60 s", "minutes of an oven"),
Comparator("minute of a tumble dryer", 150_000.0, "2.5 kW for 60 s", "minutes of a tumble dryer"),
Comparator("minute of a fan heater", 120_000.0, "2 kW for 60 s", "minutes of a fan heater"),
Comparator("espresso pulled", 45_000.0, "1.4 kW machine for about 32 s including pre-heat share", "espressos pulled"),
Comparator("cafetiere of coffee", 167_400.0, "0.4 l from 20 C to 100 C in a kettle at 80%", "cafetieres of coffee"),
Comparator("hot water bottle filled", 313_950.0, "0.75 l from 20 C to 100 C in a kettle at 80%", "hot water bottles filled"),
Comparator("pan of pasta water boiled", 837_200.0, "2 l from 20 C to 100 C in a kettle at 80%", "pans of pasta water boiled"),
Comparator("slice of pizza reheated", 90_000.0, "1 kW microwave for 90 s", "slices of pizza reheated"),
Comparator("piece of bread toasted lightly", 48_000.0, "800 W toaster for 60 s", "pieces of bread toasted lightly"),
Comparator("kettle boiled full", 627_900.0, "1.5 l from 20 C to 100 C in a kettle at 80%", "kettles boiled full"),
Comparator("laptop charged from flat", 216_000.0, "60 Wh battery", "laptops charged from flat"),
Comparator("tablet charged from flat", 118_800.0, "33 Wh battery", "tablets charged from flat"),
Comparator("e-bike battery charged", 1_800_000.0, "500 Wh battery", "e-bike batteries charged"),
Comparator("cordless drill battery charged", 194_400.0, "54 Wh pack", "cordless drill batteries charged"),
Comparator("hour of a games console", 720_000.0, "200 W for 1 h", "hours of a games console"),
Comparator("hour of a set-top box", 28_800.0, "8 W for 1 h", "hours of a set-top box"),
Comparator("hour of a phone charger left plugged in", 1800.0, "0.5 W vampire draw for 1 h", "hours of a phone charger left plugged in"),
Comparator("hour of an electric blanket", 360_000.0, "100 W for 1 h", "hours of an electric blanket"),
Comparator("hour of a dehumidifier", 1_080_000.0, "300 W for 1 h", "hours of a dehumidifier"),
Comparator("hour of a fan heater", 7_200_000.0, "2 kW for 1 h", "hours of a fan heater"),
Comparator("hour of a heat pump", 3_600_000.0, "1 kW electrical input for 1 h", "hours of a heat pump"),
Comparator("hour of a server in a rack", 1_800_000.0, "500 W for 1 h", "hours of a server in a rack"),
Comparator("day of a wifi router", 518_400.0, "6 W for 24 h", "days of a wifi router"),
Comparator("day of a smart speaker", 259_200.0, "3 W for 24 h", "days of a smart speaker"),
Comparator("day of a games console left on", 17_280_000.0, "200 W for 24 h", "days of a games console left on"),
Comparator("week of a fridge", 24_192_000.0, "40 W average duty for 7 days", "weeks of a fridge"),
Comparator("month of a wifi router", 15_768_000.0, "6 W for 30.4 days", "months of a wifi router"),
Comparator("10 km drive in an electric car", 6_480_000.0, "0.18 kWh/km x 10", "10 km drives in an electric car"),
Comparator("100 km drive in an electric car", 64_800_000.0, "0.18 kWh/km x 100", "100 km drives in an electric car"),
Comparator("litre of petrol burnt", 34_200_000.0, "9.5 kWh of chemical energy per litre", "litres of petrol burnt"),
Comparator("bag of coal burnt", 700_000_000.0, "25 kg at about 28 MJ/kg", "bags of coal burnt"),
Comparator("month of a UK home", 821_000_000.0, "2739 kWh a year, divided by twelve", "months of a UK home"),
Comparator("hour of a domestic solar array at noon", 14_400_000.0, "4 kWp array for 1 h", "hours of a domestic solar array at noon"),
Comparator("day of a domestic solar array", 43_200_000.0, "about 12 kWh on a good summer day", "days of a domestic solar array"),
Comparator("year of a fridge", 1_261_440_000.0, "40 W average duty for a year", "years of a fridge"),
Comparator("lifetime of a AAA battery", 5400.0, "1000 mAh x 1.5 V", "lifetimes of a AAA battery"),
Comparator("kilogram lifted to the top of Big Ben", 957.0, "1 kg x 9.81 x 96 m", "kilograms lifted to the top of Big Ben"),
# ---- 100 MJ upwards: fleets, and the honest top of the scale ---------
Comparator("day of a UK home", 27_000_000.0, "7.5 kWh, Ofgem typical domestic consumption", "days of a UK home"),
Comparator("week of a UK home", 189_000_000.0, "7.5 kWh x 7", "weeks of a UK home"),
Comparator("full charge of an electric car", 216_000_000.0, "60 kWh battery", "full charges of an electric car"),
Comparator("year of a UK home", 9_860_000_000.0, "2739 kWh, Ofgem typical annual electricity", "years of a UK home"),
)
#: The comparator picker aims to put the figure inside this band, so the number
#: in front of the label is one a person can picture. Below 0.1 the answer is
#: "hardly any of one", above 100 it stops being a comparison.
NICE_LOW = 0.1
NICE_HIGH = 100.0
def choose(joules: float, *, table: tuple[Comparator, ...] = COMPARATORS) -> tuple[Comparator, float] | None:
"""The comparator that puts this figure closest to one whole unit.
Closest in log space, because 0.5 of a thing and 2 of a thing are equally
easy to picture and a linear distance would prefer the wrong one of them.
"""
if not isinstance(joules, (int, float)) or isinstance(joules, bool):
return None
value = float(joules)
if value <= 0 or value != value or value in (float("inf"), float("-inf")):
return None
import math
best: tuple[Comparator, float] | None = None
best_distance = float("inf")
for entry in table:
if entry.joules <= 0:
continue
count = value / entry.joules
distance = abs(math.log10(count))
if distance < best_distance:
best, best_distance = (entry, count), distance
return best
def _round_count(count: float) -> str:
if count >= 100:
return f"{count:,.0f}"
if count >= 10:
return f"{count:.0f}"
if count >= 1:
return f"{count:.1f}".rstrip("0").rstrip(".")
if count >= 0.1:
return f"{count:.2f}".rstrip("0").rstrip(".")
return f"{count:.3f}".rstrip("0").rstrip(".")
def describe(joules: float) -> str:
""""about 1.4 mugs of tea boiled", or an empty string if there is nothing to say."""
chosen = choose(joules)
if chosen is None:
return ""
entry, count = chosen
if count < 0.01:
return f"a tiny fraction of {_article(entry.label)}"
return f"about {_round_count(count)} {entry.name(count)}"
def _article(label: str) -> str:
return ("an " if label[:1].lower() in "aeiou" else "a ") + label
def basis(joules: float) -> str:
"""The working behind whichever comparator was shown."""
chosen = choose(joules)
return "" if chosen is None else chosen[0].note