forked from MakazhanAlpamys/Soup
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathenergy.py
More file actions
291 lines (258 loc) · 11 KB
/
Copy pathenergy.py
File metadata and controls
291 lines (258 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
"""CodeCarbon + electricityMap energy/CO2 capture (v0.59.0 Part F).
Lazy-imports ``codecarbon`` so the module loads cleanly without it. When
codecarbon is absent the public API returns ``None`` from ``measure_run_energy``
so callers can fall back gracefully.
The electricityMap endpoint is SSRF-validated with full parity to v0.51.0
``utils/hubs.validate_hub_endpoint``: scheme allowlist + loopback-only HTTP
+ RFC1918 / link-local / cloud-metadata rejection + null-byte / control
char / oversize rejection.
"""
from __future__ import annotations
import ipaddress
import logging
import math
import re
from dataclasses import dataclass
from types import TracebackType
from typing import Optional
from urllib.parse import urlsplit
_LOG = logging.getLogger(__name__)
_MAX_ENDPOINT_LEN = 2048
_CTRL_RE = re.compile(r"[\x00-\x1f\x7f]")
_LOOPBACK = frozenset({"localhost", "127.0.0.1", "::1"})
_SCHEMES = frozenset({"http", "https"})
_COUNTRY_RE = re.compile(r"^[A-Za-z]{3}$")
_DEFAULT_COUNTRY = "USA"
@dataclass(frozen=True)
class EnergyMeasurement:
"""One per-run energy + CO2 reading."""
energy_kwh: float
co2_kg: float
pue: float
grid_intensity_g_per_kwh: float
source: str
def __post_init__(self) -> None:
for value, name in (
(self.energy_kwh, "energy_kwh"),
(self.co2_kg, "co2_kg"),
(self.pue, "pue"),
(self.grid_intensity_g_per_kwh, "grid_intensity_g_per_kwh"),
):
if isinstance(value, bool):
raise ValueError(f"{name} must not be bool")
if not isinstance(value, (int, float)):
raise ValueError(f"{name} must be a number")
f = float(value)
if not math.isfinite(f):
raise ValueError(f"{name} must be finite")
if f < 0:
raise ValueError(f"{name} must be >= 0")
if self.pue < 1.0:
raise ValueError("pue must be >= 1.0")
if not isinstance(self.source, str) or "\x00" in self.source:
raise ValueError("source must be a null-byte-free str")
def validate_electricity_map_endpoint(endpoint: str) -> str:
"""SSRF-harden the electricityMap query endpoint.
Mirrors v0.51.0 ``validate_hub_endpoint``: scheme allowlist (http/https),
loopback-only HTTP, private-IP rejection, no control chars / null bytes.
"""
if not isinstance(endpoint, str):
raise ValueError("endpoint must be str")
if not endpoint:
raise ValueError("endpoint must be non-empty")
if "\x00" in endpoint:
raise ValueError("endpoint must not contain null bytes")
if len(endpoint) > _MAX_ENDPOINT_LEN:
raise ValueError(f"endpoint too long (> {_MAX_ENDPOINT_LEN})")
if _CTRL_RE.search(endpoint):
raise ValueError("endpoint must not contain control chars")
try:
parts = urlsplit(endpoint)
except ValueError as exc:
raise ValueError(f"endpoint unparseable: {exc}") from exc
scheme = parts.scheme.lower()
if scheme not in _SCHEMES:
raise ValueError(
f"endpoint scheme must be http or https, got {scheme!r}"
)
host = (parts.hostname or "").lower()
if not host:
raise ValueError("endpoint must have a host")
if host == "0.0.0.0":
raise ValueError("0.0.0.0 endpoints are rejected")
is_loopback = host in _LOOPBACK
if scheme == "http" and not is_loopback:
# Reject plain HTTP except for loopback.
raise ValueError(
"http:// only permitted for loopback hosts; use https:// for remote"
)
# Reject private / link-local / cloud-metadata IPs explicitly.
# ``parts.hostname`` already strips IPv6 brackets, so feed it directly.
try:
ip = ipaddress.ip_address(host)
except ValueError:
ip = None
if ip is not None and not is_loopback:
if ip.is_private or ip.is_link_local or ip.is_reserved or ip.is_multicast:
raise ValueError(
f"endpoint host {host!r} resolves to a private/link-local IP"
)
return endpoint
def adjust_for_pue(energy_kwh: float, pue: float) -> float:
"""Multiply raw energy by PUE (Power Usage Effectiveness).
PUE must be >= 1.0 (a data centre that does no overhead-cooling at all has
PUE == 1.0; typical hyperscale is 1.1–1.5).
"""
if isinstance(energy_kwh, bool) or isinstance(pue, bool):
raise ValueError("inputs must not be bool")
if not isinstance(energy_kwh, (int, float)) or not isinstance(pue, (int, float)):
raise ValueError("inputs must be numeric")
if not math.isfinite(float(energy_kwh)) or not math.isfinite(float(pue)):
raise ValueError("inputs must be finite")
if energy_kwh < 0:
raise ValueError("energy_kwh must be >= 0")
if pue < 1.0:
raise ValueError("pue must be >= 1.0")
return float(energy_kwh) * float(pue)
def measure_run_energy(
duration_seconds: float = 0.0,
*,
grid_intensity_g_per_kwh: float = 400.0,
pue: float = 1.1,
) -> Optional[EnergyMeasurement]:
"""Best-effort capture of a single run's energy + CO2.
Returns ``None`` when ``codecarbon`` is not installed AND ``duration_seconds``
is ``<= 0`` (degenerate inputs surface as None rather than a fake zero).
Live CodeCarbon hook wiring into trainer wrappers lands in v0.59.1.
"""
if isinstance(duration_seconds, bool) or isinstance(grid_intensity_g_per_kwh, bool):
raise ValueError("numeric inputs must not be bool")
if not isinstance(duration_seconds, (int, float)):
raise ValueError("duration_seconds must be numeric")
if not math.isfinite(float(duration_seconds)) or duration_seconds < 0:
raise ValueError("duration_seconds must be a finite non-negative number")
if not isinstance(grid_intensity_g_per_kwh, (int, float)):
raise ValueError("grid_intensity_g_per_kwh must be numeric")
if (
not math.isfinite(float(grid_intensity_g_per_kwh))
or grid_intensity_g_per_kwh < 0
):
raise ValueError("grid_intensity_g_per_kwh must be a finite non-negative number")
try:
adjust_for_pue(1.0, pue)
except ValueError as exc:
raise ValueError(f"invalid pue: {exc}") from exc
try:
import codecarbon # noqa: F401, PLC0415
except ImportError:
# No live codecarbon — return None so the caller (typically the BOM
# builder) can decide whether to omit the energy properties.
return None
# ``measure_run_energy`` is the duration-only fallback: with only a wall-
# clock number it cannot read instantaneous power draw, so it returns None
# even when codecarbon IS installed. Use ``EnergyTracker`` (v0.71.3 #180)
# for a real start()/stop() measurement around the training window.
return None
class EnergyTracker:
"""Context manager that measures a training window's energy + CO2.
Wraps codecarbon's ``OfflineEmissionsTracker`` (offline = no network /
no IP-geolocation call, so the privacy guarantee is preserved). Lazy-
imports codecarbon; when it is absent the tracker is a graceful no-op and
``measurement`` stays ``None``.
The energy reading (kWh) is country-independent — it is measured power ×
time. Only the CO2 conversion uses the chosen country's grid intensity
(``country_iso_code``, default ``"USA"``). Both energy and CO2 are scaled
by ``pue`` (Power Usage Effectiveness) so data-centre overhead is counted.
Usage::
with EnergyTracker(pue=1.1) as tracker:
trainer.train()
m = tracker.measurement # EnergyMeasurement or None
"""
def __init__(
self,
*,
pue: float = 1.1,
grid_intensity_g_per_kwh: float = 400.0,
country_iso_code: str = _DEFAULT_COUNTRY,
) -> None:
# Validate PUE up front (reuses the shared bounds checker).
adjust_for_pue(1.0, pue)
if isinstance(grid_intensity_g_per_kwh, bool) or not isinstance(
grid_intensity_g_per_kwh, (int, float)
):
raise ValueError("grid_intensity_g_per_kwh must be numeric")
if (
not math.isfinite(float(grid_intensity_g_per_kwh))
or grid_intensity_g_per_kwh < 0
):
raise ValueError("grid_intensity_g_per_kwh must be finite and >= 0")
if not isinstance(country_iso_code, str) or not _COUNTRY_RE.match(
country_iso_code
):
raise ValueError(
"country_iso_code must be a 3-letter ISO 3166-1 alpha-3 code "
f"(got {country_iso_code!r})"
)
self._pue = float(pue)
self._grid_default = float(grid_intensity_g_per_kwh)
self._country = country_iso_code.upper()
self._tracker = None
self._measurement: Optional[EnergyMeasurement] = None
@property
def measurement(self) -> Optional[EnergyMeasurement]:
return self._measurement
def __enter__(self) -> "EnergyTracker":
try:
from codecarbon import OfflineEmissionsTracker # noqa: PLC0415
self._tracker = OfflineEmissionsTracker(
country_iso_code=self._country,
save_to_file=False,
log_level="error",
)
self._tracker.start()
except Exception as exc: # noqa: BLE001 — never crash training
_LOG.debug("EnergyTracker: codecarbon unavailable/failed: %s", exc)
self._tracker = None
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> bool:
tracker = self._tracker
self._tracker = None
if tracker is None:
return False
try:
emissions = tracker.stop()
data = getattr(tracker, "final_emissions_data", None)
energy_raw = float(getattr(data, "energy_consumed", 0.0) or 0.0)
co2_raw = float(
emissions
if emissions is not None
else getattr(data, "emissions", 0.0) or 0.0
)
if not math.isfinite(energy_raw) or energy_raw < 0:
energy_raw = 0.0
if not math.isfinite(co2_raw) or co2_raw < 0:
co2_raw = 0.0
grid = (
(co2_raw / energy_raw * 1000.0)
if energy_raw > 0
else self._grid_default
)
if not math.isfinite(grid) or grid < 0:
grid = self._grid_default
self._measurement = EnergyMeasurement(
energy_kwh=adjust_for_pue(energy_raw, self._pue),
co2_kg=co2_raw * self._pue,
pue=self._pue,
grid_intensity_g_per_kwh=grid,
source="codecarbon-offline",
)
except Exception as exc: # noqa: BLE001 — never crash training
_LOG.debug("EnergyTracker: stop()/measurement failed: %s", exc)
self._measurement = None
# Always return False so a body exception propagates unmasked.
return False