ecocost 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ecocost/__init__.py +108 -0
- ecocost/calc.py +308 -0
- ecocost/data/hardware.yaml +145 -0
- ecocost/data/models.yaml +1541 -0
- ecocost/data/providers.yaml +378 -0
- ecocost/data/regions.yaml +1051 -0
- ecocost/data/sources.yaml +182 -0
- ecocost/display.py +37 -0
- ecocost/errors.py +60 -0
- ecocost/loader.py +292 -0
- ecocost/py.typed +0 -0
- ecocost/result.py +107 -0
- ecocost/schema.py +332 -0
- ecocost-0.1.0.dist-info/METADATA +174 -0
- ecocost-0.1.0.dist-info/RECORD +18 -0
- ecocost-0.1.0.dist-info/WHEEL +4 -0
- ecocost-0.1.0.dist-info/licenses/LICENSE +201 -0
- ecocost-0.1.0.dist-info/licenses/NOTICE +88 -0
ecocost/__init__.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""ecocost: estimated carbon, energy and water per LLM run.
|
|
2
|
+
|
|
3
|
+
Pure Python; the only dependency is PyYAML. See METHODOLOGY.md for how the
|
|
4
|
+
estimate is built and CONTRIBUTING.md for how to update the data.
|
|
5
|
+
|
|
6
|
+
>>> from ecocost import estimate
|
|
7
|
+
>>> estimate("accounts/fireworks/models/gpt-oss-120b", provider="fireworks",
|
|
8
|
+
... input_tokens=800, output_tokens=300)["carbon"]
|
|
9
|
+
{'unit': 'gCO2e', 'value': ..., 'min': ..., 'max': ...}
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
16
|
+
from numbers import Integral
|
|
17
|
+
|
|
18
|
+
from . import calc
|
|
19
|
+
from .errors import UnknownModelError, UnknownProviderError
|
|
20
|
+
from .loader import get_kb
|
|
21
|
+
from .result import EstimateResult
|
|
22
|
+
|
|
23
|
+
__all__ = ["estimate", "EstimateResult", "UnknownModelError", "UnknownProviderError"]
|
|
24
|
+
|
|
25
|
+
# far above any single request; beyond it the float maths would overflow
|
|
26
|
+
MAX_TOKENS = 10**12
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
__version__ = version("ecocost")
|
|
30
|
+
except PackageNotFoundError: # running from a source checkout
|
|
31
|
+
__version__ = "0+unknown"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def estimate(
|
|
35
|
+
model: str,
|
|
36
|
+
*,
|
|
37
|
+
provider: str | None = None,
|
|
38
|
+
base_url: str | None = None,
|
|
39
|
+
input_tokens: int = 0,
|
|
40
|
+
output_tokens: int = 0,
|
|
41
|
+
cached_input_tokens: int = 0,
|
|
42
|
+
timestamp: datetime | None = None,
|
|
43
|
+
) -> EstimateResult:
|
|
44
|
+
"""Estimate one request.
|
|
45
|
+
|
|
46
|
+
``model`` is a model id; provider-specific ids resolve through aliases.
|
|
47
|
+
Who serves the request is taken, in order, from ``provider`` (an id from
|
|
48
|
+
providers.yaml, listed in the README), else ``base_url`` (an API endpoint
|
|
49
|
+
whose host a provider publishes), else, for a closed model, its vendor's
|
|
50
|
+
own API; otherwise wide US defaults. ``reasons`` says when it was inferred
|
|
51
|
+
or defaulted.
|
|
52
|
+
|
|
53
|
+
Ids are matched ignoring case. ``UnknownModelError`` and
|
|
54
|
+
``UnknownProviderError`` suggest close matches and where to add the
|
|
55
|
+
record. Bad arguments raise ``TypeError`` or ``ValueError``.
|
|
56
|
+
"""
|
|
57
|
+
_check_args(
|
|
58
|
+
model, provider, base_url, input_tokens, output_tokens, cached_input_tokens
|
|
59
|
+
)
|
|
60
|
+
kb = get_kb()
|
|
61
|
+
m = kb.resolve_model(model)
|
|
62
|
+
p, provider_reason = kb.resolve_provider(provider, base_url=base_url, model=m)
|
|
63
|
+
est = calc.estimate(
|
|
64
|
+
m,
|
|
65
|
+
p,
|
|
66
|
+
kb.regions[p.region],
|
|
67
|
+
kb.hardware[p.hardware],
|
|
68
|
+
kb.hardware[calc.REFERENCE_HARDWARE_ID],
|
|
69
|
+
input_tokens=input_tokens,
|
|
70
|
+
output_tokens=output_tokens,
|
|
71
|
+
cached_input_tokens=cached_input_tokens,
|
|
72
|
+
candidate_regions=[(kb.regions[rid], w) for rid, w in p.region_candidates],
|
|
73
|
+
candidate_hardware=[(kb.hardware[hid], w) for hid, w in p.hardware_candidates],
|
|
74
|
+
)
|
|
75
|
+
# timestamp is reserved for hourly grid intensity and not used yet.
|
|
76
|
+
if provider_reason:
|
|
77
|
+
est.reasons.insert(0, provider_reason)
|
|
78
|
+
out = est.to_dict()
|
|
79
|
+
out["requested"] = {"model": model, "provider": provider, "base_url": base_url}
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _check_args(
|
|
84
|
+
model, provider, base_url, input_tokens, output_tokens, cached_input_tokens
|
|
85
|
+
):
|
|
86
|
+
if not isinstance(model, str):
|
|
87
|
+
raise TypeError(f"model must be a string, got {type(model).__name__}")
|
|
88
|
+
if not model:
|
|
89
|
+
raise ValueError("model must not be empty")
|
|
90
|
+
for name, value in (("provider", provider), ("base_url", base_url)):
|
|
91
|
+
if value is not None and not isinstance(value, str):
|
|
92
|
+
raise TypeError(
|
|
93
|
+
f"{name} must be a string or None, got {type(value).__name__}"
|
|
94
|
+
)
|
|
95
|
+
for name, n in (
|
|
96
|
+
("input_tokens", input_tokens),
|
|
97
|
+
("output_tokens", output_tokens),
|
|
98
|
+
("cached_input_tokens", cached_input_tokens),
|
|
99
|
+
):
|
|
100
|
+
if isinstance(n, bool) or not isinstance(n, Integral):
|
|
101
|
+
raise TypeError(f"{name} must be an int, got {type(n).__name__}")
|
|
102
|
+
if not 0 <= n <= MAX_TOKENS:
|
|
103
|
+
raise ValueError(f"{name} must be between 0 and {MAX_TOKENS:,}, got {n}")
|
|
104
|
+
if cached_input_tokens > input_tokens:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"cached_input_tokens ({cached_input_tokens}) exceeds input_tokens "
|
|
107
|
+
f"({input_tokens}); input_tokens counts cached tokens too"
|
|
108
|
+
)
|
ecocost/calc.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""Eco-cost estimation pipeline.
|
|
2
|
+
|
|
3
|
+
tokens ──(active params, utilization)──▶ H100-seconds (work, chip-independent)
|
|
4
|
+
──(chip energy ratio, TDP, PUE)─▶ Wh at the meter
|
|
5
|
+
──(grid intensity)────────────▶ gCO2e (usage)
|
|
6
|
+
──(embodied per H100-second)──▶ gCO2e (embodied)
|
|
7
|
+
──(WUE)───────────────────────▶ mL water, on-site cooling
|
|
8
|
+
──(generation water)──────────▶ mL water, upstream at the plant
|
|
9
|
+
──(primary energy factor)─────▶ MJ primary energy
|
|
10
|
+
|
|
11
|
+
Every step is arithmetic over ``Range``. min/max is the likely range:
|
|
12
|
+
independent multiplicative inputs combine in quadrature in log space.
|
|
13
|
+
``worst_case`` keeps the plain interval bounds with every input at its
|
|
14
|
+
extreme at once. Confidence is bucketed from the max/min ratio of the likely
|
|
15
|
+
carbon range and explained by listing every tier-3 input that went into it.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from collections.abc import Sequence
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
|
|
23
|
+
from .schema import (
|
|
24
|
+
DEFAULT_HALF_WIDTH,
|
|
25
|
+
H100_TDP_W,
|
|
26
|
+
Hardware,
|
|
27
|
+
Model,
|
|
28
|
+
Provider,
|
|
29
|
+
Range,
|
|
30
|
+
Region,
|
|
31
|
+
Tier,
|
|
32
|
+
sig_round,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
METHOD_VERSION = "0.2.0"
|
|
36
|
+
|
|
37
|
+
# H100 SXM is the unit of work: an H100-second is "the work one H100 does in
|
|
38
|
+
# one second", whatever chip actually ran it.
|
|
39
|
+
REFERENCE_HARDWARE_ID = "h100-sxm"
|
|
40
|
+
|
|
41
|
+
# Cached prefix tokens cost about a tenth of a fresh prefill (assumed).
|
|
42
|
+
CACHED_INPUT_COST = 0.1
|
|
43
|
+
|
|
44
|
+
# Confidence label from the max/min ratio of the likely carbon range. "high"
|
|
45
|
+
# is within about +/-40% of the point value, "medium" within about a factor
|
|
46
|
+
# of 2.2 either way. For scale, Epoch's plausible range for a ChatGPT query
|
|
47
|
+
# is 0.1 to 4 Wh, a 40x span.
|
|
48
|
+
HIGH_CONFIDENCE_RATIO = 2.0
|
|
49
|
+
MEDIUM_CONFIDENCE_RATIO = 5.0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def estimate(
|
|
53
|
+
model: Model,
|
|
54
|
+
provider: Provider,
|
|
55
|
+
region: Region,
|
|
56
|
+
hardware: Hardware,
|
|
57
|
+
reference_hardware: Hardware,
|
|
58
|
+
*,
|
|
59
|
+
input_tokens: int = 0,
|
|
60
|
+
output_tokens: int = 0,
|
|
61
|
+
cached_input_tokens: int = 0,
|
|
62
|
+
candidate_regions: Sequence[tuple[Region, float]] = (),
|
|
63
|
+
candidate_hardware: Sequence[tuple[Hardware, float]] = (),
|
|
64
|
+
) -> Estimate:
|
|
65
|
+
"""``region`` and ``hardware`` are the provider's primary site and chip.
|
|
66
|
+
When the provider discloses several, the candidates set the ranges."""
|
|
67
|
+
chips = list(candidate_hardware) or [(hardware, 1.0)]
|
|
68
|
+
|
|
69
|
+
def regional(factor: str, floor: float = 0.0) -> Range:
|
|
70
|
+
return _regional(factor, region, provider.region_tier, candidate_regions, floor)
|
|
71
|
+
|
|
72
|
+
# 1. Work, in H100-seconds on the reference chip so the unit does not
|
|
73
|
+
# depend on the fleet; the fleet enters in step 2.
|
|
74
|
+
fresh_input = max(input_tokens - cached_input_tokens, 0)
|
|
75
|
+
work = _h100_seconds(
|
|
76
|
+
model,
|
|
77
|
+
reference_hardware,
|
|
78
|
+
fresh_input + cached_input_tokens * CACHED_INPUT_COST,
|
|
79
|
+
provider.prefill_utilization.as_range(),
|
|
80
|
+
)
|
|
81
|
+
measured = model.measured_wh_per_1k_output_tokens_h100
|
|
82
|
+
if measured is not None:
|
|
83
|
+
# Measured path: Wh on an H100 -> H100-seconds at its TDP.
|
|
84
|
+
wh = measured.as_range() * (output_tokens / 1000)
|
|
85
|
+
work += wh * 3600 / reference_hardware.tdp_w.as_range()
|
|
86
|
+
else:
|
|
87
|
+
work += _h100_seconds(
|
|
88
|
+
model,
|
|
89
|
+
reference_hardware,
|
|
90
|
+
output_tokens,
|
|
91
|
+
provider.decode_utilization.as_range(),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# 2. Energy at the meter. The chip ratio is how much energy this fleet
|
|
95
|
+
# spends per H100-second of work (B200 ~0.75, H200 ~0.85). Serving
|
|
96
|
+
# overhead covers host CPU/RAM/network and idle capacity: Google reports
|
|
97
|
+
# 0.24 Wh/prompt all-in vs 0.10 Wh for the accelerators alone.
|
|
98
|
+
chip_ratio = _envelope([(hw.energy_vs_h100.as_range(), w) for hw, w in chips])
|
|
99
|
+
energy_wh = (
|
|
100
|
+
work
|
|
101
|
+
* H100_TDP_W
|
|
102
|
+
/ 3600
|
|
103
|
+
* chip_ratio
|
|
104
|
+
* provider.serving_overhead.as_range()
|
|
105
|
+
* provider.pue.as_range()
|
|
106
|
+
)
|
|
107
|
+
kwh = energy_wh / 1000
|
|
108
|
+
|
|
109
|
+
# 3. Carbon. An uncertain site widens the grid range too: "somewhere in
|
|
110
|
+
# the US" spans ~100-700 gCO2e/kWh.
|
|
111
|
+
grid = regional("carbon_intensity_gco2e_per_kwh", floor=10.0)
|
|
112
|
+
embodied_rate = _envelope(
|
|
113
|
+
[(hw.embodied_gco2e_per_h100_second, w) for hw, w in chips]
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# 4. Water, in two parts because they follow different conventions.
|
|
117
|
+
# On-site is what the operator's WUE measures (and what Google's
|
|
118
|
+
# 0.26 mL per prompt counts). Generation water (WRI 2020) is the LCA
|
|
119
|
+
# convention; it is usually larger and, on hydro grids, contested.
|
|
120
|
+
return Estimate(
|
|
121
|
+
model=model,
|
|
122
|
+
provider=provider,
|
|
123
|
+
region=region,
|
|
124
|
+
hardware=hardware,
|
|
125
|
+
chips=tuple(hw.id for hw, _ in chips),
|
|
126
|
+
tokens=(input_tokens, output_tokens, cached_input_tokens),
|
|
127
|
+
h100_seconds=work,
|
|
128
|
+
chip_energy_ratio=chip_ratio,
|
|
129
|
+
energy_wh=energy_wh,
|
|
130
|
+
primary_energy_mj=kwh * 3.6 * regional("primary_energy_factor"),
|
|
131
|
+
grid_intensity=grid,
|
|
132
|
+
carbon_usage_g=kwh * grid,
|
|
133
|
+
carbon_embodied_g=work * embodied_rate,
|
|
134
|
+
water_onsite_ml=kwh * provider.wue_l_per_kwh.as_range() * 1000,
|
|
135
|
+
water_generation_ml=kwh * regional("water_l_per_kwh_generation") * 1000,
|
|
136
|
+
reasons=_assumed_inputs(model, provider, region, [hw for hw, _ in chips]),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass(frozen=True)
|
|
141
|
+
class Estimate:
|
|
142
|
+
model: Model
|
|
143
|
+
provider: Provider
|
|
144
|
+
region: Region
|
|
145
|
+
hardware: Hardware
|
|
146
|
+
chips: tuple[str, ...]
|
|
147
|
+
tokens: tuple[int, int, int] # input, output, cached input
|
|
148
|
+
h100_seconds: Range
|
|
149
|
+
chip_energy_ratio: Range # vs H100, envelope over the fleet
|
|
150
|
+
energy_wh: Range
|
|
151
|
+
primary_energy_mj: Range
|
|
152
|
+
grid_intensity: Range
|
|
153
|
+
carbon_usage_g: Range
|
|
154
|
+
carbon_embodied_g: Range
|
|
155
|
+
water_onsite_ml: Range
|
|
156
|
+
water_generation_ml: Range
|
|
157
|
+
reasons: list[str]
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def carbon_g(self) -> Range:
|
|
161
|
+
return self.carbon_usage_g + self.carbon_embodied_g
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def water_ml(self) -> Range:
|
|
165
|
+
return self.water_onsite_ml + self.water_generation_ml
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def confidence_level(self) -> str:
|
|
169
|
+
ratio = self.carbon_g.ratio
|
|
170
|
+
if ratio < HIGH_CONFIDENCE_RATIO:
|
|
171
|
+
return "high"
|
|
172
|
+
if ratio < MEDIUM_CONFIDENCE_RATIO:
|
|
173
|
+
return "medium"
|
|
174
|
+
return "low"
|
|
175
|
+
|
|
176
|
+
def to_dict(self) -> dict:
|
|
177
|
+
input_tokens, output_tokens, cached_input_tokens = self.tokens
|
|
178
|
+
grid = self.grid_intensity
|
|
179
|
+
return {
|
|
180
|
+
"model": self.model.id,
|
|
181
|
+
"provider": self.provider.id,
|
|
182
|
+
"tokens": {
|
|
183
|
+
"input": input_tokens,
|
|
184
|
+
"output": output_tokens,
|
|
185
|
+
"cached_input": cached_input_tokens,
|
|
186
|
+
},
|
|
187
|
+
"carbon": self.carbon_g.to_dict("gCO2e"),
|
|
188
|
+
"energy": {
|
|
189
|
+
**self.energy_wh.to_dict("Wh"),
|
|
190
|
+
"primary_energy_mj": sig_round(self.primary_energy_mj.mid),
|
|
191
|
+
},
|
|
192
|
+
"water": {
|
|
193
|
+
**self.water_ml.to_dict("mL"),
|
|
194
|
+
"on_site": self.water_onsite_ml.to_dict("mL"),
|
|
195
|
+
"generation": self.water_generation_ml.to_dict("mL"),
|
|
196
|
+
},
|
|
197
|
+
"confidence": {
|
|
198
|
+
"level": self.confidence_level,
|
|
199
|
+
"ratio": round(self.carbon_g.ratio, 2),
|
|
200
|
+
"reasons": self.reasons,
|
|
201
|
+
},
|
|
202
|
+
"electricity": {
|
|
203
|
+
"region": self.region.id,
|
|
204
|
+
"country": self.region.country,
|
|
205
|
+
"gco2e_per_kwh": sig_round(grid.mid),
|
|
206
|
+
"gco2e_per_kwh_range": [sig_round(grid.lo), sig_round(grid.hi)],
|
|
207
|
+
"primary_source": self.region.primary_source.value,
|
|
208
|
+
"mix": {k.value: v for k, v in self.region.mix.items()},
|
|
209
|
+
"dataset_year": self.region.dataset_year,
|
|
210
|
+
},
|
|
211
|
+
"breakdown": {
|
|
212
|
+
"h100_seconds": self.h100_seconds.to_dict("s"),
|
|
213
|
+
"usage": {"gco2e": sig_round(self.carbon_usage_g.mid)},
|
|
214
|
+
"embodied": {"gco2e": sig_round(self.carbon_embodied_g.mid)},
|
|
215
|
+
},
|
|
216
|
+
"assumptions": {
|
|
217
|
+
"active_params_b": {
|
|
218
|
+
"min": self.model.active_params_b.min,
|
|
219
|
+
"max": self.model.active_params_b.max,
|
|
220
|
+
},
|
|
221
|
+
"hardware": list(self.chips),
|
|
222
|
+
"chip_energy_vs_h100": sig_round(self.chip_energy_ratio.mid),
|
|
223
|
+
"pue": self.provider.pue.value,
|
|
224
|
+
"decode_utilization": self.provider.decode_utilization.value,
|
|
225
|
+
"serving_overhead": self.provider.serving_overhead.value,
|
|
226
|
+
"method_version": METHOD_VERSION,
|
|
227
|
+
},
|
|
228
|
+
"provenance": {
|
|
229
|
+
name: {
|
|
230
|
+
"trust": record.provenance.trust.value,
|
|
231
|
+
"status": record.provenance.status.value,
|
|
232
|
+
"generated_by": record.provenance.generated_by,
|
|
233
|
+
"stale": record.provenance.is_stale(),
|
|
234
|
+
}
|
|
235
|
+
for name, record in (
|
|
236
|
+
("model", self.model),
|
|
237
|
+
("provider", self.provider),
|
|
238
|
+
("region", self.region),
|
|
239
|
+
("hardware", self.hardware),
|
|
240
|
+
)
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _h100_seconds(
|
|
246
|
+
model: Model, reference: Hardware, tokens: float, utilization: Range
|
|
247
|
+
) -> Range:
|
|
248
|
+
"""A forward pass is ~2 FLOPs per active parameter per token."""
|
|
249
|
+
if tokens <= 0:
|
|
250
|
+
return Range(0.0, 0.0, 0.0)
|
|
251
|
+
flops = model.active_params_b.as_range() * 2e9 * tokens
|
|
252
|
+
return flops / (reference.peak_dense_flops.as_range() * utilization)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _envelope(ranges: Sequence[tuple[Range, float]]) -> Range:
|
|
256
|
+
"""Weighted mean as the point, min/max of the members as the range."""
|
|
257
|
+
total = sum(w for _, w in ranges)
|
|
258
|
+
return Range(
|
|
259
|
+
min(r.lo for r, _ in ranges),
|
|
260
|
+
sum(r.mid * w for r, w in ranges) / total,
|
|
261
|
+
max(r.hi for r, _ in ranges),
|
|
262
|
+
min(r.wlo for r, _ in ranges),
|
|
263
|
+
max(r.whi for r, _ in ranges),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _regional(
|
|
268
|
+
factor: str,
|
|
269
|
+
region: Region,
|
|
270
|
+
region_tier: Tier,
|
|
271
|
+
candidates: Sequence[tuple[Region, float]],
|
|
272
|
+
floor: float,
|
|
273
|
+
) -> Range:
|
|
274
|
+
"""A per-kWh regional factor for a provider whose site may be uncertain.
|
|
275
|
+
|
|
276
|
+
Disclosed candidate sites give an envelope, which is evidence. Without
|
|
277
|
+
them, the national figure is widened by the region tier's half-width.
|
|
278
|
+
"""
|
|
279
|
+
if candidates:
|
|
280
|
+
return _envelope([(getattr(c, factor).as_range(), w) for c, w in candidates])
|
|
281
|
+
r = getattr(region, factor).as_range()
|
|
282
|
+
if region_tier == Tier.primary:
|
|
283
|
+
return r
|
|
284
|
+
half_width = DEFAULT_HALF_WIDTH[region_tier]
|
|
285
|
+
return Range(max(r.lo / half_width, floor), r.mid, r.hi * half_width)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _assumed_inputs(
|
|
289
|
+
model: Model, provider: Provider, region: Region, chips: Sequence[Hardware]
|
|
290
|
+
) -> list[str]:
|
|
291
|
+
"""Reason codes: every tier-3 input, most important first."""
|
|
292
|
+
measured = model.measured_wh_per_1k_output_tokens_h100 is not None
|
|
293
|
+
reasons = []
|
|
294
|
+
if not measured and not model.open_weights:
|
|
295
|
+
reasons.append("active_params_undisclosed")
|
|
296
|
+
tiers = {
|
|
297
|
+
# measured energy replaces the parameter count on the decode path
|
|
298
|
+
"active_params_estimated": None if measured else model.active_params_b.tier,
|
|
299
|
+
"region_inferred": provider.region_tier,
|
|
300
|
+
"hardware_assumed": provider.hardware_tier,
|
|
301
|
+
"pue_assumed": provider.pue.tier,
|
|
302
|
+
"wue_assumed": provider.wue_l_per_kwh.tier,
|
|
303
|
+
"utilization_assumed": provider.decode_utilization.tier,
|
|
304
|
+
"serving_overhead_assumed": provider.serving_overhead.tier,
|
|
305
|
+
"grid_intensity_national_average": region.carbon_intensity_gco2e_per_kwh.tier,
|
|
306
|
+
"embodied_carbon_default": max(hw.embodied_tier for hw in chips),
|
|
307
|
+
}
|
|
308
|
+
return reasons + [code for code, tier in tiers.items() if tier == Tier.assumed]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Reference hardware. H100 SXM is the unit everything is normalised to.
|
|
2
|
+
# Leaf shape: {value, unit, tier, [min, max], source, note}
|
|
3
|
+
hardware:
|
|
4
|
+
h100-sxm:
|
|
5
|
+
label: NVIDIA H100 SXM5 80GB
|
|
6
|
+
tdp_w:
|
|
7
|
+
value: 700
|
|
8
|
+
unit: W
|
|
9
|
+
tier: 1
|
|
10
|
+
source: https://www.nvidia.com/en-us/data-center/h100/
|
|
11
|
+
note: Board TDP. Real draw under inference load is lower; captured via utilization.
|
|
12
|
+
peak_dense_flops:
|
|
13
|
+
value: 9.89e14
|
|
14
|
+
unit: FLOP/s
|
|
15
|
+
tier: 1
|
|
16
|
+
source: https://www.nvidia.com/en-us/data-center/h100/
|
|
17
|
+
note: BF16 dense, no sparsity.
|
|
18
|
+
# Embodied carbon per H100-second = kg / (years * active fraction). Kept as
|
|
19
|
+
# three inputs so the range is propagated instead of stacking every extreme
|
|
20
|
+
# (276 kg over 6 busy years vs 985 kg over 3 half-idle years is a 14x span
|
|
21
|
+
# that no single deployment sits at both ends of).
|
|
22
|
+
embodied_kgco2e_per_gpu:
|
|
23
|
+
value: 500
|
|
24
|
+
min: 276
|
|
25
|
+
max: 985
|
|
26
|
+
unit: kgCO2e per GPU incl. server share
|
|
27
|
+
tier: 2
|
|
28
|
+
source: https://images.nvidia.com/aem-dam/Solutions/documents/HGX-H100-PCF-Summary.pdf
|
|
29
|
+
note: >-
|
|
30
|
+
Cradle-to-gate GPU 164 kgCO2e (NVIDIA HGX H100 PCF, 1,312 kg / 8) to
|
|
31
|
+
273 kgCO2e (ADEME, Lees-Perasso et al. 2026), plus server share per GPU
|
|
32
|
+
112 kg (BoaviztAPI default server / 8) to 712 kg (EcoLogits p5.48xlarge
|
|
33
|
+
5,700 kg / 8). Min and max are the sums of the low and high ends.
|
|
34
|
+
lifetime_years:
|
|
35
|
+
value: 4.5
|
|
36
|
+
min: 3
|
|
37
|
+
max: 6
|
|
38
|
+
unit: years
|
|
39
|
+
tier: 3
|
|
40
|
+
source: assumption
|
|
41
|
+
note: Policy choice, not a measurement. 3 years is the pessimistic economic life of an AI accelerator, 6 the longest server depreciation schedule in use.
|
|
42
|
+
active_fraction:
|
|
43
|
+
value: 0.7
|
|
44
|
+
min: 0.5
|
|
45
|
+
max: 1.0
|
|
46
|
+
unit: fraction of lifetime spent serving
|
|
47
|
+
tier: 3
|
|
48
|
+
source: assumption
|
|
49
|
+
note: Assumed. Embodied carbon is charged only to seconds that do work, so idle time raises the per-second rate.
|
|
50
|
+
energy_vs_h100:
|
|
51
|
+
value: 1.0
|
|
52
|
+
min: 1.0
|
|
53
|
+
max: 1.0
|
|
54
|
+
unit: ratio
|
|
55
|
+
tier: 1
|
|
56
|
+
source: definition
|
|
57
|
+
note: Reference chip by definition.
|
|
58
|
+
provenance:
|
|
59
|
+
generated: {by: agent:claude-fable-5-1, at: 2026-09-18}
|
|
60
|
+
verified: []
|
|
61
|
+
status: draft
|
|
62
|
+
stale_after: 2027-03-31
|
|
63
|
+
sources:
|
|
64
|
+
- {resource: https://www.nvidia.com/en-us/data-center/h100/}
|
|
65
|
+
- {resource: https://images.nvidia.com/aem-dam/Solutions/documents/HGX-H100-PCF-Summary.pdf}
|
|
66
|
+
- {resource: https://www.actu-environnement.com/media/pdf/news-47749-acv-gpu-ademe.pdf}
|
|
67
|
+
- {resource: https://ecologits.ai/latest/methodology/llm_inference/}
|
|
68
|
+
- {resource: https://doc.api.boavizta.org/getting_started/single_server/}
|
|
69
|
+
|
|
70
|
+
# ---- Newer chips. Work stays in H100-seconds; each chip carries the energy it
|
|
71
|
+
# spends per H100-second of work relative to an H100. The honest finding from
|
|
72
|
+
# ML.ENERGY's paired H100/B200 runs (55 matched batch sizes, 10 models, Feb
|
|
73
|
+
# 2026): a newer chip is only greener when it is kept busy. gpt-oss-120b on
|
|
74
|
+
# B200 uses 0.66 to 0.80x the energy per token; a 1 kW B200 serving an 8B
|
|
75
|
+
# dense model at batch 8 uses 1.9x. Median across every pair is 0.98.
|
|
76
|
+
# Embodied figures for non-H100 chips are scaled from the H100 record by die
|
|
77
|
+
# area and HBM and are tier 3 until a PCF exists.
|
|
78
|
+
|
|
79
|
+
h200-sxm:
|
|
80
|
+
label: NVIDIA H200 SXM 141GB
|
|
81
|
+
tdp_w: {value: 700, unit: W, tier: 1, source: https://www.nvidia.com/en-us/data-center/h200/, note: "Up to 700W (configurable)"}
|
|
82
|
+
peak_dense_flops: {value: 9.89e14, unit: FLOP/s, tier: 1, source: https://www.nvidia.com/en-us/data-center/h200/, note: "Same GH100 die; 1,979 TFLOPS BF16 with sparsity = 989 dense."}
|
|
83
|
+
embodied_kgco2e_per_gpu: {value: 550, min: 300, max: 1080, unit: kgCO2e per GPU incl. server share, tier: 3, source: derived, note: "H100 record x1.1 for 141 GB HBM3e vs 80 GB; no PCF published."}
|
|
84
|
+
lifetime_years: {value: 4.5, min: 3, max: 6, unit: years, tier: 3, source: assumption, note: Same policy as H100.}
|
|
85
|
+
active_fraction: {value: 0.7, min: 0.5, max: 1.0, unit: fraction, tier: 3, source: assumption, note: Same policy as H100.}
|
|
86
|
+
energy_vs_h100:
|
|
87
|
+
value: 0.85
|
|
88
|
+
min: 0.7
|
|
89
|
+
max: 1.0
|
|
90
|
+
unit: ratio
|
|
91
|
+
tier: 2
|
|
92
|
+
source: https://www.nvidia.com/en-us/data-center/h200/
|
|
93
|
+
note: "Spec-derived: identical compute and TDP, 4.8 vs 3.35 TB/s HBM. Decode is memory-bound so the ceiling is 0.7x; prefill is unchanged at 1.0x. No ML.ENERGY pair yet."
|
|
94
|
+
provenance: {generated: {by: agent:claude-fable-5-1, at: 2026-09-22}, verified: [], status: draft, stale_after: 2027-03-31, sources: [{resource: https://www.nvidia.com/en-us/data-center/h200/}]}
|
|
95
|
+
|
|
96
|
+
b200-sxm:
|
|
97
|
+
label: NVIDIA B200 SXM 180GB
|
|
98
|
+
tdp_w: {value: 1000, unit: W, tier: 1, source: https://www.nvidia.com/en-us/data-center/dgx-b200/, note: "DGX B200 ~14.3 kW for 8 GPUs; per-GPU TDP up to 1,000 W."}
|
|
99
|
+
peak_dense_flops: {value: 2.25e15, unit: FLOP/s, tier: 1, source: https://www.nvidia.com/en-us/data-center/dgx-b200/, note: "FP8 72 PFLOPS sparse per 8-GPU system = 4.5 PFLOPS dense FP8 per GPU = 2.25 PFLOPS dense BF16."}
|
|
100
|
+
embodied_kgco2e_per_gpu: {value: 750, min: 414, max: 1478, unit: kgCO2e per GPU incl. server share, tier: 3, source: derived, note: "H100 record x1.5: dual-die 208 B transistors, 180 to 192 GB HBM3e, heavier board. No PCF published."}
|
|
101
|
+
lifetime_years: {value: 4.5, min: 3, max: 6, unit: years, tier: 3, source: assumption, note: Same policy as H100.}
|
|
102
|
+
active_fraction: {value: 0.7, min: 0.5, max: 1.0, unit: fraction, tier: 3, source: assumption, note: Same policy as H100.}
|
|
103
|
+
energy_vs_h100:
|
|
104
|
+
value: 0.8
|
|
105
|
+
min: 0.65
|
|
106
|
+
max: 1.6
|
|
107
|
+
unit: ratio
|
|
108
|
+
tier: 1
|
|
109
|
+
source: https://raw.githubusercontent.com/ml-energy/leaderboard/master/public/data/tasks/gpqa.json
|
|
110
|
+
note: "Measured. ML.ENERGY v3.0 paired runs: gpt-oss-120b 0.66 to 0.80 across batch 8 to 2048, gpt-oss-20b 0.65 to 0.88, Qwen3-235B FP8 0.71 to 0.81; but BF16 dense 8 to 32B models at batch 8 to 16 run 1.2 to 2.0x because the 1 kW chip idles. Point 0.8 for the MoE, high-batch serving our providers do; max covers the small-dense-low-batch case."
|
|
111
|
+
provenance: {generated: {by: agent:claude-fable-5-1, at: 2026-09-22}, verified: [], status: draft, stale_after: 2027-03-31, sources: [{resource: https://www.nvidia.com/en-us/data-center/dgx-b200/}, {resource: https://raw.githubusercontent.com/ml-energy/leaderboard/master/public/data/tasks/gpqa.json}]}
|
|
112
|
+
|
|
113
|
+
mi300x:
|
|
114
|
+
label: AMD Instinct MI300X 192GB
|
|
115
|
+
tdp_w: {value: 750, unit: W, tier: 1, source: https://www.amd.com/content/dam/amd/en/documents/instinct-tech-docs/data-sheets/amd-instinct-mi300x-data-sheet.pdf, note: "Datasheet: Maximum TBP 750W (read from the PDF 2026-09-22)."}
|
|
116
|
+
peak_dense_flops: {value: 1.3074e15, unit: FLOP/s, tier: 1, source: https://www.amd.com/content/dam/amd/en/documents/instinct-tech-docs/data-sheets/amd-instinct-mi300x-data-sheet.pdf, note: "Datasheet: BFLOAT16 1307.4 TFLOPs dense, 2614.9 with sparsity; 192 GB HBM3 at 5.3 TB/s."}
|
|
117
|
+
embodied_kgco2e_per_gpu: {value: 650, min: 360, max: 1280, unit: kgCO2e per GPU incl. server share, tier: 3, source: derived, note: "H100 record x1.3: 8 XCD + 4 IOD chiplets, 192 GB HBM3. No PCF published."}
|
|
118
|
+
lifetime_years: {value: 4.5, min: 3, max: 6, unit: years, tier: 3, source: assumption, note: Same policy as H100.}
|
|
119
|
+
active_fraction: {value: 0.7, min: 0.5, max: 1.0, unit: fraction, tier: 3, source: assumption, note: Same policy as H100.}
|
|
120
|
+
energy_vs_h100:
|
|
121
|
+
value: 0.9
|
|
122
|
+
min: 0.7
|
|
123
|
+
max: 1.3
|
|
124
|
+
unit: ratio
|
|
125
|
+
tier: 3
|
|
126
|
+
note: "Spec says 0.7 to 0.8 (1.07x TBP, 1.32x FLOPS, 1.58x bandwidth vs H100) but ROCm inference kernels reach lower MFU than CUDA in every public benchmark, so the point sits near parity. No ML.ENERGY measurement."
|
|
127
|
+
source: https://www.amd.com/content/dam/amd/en/documents/instinct-tech-docs/data-sheets/amd-instinct-mi300x-data-sheet.pdf
|
|
128
|
+
provenance: {generated: {by: agent:claude-fable-5-1, at: 2026-09-22}, verified: [{by: agent:claude-fable-5-1, at: 2026-09-22}], status: draft, stale_after: 2027-03-31, sources: [{resource: https://www.amd.com/content/dam/amd/en/documents/instinct-tech-docs/data-sheets/amd-instinct-mi300x-data-sheet.pdf}]}
|
|
129
|
+
|
|
130
|
+
mi325x:
|
|
131
|
+
label: AMD Instinct MI325X 256GB
|
|
132
|
+
tdp_w: {value: 1000, unit: W, tier: 1, source: https://www.amd.com/content/dam/amd/en/documents/instinct-tech-docs/product-briefs/instinct-mi325x-datasheet.pdf, note: "Datasheet: Maximum TBP 1000W (read from the PDF 2026-09-22)."}
|
|
133
|
+
peak_dense_flops: {value: 1.3074e15, unit: FLOP/s, tier: 1, source: https://www.amd.com/content/dam/amd/en/documents/instinct-tech-docs/product-briefs/instinct-mi325x-datasheet.pdf, note: "Datasheet: BFLOAT16 1307.4 TFLOPs dense, 2614.9 with sparsity; 256 GB HBM3E at 6 TB/s. Same CDNA 3 compute as MI300X."}
|
|
134
|
+
embodied_kgco2e_per_gpu: {value: 700, min: 390, max: 1380, unit: kgCO2e per GPU incl. server share, tier: 3, source: derived, note: "H100 record x1.4: MI300X package plus 256 GB HBM3e. No PCF published."}
|
|
135
|
+
lifetime_years: {value: 4.5, min: 3, max: 6, unit: years, tier: 3, source: assumption, note: Same policy as H100.}
|
|
136
|
+
active_fraction: {value: 0.7, min: 0.5, max: 1.0, unit: fraction, tier: 3, source: assumption, note: Same policy as H100.}
|
|
137
|
+
energy_vs_h100:
|
|
138
|
+
value: 1.0
|
|
139
|
+
min: 0.75
|
|
140
|
+
max: 1.4
|
|
141
|
+
unit: ratio
|
|
142
|
+
tier: 3
|
|
143
|
+
note: "Same compute as MI300X at 1.33x the power; the extra bandwidth helps decode but not enough to pay for the watts unless batches are large. Spec-derived, no measurement."
|
|
144
|
+
source: https://www.amd.com/content/dam/amd/en/documents/instinct-tech-docs/product-briefs/instinct-mi325x-datasheet.pdf
|
|
145
|
+
provenance: {generated: {by: agent:claude-fable-5-1, at: 2026-09-22}, verified: [{by: agent:claude-fable-5-1, at: 2026-09-22}], status: draft, stale_after: 2027-03-31, sources: [{resource: https://www.amd.com/content/dam/amd/en/documents/instinct-tech-docs/product-briefs/instinct-mi325x-datasheet.pdf}]}
|