Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 39 additions & 18 deletions codecarbon/core/cpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@

from __future__ import annotations

import csv
import os
import re
import shutil
import statistics
import subprocess
import sys
from functools import lru_cache
from typing import TYPE_CHECKING, Dict, Optional, Tuple
from typing import Dict, Optional, Tuple

import psutil
from rapidfuzz import fuzz, process, utils
Expand All @@ -22,9 +24,6 @@
from codecarbon.core.util import count_cpus, detect_cpu_model
from codecarbon.external.logger import logger

if TYPE_CHECKING:
import pandas as pd

# default W value per core for a CPU if no model is found in the ref csv
DEFAULT_POWER_PER_CORE = 4

Expand Down Expand Up @@ -375,16 +374,36 @@ def get_cpu_details(self) -> Dict:
self._log_values()
cpu_details = {}
try:
import pandas as pd

cpu_data = pd.read_csv(self._log_file_path).dropna()
for col_name in cpu_data.columns:
with open(self._log_file_path, newline="", encoding="utf-8-sig") as f:
reader = csv.DictReader(f)
columns = reader.fieldnames or []
# pandas named blank headers "Unnamed: <i>"; keep those keys
# stable for anyone consuming the returned details dict.
renames = {c: f"Unnamed: {i}" for i, c in enumerate(columns) if not c}
# Intel Power Gadget appends a ragged summary block after the
# samples; ``.dropna()`` used to discard those rows, and dropping
# rows with any missing/blank field does the same thing.
rows = [
row
for row in reader
if all(v not in (None, "") for v in row.values())
and None not in row
]

for col_name in columns:
if col_name in ["System Time", "Elapsed Time (sec)", "RDTSC"]:
continue
try:
values = [float(row[col_name]) for row in rows]
except (TypeError, ValueError):
continue # non-numeric column, nothing to average
if not values:
continue
key = renames.get(col_name, col_name)
if "Cumulative" in col_name:
cpu_details[col_name] = cpu_data[col_name].iloc[-1]
cpu_details[key] = values[-1]
else:
cpu_details[col_name] = cpu_data[col_name].mean()
cpu_details[key] = statistics.fmean(values)
except Exception as e:
logger.info(
f"Unable to read Intel Power Gadget logged file at {self._log_file_path}\n \
Expand Down Expand Up @@ -898,30 +917,30 @@ def __init__(self):
self.model, self.tdp = self._main()

@staticmethod
def _get_cpu_constant_power(match: str, cpu_power_df: pd.DataFrame) -> int:
def _get_cpu_constant_power(match: str, cpu_power_rows: list[dict]) -> int:
"""Extract constant power from matched CPU"""
return float(cpu_power_df[cpu_power_df["Name"] == match]["TDP"].values[0])
return float(next(r for r in cpu_power_rows if r["Name"] == match)["TDP"])

def _get_cpu_power_from_registry(self, cpu_model_raw: str) -> Optional[int]:
from codecarbon.input import DataSource

cpu_power_df = DataSource().get_cpu_power_data()
cpu_power_df = DataSource().get_cpu_power_rows()
cpu_matching = self._get_matching_cpu(cpu_model_raw, cpu_power_df)
if cpu_matching:
power = self._get_cpu_constant_power(cpu_matching, cpu_power_df)
return power
return None

def _get_matching_cpu(
self, model_raw: str, cpu_df: pd.DataFrame, greedy=False
self, model_raw: str, cpu_df: list[dict], greedy=False
) -> str:
"""
Get matching cpu name

:args:
model_raw (str): raw name of the cpu model detected on the machine

cpu_df (DataFrame): table containing cpu models along their tdp
cpu_df (list[dict]): rows of cpu models along their tdp

greedy (default False): if multiple cpu models match with an equal
ratio of similarity, greedy (True) selects the first model,
Expand All @@ -945,9 +964,11 @@ def _get_matching_cpu(
THRESHOLD_DIRECT: int = 100
THRESHOLD_TOKEN_SET: int = 100

names = [row["Name"] for row in cpu_df]

direct_match = process.extractOne(
model_raw,
cpu_df["Name"],
names,
processor=lambda s: s.lower(),
scorer=fuzz.ratio,
score_cutoff=THRESHOLD_DIRECT,
Expand All @@ -964,7 +985,7 @@ def _get_matching_cpu(
model_raw = re.sub(r" @\s*\d+\.\d+GHz", "", model_raw)
direct_match = process.extractOne(
model_raw,
cpu_df["Name"],
names,
processor=lambda s: s.lower(),
scorer=fuzz.ratio,
score_cutoff=THRESHOLD_DIRECT,
Expand All @@ -974,7 +995,7 @@ def _get_matching_cpu(
return direct_match[0]
indirect_matches = process.extract(
model_raw,
cpu_df["Name"],
names,
processor=utils.default_process,
scorer=fuzz.token_set_ratio,
score_cutoff=THRESHOLD_TOKEN_SET,
Expand Down
44 changes: 17 additions & 27 deletions codecarbon/core/emissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,14 @@
https://github.com/responsibleproblemsolving/energy-usage
"""

from typing import TYPE_CHECKING, Dict, Optional
from typing import Dict, Optional

from codecarbon.core import electricitymaps_api
from codecarbon.core.units import EmissionsPerKWh, Energy
from codecarbon.external.geography import CloudMetadata, GeoMetadata
from codecarbon.external.logger import logger
from codecarbon.input import DataSource, DataSourceException

if TYPE_CHECKING:
import pandas as pd

_NORDIC_REGIONS_BY_COUNTRY = {
"SWE": {"SE1", "SE2", "SE3", "SE4"},
"NOR": {"NO1", "NO2", "NO3", "NO4", "NO5"},
Expand Down Expand Up @@ -65,12 +62,12 @@ def get_cloud_emissions(
)
return energy.kWh * (self._force_carbon_intensity_g_co2e_kwh / 1000.0)

df: pd.DataFrame = self._data_source.get_cloud_emissions_data()
try:
row = self._data_source.find_cloud_region(cloud.provider, cloud.region)
if row is None:
raise KeyError(f"{cloud.provider}/{cloud.region}")
emissions_per_kWh: EmissionsPerKWh = EmissionsPerKWh.from_g_per_kWh(
df.loc[
(df["provider"] == cloud.provider) & (df["region"] == cloud.region)
]["impact"].item()
row["impact"]
)
emissions = emissions_per_kWh.kgs_per_kWh * energy.kWh # kgs
except Exception as e:
Expand Down Expand Up @@ -100,51 +97,44 @@ def get_cloud_country_name(self, cloud: CloudMetadata) -> str:
"""
Returns the Country Name where the cloud region is located
"""
df: pd.DataFrame = self._data_source.get_cloud_emissions_data()
flags = (df["provider"] == cloud.provider) & (df["region"] == cloud.region)
selected = df.loc[flags]
if not len(selected):
row = self._data_source.find_cloud_region(cloud.provider, cloud.region)
if row is None:
raise ValueError(
"Unable to find country name for "
f"cloud_provider={cloud.provider}, "
f"cloud_region={cloud.region}"
)
return selected["country_name"].item()
return row["country_name"]

def get_cloud_country_iso_code(self, cloud: CloudMetadata) -> str:
"""
Returns the Country ISO Code where the cloud region is located
"""
df: pd.DataFrame = self._data_source.get_cloud_emissions_data()
flags = (df["provider"] == cloud.provider) & (df["region"] == cloud.region)
selected = df.loc[flags]
if not len(selected):
row = self._data_source.find_cloud_region(cloud.provider, cloud.region)
if row is None:
raise ValueError(
"Unable to find country ISO Code for "
f"cloud_provider={cloud.provider}, "
f"cloud_region={cloud.region}"
)
return selected["countryIsoCode"].item()
return row["countryIsoCode"]

def get_cloud_geo_region(self, cloud: CloudMetadata) -> str:
"""
Returns the State/City where the cloud region is located
"""
df: pd.DataFrame = self._data_source.get_cloud_emissions_data()
flags = (df["provider"] == cloud.provider) & (df["region"] == cloud.region)
selected = df.loc[flags]
if not len(selected):
row = self._data_source.find_cloud_region(cloud.provider, cloud.region)
if row is None:
raise ValueError(
"Unable to find State/City name for "
f"cloud_provider={cloud.provider}, "
f"cloud_region={cloud.region}"
)

state = selected["state"].item()
if state is not None:
return state
city = selected["city"].item()
return city
# Empty ``state`` used to arrive here as pandas' NaN, which is not None,
# so this returned NaN for the 30 of 40 rows that only have a city.
# It now falls through to the city as originally intended.
return row["state"] if row["state"] is not None else row["city"]

def get_private_infra_emissions(self, energy: Energy, geo: GeoMetadata) -> float:
"""
Expand Down
14 changes: 7 additions & 7 deletions codecarbon/core/powermetrics.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import math
import os
import re
import shutil
import statistics
import subprocess
import sys
import time
from functools import lru_cache
from typing import Dict

import numpy as np

from codecarbon.core.util import detect_cpu_model
from codecarbon.external.logger import logger

Expand Down Expand Up @@ -178,8 +178,8 @@ def get_details(self) -> Dict:
for chip_part in ("CPU", "GPU"):
power_list = re.findall(rf"{chip_part} Power: (\d+) mW", logfile)
if not power_list:
# np.mean([]) is NaN, and NaN poisons every downstream total,
# so report 0 W instead and make the situation visible.
# An empty mean is NaN, and NaN poisons every downstream
# total, so report 0 W instead and make the situation visible.
logger.warning(
f"Powermetrics returned no '{chip_part} Power' sample in "
+ f"{self._log_file_path}, reporting 0 W."
Expand All @@ -188,9 +188,9 @@ def get_details(self) -> Dict:
details[f"{chip_part} Energy Delta"] = 0.0
continue
watts = [float(power) / 1000 for power in power_list]
details[f"{chip_part} Power"] = np.mean(watts)
details[f"{chip_part} Energy Delta"] = np.sum(
[(self._interval / 1000) * watt for watt in watts]
details[f"{chip_part} Power"] = statistics.fmean(watts)
details[f"{chip_part} Energy Delta"] = math.fsum(
(self._interval / 1000) * watt for watt in watts
)
except Exception as e:
logger.info(
Expand Down
13 changes: 4 additions & 9 deletions codecarbon/emissions_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,6 @@ def _init_output_methods(self, *, api_key: str = None):
from codecarbon.output_methods.file import FileOutput
from codecarbon.output_methods.http import CodeCarbonAPIOutput, HTTPOutput
from codecarbon.output_methods.metrics.logfire import LogfireOutput
from codecarbon.output_methods.metrics.prometheus import PrometheusOutput

methods = set(self._output_methods) if self._output_methods else set()

Expand Down Expand Up @@ -651,6 +650,8 @@ def _init_output_methods(self, *, api_key: str = None):
self.run_id = uuid.uuid4()

if OutputMethod.PROMETHEUS in methods:
from codecarbon.output_methods.metrics.prometheus import PrometheusOutput

self._output_handlers.append(
PrometheusOutput(
self._prometheus_url,
Expand Down Expand Up @@ -1381,15 +1382,9 @@ def _resolve_offline_country_name(self) -> None:
def _validate_offline_cloud_provider(self) -> None:
if not self._cloud_provider:
return
df = DataSource().get_cloud_emissions_data()
if (
len(
df.loc[
(df["provider"] == self._cloud_provider)
& (df["region"] == self._cloud_region)
]
)
== 0
DataSource().find_cloud_region(self._cloud_provider, self._cloud_region)
is None
):
logger.error(
"Cloud Provider/Region "
Expand Down
Loading
Loading