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
12 changes: 11 additions & 1 deletion codecarbon/core/cpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,16 @@
from typing import Dict, Optional, Tuple

import pandas as pd
import psutil
from rapidfuzz import fuzz, process, utils

try:
import psutil

PSUTIL_AVAILABLE = True
except ImportError:
PSUTIL_AVAILABLE = False
psutil = None

from codecarbon.core.rapl import RAPLFile
from codecarbon.core.units import Time
from codecarbon.core.util import count_cpus, detect_cpu_model
Expand Down Expand Up @@ -207,6 +214,9 @@ def is_rapl_available(rapl_dir: Optional[str] = None) -> bool:


def is_psutil_available():
if not PSUTIL_AVAILABLE:
logger.debug("psutil module is not available.")
return False
try:
cpu_times = psutil.cpu_times()

Expand Down
24 changes: 23 additions & 1 deletion codecarbon/core/resource_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,29 @@ def _setup_fallback_tracking(self, tdp, max_power):
logger.info(f"CPU Model on constant consumption mode: {model}")
self.tracker._conf["cpu_model"] = model

if tdp:
# Check for forced constant mode first
if self.tracker._conf.get("force_mode_constant", False):
logger.info(
"Force constant mode requested - bypassing psutil and using constant CPU power"
)
model = tdp.model
if max_power is None and self.tracker._force_cpu_power:
max_power = self.tracker._force_cpu_power
logger.debug(f"Using user input TDP for constant mode: {max_power} W")
self.cpu_tracker = "User Input TDP constant"
else:
self.cpu_tracker = "TDP constant"
logger.info(f"CPU Model on forced constant consumption mode: {model}")
self.tracker._conf["cpu_model"] = model
hardware_cpu = CPU.from_utils(
self.tracker._output_dir, "constant", model, max_power
)
self.tracker._hardware.append(hardware_cpu)
return

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():
logger.warning(
"No CPU tracking mode found. Falling back on CPU load mode."
Expand Down
15 changes: 14 additions & 1 deletion codecarbon/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@
from typing import Optional, Union

import cpuinfo
import psutil

try:
import psutil

PSUTIL_AVAILABLE = True
except ImportError:
PSUTIL_AVAILABLE = False
psutil = None

from codecarbon.external.logger import logger

Expand Down Expand Up @@ -146,6 +153,12 @@ def _windows_get_physical_sockets():


def count_cpus() -> int:
if not PSUTIL_AVAILABLE:
logger.warning("psutil not available, using fallback CPU count detection")
# Fallback to using os.cpu_count() or physical CPU count
cpu_count = os.cpu_count()
return cpu_count if cpu_count is not None else 1

if SLURM_JOB_ID is None:
return psutil.cpu_count()

Expand Down
4 changes: 4 additions & 0 deletions codecarbon/emissions_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ def __init__(
wue: Optional[float] = _sentinel,
force_carbon_intensity_g_co2e_kwh: Optional[float] = _sentinel,
force_mode_cpu_load: Optional[bool] = _sentinel,
force_mode_constant: Optional[bool] = _sentinel,
allow_multiple_runs: Optional[bool] = _sentinel,
rapl_include_dram: Optional[bool] = _sentinel,
rapl_prefer_psys: Optional[bool] = _sentinel,
Expand Down Expand Up @@ -275,6 +276,8 @@ def __init__(
(CPU + chipset + PCIe). When False, uses package domains which
are more reliable. Note: psys can report higher values than
CPU TDP and may be unreliable on older systems.
:param force_mode_constant: Force the addition of a CPU in constant mode, bypassing psutil
:param allow_multiple_runs: Allow multiple instances of codecarbon running in parallel. Defaults to False.
"""

# logger.info("base tracker init")
Expand Down Expand Up @@ -377,6 +380,7 @@ def __init__(
self._set_from_conf(force_mode_cpu_load, "force_mode_cpu_load", False, bool)
self._set_from_conf(rapl_include_dram, "rapl_include_dram", False, bool)
self._set_from_conf(rapl_prefer_psys, "rapl_prefer_psys", False, bool)
self._set_from_conf(force_mode_constant, "force_mode_constant", False, bool)
self._set_from_conf(
experiment_id, "experiment_id", "5b0fa12a-3dd7-45bb-9766-cc326314d9f1"
)
Expand Down
146 changes: 146 additions & 0 deletions tests/test_force_constant_mode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import os
import tempfile
import time
import unittest
from unittest import mock

import pandas as pd

from codecarbon.emissions_tracker import (
EmissionsTracker,
OfflineEmissionsTracker,
)


def light_computation(run_time_secs: int = 1):
end_time: float = (
time.perf_counter() + run_time_secs
) # Run for `run_time_secs` seconds
while time.perf_counter() < end_time:
pass


class TestForceConstantMode(unittest.TestCase):
def setUp(self) -> None:
self.project_name = "project_TestForceConstantMode"
self.emissions_file = "emissions-test-TestForceConstantMode.csv"
self.emissions_path = tempfile.gettempdir()
self.emissions_file_path = os.path.join(
self.emissions_path, self.emissions_file
)
if os.path.isfile(self.emissions_file_path):
os.remove(self.emissions_file_path)

def tearDown(self) -> None:
if os.path.isfile(self.emissions_file_path):
os.remove(self.emissions_file_path)

def test_force_constant_mode_online(self):
"""Test force_mode_constant parameter with online tracker"""
tracker = EmissionsTracker(
output_dir=self.emissions_path,
output_file=self.emissions_file,
force_mode_constant=True,
)
tracker.start()
light_computation(run_time_secs=1)
emissions = tracker.stop()

# Check that emissions were calculated
assert isinstance(emissions, float)
self.assertNotEqual(emissions, 0.0)

# Verify output file was created
self.verify_output_file(self.emissions_file_path)

# Check CSV content shows constant mode
df = pd.read_csv(self.emissions_file_path)
# The cpu_power should be a constant value (not varying like in load mode)
self.assertGreater(df["cpu_power"].iloc[0], 0)

def test_force_constant_mode_offline(self):
"""Test force_mode_constant parameter with offline tracker"""
tracker = OfflineEmissionsTracker(
country_iso_code="USA",
output_dir=self.emissions_path,
output_file=self.emissions_file,
force_mode_constant=True,
)
tracker.start()
light_computation(run_time_secs=1)
emissions = tracker.stop()

assert isinstance(emissions, float)
self.assertNotEqual(emissions, 0.0)
self.verify_output_file(self.emissions_file_path)

def test_force_constant_mode_with_custom_cpu_power(self):
"""Test force_mode_constant with custom CPU power"""
custom_cpu_power = 200 # 200W
tracker = EmissionsTracker(
output_dir=self.emissions_path,
output_file=self.emissions_file,
force_mode_constant=True,
force_cpu_power=custom_cpu_power,
)
tracker.start()
light_computation(run_time_secs=1)
emissions = tracker.stop()

assert isinstance(emissions, float)
self.assertNotEqual(emissions, 0.0)

# Check that the custom CPU power was used
df = pd.read_csv(self.emissions_file_path)
# CPU power should be 50% of the TDP (constant mode assumption)
expected_cpu_power = custom_cpu_power / 2
self.assertEqual(df["cpu_power"].iloc[0], expected_cpu_power)

@mock.patch("codecarbon.core.cpu.PSUTIL_AVAILABLE", False)
@mock.patch("codecarbon.core.util.PSUTIL_AVAILABLE", False)
def test_force_constant_mode_without_psutil(self):
"""Test that force_mode_constant works when psutil is not available"""
tracker = EmissionsTracker(
output_dir=self.emissions_path,
output_file=self.emissions_file,
force_mode_constant=True,
)
tracker.start()
light_computation(run_time_secs=1)
emissions = tracker.stop()

assert isinstance(emissions, float)
self.assertNotEqual(emissions, 0.0)
self.verify_output_file(self.emissions_file_path)

def test_force_constant_mode_takes_precedence_over_cpu_load(self):
"""Test that force_mode_constant takes precedence over force_mode_cpu_load"""
tracker = EmissionsTracker(
output_dir=self.emissions_path,
output_file=self.emissions_file,
force_mode_constant=True,
force_mode_cpu_load=True, # This should be ignored
)
tracker.start()
light_computation(run_time_secs=1)
emissions = tracker.stop()

assert isinstance(emissions, float)
self.assertNotEqual(emissions, 0.0)
self.verify_output_file(self.emissions_file_path)

def verify_output_file(self, file_path: str) -> None:
"""Verify that the output CSV file exists and has expected structure"""
with open(file_path, "r") as f:
lines = [line.rstrip() for line in f]
assert len(lines) == 2 # Header + 1 data row

# Check that it's a valid CSV with expected columns
df = pd.read_csv(file_path)
expected_columns = ["emissions", "cpu_power", "cpu_energy"]
for col in expected_columns:
self.assertIn(col, df.columns)


if __name__ == "__main__":
unittest.main()
Loading