codecarbon 2.3.2__tar.gz → 2.3.4__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 (108) hide show
  1. {codecarbon-2.3.2/codecarbon.egg-info → codecarbon-2.3.4}/PKG-INFO +2 -2
  2. codecarbon-2.3.4/codecarbon/_version.py +1 -0
  3. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/core/cpu.py +47 -49
  4. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/core/emissions.py +29 -12
  5. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/core/measure.py +18 -3
  6. codecarbon-2.3.4/codecarbon/core/powermetrics.py +173 -0
  7. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/emissions_tracker.py +41 -8
  8. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/external/hardware.py +74 -1
  9. {codecarbon-2.3.2 → codecarbon-2.3.4/codecarbon.egg-info}/PKG-INFO +2 -2
  10. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon.egg-info/SOURCES.txt +2 -0
  11. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon.egg-info/requires.txt +1 -1
  12. {codecarbon-2.3.2 → codecarbon-2.3.4}/setup.py +2 -2
  13. {codecarbon-2.3.2 → codecarbon-2.3.4}/tests/test_cpu.py +20 -14
  14. {codecarbon-2.3.2 → codecarbon-2.3.4}/tests/test_emissions_tracker_constant.py +31 -0
  15. codecarbon-2.3.4/tests/test_powermetrics.py +35 -0
  16. codecarbon-2.3.2/codecarbon/_version.py +0 -1
  17. {codecarbon-2.3.2 → codecarbon-2.3.4}/LICENSE +0 -0
  18. {codecarbon-2.3.2 → codecarbon-2.3.4}/README.md +0 -0
  19. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/__init__.py +0 -0
  20. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/__init__.py +0 -0
  21. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/api/__init__.py +0 -0
  22. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/api/dependencies.py +0 -0
  23. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/api/domain/__init__.py +0 -0
  24. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/api/domain/emissions.py +0 -0
  25. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/api/domain/experiments.py +0 -0
  26. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/api/domain/organizations.py +0 -0
  27. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/api/domain/projects.py +0 -0
  28. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/api/domain/runs.py +0 -0
  29. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/api/domain/teams.py +0 -0
  30. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/api/domain/users.py +0 -0
  31. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/api/errors.py +0 -0
  32. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/api/routers/__init__.py +0 -0
  33. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/api/routers/authenticate.py +0 -0
  34. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/api/routers/emissions.py +0 -0
  35. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/api/routers/experiments.py +0 -0
  36. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/api/routers/organizations.py +0 -0
  37. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/api/routers/projects.py +0 -0
  38. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/api/routers/runs.py +0 -0
  39. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/api/routers/teams.py +0 -0
  40. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/api/routers/users.py +0 -0
  41. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/api/schemas.py +0 -0
  42. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/config.py +0 -0
  43. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/database/__init__.py +0 -0
  44. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/database/database.py +0 -0
  45. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/carbonserver/logger.py +0 -0
  46. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/container.py +0 -0
  47. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/main.py +0 -0
  48. {codecarbon-2.3.2 → codecarbon-2.3.4}/carbonserver/setup.py +0 -0
  49. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/__init__.py +0 -0
  50. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/cli/__init__.py +0 -0
  51. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/cli/cli_utils.py +0 -0
  52. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/cli/main.py +0 -0
  53. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/core/__init__.py +0 -0
  54. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/core/api_client.py +0 -0
  55. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/core/cloud.py +0 -0
  56. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/core/co2_signal.py +0 -0
  57. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/core/config.py +0 -0
  58. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/core/gpu.py +0 -0
  59. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/core/rapl.py +0 -0
  60. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/core/schemas.py +0 -0
  61. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/core/units.py +0 -0
  62. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/core/util.py +0 -0
  63. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/data/cloud/impact.csv +0 -0
  64. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/data/hardware/cpu_power.csv +0 -0
  65. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/data/private_infra/2016/canada_energy_mix.json +0 -0
  66. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/data/private_infra/2016/usa_emissions.json +0 -0
  67. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/data/private_infra/carbon_intensity_per_source.json +0 -0
  68. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/data/private_infra/global_energy_mix.json +0 -0
  69. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/external/__init__.py +0 -0
  70. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/external/geography.py +0 -0
  71. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/external/logger.py +0 -0
  72. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/external/scheduler.py +0 -0
  73. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/external/task.py +0 -0
  74. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/input.py +0 -0
  75. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/output.py +0 -0
  76. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/prometheus/__init__.py +0 -0
  77. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/prometheus/metric_definitions.py +0 -0
  78. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/prometheus/prometheus.py +0 -0
  79. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/viz/__init__.py +0 -0
  80. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/viz/assets/__init__.py +0 -0
  81. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/viz/assets/car_icon.png +0 -0
  82. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/viz/assets/house_icon.png +0 -0
  83. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/viz/assets/tv_icon.png +0 -0
  84. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/viz/carbonboard.py +0 -0
  85. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/viz/carbonboard_on_api.py +0 -0
  86. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/viz/components.py +0 -0
  87. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon/viz/data.py +0 -0
  88. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon.egg-info/dependency_links.txt +0 -0
  89. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon.egg-info/entry_points.txt +0 -0
  90. {codecarbon-2.3.2 → codecarbon-2.3.4}/codecarbon.egg-info/top_level.txt +0 -0
  91. {codecarbon-2.3.2 → codecarbon-2.3.4}/setup.cfg +0 -0
  92. {codecarbon-2.3.2 → codecarbon-2.3.4}/tests/test_api_call.py +0 -0
  93. {codecarbon-2.3.2 → codecarbon-2.3.4}/tests/test_cloud.py +0 -0
  94. {codecarbon-2.3.2 → codecarbon-2.3.4}/tests/test_co2_signal.py +0 -0
  95. {codecarbon-2.3.2 → codecarbon-2.3.4}/tests/test_config.py +0 -0
  96. {codecarbon-2.3.2 → codecarbon-2.3.4}/tests/test_core_util.py +0 -0
  97. {codecarbon-2.3.2 → codecarbon-2.3.4}/tests/test_emissions.py +0 -0
  98. {codecarbon-2.3.2 → codecarbon-2.3.4}/tests/test_emissions_tracker.py +0 -0
  99. {codecarbon-2.3.2 → codecarbon-2.3.4}/tests/test_emissions_tracker_flush.py +0 -0
  100. {codecarbon-2.3.2 → codecarbon-2.3.4}/tests/test_energy.py +0 -0
  101. {codecarbon-2.3.2 → codecarbon-2.3.4}/tests/test_geography.py +0 -0
  102. {codecarbon-2.3.2 → codecarbon-2.3.4}/tests/test_gpu.py +0 -0
  103. {codecarbon-2.3.2 → codecarbon-2.3.4}/tests/test_logging_output.py +0 -0
  104. {codecarbon-2.3.2 → codecarbon-2.3.4}/tests/test_ram.py +0 -0
  105. {codecarbon-2.3.2 → codecarbon-2.3.4}/tests/test_tracking_inference.py +0 -0
  106. {codecarbon-2.3.2 → codecarbon-2.3.4}/tests/test_viz_data.py +0 -0
  107. {codecarbon-2.3.2 → codecarbon-2.3.4}/tests/testdata.py +0 -0
  108. {codecarbon-2.3.2 → codecarbon-2.3.4}/tests/testutils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: codecarbon
3
- Version: 2.3.2
3
+ Version: 2.3.4
4
4
  Author: Mila, DataForGood, BCG GAMMA, Comet.ml, Haverford College
5
5
  Classifier: Natural Language :: English
6
6
  Classifier: Programming Language :: Python :: 3.7
@@ -18,7 +18,7 @@ Requires-Dist: pynvml
18
18
  Requires-Dist: requests
19
19
  Requires-Dist: psutil
20
20
  Requires-Dist: py-cpuinfo
21
- Requires-Dist: fuzzywuzzy
21
+ Requires-Dist: rapidfuzz
22
22
  Requires-Dist: click
23
23
  Requires-Dist: prometheus_client
24
24
  Provides-Extra: viz
@@ -0,0 +1 @@
1
+ __version__ = "2.3.4"
@@ -7,13 +7,10 @@ import os
7
7
  import shutil
8
8
  import subprocess
9
9
  import sys
10
- import warnings
11
10
  from typing import Dict, Tuple
12
11
 
13
12
  import pandas as pd
14
-
15
- with warnings.catch_warnings(record=True) as w:
16
- from fuzzywuzzy import fuzz
13
+ from rapidfuzz import fuzz, process, utils
17
14
 
18
15
  from codecarbon.core.rapl import RAPLFile
19
16
  from codecarbon.core.units import Time
@@ -50,8 +47,6 @@ class IntelPowerGadget:
50
47
  _osx_exec = "PowerLog"
51
48
  _osx_exec_backup = "/Applications/Intel Power Gadget/PowerLog"
52
49
  _windows_exec = "PowerLog3.0.exe"
53
- # TODO: There is now a 3.6 version.
54
- _windows_exec_backup = "C:\\Program Files\\Intel\\Power Gadget 3.5\\PowerLog3.0.exe"
55
50
 
56
51
  def __init__(
57
52
  self,
@@ -71,6 +66,7 @@ class IntelPowerGadget:
71
66
  Setup cli command to run Intel Power Gadget
72
67
  """
73
68
  if self._system.startswith("win"):
69
+ self._get_windows_exec_backup()
74
70
  if shutil.which(self._windows_exec):
75
71
  self._cli = shutil.which(
76
72
  self._windows_exec
@@ -93,6 +89,27 @@ class IntelPowerGadget:
93
89
  else:
94
90
  raise SystemError("Platform not supported by Intel Power Gadget")
95
91
 
92
+ def _get_windows_exec_backup(self) -> None:
93
+ """
94
+ Find the windows executable for the current version of intel power gadget.
95
+ Example: "C:\\Program Files\\Intel\\Power Gadget 3.5\\PowerLog3.0.exe"
96
+ """
97
+ parent_folder = "C:\\Program Files\\Intel\\"
98
+
99
+ # Get a list of all subdirectories in the parent folder
100
+ subfolders = [f.name for f in os.scandir(parent_folder) if f.is_dir()]
101
+
102
+ # Look for a folder that contains "Power Gadget" in its name
103
+ desired_folder = next(
104
+ (folder for folder in subfolders if "Power Gadget" in folder), None
105
+ )
106
+ if desired_folder:
107
+ self._windows_exec_backup = os.path.join(
108
+ parent_folder, desired_folder, self._windows_exec
109
+ )
110
+ else:
111
+ self._windows_exec_backup = None
112
+
96
113
  def _log_values(self):
97
114
  """
98
115
  Logs output from Intel Power Gadget command line to a file
@@ -278,27 +295,6 @@ class TDP:
278
295
  return power
279
296
  return None
280
297
 
281
- @staticmethod
282
- def _get_cpus(cpu_df, cpu_idxs) -> list:
283
- return [cpu_df["Name"][idx] for idx in cpu_idxs]
284
-
285
- @staticmethod
286
- def _get_direct_matches(moodel: str, cpu_df: pd.DataFrame) -> list:
287
- model_l = moodel.lower()
288
- return [fuzz.ratio(model_l, cpu.lower()) for cpu in cpu_df["Name"]]
289
-
290
- @staticmethod
291
- def _get_token_set_matches(model: str, cpu_df: pd.DataFrame) -> list:
292
- return [fuzz.token_set_ratio(model, cpu) for cpu in cpu_df["Name"]]
293
-
294
- @staticmethod
295
- def _get_single_direct_match(
296
- ratios: list, max_ratio: int, cpu_df: pd.DataFrame
297
- ) -> str:
298
- idx = ratios.index(max_ratio)
299
- cpu_matched = cpu_df["Name"].iloc[idx]
300
- return cpu_matched
301
-
302
298
  def _get_matching_cpu(
303
299
  self, model_raw: str, cpu_df: pd.DataFrame, greedy=False
304
300
  ) -> str:
@@ -332,33 +328,35 @@ class TDP:
332
328
  THRESHOLD_DIRECT = 100
333
329
  THRESHOLD_TOKEN_SET = 100
334
330
 
335
- ratios_direct = self._get_direct_matches(model_raw, cpu_df)
336
- ratios_token_set = self._get_token_set_matches(model_raw, cpu_df)
337
- max_ratio_direct = max(ratios_direct)
338
- max_ratio_token_set = max(ratios_token_set)
331
+ direct_match = process.extractOne(
332
+ model_raw,
333
+ cpu_df["Name"],
334
+ processor=lambda s: s.lower(),
335
+ scorer=fuzz.ratio,
336
+ score_cutoff=THRESHOLD_DIRECT,
337
+ )
338
+
339
+ if direct_match:
340
+ return direct_match[0]
339
341
 
340
- # Check if a direct match exists
341
- if max_ratio_direct >= THRESHOLD_DIRECT:
342
- cpu_matched = self._get_single_direct_match(
343
- ratios_direct, max_ratio_direct, cpu_df
344
- )
345
- return cpu_matched
342
+ indirect_matches = process.extract(
343
+ model_raw,
344
+ cpu_df["Name"],
345
+ processor=utils.default_process,
346
+ scorer=fuzz.token_set_ratio,
347
+ score_cutoff=THRESHOLD_TOKEN_SET,
348
+ )
346
349
 
347
- # Check if an indirect match exists
348
- if max_ratio_token_set < THRESHOLD_TOKEN_SET:
349
- return None
350
- cpu_idxs = self._get_max_idxs(ratios_token_set, max_ratio_token_set)
351
- cpu_machings = self._get_cpus(cpu_df, cpu_idxs)
350
+ if indirect_matches:
351
+ if (
352
+ greedy
353
+ or len(indirect_matches) == 1
354
+ or indirect_matches[0][1] != indirect_matches[1][1]
355
+ ):
356
+ return indirect_matches[0][0]
352
357
 
353
- if (cpu_machings and len(cpu_machings) == 1) or greedy:
354
- cpu_matched = cpu_machings[0]
355
- return cpu_matched
356
358
  return None
357
359
 
358
- @staticmethod
359
- def _get_max_idxs(ratios: list, max_ratio: int) -> list:
360
- return [idx for idx, ratio in enumerate(ratios) if ratio == max_ratio]
361
-
362
360
  def _main(self) -> Tuple[str, int]:
363
361
  """
364
362
  Get CPU power from constant mode
@@ -71,32 +71,49 @@ class Emissions:
71
71
  Returns the Country Name where the cloud region is located
72
72
  """
73
73
  df: pd.DataFrame = self._data_source.get_cloud_emissions_data()
74
- return df.loc[
75
- (df["provider"] == cloud.provider) & (df["region"] == cloud.region)
76
- ]["country_name"].item()
74
+ flags = (df["provider"] == cloud.provider) & (df["region"] == cloud.region)
75
+ selected = df.loc[flags]
76
+ if not len(selected):
77
+ raise ValueError(
78
+ "Unable to find country name for "
79
+ f"cloud_provider={cloud.provider}, "
80
+ f"cloud_region={cloud.region}"
81
+ )
82
+ return selected["country_name"].item()
77
83
 
78
84
  def get_cloud_country_iso_code(self, cloud: CloudMetadata) -> str:
79
85
  """
80
86
  Returns the Country ISO Code where the cloud region is located
81
87
  """
82
88
  df: pd.DataFrame = self._data_source.get_cloud_emissions_data()
83
- return df.loc[
84
- (df["provider"] == cloud.provider) & (df["region"] == cloud.region)
85
- ]["countryIsoCode"].item()
89
+ flags = (df["provider"] == cloud.provider) & (df["region"] == cloud.region)
90
+ selected = df.loc[flags]
91
+ if not len(selected):
92
+ raise ValueError(
93
+ "Unable to find country name for "
94
+ f"cloud_provider={cloud.provider}, "
95
+ f"cloud_region={cloud.region}"
96
+ )
97
+ return selected["countryIsoCode"].item()
86
98
 
87
99
  def get_cloud_geo_region(self, cloud: CloudMetadata) -> str:
88
100
  """
89
101
  Returns the State/City where the cloud region is located
90
102
  """
91
103
  df: pd.DataFrame = self._data_source.get_cloud_emissions_data()
92
- state = df.loc[
93
- (df["provider"] == cloud.provider) & (df["region"] == cloud.region)
94
- ]["state"].item()
104
+ flags = (df["provider"] == cloud.provider) & (df["region"] == cloud.region)
105
+ selected = df.loc[flags]
106
+ if not len(selected):
107
+ raise ValueError(
108
+ "Unable to find country name for "
109
+ f"cloud_provider={cloud.provider}, "
110
+ f"cloud_region={cloud.region}"
111
+ )
112
+
113
+ state = selected["state"].item()
95
114
  if state is not None:
96
115
  return state
97
- city = df.loc[
98
- (df["provider"] == cloud.provider) & (df["region"] == cloud.region)
99
- ]["city"].item()
116
+ city = selected["city"].item()
100
117
  return city
101
118
 
102
119
  def get_private_infra_emissions(self, energy: Energy, geo: GeoMetadata) -> float:
@@ -1,6 +1,6 @@
1
1
  from time import time
2
2
 
3
- from codecarbon.external.hardware import CPU, GPU, RAM
3
+ from codecarbon.external.hardware import CPU, GPU, RAM, AppleSiliconChip
4
4
  from codecarbon.external.logger import logger
5
5
 
6
6
 
@@ -67,9 +67,24 @@ class MeasurePowerEnergy:
67
67
  self._total_ram_energy += energy
68
68
  self._ram_power = power
69
69
  logger.info(
70
- f"Energy consumed for RAM : {self._total_ram_energy.kWh:.6f} kWh"
71
- + f". RAM Power : {self._ram_power.W} W"
70
+ f"Energy consumed for RAM : {self._total_ram_energy.kWh:.6f} kWh."
71
+ + f"RAM Power : {self._ram_power.W} W"
72
72
  )
73
+ elif isinstance(hardware, AppleSiliconChip):
74
+ if hardware.chip_part == "CPU":
75
+ self._total_cpu_energy += energy
76
+ self._cpu_power = power
77
+ logger.info(
78
+ f"Energy consumed for AppleSilicon CPU : {self._total_cpu_energy.kWh:.6f} kWh"
79
+ + f".Apple Silicon CPU Power : {self._cpu_power.W} W"
80
+ )
81
+ elif hardware.chip_part == "GPU":
82
+ self._total_gpu_energy += energy
83
+ self._gpu_power = power
84
+ logger.info(
85
+ f"Energy consumed for AppleSilicon GPU : {self._total_gpu_energy.kWh:.6f} kWh"
86
+ + f".Apple Silicon GPU Power : {self._gpu_power.W} W"
87
+ )
73
88
  else:
74
89
  logger.error(f"Unknown hardware type: {hardware} ({type(hardware)})")
75
90
  h_time = time.time() - h_time
@@ -0,0 +1,173 @@
1
+ import os
2
+ import re
3
+ import shutil
4
+ import subprocess
5
+ import sys
6
+ from typing import Dict
7
+
8
+ import numpy as np
9
+
10
+ from codecarbon.core.util import detect_cpu_model
11
+ from codecarbon.external.logger import logger
12
+
13
+
14
+ def is_powermetrics_available():
15
+ try:
16
+ ApplePowermetrics()
17
+ response = _has_powermetrics_sudo()
18
+ return response
19
+ except Exception as e:
20
+ logger.debug(
21
+ "Not using PowerMetrics, an exception occurred while instantiating"
22
+ + f" Powermetrics : {e}",
23
+ )
24
+ return False
25
+
26
+
27
+ def _has_powermetrics_sudo():
28
+ process = subprocess.Popen(
29
+ [
30
+ "sudo",
31
+ "powermetrics",
32
+ "--samplers",
33
+ "cpu_power",
34
+ "-n",
35
+ "1",
36
+ "-i",
37
+ "1",
38
+ "-o",
39
+ "/dev/null",
40
+ ],
41
+ stdout=subprocess.PIPE,
42
+ stderr=subprocess.PIPE,
43
+ text=True,
44
+ )
45
+
46
+ _, stderr = process.communicate()
47
+
48
+ if re.search(r"[sudo].*password", stderr):
49
+ logger.debug(
50
+ """Not using PowerMetrics, sudo password prompt detected.
51
+ If you want to enable Powermetrics please modify your sudoers file
52
+ as described in :
53
+ https://mlco2.github.io/codecarbon/methodology.html#power-usage
54
+ """
55
+ )
56
+ return False
57
+ if process.returncode != 0:
58
+ raise Exception("Return code != 0")
59
+ else:
60
+ return True
61
+
62
+
63
+ class ApplePowermetrics:
64
+ _osx_silicon_exec = "powermetrics"
65
+
66
+ def __init__(
67
+ self,
68
+ output_dir: str = ".",
69
+ n_points=10,
70
+ interval=100,
71
+ log_file_name="powermetrics_log.txt",
72
+ ):
73
+ self._log_file_path = os.path.join(output_dir, log_file_name)
74
+ self._system = sys.platform.lower()
75
+ self._n_points = n_points
76
+ self._interval = interval
77
+ self._setup_cli()
78
+
79
+ def _setup_cli(self):
80
+ """
81
+ Setup cli command to run Powermetrics
82
+ """
83
+ if self._system.startswith("darwin"):
84
+ cpu_model = detect_cpu_model()
85
+ if cpu_model.startswith("Apple"):
86
+ if shutil.which(self._osx_silicon_exec):
87
+ self._cli = self._osx_silicon_exec
88
+ else:
89
+ raise FileNotFoundError(
90
+ f"Powermetrics executable not found on {self._system}"
91
+ )
92
+ else:
93
+ raise SystemError("Platform not supported by Powermetrics")
94
+
95
+ def _log_values(self):
96
+ """
97
+ Logs output from Powermetrics to a file
98
+ """
99
+ returncode = None
100
+
101
+ if self._system.startswith("darwin"):
102
+ # Run the powermetrics command with sudo and capture its output
103
+ cmd = [
104
+ "sudo",
105
+ "powermetrics",
106
+ "-n",
107
+ str(self._n_points),
108
+ "",
109
+ "--samplers",
110
+ "cpu_power",
111
+ "--format",
112
+ "csv",
113
+ "-i",
114
+ str(self._interval),
115
+ "-o",
116
+ self._log_file_path,
117
+ ]
118
+ returncode = subprocess.call(cmd, universal_newlines=True)
119
+
120
+ else:
121
+ return None
122
+
123
+ if returncode != 0:
124
+ logger.warning(
125
+ "Returncode while logging power values using "
126
+ + f"Powermetrics: {returncode}"
127
+ )
128
+ return
129
+
130
+ def get_details(self, **kwargs) -> Dict:
131
+ """
132
+ Fetches the CPU Power Details by fetching values from a logged csv file
133
+ in _log_values function
134
+ """
135
+ self._log_values()
136
+ details = dict()
137
+ try:
138
+ with open(self._log_file_path) as f:
139
+ logfile = f.read()
140
+ cpu_pattern = r"CPU Power: (\d+) mW"
141
+ cpu_power_list = re.findall(cpu_pattern, logfile)
142
+
143
+ details["CPU Power"] = np.mean(
144
+ [float(power) / 1000 for power in cpu_power_list]
145
+ )
146
+ details["CPU Energy Delta"] = np.sum(
147
+ [
148
+ (self._interval / 1000) * (float(power) / 1000)
149
+ for power in cpu_power_list
150
+ ]
151
+ )
152
+ gpu_pattern = r"GPU Power: (\d+) mW"
153
+ gpu_power_list = re.findall(gpu_pattern, logfile)
154
+ details["GPU Power"] = np.mean(
155
+ [float(power) / 1000 for power in gpu_power_list]
156
+ )
157
+ details["GPU Energy Delta"] = np.sum(
158
+ [
159
+ (self._interval / 1000) * (float(power) / 1000)
160
+ for power in gpu_power_list
161
+ ]
162
+ )
163
+ except Exception as e:
164
+ logger.info(
165
+ f"Unable to read Powermetrics logged file at {self._log_file_path}\n \
166
+ Exception occurred {e}",
167
+ exc_info=True,
168
+ )
169
+ return details
170
+
171
+ def start(self):
172
+ # TODO: Read energy
173
+ pass
@@ -14,13 +14,13 @@ from functools import wraps
14
14
  from typing import Any, Callable, Dict, List, Optional, Union
15
15
 
16
16
  from codecarbon._version import __version__
17
- from codecarbon.core import cpu, gpu
17
+ from codecarbon.core import cpu, gpu, powermetrics
18
18
  from codecarbon.core.config import get_hierarchical_config, parse_gpu_ids
19
19
  from codecarbon.core.emissions import Emissions
20
20
  from codecarbon.core.units import Energy, Power, Time
21
21
  from codecarbon.core.util import count_cpus, suppress
22
22
  from codecarbon.external.geography import CloudMetadata, GeoMetadata
23
- from codecarbon.external.hardware import CPU, GPU, RAM
23
+ from codecarbon.external.hardware import CPU, GPU, RAM, AppleSiliconChip
24
24
  from codecarbon.external.logger import logger, set_logger_format, set_logger_level
25
25
  from codecarbon.external.scheduler import PeriodicScheduler
26
26
  from codecarbon.external.task import Task
@@ -275,7 +275,7 @@ class BaseEmissionsTracker(ABC):
275
275
  logger.info("[setup] RAM Tracking...")
276
276
  ram = RAM(tracking_mode=self._tracking_mode)
277
277
  self._conf["ram_total_size"] = ram.machine_memory_GB
278
- self._hardware: List[Union[RAM, CPU, GPU]] = [ram]
278
+ self._hardware: List[Union[RAM, CPU, GPU, AppleSiliconChip]] = [ram]
279
279
 
280
280
  # Hardware detection
281
281
  logger.info("[setup] GPU Tracking...")
@@ -293,7 +293,7 @@ class BaseEmissionsTracker(ABC):
293
293
  logger.info("No GPU found.")
294
294
 
295
295
  logger.info("[setup] CPU Tracking...")
296
- if cpu.is_powergadget_available():
296
+ if cpu.is_powergadget_available() and self._default_cpu_power is None:
297
297
  logger.info("Tracking Intel CPU via Power Gadget")
298
298
  hardware = CPU.from_utils(self._output_dir, "intel_power_gadget")
299
299
  self._hardware.append(hardware)
@@ -303,6 +303,23 @@ class BaseEmissionsTracker(ABC):
303
303
  hardware = CPU.from_utils(self._output_dir, "intel_rapl")
304
304
  self._hardware.append(hardware)
305
305
  self._conf["cpu_model"] = hardware.get_model()
306
+ elif (
307
+ powermetrics.is_powermetrics_available() and self._default_cpu_power is None
308
+ ):
309
+ logger.info("Tracking Apple CPU and GPU via PowerMetrics")
310
+ hardware_cpu = AppleSiliconChip.from_utils(
311
+ self._output_dir, chip_part="CPU"
312
+ )
313
+ self._hardware.append(hardware_cpu)
314
+ self._conf["cpu_model"] = hardware_cpu.get_model()
315
+
316
+ hardware_gpu = AppleSiliconChip.from_utils(
317
+ self._output_dir, chip_part="GPU"
318
+ )
319
+ self._hardware.append(hardware_gpu)
320
+
321
+ self._conf["gpu_model"] = hardware_gpu.get_model()
322
+ self._conf["gpu_count"] = 1
306
323
  else:
307
324
  logger.warning(
308
325
  "No CPU tracking mode found. Falling back on CPU constant mode."
@@ -640,9 +657,10 @@ class BaseEmissionsTracker(ABC):
640
657
  h_time = time.time()
641
658
  # Compute last_duration again for more accuracy
642
659
  last_duration = time.time() - self._last_measured_time
643
- power, energy = hardware.measure_power_and_energy(
644
- last_duration=last_duration
645
- )
660
+ (
661
+ power,
662
+ energy,
663
+ ) = hardware.measure_power_and_energy(last_duration=last_duration)
646
664
  # Apply the PUE of the datacenter to the consumed energy
647
665
  energy *= self._pue
648
666
  self._total_energy += energy
@@ -667,6 +685,21 @@ class BaseEmissionsTracker(ABC):
667
685
  f"Energy consumed for RAM : {self._total_ram_energy.kWh:.6f} kWh"
668
686
  + f". RAM Power : {self._ram_power.W} W"
669
687
  )
688
+ elif isinstance(hardware, AppleSiliconChip):
689
+ if hardware.chip_part == "CPU":
690
+ self._total_cpu_energy += energy
691
+ self._cpu_power = power
692
+ logger.info(
693
+ f"Energy consumed for all CPUs : {self._total_cpu_energy.kWh:.6f} kWh"
694
+ + f". Total CPU Power : {self._cpu_power.W} W"
695
+ )
696
+ elif hardware.chip_part == "GPU":
697
+ self._total_gpu_energy += energy
698
+ self._gpu_power = power
699
+ logger.info(
700
+ f"Energy consumed for all GPUs : {self._total_gpu_energy.kWh:.6f} kWh"
701
+ + f". Total GPU Power : {self._gpu_power.W} W"
702
+ )
670
703
  else:
671
704
  logger.error(f"Unknown hardware type: {hardware} ({type(hardware)})")
672
705
  h_time = time.time() - h_time
@@ -704,7 +737,7 @@ class BaseEmissionsTracker(ABC):
704
737
  emissions = self._prepare_emissions_data(delta=True)
705
738
  logger.info(
706
739
  f"{emissions.emissions_rate * 1000:.6f} g.CO2eq/s mean an estimation of "
707
- + f"{emissions.emissions_rate*3600*24*365:,} kg.CO2eq/year"
740
+ + f"{emissions.emissions_rate * 3600 * 24 * 365:,} kg.CO2eq/year"
708
741
  )
709
742
  if self._cc_api__out:
710
743
  self._cc_api__out.out(emissions)
@@ -11,6 +11,7 @@ import psutil
11
11
 
12
12
  from codecarbon.core.cpu import IntelPowerGadget, IntelRAPL
13
13
  from codecarbon.core.gpu import AllGPUDevices
14
+ from codecarbon.core.powermetrics import ApplePowermetrics
14
15
  from codecarbon.core.units import Energy, Power, Time
15
16
  from codecarbon.core.util import SLURM_JOB_ID, detect_cpu_model
16
17
  from codecarbon.external.logger import logger
@@ -202,7 +203,7 @@ class CPU(BaseHardware):
202
203
  return super().measure_power_and_energy(last_duration=last_duration)
203
204
 
204
205
  def start(self):
205
- if self._mode in ["intel_power_gadget", "intel_rapl"]:
206
+ if self._mode in ["intel_power_gadget", "intel_rapl", "apple_powermetrics"]:
206
207
  self._intel_interface.start()
207
208
  pass
208
209
 
@@ -395,3 +396,75 @@ class RAM(BaseHardware):
395
396
  ram_power = Power.from_watts(0)
396
397
 
397
398
  return ram_power
399
+
400
+
401
+ @dataclass
402
+ class AppleSiliconChip(BaseHardware):
403
+ def __init__(
404
+ self,
405
+ output_dir: str,
406
+ model: str,
407
+ chip_part: str = "CPU",
408
+ ):
409
+ self._output_dir = output_dir
410
+ self._model = model
411
+ self._interface = ApplePowermetrics(self._output_dir)
412
+ self.chip_part = chip_part
413
+
414
+ def __repr__(self) -> str:
415
+ return f"AppleSiliconChip ({self._model} > {self.chip_part})"
416
+
417
+ def _get_power(self) -> Power:
418
+ """
419
+ Get Chip part power
420
+ Args:
421
+ chip_part (str): Chip part to get power from (CPU, GPU)
422
+ :return: power in kW
423
+ """
424
+
425
+ all_details: Dict = self._interface.get_details()
426
+
427
+ power = 0
428
+ for metric, value in all_details.items():
429
+ if re.match(rf"^{self.chip_part} Power", metric):
430
+ power += value
431
+ logger.debug(f"_get_power_from_cpus - MATCH {metric} : {value}")
432
+
433
+ else:
434
+ logger.debug(f"_get_power_from_cpus - DONT MATCH {metric} : {value}")
435
+ return Power.from_watts(power)
436
+
437
+ def _get_energy(self, delay: Time) -> Energy:
438
+ """
439
+ Get Chip part energy deltas
440
+ Args:
441
+ chip_part (str): Chip part to get power from (Processor, GPU, etc.)
442
+ :return: energy in kWh
443
+ """
444
+ all_details: Dict = self._interface.get_details(delay)
445
+
446
+ energy = 0
447
+ for metric, value in all_details.items():
448
+ if re.match(rf"^{self.chip_part} Energy Delta_\d", metric):
449
+ energy += value
450
+ return Energy.from_energy(energy)
451
+
452
+ def total_power(self) -> Power:
453
+ return self._get_power()
454
+
455
+ def start(self):
456
+ self._interface.start()
457
+
458
+ def get_model(self):
459
+ return self._model
460
+
461
+ @classmethod
462
+ def from_utils(
463
+ cls, output_dir: str, model: Optional[str] = None, chip_part: str = "Processor"
464
+ ) -> "AppleSiliconChip":
465
+ if model is None:
466
+ model = detect_cpu_model()
467
+ if model is None:
468
+ logger.warning("Could not read AppleSiliconChip model.")
469
+
470
+ return cls(output_dir=output_dir, model=model, chip_part=chip_part)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: codecarbon
3
- Version: 2.3.2
3
+ Version: 2.3.4
4
4
  Author: Mila, DataForGood, BCG GAMMA, Comet.ml, Haverford College
5
5
  Classifier: Natural Language :: English
6
6
  Classifier: Programming Language :: Python :: 3.7
@@ -18,7 +18,7 @@ Requires-Dist: pynvml
18
18
  Requires-Dist: requests
19
19
  Requires-Dist: psutil
20
20
  Requires-Dist: py-cpuinfo
21
- Requires-Dist: fuzzywuzzy
21
+ Requires-Dist: rapidfuzz
22
22
  Requires-Dist: click
23
23
  Requires-Dist: prometheus_client
24
24
  Provides-Extra: viz
@@ -54,6 +54,7 @@ codecarbon/core/cpu.py
54
54
  codecarbon/core/emissions.py
55
55
  codecarbon/core/gpu.py
56
56
  codecarbon/core/measure.py
57
+ codecarbon/core/powermetrics.py
57
58
  codecarbon/core/rapl.py
58
59
  codecarbon/core/schemas.py
59
60
  codecarbon/core/units.py
@@ -96,6 +97,7 @@ tests/test_energy.py
96
97
  tests/test_geography.py
97
98
  tests/test_gpu.py
98
99
  tests/test_logging_output.py
100
+ tests/test_powermetrics.py
99
101
  tests/test_ram.py
100
102
  tests/test_tracking_inference.py
101
103
  tests/test_viz_data.py
@@ -4,7 +4,7 @@ pynvml
4
4
  requests
5
5
  psutil
6
6
  py-cpuinfo
7
- fuzzywuzzy
7
+ rapidfuzz
8
8
  click
9
9
  prometheus_client
10
10
 
@@ -10,7 +10,7 @@ DEPENDENCIES = [
10
10
  "requests",
11
11
  "psutil",
12
12
  "py-cpuinfo",
13
- "fuzzywuzzy",
13
+ "rapidfuzz",
14
14
  "click",
15
15
  "prometheus_client",
16
16
  ]
@@ -20,7 +20,7 @@ TEST_DEPENDENCIES = ["mock", "pytest", "responses", "tox", "numpy", "requests-mo
20
20
 
21
21
  setuptools.setup(
22
22
  name="codecarbon",
23
- version="2.3.2",
23
+ version="2.3.4",
24
24
  author="Mila, DataForGood, BCG GAMMA, Comet.ml, Haverford College",
25
25
  long_description=long_description,
26
26
  long_description_content_type="text/markdown",
@@ -5,7 +5,12 @@ from unittest import mock
5
5
 
6
6
  import pytest
7
7
 
8
- from codecarbon.core.cpu import TDP, IntelPowerGadget, IntelRAPL
8
+ from codecarbon.core.cpu import (
9
+ TDP,
10
+ IntelPowerGadget,
11
+ IntelRAPL,
12
+ is_powergadget_available,
13
+ )
9
14
  from codecarbon.core.units import Energy, Power, Time
10
15
  from codecarbon.external.hardware import CPU
11
16
  from codecarbon.input import DataSource
@@ -14,9 +19,10 @@ from codecarbon.input import DataSource
14
19
  class TestIntelPowerGadget(unittest.TestCase):
15
20
  @pytest.mark.integ_test
16
21
  def test_intel_power_gadget(self):
17
- power_gadget = IntelPowerGadget()
18
- cpu_details = power_gadget.get_cpu_details()
19
- assert len(cpu_details) > 0
22
+ if is_powergadget_available():
23
+ power_gadget = IntelPowerGadget()
24
+ cpu_details = power_gadget.get_cpu_details()
25
+ assert len(cpu_details) > 0
20
26
 
21
27
  @mock.patch("codecarbon.core.cpu.IntelPowerGadget._log_values")
22
28
  @mock.patch("codecarbon.core.cpu.IntelPowerGadget._setup_cli")
@@ -45,16 +51,16 @@ class TestIntelPowerGadget(unittest.TestCase):
45
51
  "GT Frequency(MHz)": 125.0,
46
52
  "GT Requsted Frequency(MHz)": 125.0,
47
53
  }
48
-
49
- power_gadget = IntelPowerGadget(
50
- output_dir=os.path.join(os.path.dirname(__file__), "test_data"),
51
- log_file_name="mock_intel_power_gadget_data.csv",
52
- )
53
- cpu_details = power_gadget.get_cpu_details()
54
- cpu_details["Cumulative IA Energy_0(mWh)"] = round(
55
- cpu_details["Cumulative IA Energy_0(mWh)"], 3
56
- )
57
- self.assertDictEqual(expected_cpu_details, cpu_details)
54
+ if is_powergadget_available():
55
+ power_gadget = IntelPowerGadget(
56
+ output_dir=os.path.join(os.path.dirname(__file__), "test_data"),
57
+ log_file_name="mock_intel_power_gadget_data.csv",
58
+ )
59
+ cpu_details = power_gadget.get_cpu_details()
60
+ cpu_details["Cumulative IA Energy_0(mWh)"] = round(
61
+ cpu_details["Cumulative IA Energy_0(mWh)"], 3
62
+ )
63
+ self.assertDictEqual(expected_cpu_details, cpu_details)
58
64
 
59
65
 
60
66
  class TestIntelRAPL(unittest.TestCase):
@@ -99,3 +99,34 @@ class TestCarbonTrackerConstant(unittest.TestCase):
99
99
  with open(file_path, "r") as f:
100
100
  lines = [line.rstrip() for line in f]
101
101
  assert len(lines) == 2
102
+
103
+ def test_carbon_tracker_offline_region_error(self):
104
+ # Test errors when unknown information is given to the offline tracker
105
+ from codecarbon.external.geography import CloudMetadata
106
+
107
+ tracker = OfflineEmissionsTracker(
108
+ country_iso_code="USA",
109
+ cloud_provider="aws",
110
+ cloud_region="us-east-1",
111
+ output_dir=self.emissions_path,
112
+ output_file=self.emissions_file,
113
+ )
114
+ tracker.start()
115
+ tracker._measure_power_and_energy()
116
+ cloud: CloudMetadata = tracker._get_cloud_metadata()
117
+
118
+ try:
119
+ with self.assertRaises(ValueError) as context:
120
+ tracker._emissions.get_cloud_country_iso_code(cloud)
121
+ self.assertTrue("Unable to find country name" in context.exception.args[0])
122
+
123
+ with self.assertRaises(ValueError) as context:
124
+ tracker._emissions.get_cloud_geo_region(cloud)
125
+ self.assertTrue("Unable to find country name" in context.exception.args[0])
126
+
127
+ with self.assertRaises(ValueError) as context:
128
+ tracker._emissions.get_cloud_country_name(cloud)
129
+ self.assertTrue("Unable to find country name" in context.exception.args[0])
130
+
131
+ finally:
132
+ tracker.stop()
@@ -0,0 +1,35 @@
1
+ import os
2
+ import unittest
3
+ import unittest.result
4
+ from unittest import mock
5
+
6
+ import pytest
7
+
8
+ from codecarbon.core.powermetrics import ApplePowermetrics, is_powermetrics_available
9
+
10
+
11
+ class TestApplePowerMetrics(unittest.TestCase):
12
+ @pytest.mark.integ_test
13
+ def test_apple_powermetrics(self):
14
+ if is_powermetrics_available():
15
+ power_gadget = ApplePowermetrics()
16
+ details = power_gadget.get_details()
17
+ assert len(details) > 0
18
+
19
+ @mock.patch("codecarbon.core.powermetrics.ApplePowermetrics._log_values")
20
+ @mock.patch("codecarbon.core.powermetrics.ApplePowermetrics._setup_cli")
21
+ def test_get_details(self, mock_setup, mock_log_values):
22
+ expected_details = {
23
+ "CPU Power": 0.3146,
24
+ "CPU Energy Delta": 0.3146,
25
+ "GPU Power": 0.0386,
26
+ "GPU Energy Delta": 0.0386,
27
+ }
28
+ if is_powermetrics_available():
29
+ powermetrics = ApplePowermetrics(
30
+ output_dir=os.path.join(os.path.dirname(__file__), "test_data"),
31
+ log_file_name="mock_powermetrics_log.txt",
32
+ )
33
+ cpu_details = powermetrics.get_details()
34
+
35
+ self.assertDictEqual(expected_details, cpu_details)
@@ -1 +0,0 @@
1
- __version__ = "2.3.2"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes