Skip to content

[Version 3] New heuristic for RAM #804

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Apr 13, 2025
Merged
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
2 changes: 1 addition & 1 deletion codecarbon/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "3.0.0_rc3"
__version__ = "3.0.0_rc7"
7 changes: 5 additions & 2 deletions codecarbon/core/cpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,17 @@ def is_rapl_available() -> bool:
def is_psutil_available():
try:
nice = psutil.cpu_times().nice
if nice > 0.1:
if nice > 0.0001:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because on huge CPU, it could be above 0 but under 0.1.

return True
else:
logger.debug(
f"is_psutil_available() : psutil.cpu_times().nice is too small : {nice} !"
)
return False
except Exception as e:
logger.debug(
"Not using the psutil interface, an exception occurred while instantiating "
+ f"psutil.cpu_percent : {e}",
+ f"psutil.cpu_times : {e}",
)
return False

Expand Down
48 changes: 32 additions & 16 deletions codecarbon/core/resource_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
from codecarbon.core import cpu, gpu, powermetrics
from codecarbon.core.config import parse_gpu_ids
from codecarbon.core.util import detect_cpu_model, is_linux_os, is_mac_os, is_windows_os
from codecarbon.external.hardware import CPU, GPU, MODE_CPU_LOAD, RAM, AppleSiliconChip
from codecarbon.external.hardware import CPU, GPU, MODE_CPU_LOAD, AppleSiliconChip
from codecarbon.external.logger import logger
from codecarbon.external.ram import RAM


class ResourceTracker:
Expand All @@ -16,23 +17,38 @@ def __init__(self, tracker):

def set_RAM_tracking(self):
logger.info("[setup] RAM Tracking...")
self.ram_tracker = "3 Watts for 8 GB ratio constant"
ram = RAM(tracking_mode=self.tracker._tracking_mode)
if self.tracker._force_ram_power is not None:
self.ram_tracker = (
f"User specified constant: {self.tracker._force_ram_power} Watts"
)
logger.info(
f"Using user-provided RAM power: {self.tracker._force_ram_power} Watts"
)
else:
self.ram_tracker = "RAM power estimation model"
ram = RAM(
tracking_mode=self.tracker._tracking_mode,
force_ram_power=self.tracker._force_ram_power,
)
self.tracker._conf["ram_total_size"] = ram.machine_memory_GB
self.tracker._hardware: List[Union[RAM, CPU, GPU, AppleSiliconChip]] = [ram]

def set_CPU_tracking(self):
logger.info("[setup] CPU Tracking...")
cpu_number = self.tracker._conf.get("cpu_physical_count")
tdp = cpu.TDP()

max_power = tdp.tdp * cpu_number if tdp.tdp is not None else None
if self.tracker._conf.get("force_mode_cpu_load", False) and tdp.tdp is not None:
if tdp.tdp is None:
logger.warning(
"Force CPU load mode requested but TDP could not be calculated. Falling back to another mode."
)
elif cpu.is_psutil_available():
if self.tracker._force_cpu_power is not None:
logger.info(
f"Using user-provided CPU power: {self.tracker._force_cpu_power} Watts"
)
self.cpu_tracker = "User Input TDP constant"
max_power = self.tracker._force_cpu_power
else:
max_power = tdp.tdp * cpu_number if tdp.tdp is not None else None
if self.tracker._conf.get("force_mode_cpu_load", False) and (
tdp.tdp is not None or self.tracker._force_cpu_power is not None
):
if cpu.is_psutil_available():
# Register a CPU with MODE_CPU_LOAD
model = tdp.model
hardware_cpu = CPU.from_utils(
Expand All @@ -50,7 +66,7 @@ def set_CPU_tracking(self):
logger.warning(
"Force CPU load mode requested but psutil is not available."
)
if cpu.is_powergadget_available() and self.tracker._default_cpu_power is None:
if cpu.is_powergadget_available() and self.tracker._force_cpu_power is None:
logger.info("Tracking Intel CPU via Power Gadget")
self.cpu_tracker = "Power Gadget"
hardware_cpu = CPU.from_utils(
Expand All @@ -73,7 +89,7 @@ def set_CPU_tracking(self):
# change code to check if powermetrics needs to be installed or just sudo setup
elif (
powermetrics.is_powermetrics_available()
and self.tracker._default_cpu_power is None
and self.tracker._force_cpu_power is None
):
logger.info("Tracking Apple CPU and GPU via PowerMetrics")
self.gpu_tracker = "PowerMetrics"
Expand Down Expand Up @@ -113,9 +129,9 @@ def set_CPU_tracking(self):
)
self.cpu_tracker = "TDP constant"
model = tdp.model
if (max_power is None) and self.tracker._default_cpu_power:
if (max_power is None) and self.tracker._force_cpu_power:
# We haven't been able to calculate CPU power but user has input a default one. We use it
user_input_power = self.tracker._default_cpu_power
user_input_power = self.tracker._force_cpu_power
logger.debug(f"Using user input TDP: {user_input_power} W")
self.cpu_tracker = "User Input TDP constant"
max_power = user_input_power
Expand Down Expand Up @@ -205,7 +221,7 @@ def set_CPU_GPU_ram_tracking(self):
self.set_CPU_tracking()
self.set_GPU_tracking()

logger.debug(
logger.info(
f"""The below tracking methods have been set up:
RAM Tracking Method: {self.ram_tracker}
CPU Tracking Method: {self.cpu_tracker}
Expand Down
14 changes: 12 additions & 2 deletions codecarbon/core/units.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from dataclasses import dataclass, field

# from pydantic.dataclasses import dataclass, field


@dataclass
class Time:
Expand Down Expand Up @@ -61,7 +63,10 @@ class Energy:

@classmethod
def from_power_and_time(cls, *, power: "Power", time: "Time") -> "Energy":
return cls(kWh=power.kW * time.hours)
assert isinstance(power.kW, float)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quel est l'intérêt de l'assert ici ? ça ne risque pas de casser le programme à un endroit non désiré ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cette endroit est la base du calcul, si jamais il n'y a pas la bonne unité il y a plein d'effets de bord.
Ca m'est arrivé lors du dev et j'ai mis du temps à trouver la source du problème. Je me suis dit qu'en le vérifiant ici, cela évitera des problèmes à l'avenir.

assert isinstance(time.hours, float)
energy = power.kW * time.hours
return cls(kWh=energy)

@classmethod
def from_ujoules(cls, energy: float) -> "Energy":
Expand All @@ -82,7 +87,10 @@ def __add__(self, other: "Energy") -> "Energy":
return Energy(self.kWh + other.kWh)

def __mul__(self, factor: float) -> "Energy":
return Energy(self.kWh * factor)
assert isinstance(factor, float)
assert isinstance(self.kWh, float)
result = Energy(self.kWh * factor)
return result

def __float__(self) -> float:
return float(self.kWh)
Expand Down Expand Up @@ -127,7 +135,9 @@ def from_energies_and_delay(cls, e1: "Energy", e2: "Energy", delay: "Time"):
Power: Resulting Power estimation
"""
delta_energy = abs(e2.kWh - e1.kWh)
assert isinstance(delta_energy, float)
kW = delta_energy / delay.hours if delay.hours != 0.0 else 0.0
assert isinstance(delta_energy, float)
return cls(kW=kW)

@classmethod
Expand Down
43 changes: 31 additions & 12 deletions codecarbon/emissions_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
from codecarbon.core.units import Energy, Power, Time
from codecarbon.core.util import count_cpus, count_physical_cpus, suppress
from codecarbon.external.geography import CloudMetadata, GeoMetadata
from codecarbon.external.hardware import CPU, GPU, RAM, AppleSiliconChip
from codecarbon.external.hardware import CPU, GPU, AppleSiliconChip
from codecarbon.external.logger import logger, set_logger_format, set_logger_level
from codecarbon.external.ram import RAM
from codecarbon.external.scheduler import PeriodicScheduler
from codecarbon.external.task import Task
from codecarbon.input import DataSource
Expand Down Expand Up @@ -171,7 +172,8 @@ def __init__(
log_level: Optional[Union[int, str]] = _sentinel,
on_csv_write: Optional[str] = _sentinel,
logger_preamble: Optional[str] = _sentinel,
default_cpu_power: Optional[int] = _sentinel,
force_cpu_power: Optional[int] = _sentinel,
force_ram_power: Optional[int] = _sentinel,
pue: Optional[int] = _sentinel,
force_mode_cpu_load: Optional[bool] = _sentinel,
allow_multiple_runs: Optional[bool] = _sentinel,
Expand Down Expand Up @@ -227,7 +229,8 @@ def __init__(
Accepts one of "append" or "update". Default is "append".
:param logger_preamble: String to systematically include in the logger.
messages. Defaults to "".
:param default_cpu_power: cpu power to be used as default if the cpu is not known.
:param force_cpu_power: cpu power to be used instead of automatic detection.
:param force_ram_power: ram power to be used instead of automatic detection.
:param pue: PUE (Power Usage Effectiveness) of the datacenter.
:param force_mode_cpu_load: Force the addition of a CPU in MODE_CPU_LOAD
:param allow_multiple_runs: Allow multiple instances of codecarbon running in parallel. Defaults to False.
Expand Down Expand Up @@ -277,7 +280,8 @@ def __init__(
self._set_from_conf(tracking_mode, "tracking_mode", "machine")
self._set_from_conf(on_csv_write, "on_csv_write", "append")
self._set_from_conf(logger_preamble, "logger_preamble", "")
self._set_from_conf(default_cpu_power, "default_cpu_power")
self._set_from_conf(force_cpu_power, "force_cpu_power")
self._set_from_conf(force_ram_power, "force_ram_power")
self._set_from_conf(pue, "pue", 1.0, float)
self._set_from_conf(force_mode_cpu_load, "force_mode_cpu_load", False)
self._set_from_conf(
Expand Down Expand Up @@ -521,6 +525,15 @@ def flush(self) -> Optional[float]:
but keep running the experiment.
:return: CO2 emissions in kgs
"""
# if another instance of codecarbon is already running, Nothing to do here
if (
hasattr(self, "_another_instance_already_running")
and self._another_instance_already_running
):
logger.warning(
"Another instance of codecarbon is already running. Exiting."
)
return
if self._start_time is None:
logger.error("You first need to start the tracker.")
return None
Expand Down Expand Up @@ -996,15 +1009,16 @@ def track_emissions(
log_level: Optional[Union[int, str]] = _sentinel,
on_csv_write: Optional[str] = _sentinel,
logger_preamble: Optional[str] = _sentinel,
default_cpu_power: Optional[int] = _sentinel,
pue: Optional[int] = _sentinel,
allow_multiple_runs: Optional[bool] = _sentinel,
offline: Optional[bool] = _sentinel,
country_iso_code: Optional[str] = _sentinel,
region: Optional[str] = _sentinel,
cloud_provider: Optional[str] = _sentinel,
cloud_region: Optional[str] = _sentinel,
country_2letter_iso_code: Optional[str] = _sentinel,
force_cpu_power: Optional[int] = _sentinel,
force_ram_power: Optional[int] = _sentinel,
pue: Optional[int] = _sentinel,
allow_multiple_runs: Optional[bool] = _sentinel,
):
"""
Decorator that supports both `EmissionsTracker` and `OfflineEmissionsTracker`
Expand Down Expand Up @@ -1057,8 +1071,6 @@ def track_emissions(
Accepts one of "append" or "update". Default is "append".
:param logger_preamble: String to systematically include in the logger.
messages. Defaults to "".
:param default_cpu_power: cpu power to be used as default if the cpu is not known.
:param pue: PUE (Power Usage Effectiveness) of the datacenter.
:param allow_multiple_runs: Prevent multiple instances of codecarbon running. Defaults to False.
:param offline: Indicates if the tracker should be run in offline mode.
:param country_iso_code: 3 letter ISO Code of the country where the experiment is
Expand All @@ -1078,6 +1090,10 @@ def track_emissions(
See http://api.electricitymap.org/v3/zones for
a list of codes and their corresponding
locations.
:param force_cpu_power: cpu power to be used instead of automatic detection.
:param force_ram_power: ram power to be used instead of automatic detection.
:param pue: PUE (Power Usage Effectiveness) of the datacenter.
:param allow_multiple_runs: Prevent multiple instances of codecarbon running. Defaults to False.

:return: The decorated function
"""
Expand Down Expand Up @@ -1109,14 +1125,16 @@ def wrapped_fn(*args, **kwargs):
log_level=log_level,
on_csv_write=on_csv_write,
logger_preamble=logger_preamble,
default_cpu_power=default_cpu_power,
pue=pue,
allow_multiple_runs=allow_multiple_runs,
country_iso_code=country_iso_code,
region=region,
cloud_provider=cloud_provider,
cloud_region=cloud_region,
country_2letter_iso_code=country_2letter_iso_code,
force_cpu_power=force_cpu_power,
force_ram_power=force_ram_power,
pue=pue,
allow_multiple_runs=allow_multiple_runs,
)
else:
tracker = EmissionsTracker(
Expand Down Expand Up @@ -1144,7 +1162,8 @@ def wrapped_fn(*args, **kwargs):
log_level=log_level,
on_csv_write=on_csv_write,
logger_preamble=logger_preamble,
default_cpu_power=default_cpu_power,
force_cpu_power=force_cpu_power,
force_ram_power=force_ram_power,
pue=pue,
allow_multiple_runs=allow_multiple_runs,
)
Expand Down
Loading
Loading