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.
Files changed (145) hide show
  1. {codecarbon-3.3.1 → codecarbon-3.3.2}/PKG-INFO +15 -1
  2. codecarbon-3.3.2/codecarbon/_version.py +1 -0
  3. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/cli/main.py +5 -0
  4. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/cli/monitor.py +6 -7
  5. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/cpu.py +41 -14
  6. codecarbon-3.3.2/codecarbon/core/electricitymaps_api.py +152 -0
  7. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/emissions.py +49 -4
  8. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/gpu.py +13 -1
  9. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/gpu_amd.py +11 -3
  10. codecarbon-3.3.2/codecarbon/core/gpu_device.py +298 -0
  11. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/gpu_nvidia.py +3 -2
  12. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/hardware_cache.py +16 -6
  13. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/powermetrics.py +24 -38
  14. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/rapl.py +30 -5
  15. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/resource_tracker.py +27 -2
  16. codecarbon-3.3.2/codecarbon/core/slurm.py +30 -0
  17. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/data/hardware/cpu_power.csv +2 -0
  18. codecarbon-3.3.2/codecarbon/data/private_infra/2023/china_provincial_emissions.json +292 -0
  19. {codecarbon-3.3.1/codecarbon/data/private_infra/2016 → codecarbon-3.3.2/codecarbon/data/private_infra/2023}/usa_emissions.json +157 -151
  20. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/data/private_infra/carbon_intensity_per_source.json +7 -1
  21. codecarbon-3.3.2/codecarbon/data/private_infra/update_usa_emissions.py +145 -0
  22. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/emissions_tracker.py +125 -39
  23. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/external/geography.py +1 -1
  24. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/external/hardware.py +209 -50
  25. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/external/ram.py +4 -3
  26. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/input.py +14 -1
  27. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/boamps/output.py +29 -0
  28. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/file.py +29 -18
  29. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/viz/carbonboard.py +2 -2
  30. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon.egg-info/PKG-INFO +15 -1
  31. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon.egg-info/SOURCES.txt +10 -1
  32. {codecarbon-3.3.1 → codecarbon-3.3.2}/pyproject.toml +34 -2
  33. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_boamps_output.py +6 -0
  34. codecarbon-3.3.2/tests/test_cpu_load.py +384 -0
  35. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_electricitymaps_api.py +13 -0
  36. codecarbon-3.3.2/tests/test_electricitymaps_cache.py +183 -0
  37. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_emissions.py +231 -2
  38. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_emissions_tracker.py +245 -1
  39. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_emissions_tracker_constant.py +3 -1
  40. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_emissions_tracker_flush.py +13 -0
  41. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_geography.py +15 -0
  42. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_gpu_amd.py +35 -6
  43. codecarbon-3.3.2/tests/test_gpu_nvidia.py +938 -0
  44. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_hardware_cache.py +34 -0
  45. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_powermetrics.py +87 -1
  46. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_ram.py +6 -0
  47. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_rapl_mmio_scanning.py +50 -0
  48. codecarbon-3.3.2/tests/test_rapl_permissions.py +278 -0
  49. codecarbon-3.3.2/tests/test_raspberry.py +253 -0
  50. codecarbon-3.3.2/tests/test_readme_links.py +26 -0
  51. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_resource_tracker.py +73 -0
  52. codecarbon-3.3.2/tests/test_slurm.py +40 -0
  53. codecarbon-3.3.2/tests/test_update_usa_emissions.py +102 -0
  54. codecarbon-3.3.2/tests/test_zero_carbon_intensity.py +34 -0
  55. codecarbon-3.3.1/codecarbon/_version.py +0 -1
  56. codecarbon-3.3.1/codecarbon/core/electricitymaps_api.py +0 -69
  57. codecarbon-3.3.1/codecarbon/core/gpu_device.py +0 -130
  58. codecarbon-3.3.1/tests/test_cpu_load.py +0 -195
  59. codecarbon-3.3.1/tests/test_gpu_nvidia.py +0 -469
  60. codecarbon-3.3.1/tests/test_rapl_permissions.py +0 -112
  61. {codecarbon-3.3.1 → codecarbon-3.3.2}/LICENSE +0 -0
  62. {codecarbon-3.3.1 → codecarbon-3.3.2}/README.md +0 -0
  63. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/__init__.py +0 -0
  64. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/cli/__init__.py +0 -0
  65. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/cli/auth.py +0 -0
  66. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/cli/cli_utils.py +0 -0
  67. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/__init__.py +0 -0
  68. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/api_client.py +0 -0
  69. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/cloud.py +0 -0
  70. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/config.py +0 -0
  71. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/measure.py +0 -0
  72. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/schemas.py +0 -0
  73. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/telemetry_client.py +0 -0
  74. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/telemetry_schemas.py +0 -0
  75. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/units.py +0 -0
  76. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/util.py +0 -0
  77. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/core/windows_emi.py +0 -0
  78. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/data/canada_provinces.geojson +0 -0
  79. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/data/cloud/impact.csv +0 -0
  80. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/data/hardware/cpu_dataset_builder/amd_cpu_scrapper.py +0 -0
  81. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/data/hardware/cpu_dataset_builder/intel_cpu_scrapper.py +0 -0
  82. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/data/hardware/cpu_dataset_builder/merge_scrapped_cpu_power.py +0 -0
  83. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/data/private_infra/2023/canada_energy_mix.json +0 -0
  84. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/data/private_infra/global_energy_mix.json +0 -0
  85. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/data/private_infra/nordic_emissions.json +0 -0
  86. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/external/__init__.py +0 -0
  87. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/external/logger.py +0 -0
  88. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/external/scheduler.py +0 -0
  89. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/external/task.py +0 -0
  90. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/lock.py +0 -0
  91. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output.py +0 -0
  92. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/__init__.py +0 -0
  93. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/base_output.py +0 -0
  94. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/boamps/__init__.py +0 -0
  95. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/boamps/mapper.py +0 -0
  96. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/boamps/models.py +0 -0
  97. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/emissions_data.py +0 -0
  98. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/http.py +0 -0
  99. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/logger.py +0 -0
  100. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/metrics/__init__.py +0 -0
  101. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/metrics/logfire.py +0 -0
  102. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/metrics/metric_docs.py +0 -0
  103. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/output_methods/metrics/prometheus.py +0 -0
  104. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/py.typed +0 -0
  105. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/viz/__init__.py +0 -0
  106. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/viz/assets/__init__.py +0 -0
  107. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/viz/assets/car_icon.png +0 -0
  108. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/viz/assets/house_icon.png +0 -0
  109. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/viz/assets/tv_icon.png +0 -0
  110. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/viz/carbonboard_on_api.py +0 -0
  111. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/viz/components.py +0 -0
  112. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/viz/data.py +0 -0
  113. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon/viz/units.py +0 -0
  114. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon.egg-info/dependency_links.txt +0 -0
  115. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon.egg-info/entry_points.txt +0 -0
  116. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon.egg-info/requires.txt +0 -0
  117. {codecarbon-3.3.1 → codecarbon-3.3.2}/codecarbon.egg-info/top_level.txt +0 -0
  118. {codecarbon-3.3.1 → codecarbon-3.3.2}/setup.cfg +0 -0
  119. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_api_call.py +0 -0
  120. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_cloud.py +0 -0
  121. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_config.py +0 -0
  122. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_core_util.py +0 -0
  123. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_cpu.py +0 -0
  124. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_custom_handler.py +0 -0
  125. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_docs_examples.py +0 -0
  126. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_electricitymaps_backward_compatibility.py +0 -0
  127. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_electricitymaps_config_backward_compatibility.py +0 -0
  128. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_energy.py +0 -0
  129. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_gpu.py +0 -0
  130. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_input.py +0 -0
  131. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_lock.py +0 -0
  132. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_logger.py +0 -0
  133. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_logging_output.py +0 -0
  134. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_offline_emissions_tracker.py +0 -0
  135. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_package_integrity.py +0 -0
  136. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_rapl_parameters.py +0 -0
  137. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_telemetry_client.py +0 -0
  138. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_tracking_inference.py +0 -0
  139. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_unsupported_gpu.py +0 -0
  140. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_utilization_tracking.py +0 -0
  141. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_viz_data.py +0 -0
  142. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_viz_units.py +0 -0
  143. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/test_windows_emi.py +0 -0
  144. {codecarbon-3.3.1 → codecarbon-3.3.2}/tests/testdata.py +0 -0
  145. {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.1
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
- if hasattr(tracker, "_conf") and "output_file" in tracker._conf:
125
- output_path = tracker._conf["output_file"]
126
- # Make it absolute if it's relative
127
- if not os.path.isabs(output_path):
128
- output_path = os.path.abspath(output_path)
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
- suggestion = "You can grant read permission with: sudo chmod -R a+r /sys/class/powercap/*"
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 delta <= 0 and reference_delta <= 0:
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
- + " Please contact us.",
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"] or self._is_supported_nordic_region(geo)
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
- logger.warning(
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
- logger.warning(
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