codecarbon 3.3.1__tar.gz → 3.3.2__tar.gz
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.
- {codecarbon-3.3.1 → codecarbon-3.3.2}/PKG-INFO +15 -1
- codecarbon-3.3.2/codecarbon/_version.py +1 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/cli/main.py +5 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/cli/monitor.py +6 -7
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/cpu.py +41 -14
- codecarbon-3.3.2/codecarbon/core/electricitymaps_api.py +152 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/emissions.py +49 -4
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/gpu.py +13 -1
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/gpu_amd.py +11 -3
- codecarbon-3.3.2/codecarbon/core/gpu_device.py +298 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/gpu_nvidia.py +3 -2
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/hardware_cache.py +16 -6
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/powermetrics.py +24 -38
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/rapl.py +30 -5
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/resource_tracker.py +27 -2
- codecarbon-3.3.2/codecarbon/core/slurm.py +30 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/data/hardware/cpu_power.csv +2 -0
- codecarbon-3.3.2/codecarbon/data/private_infra/2023/china_provincial_emissions.json +292 -0
- {codecarbon-3.3.1/codecarbon/data/private_infra/2016 → codecarbon-3.3.2/codecarbon/data/private_infra/2023}/usa_emissions.json +157 -151
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/data/private_infra/carbon_intensity_per_source.json +7 -1
- codecarbon-3.3.2/codecarbon/data/private_infra/update_usa_emissions.py +145 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/emissions_tracker.py +125 -39
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/external/geography.py +1 -1
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/external/hardware.py +209 -50
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/external/ram.py +4 -3
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/input.py +14 -1
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/boamps/output.py +29 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/file.py +29 -18
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/viz/carbonboard.py +2 -2
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon.egg-info/PKG-INFO +15 -1
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon.egg-info/SOURCES.txt +10 -1
- {codecarbon-3.3.1 → codecarbon-3.3.2}/pyproject.toml +34 -2
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_boamps_output.py +6 -0
- codecarbon-3.3.2/tests/test_cpu_load.py +384 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_electricitymaps_api.py +13 -0
- codecarbon-3.3.2/tests/test_electricitymaps_cache.py +183 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_emissions.py +231 -2
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_emissions_tracker.py +245 -1
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_emissions_tracker_constant.py +3 -1
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_emissions_tracker_flush.py +13 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_geography.py +15 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_gpu_amd.py +35 -6
- codecarbon-3.3.2/tests/test_gpu_nvidia.py +938 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_hardware_cache.py +34 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_powermetrics.py +87 -1
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_ram.py +6 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_rapl_mmio_scanning.py +50 -0
- codecarbon-3.3.2/tests/test_rapl_permissions.py +278 -0
- codecarbon-3.3.2/tests/test_raspberry.py +253 -0
- codecarbon-3.3.2/tests/test_readme_links.py +26 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_resource_tracker.py +73 -0
- codecarbon-3.3.2/tests/test_slurm.py +40 -0
- codecarbon-3.3.2/tests/test_update_usa_emissions.py +102 -0
- codecarbon-3.3.2/tests/test_zero_carbon_intensity.py +34 -0
- codecarbon-3.3.1/codecarbon/_version.py +0 -1
- codecarbon-3.3.1/codecarbon/core/electricitymaps_api.py +0 -69
- codecarbon-3.3.1/codecarbon/core/gpu_device.py +0 -130
- codecarbon-3.3.1/tests/test_cpu_load.py +0 -195
- codecarbon-3.3.1/tests/test_gpu_nvidia.py +0 -469
- codecarbon-3.3.1/tests/test_rapl_permissions.py +0 -112
- {codecarbon-3.3.1 → codecarbon-3.3.2}/LICENSE +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/README.md +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/__init__.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/cli/__init__.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/cli/auth.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/cli/cli_utils.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/__init__.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/api_client.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/cloud.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/config.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/measure.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/schemas.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/telemetry_client.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/telemetry_schemas.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/units.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/util.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/windows_emi.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/data/canada_provinces.geojson +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/data/cloud/impact.csv +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/data/hardware/cpu_dataset_builder/amd_cpu_scrapper.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/data/hardware/cpu_dataset_builder/intel_cpu_scrapper.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/data/hardware/cpu_dataset_builder/merge_scrapped_cpu_power.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/data/private_infra/2023/canada_energy_mix.json +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/data/private_infra/global_energy_mix.json +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/data/private_infra/nordic_emissions.json +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/external/__init__.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/external/logger.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/external/scheduler.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/external/task.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/lock.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/__init__.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/base_output.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/boamps/__init__.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/boamps/mapper.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/boamps/models.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/emissions_data.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/http.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/logger.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/metrics/__init__.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/metrics/logfire.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/metrics/metric_docs.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/metrics/prometheus.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/py.typed +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/viz/__init__.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/viz/assets/__init__.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/viz/assets/car_icon.png +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/viz/assets/house_icon.png +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/viz/assets/tv_icon.png +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/viz/carbonboard_on_api.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/viz/components.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/viz/data.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/viz/units.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon.egg-info/dependency_links.txt +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon.egg-info/entry_points.txt +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon.egg-info/requires.txt +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon.egg-info/top_level.txt +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/setup.cfg +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_api_call.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_cloud.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_config.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_core_util.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_cpu.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_custom_handler.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_docs_examples.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_electricitymaps_backward_compatibility.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_electricitymaps_config_backward_compatibility.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_energy.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_gpu.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_input.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_lock.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_logger.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_logging_output.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_offline_emissions_tracker.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_package_integrity.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_rapl_parameters.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_telemetry_client.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_tracking_inference.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_unsupported_gpu.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_utilization_tracking.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_viz_data.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_viz_units.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_windows_emi.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/testdata.py +0 -0
- {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/testutils.py +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecarbon
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.2
|
|
4
|
+
Summary: Track and reduce the CO2 emissions of your computing
|
|
4
5
|
Author: Mila, DataForGood, BCG GAMMA, Comet.ml, Haverford College
|
|
5
6
|
License-Expression: MIT
|
|
6
7
|
Project-URL: Homepage, https://codecarbon.io/
|
|
@@ -9,12 +10,25 @@ Project-URL: Dashboard, http://dashboard.codecarbon.io/
|
|
|
9
10
|
Project-URL: Documentation, https://docs.codecarbon.io/
|
|
10
11
|
Project-URL: Issues, https://github.com/mlco2/codecarbon/issues
|
|
11
12
|
Project-URL: Changelog, https://github.com/mlco2/codecarbon/releases
|
|
13
|
+
Keywords: carbon,co2,emissions,carbon-footprint,sustainability,green-ai,green-computing,energy,energy-consumption,power-consumption,rapl,gpu,machine-learning,mlops,monitoring
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Intended Audience :: Science/Research
|
|
18
|
+
Classifier: Intended Audience :: System Administrators
|
|
12
19
|
Classifier: Natural Language :: English
|
|
20
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
21
|
+
Classifier: Operating System :: MacOS
|
|
22
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
13
23
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
24
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
25
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
26
|
Classifier: Programming Language :: Python :: 3.13
|
|
17
27
|
Classifier: Programming Language :: Python :: 3.14
|
|
28
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
29
|
+
Classifier: Topic :: System :: Monitoring
|
|
30
|
+
Classifier: Topic :: System :: Benchmark
|
|
31
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
32
|
Requires-Python: >=3.10
|
|
19
33
|
Description-Content-Type: text/markdown
|
|
20
34
|
License-File: LICENSE
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.3.2"
|
|
@@ -393,6 +393,10 @@ def _external_config() -> dict:
|
|
|
393
393
|
)
|
|
394
394
|
def monitor(
|
|
395
395
|
ctx: typer.Context,
|
|
396
|
+
project_name: Annotated[
|
|
397
|
+
str,
|
|
398
|
+
typer.Option(help="Project name for the current experiment."),
|
|
399
|
+
] = None,
|
|
396
400
|
measure_power_secs: Annotated[
|
|
397
401
|
int,
|
|
398
402
|
typer.Option(help="Interval between two measures."),
|
|
@@ -427,6 +431,7 @@ def monitor(
|
|
|
427
431
|
# Only the options actually given are forwarded: the others are left to the
|
|
428
432
|
# tracker, which resolves them from the configuration file and environment.
|
|
429
433
|
cli_defaults = (
|
|
434
|
+
("project_name", project_name),
|
|
430
435
|
("measure_power_secs", measure_power_secs),
|
|
431
436
|
("api_call_interval", api_call_interval),
|
|
432
437
|
("log_level", log_level),
|
|
@@ -120,13 +120,12 @@ def run_and_monitor(
|
|
|
120
120
|
else:
|
|
121
121
|
print(" Emissions: N/A")
|
|
122
122
|
|
|
123
|
-
# Show where the data was saved
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if
|
|
128
|
-
|
|
129
|
-
print(f" Saved to: {output_path}")
|
|
123
|
+
# Show where the data was saved, asking the output handlers themselves
|
|
124
|
+
# so the path stays right whatever `output_dir` / `output_file` are.
|
|
125
|
+
for handler in getattr(tracker, "_output_handlers", []):
|
|
126
|
+
save_file_path = getattr(handler, "save_file_path", None)
|
|
127
|
+
if save_file_path:
|
|
128
|
+
print(f" Saved to: {os.path.abspath(save_file_path)}")
|
|
130
129
|
|
|
131
130
|
print(" ⚠️ Note: Tracked the command process and its children")
|
|
132
131
|
print("=" * 60)
|
|
@@ -33,6 +33,13 @@ if TYPE_CHECKING:
|
|
|
33
33
|
# default W value per core for a CPU if no model is found in the ref csv
|
|
34
34
|
DEFAULT_POWER_PER_CORE = 4
|
|
35
35
|
|
|
36
|
+
_TROUBLESHOOTING_URL = "https://docs.codecarbon.io/latest/how-to/troubleshooting/"
|
|
37
|
+
RAPL_PERMISSION_HELP = (
|
|
38
|
+
"You can grant read permission with: sudo chmod -R a+r /sys/class/powercap/* "
|
|
39
|
+
"(this does not persist across reboots, see "
|
|
40
|
+
f"{_TROUBLESHOOTING_URL}#rapl-permission-denied for a permanent setup)"
|
|
41
|
+
)
|
|
42
|
+
|
|
36
43
|
|
|
37
44
|
@lru_cache(maxsize=1)
|
|
38
45
|
def is_powergadget_available() -> bool:
|
|
@@ -180,18 +187,16 @@ def _create_warn_function():
|
|
|
180
187
|
nonlocal already_warned
|
|
181
188
|
if not already_warned:
|
|
182
189
|
logger.warning(
|
|
183
|
-
"\tRAPL - Permission denied reading RAPL file %s. "
|
|
184
|
-
"You can grant read permission with: "
|
|
185
|
-
"sudo chmod -R a+r /sys/class/powercap/*",
|
|
190
|
+
"\tRAPL - Permission denied reading RAPL file %s. %s",
|
|
186
191
|
energy_path,
|
|
192
|
+
RAPL_PERMISSION_HELP,
|
|
187
193
|
)
|
|
188
194
|
already_warned = True
|
|
189
195
|
else:
|
|
190
196
|
logger.debug(
|
|
191
|
-
"\tRAPL - Permission denied reading RAPL file %s. "
|
|
192
|
-
"You can grant read permission with: "
|
|
193
|
-
"sudo chmod -R a+r /sys/class/powercap/*",
|
|
197
|
+
"\tRAPL - Permission denied reading RAPL file %s. %s",
|
|
194
198
|
energy_path,
|
|
199
|
+
RAPL_PERMISSION_HELP,
|
|
195
200
|
)
|
|
196
201
|
|
|
197
202
|
return warn_permission_denied
|
|
@@ -253,6 +258,24 @@ def is_psutil_available():
|
|
|
253
258
|
return False
|
|
254
259
|
|
|
255
260
|
|
|
261
|
+
def is_raspberry() -> bool:
|
|
262
|
+
"""
|
|
263
|
+
Check if the Raspberry Pi PMIC power readings are available.
|
|
264
|
+
|
|
265
|
+
`vcgencmd` ships with every Raspberry Pi OS, but only the Pi 5 answers
|
|
266
|
+
`pmic_read_adc`: older boards fall back to the TDP of their CPU.
|
|
267
|
+
"""
|
|
268
|
+
if not os.path.exists("/usr/bin/vcgencmd"):
|
|
269
|
+
return False
|
|
270
|
+
try:
|
|
271
|
+
res = subprocess.run(
|
|
272
|
+
["vcgencmd", "pmic_read_adc"], capture_output=True, timeout=5
|
|
273
|
+
)
|
|
274
|
+
except (OSError, subprocess.SubprocessError):
|
|
275
|
+
return False
|
|
276
|
+
return res.returncode == 0 and b"volt(" in res.stdout
|
|
277
|
+
|
|
278
|
+
|
|
256
279
|
class IntelPowerGadget:
|
|
257
280
|
"""
|
|
258
281
|
A class to interface with Intel Power Gadget for monitoring CPU power consumption on Windows and (non-Apple Silicon) macOS.
|
|
@@ -489,7 +512,7 @@ class IntelRAPL:
|
|
|
489
512
|
"""Scan subdirectories of a RAPL entry for domain directories."""
|
|
490
513
|
subdirs = []
|
|
491
514
|
try:
|
|
492
|
-
for sub in os.listdir(entry_path):
|
|
515
|
+
for sub in sorted(os.listdir(entry_path)):
|
|
493
516
|
sub_path = os.path.join(entry_path, sub)
|
|
494
517
|
if ":" in sub and os.path.isdir(sub_path):
|
|
495
518
|
if os.path.exists(os.path.join(sub_path, "energy_uj")):
|
|
@@ -508,7 +531,7 @@ class IntelRAPL:
|
|
|
508
531
|
"""Scan a base directory for intel-rapl entries."""
|
|
509
532
|
domain_dirs = []
|
|
510
533
|
try:
|
|
511
|
-
for entry in os.listdir(base):
|
|
534
|
+
for entry in sorted(os.listdir(base)):
|
|
512
535
|
if not entry.startswith("intel-rapl"):
|
|
513
536
|
continue
|
|
514
537
|
entry_path = os.path.join(base, entry)
|
|
@@ -537,7 +560,7 @@ class IntelRAPL:
|
|
|
537
560
|
if domain_dirs:
|
|
538
561
|
return domain_dirs
|
|
539
562
|
try:
|
|
540
|
-
for item in os.listdir(self._lin_rapl_dir):
|
|
563
|
+
for item in sorted(os.listdir(self._lin_rapl_dir)):
|
|
541
564
|
if ":" in item:
|
|
542
565
|
path = os.path.join(self._lin_rapl_dir, item)
|
|
543
566
|
if os.path.isdir(path) and os.path.exists(
|
|
@@ -592,8 +615,7 @@ class IntelRAPL:
|
|
|
592
615
|
return True, is_required_main
|
|
593
616
|
except PermissionError:
|
|
594
617
|
msg = f"\tRAPL - Permission denied reading RAPL file {rapl_file}."
|
|
595
|
-
|
|
596
|
-
logger.warning("%s %s; skipping.", msg, suggestion)
|
|
618
|
+
logger.warning("%s %s; skipping.", msg, RAPL_PERMISSION_HELP)
|
|
597
619
|
return False, False
|
|
598
620
|
except Exception as e:
|
|
599
621
|
logger.debug(
|
|
@@ -893,6 +915,7 @@ class IntelRAPL:
|
|
|
893
915
|
(rapl_file.path, float(rapl_file.last_energy))
|
|
894
916
|
for rapl_file in self._rapl_files
|
|
895
917
|
if "dram" not in rapl_file.name.lower()
|
|
918
|
+
and rapl_file.last_energy is not None
|
|
896
919
|
]
|
|
897
920
|
self._mirrored_candidates = find_mirrored_counters(
|
|
898
921
|
counters, SEQUENTIAL_READ_TOLERANCE_KWH
|
|
@@ -928,7 +951,10 @@ class IntelRAPL:
|
|
|
928
951
|
continue
|
|
929
952
|
delta = float(rapl_file.energy_delta)
|
|
930
953
|
reference_delta = float(reference.energy_delta)
|
|
931
|
-
if
|
|
954
|
+
if rapl_file.sample_skipped or reference.sample_skipped:
|
|
955
|
+
# A skipped sample reports no energy: the interval is not conclusive
|
|
956
|
+
pending[path] = reference_path
|
|
957
|
+
elif delta <= 0 and reference_delta <= 0:
|
|
932
958
|
# Nothing was accumulated: the interval is not conclusive
|
|
933
959
|
pending[path] = reference_path
|
|
934
960
|
elif counters_match(delta, reference_delta, SEQUENTIAL_READ_TOLERANCE_KWH):
|
|
@@ -1088,9 +1114,10 @@ class TDP:
|
|
|
1088
1114
|
)
|
|
1089
1115
|
return cpu_model_detected, power
|
|
1090
1116
|
logger.warning(
|
|
1091
|
-
"We saw that you have a %s but we don't know it."
|
|
1092
|
-
|
|
1117
|
+
"We saw that you have a %s but we don't know it. "
|
|
1118
|
+
"Please help us add it, see %s#unknown-cpu-model",
|
|
1093
1119
|
cpu_model_detected,
|
|
1120
|
+
_TROUBLESHOOTING_URL,
|
|
1094
1121
|
)
|
|
1095
1122
|
if is_psutil_available():
|
|
1096
1123
|
# Count thread of the CPU
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import time
|
|
3
|
+
from typing import Any, Dict, Tuple
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from codecarbon.core.units import EmissionsPerKWh, Energy
|
|
8
|
+
from codecarbon.external.geography import GeoMetadata
|
|
9
|
+
from codecarbon.external.logger import logger
|
|
10
|
+
|
|
11
|
+
_Key = Tuple[Tuple[Tuple[str, Any], ...], str]
|
|
12
|
+
|
|
13
|
+
URL: str = "https://api.electricitymaps.com/v3/carbon-intensity/latest"
|
|
14
|
+
# Kept short: the lookup runs on the measurement thread, which stop() joins.
|
|
15
|
+
ELECTRICITYMAPS_API_TIMEOUT: int = 10
|
|
16
|
+
|
|
17
|
+
# Grid carbon intensity is published hourly at best, while emissions are computed
|
|
18
|
+
# on every measurement tick, so the value is cached instead of refetched.
|
|
19
|
+
ELECTRICITYMAPS_CACHE_TTL: int = 60
|
|
20
|
+
# After a failure (bad token, network down), wait before retrying instead of
|
|
21
|
+
# issuing one doomed request per measurement tick. The wait doubles on each
|
|
22
|
+
# consecutive failure, up to the maximum, and resets on success.
|
|
23
|
+
ELECTRICITYMAPS_COOLDOWN: int = 30
|
|
24
|
+
ELECTRICITYMAPS_MAX_COOLDOWN: int = 3600
|
|
25
|
+
|
|
26
|
+
# {(sorted query params, token): (monotonic fetch time, intensity in gCO2e/kWh)}
|
|
27
|
+
_cache: Dict[_Key, Tuple[float, float]] = {}
|
|
28
|
+
# {cache key: (monotonic time until which requests are skipped, cooldown length)}
|
|
29
|
+
# Keyed like the cache: one tracker's bad token must not block another's good one.
|
|
30
|
+
_cooldown: Dict[_Key, Tuple[float, float]] = {}
|
|
31
|
+
# The state above is read-modify-written from the measurement thread.
|
|
32
|
+
_lock = threading.Lock()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def reset_cache() -> None:
|
|
36
|
+
"""Drop the cached carbon intensities and any pending failure cooldown."""
|
|
37
|
+
with _lock:
|
|
38
|
+
_cache.clear()
|
|
39
|
+
_cooldown.clear()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _start_cooldown(key: _Key) -> None:
|
|
43
|
+
with _lock:
|
|
44
|
+
previous = _cooldown.get(key, (0.0, 0.0))[1]
|
|
45
|
+
delay = min(
|
|
46
|
+
max(previous * 2, ELECTRICITYMAPS_COOLDOWN), ELECTRICITYMAPS_MAX_COOLDOWN
|
|
47
|
+
)
|
|
48
|
+
_cooldown[key] = (time.monotonic() + delay, delay)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_carbon_intensity(
|
|
52
|
+
geo: GeoMetadata, electricitymaps_api_token: str = ""
|
|
53
|
+
) -> float:
|
|
54
|
+
"""
|
|
55
|
+
Retrieve the carbon intensity of the grid, in gCO2e/kWh, from the Electricity
|
|
56
|
+
Maps API (formerly CO2 Signal) for the given geographic location.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ElectricityMapsAPIError: the request failed, returned an error, or was
|
|
60
|
+
skipped because a previous one failed (``...CooldownError``).
|
|
61
|
+
"""
|
|
62
|
+
if geo.latitude is not None:
|
|
63
|
+
params: Dict[str, Any] = {"lat": geo.latitude, "lon": geo.longitude}
|
|
64
|
+
else:
|
|
65
|
+
params = {"countryCode": geo.country_2letter_iso_code}
|
|
66
|
+
key = (tuple(sorted(params.items())), electricitymaps_api_token)
|
|
67
|
+
with _lock:
|
|
68
|
+
cached = _cache.get(key)
|
|
69
|
+
cooldown_until = _cooldown.get(key, (0.0, 0.0))[0]
|
|
70
|
+
if cached and time.monotonic() - cached[0] <= ELECTRICITYMAPS_CACHE_TTL:
|
|
71
|
+
logger.debug(
|
|
72
|
+
f"electricitymaps_api: using cached carbon intensity {cached[1]} gCO2e/kWh"
|
|
73
|
+
)
|
|
74
|
+
return cached[1]
|
|
75
|
+
|
|
76
|
+
remaining = cooldown_until - time.monotonic()
|
|
77
|
+
if remaining > 0:
|
|
78
|
+
raise ElectricityMapsAPICooldownError(
|
|
79
|
+
"Electricity Maps API is in cooldown after a previous failure, "
|
|
80
|
+
f"retrying in {remaining:.0f} seconds"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
resp = requests.get(
|
|
85
|
+
URL,
|
|
86
|
+
params=params,
|
|
87
|
+
headers={"auth-token": electricitymaps_api_token},
|
|
88
|
+
timeout=ELECTRICITYMAPS_API_TIMEOUT,
|
|
89
|
+
)
|
|
90
|
+
if resp.status_code != 200:
|
|
91
|
+
try:
|
|
92
|
+
body = resp.json()
|
|
93
|
+
except ValueError:
|
|
94
|
+
body = {}
|
|
95
|
+
raise ElectricityMapsAPIError(
|
|
96
|
+
body.get("error") or body.get("message") or resp.text
|
|
97
|
+
)
|
|
98
|
+
# API v3 response structure: carbonIntensity is at the root level
|
|
99
|
+
carbon_intensity_g_per_kWh = resp.json().get("carbonIntensity")
|
|
100
|
+
if carbon_intensity_g_per_kWh is None:
|
|
101
|
+
raise ElectricityMapsAPIError("No carbonIntensity data in response")
|
|
102
|
+
except Exception:
|
|
103
|
+
_start_cooldown(key)
|
|
104
|
+
raise
|
|
105
|
+
|
|
106
|
+
with _lock:
|
|
107
|
+
_cache[key] = (time.monotonic(), carbon_intensity_g_per_kWh)
|
|
108
|
+
_cooldown.pop(key, None)
|
|
109
|
+
return carbon_intensity_g_per_kWh
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_emissions(
|
|
113
|
+
energy: Energy, geo: GeoMetadata, electricitymaps_api_token: str = ""
|
|
114
|
+
) -> float:
|
|
115
|
+
"""
|
|
116
|
+
Calculate the CO2 emissions based on energy consumption and geographic location.
|
|
117
|
+
|
|
118
|
+
This function retrieves the carbon intensity (in grams of CO2 per kWh) from the
|
|
119
|
+
Electricity Maps API (formerly CO2 Signal) based on the geographic location provided.
|
|
120
|
+
It then calculates the total CO2 emissions for a given amount of energy consumption.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
energy (Energy):
|
|
124
|
+
An object representing the energy consumption in kilowatt-hours (kWh).
|
|
125
|
+
geo (GeoMetadata):
|
|
126
|
+
Geographic metadata, including either latitude/longitude
|
|
127
|
+
or a country code.
|
|
128
|
+
electricitymaps_api_token (str, optional):
|
|
129
|
+
The API token for authenticating with the Electricity Maps API (default is an empty string).
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
float:
|
|
133
|
+
The total CO2 emissions in kilograms based on the provided energy consumption and
|
|
134
|
+
carbon intensity of the specified geographic location.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
ElectricityMapsAPIError:
|
|
138
|
+
If the Electricity Maps API request fails or returns an error.
|
|
139
|
+
"""
|
|
140
|
+
carbon_intensity_g_per_kWh = get_carbon_intensity(geo, electricitymaps_api_token)
|
|
141
|
+
emissions_per_kWh: EmissionsPerKWh = EmissionsPerKWh.from_g_per_kWh(
|
|
142
|
+
carbon_intensity_g_per_kWh
|
|
143
|
+
)
|
|
144
|
+
return emissions_per_kWh.kgs_per_kWh * energy.kWh
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class ElectricityMapsAPIError(Exception):
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class ElectricityMapsAPICooldownError(ElectricityMapsAPIError):
|
|
152
|
+
"""Raised when a request is skipped because a previous one failed."""
|
|
@@ -6,6 +6,7 @@ https://github.com/mlco2/impact
|
|
|
6
6
|
https://github.com/responsibleproblemsolving/energy-usage
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import re
|
|
9
10
|
from typing import TYPE_CHECKING, Dict, Optional
|
|
10
11
|
|
|
11
12
|
from codecarbon.core import electricitymaps_api
|
|
@@ -23,6 +24,10 @@ _NORDIC_REGIONS_BY_COUNTRY = {
|
|
|
23
24
|
"FIN": {"FI"},
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
_CHINA_REGION_SUFFIX = re.compile(
|
|
28
|
+
r"\s+(autonomous region|municipality|province|sheng|shi|zizhiqu)$"
|
|
29
|
+
)
|
|
30
|
+
|
|
26
31
|
|
|
27
32
|
class Emissions:
|
|
28
33
|
def __init__(
|
|
@@ -169,6 +174,13 @@ class Emissions:
|
|
|
169
174
|
+ f"Retrieved emissions for {geo.country_name} using Electricity Maps API :{emissions * 1000} g CO2eq"
|
|
170
175
|
)
|
|
171
176
|
return emissions
|
|
177
|
+
except electricitymaps_api.ElectricityMapsAPICooldownError as e:
|
|
178
|
+
# The failure that started the cooldown was already logged.
|
|
179
|
+
logger.debug(
|
|
180
|
+
"electricitymaps_api.get_emissions: "
|
|
181
|
+
+ str(e)
|
|
182
|
+
+ " >>> Using CodeCarbon's data."
|
|
183
|
+
)
|
|
172
184
|
except Exception as e:
|
|
173
185
|
logger.error(
|
|
174
186
|
"electricitymaps_api.get_emissions: "
|
|
@@ -180,7 +192,8 @@ class Emissions:
|
|
|
180
192
|
geo.country_iso_code.upper() if geo.country_iso_code is not None else None
|
|
181
193
|
)
|
|
182
194
|
compute_with_regional_data: bool = (geo.region is not None) and (
|
|
183
|
-
country_iso_code in ["USA", "CAN"]
|
|
195
|
+
country_iso_code in ["USA", "CAN", "CHN"]
|
|
196
|
+
or self._is_supported_nordic_region(geo)
|
|
184
197
|
)
|
|
185
198
|
|
|
186
199
|
if compute_with_regional_data:
|
|
@@ -231,6 +244,30 @@ class Emissions:
|
|
|
231
244
|
)
|
|
232
245
|
return geo.region.upper() in country_regions
|
|
233
246
|
|
|
247
|
+
def _try_get_china_province_emissions(
|
|
248
|
+
self, energy: Energy, geo: GeoMetadata
|
|
249
|
+
) -> Optional[float]:
|
|
250
|
+
"""
|
|
251
|
+
Computes emissions with the official 2023 MEE/NBS provincial factor.
|
|
252
|
+
Returns None if the province is unknown or not published (Tibet,
|
|
253
|
+
Hong Kong, Macao and Taiwan), so the caller uses the national value.
|
|
254
|
+
"""
|
|
255
|
+
if geo.region is None:
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
name = _CHINA_REGION_SUFFIX.sub("", " ".join(geo.region.lower().split()))
|
|
259
|
+
china_data = self._data_source.get_china_provincial_emissions_data()
|
|
260
|
+
for province, province_data in china_data["data"].items():
|
|
261
|
+
if name in province_data["names"]:
|
|
262
|
+
emission_factor_g = province_data["emission_factor"]
|
|
263
|
+
emissions = emission_factor_g / 1000 * energy.kWh
|
|
264
|
+
logger.debug(
|
|
265
|
+
f"China province {province}: Retrieved emissions using static factor "
|
|
266
|
+
+ f"{emission_factor_g} gCO2/kWh: {emissions * 1000} g CO2"
|
|
267
|
+
)
|
|
268
|
+
return emissions
|
|
269
|
+
return None
|
|
270
|
+
|
|
234
271
|
def get_region_emissions(self, energy: Energy, geo: GeoMetadata) -> float:
|
|
235
272
|
"""
|
|
236
273
|
Computes emissions for a region on private infra.
|
|
@@ -240,10 +277,11 @@ class Emissions:
|
|
|
240
277
|
|
|
241
278
|
get_private_infra_emissions
|
|
242
279
|
├─ Electricity Maps API (si token)
|
|
243
|
-
├─ get_region_emissions (USA/CAN/SWE/NOR/FIN)
|
|
280
|
+
├─ get_region_emissions (USA/CAN/CHN/SWE/NOR/FIN)
|
|
244
281
|
│ └─ _try_get_nordic_region_emissions (pour SWE/NOR/FIN)
|
|
245
282
|
│ └─ country_emissions_data (pour USA)
|
|
246
283
|
│ └─ country_energy_mix_data (pour CAN)
|
|
284
|
+
│ └─ _try_get_china_province_emissions (pour CHN)
|
|
247
285
|
└─ get_country_emissions (fallback)
|
|
248
286
|
|
|
249
287
|
:param energy: Mean power consumption of the process (kWh)
|
|
@@ -260,6 +298,13 @@ class Emissions:
|
|
|
260
298
|
):
|
|
261
299
|
return self.get_country_emissions(energy, geo)
|
|
262
300
|
|
|
301
|
+
# Handle China provinces (official MEE/NBS factors)
|
|
302
|
+
if geo.country_iso_code is not None and geo.country_iso_code.upper() == "CHN":
|
|
303
|
+
china_emissions = self._try_get_china_province_emissions(energy, geo)
|
|
304
|
+
if china_emissions is not None:
|
|
305
|
+
return china_emissions
|
|
306
|
+
return self.get_country_emissions(energy, geo)
|
|
307
|
+
|
|
263
308
|
# Handle USA and Canada regional data
|
|
264
309
|
try:
|
|
265
310
|
country_emissions_data = self._data_source.get_country_emissions_data(
|
|
@@ -335,7 +380,7 @@ class Emissions:
|
|
|
335
380
|
in Kgs.CO2 / kWh
|
|
336
381
|
"""
|
|
337
382
|
# If we have the chance to have the carbon intensity for this country
|
|
338
|
-
if energy_mix.get("carbon_intensity"):
|
|
383
|
+
if energy_mix.get("carbon_intensity") is not None:
|
|
339
384
|
return EmissionsPerKWh.from_g_per_kWh(energy_mix.get("carbon_intensity"))
|
|
340
385
|
|
|
341
386
|
# Else we compute it from the energy mix.
|
|
@@ -353,7 +398,7 @@ class Emissions:
|
|
|
353
398
|
carbon_intensity_for_type = carbon_intensity_per_source.get(
|
|
354
399
|
energy_type[: -len("_TWh")]
|
|
355
400
|
)
|
|
356
|
-
if carbon_intensity_for_type: # to ignore "total_TWh"
|
|
401
|
+
if carbon_intensity_for_type is not None: # to ignore "total_TWh"
|
|
357
402
|
carbon_intensity += (
|
|
358
403
|
energy_per_year / energy_sum
|
|
359
404
|
) * carbon_intensity_for_type
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List
|
|
1
|
+
from typing import Iterable, List, Optional
|
|
2
2
|
|
|
3
3
|
from codecarbon.core import gpu_amd, gpu_nvidia
|
|
4
4
|
from codecarbon.core.gpu_device import GPUDevice
|
|
@@ -154,6 +154,18 @@ class AllGPUDevices:
|
|
|
154
154
|
logger.warning("Failed to retrieve gpu utilization", exc_info=True)
|
|
155
155
|
return []
|
|
156
156
|
|
|
157
|
+
def sample_power(self, gpu_ids: Optional[Iterable[int]] = None) -> None:
|
|
158
|
+
"""Record a power reading for devices without a usable energy
|
|
159
|
+
counter, called from the tracker's 1s monitoring loop.
|
|
160
|
+
:param gpu_ids: indexes of the tracked GPUs, all of them if None."""
|
|
161
|
+
for device in self.devices:
|
|
162
|
+
if gpu_ids is not None and device.gpu_index not in gpu_ids:
|
|
163
|
+
continue
|
|
164
|
+
try:
|
|
165
|
+
device.sample_power()
|
|
166
|
+
except Exception:
|
|
167
|
+
logger.debug("Failed to sample gpu power", exc_info=True)
|
|
168
|
+
|
|
157
169
|
def get_delta(self, last_duration: Time) -> List:
|
|
158
170
|
"""Get difference since last time this function was called
|
|
159
171
|
>>> get_delta()
|
|
@@ -62,6 +62,11 @@ class AMDGPUDevice(GPUDevice):
|
|
|
62
62
|
self._gpu_name
|
|
63
63
|
)
|
|
64
64
|
|
|
65
|
+
def _reports_zero_power_by_design(self) -> bool:
|
|
66
|
+
# The second GCD of a dual-GCD device reports 0, its power is
|
|
67
|
+
# included in the first GCD's reading.
|
|
68
|
+
return self._known_zero_energy_counter and self.gpu_index % 2 == 1
|
|
69
|
+
|
|
65
70
|
def emit_selection_warning(self) -> None:
|
|
66
71
|
if not self._known_zero_energy_counter:
|
|
67
72
|
return
|
|
@@ -124,7 +129,8 @@ class AMDGPUDevice(GPUDevice):
|
|
|
124
129
|
elif "power" in energy_count:
|
|
125
130
|
energy_key = "power"
|
|
126
131
|
if energy_key is None:
|
|
127
|
-
|
|
132
|
+
self._counter_error = "amdsmi_get_energy_count has no energy counter"
|
|
133
|
+
logger.debug(
|
|
128
134
|
f"Neither 'energy_accumulator' nor 'power' found in energy_count: {energy_count}"
|
|
129
135
|
)
|
|
130
136
|
return None
|
|
@@ -139,14 +145,16 @@ class AMDGPUDevice(GPUDevice):
|
|
|
139
145
|
if counter_value == 0:
|
|
140
146
|
if getattr(self, "_known_zero_energy_counter", False):
|
|
141
147
|
return 0
|
|
148
|
+
self._counter_error = "energy counter reads 0"
|
|
142
149
|
return None
|
|
143
150
|
|
|
144
151
|
# energy_in_µJ = counter_value * resolution_in_µJ
|
|
145
152
|
# Divide by 1000 to convert µJ to mJ
|
|
146
153
|
energy_mj = counter_value * counter_resolution_uj / 1000
|
|
147
154
|
return energy_mj
|
|
148
|
-
except Exception:
|
|
149
|
-
|
|
155
|
+
except Exception as e:
|
|
156
|
+
self._counter_error = repr(e)
|
|
157
|
+
logger.debug(
|
|
150
158
|
"Failed to retrieve AMD GPU total energy consumption", exc_info=True
|
|
151
159
|
)
|
|
152
160
|
return None
|