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
20 changes: 15 additions & 5 deletions codecarbon/core/cpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,11 @@ def _select_domains_to_use(
logger.warning(
"\tRAPL - No package or psys domains found, using all available domains"
)
domains_to_use = readable_domains
domains_to_use = [
domain
for domain in readable_domains
if self.rapl_include_dram or "dram" not in (domain[5] or "").lower()
]

return domains_to_use

Expand Down Expand Up @@ -779,8 +783,9 @@ def _create_rapl_files(self, domain_map: dict, found_main_readable: bool):
domain_name,
) in domain_map.values():
try:
if domain_name and (
"package" in domain_name.lower() or "psys" in domain_name.lower()
domain_lower = (domain_name or "").lower()
if any(
keyword in domain_lower for keyword in ("package", "psys", "dram")
):
display_name = f"Processor Energy Delta_{domain_index}(kWh)"
domain_index += 1
Expand All @@ -789,7 +794,12 @@ def _create_rapl_files(self, domain_map: dict, found_main_readable: bool):

interface_type = "MMIO" if is_mmio else "MSR"
self._rapl_files.append(
RAPLFile(name=display_name, path=rapl_file, max_path=rapl_file_max)
RAPLFile(
name=display_name,
path=rapl_file,
max_path=rapl_file_max,
is_dram="dram" in domain_lower,
)
)
logger.info(
"\tRAPL - Monitoring domain '%s' (displayed as '%s') via %s at %s",
Expand Down Expand Up @@ -892,7 +902,7 @@ def start(self) -> None:
counters = [
(rapl_file.path, float(rapl_file.last_energy))
for rapl_file in self._rapl_files
if "dram" not in rapl_file.name.lower()
if not rapl_file.is_dram
]
self._mirrored_candidates = find_mirrored_counters(
counters, SEQUENTIAL_READ_TOLERANCE_KWH
Expand Down
2 changes: 2 additions & 0 deletions codecarbon/core/rapl.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ class RAPLFile:
last_energy: Energy = field(default_factory=lambda: Energy(0))
# Max value energy can hold before it wraps
max_energy_reading: Energy = field(default_factory=lambda: Energy(0))
# DRAM domain: a separate measurement, never a mirror of a package counter
is_dram: bool = False

def __post_init__(self):
self.last_energy = self._get_value()
Expand Down
4 changes: 3 additions & 1 deletion tests/test_rapl_mmio_scanning.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ def test_rapl_start_keeps_dram_when_it_matches_a_package_counter(tmp_path, monke
assert len(rapl._mirrored_candidates) == 1
details = rapl.get_cpu_details(Time.from_seconds(1))
assert len(_counted_domains(details)) == 2
assert "dram" in details
dram = next(f for f in rapl._rapl_files if f.is_dram)
assert dram.path not in rapl._mirrored_candidates
assert dram.name in details


def test_rapl_start_reevaluates_mirror_candidates(tmp_path, monkeypatch):
Expand Down
101 changes: 97 additions & 4 deletions tests/test_rapl_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
"""

import logging
import re
import sys

import pytest

from codecarbon.core.cpu import IntelRAPL
from codecarbon.core.units import Time


@pytest.mark.skipif(not sys.platform.lower().startswith("lin"), reason="requires Linux")
Expand Down Expand Up @@ -112,8 +114,50 @@ def test_rapl_include_dram_true_explicit(tmp_path):

# Verify both package and dram are present
names = [f.name for f in rapl._rapl_files]
assert any("Processor Energy" in name for name in names), "Missing package domain"
assert any("dram" in name.lower() for name in names), "Missing DRAM domain"
# Both domains must be named so that they are summed in the reported energy
assert (
len([name for name in names if "Processor Energy" in name]) == 2
), f"Expected package and DRAM to both be aggregated, got {names}"


@pytest.mark.skipif(not sys.platform.lower().startswith("lin"), reason="requires Linux")
@pytest.mark.parametrize("include_dram, expected_uj", [(False, 100000), (True, 125000)])
def test_rapl_include_dram_energy_is_aggregated(tmp_path, include_dram, expected_uj):
"""
Verify that DRAM energy actually contributes to the total reported by
get_cpu_details() when rapl_include_dram=True.
"""
base = tmp_path
rapl_provider = base / "intel-rapl"
rapl_provider.mkdir()

d_package = rapl_provider / "intel-rapl:0"
d_package.mkdir()
(d_package / "name").write_text("package-0")
(d_package / "energy_uj").write_text("1000000")
(d_package / "max_energy_range_uj").write_text("262143328850")

d_dram = rapl_provider / "intel-rapl:1"
d_dram.mkdir()
(d_dram / "name").write_text("dram")
(d_dram / "energy_uj").write_text("500000")
(d_dram / "max_energy_range_uj").write_text("262143328850")

rapl = IntelRAPL(rapl_dir=str(base), rapl_include_dram=include_dram)

# Simulate one second of consumption
(d_package / "energy_uj").write_text(str(1000000 + 100000))
(d_dram / "energy_uj").write_text(str(500000 + 25000))

details = rapl.get_cpu_details(Time.from_seconds(1))

energy = sum(
value
for metric, value in details.items()
if re.match(r"^Processor Energy Delta_\d", metric)
)
# micro joules -> kWh
assert energy == pytest.approx(expected_uj / (1000 * 3600 * 1e6))


@pytest.mark.skipif(not sys.platform.lower().startswith("lin"), reason="requires Linux")
Expand Down Expand Up @@ -295,9 +339,14 @@ def test_rapl_both_parameters_together(tmp_path):
assert (
len(rapl2._rapl_files) == 2
), f"Expected 2 files (package + dram), got {len(rapl2._rapl_files)}"
# Both domains get an aggregated name so their energy is summed in the total
names = [f.name for f in rapl2._rapl_files]
assert any("Processor Energy" in name for name in names), "Missing package domain"
assert any("dram" in name.lower() for name in names), "Missing DRAM domain"
assert all(
"Processor Energy" in name for name in names
), f"All selected domains should be aggregated, got: {names}"
paths = [f.path for f in rapl2._rapl_files]
assert any("intel-rapl:0" in path for path in paths), "Missing package domain"
assert any("intel-rapl:1" in path for path in paths), "Missing DRAM domain"

# Test 3: rapl_prefer_psys=False with rapl_include_dram=False (should use only package)
rapl3 = IntelRAPL(
Expand Down Expand Up @@ -475,3 +524,47 @@ def test_rapl_parameters_stored_correctly(tmp_path):
)
assert rapl_true.rapl_include_dram is True
assert rapl_true.rapl_prefer_psys is True


@pytest.mark.skipif(not sys.platform.lower().startswith("lin"), reason="requires Linux")
@pytest.mark.parametrize("include_dram", [False, True])
def test_rapl_fallback_respects_include_dram(tmp_path, include_dram):
"""
Without package/psys domains every readable domain is used as a fallback, but
DRAM must still only be counted when rapl_include_dram is set.
"""
rapl_provider = tmp_path / "intel-rapl"
rapl_provider.mkdir()
for index, name in enumerate(("core", "dram")):
domain = rapl_provider / f"intel-rapl:{index}"
domain.mkdir()
(domain / "name").write_text(name)
(domain / "energy_uj").write_text("1000000")
(domain / "max_energy_range_uj").write_text("262143328850")

rapl = IntelRAPL(rapl_dir=str(tmp_path), rapl_include_dram=include_dram)

assert any(f.is_dram for f in rapl._rapl_files) is include_dram


@pytest.mark.skipif(not sys.platform.lower().startswith("lin"), reason="requires Linux")
def test_rapl_non_power_domain_keeps_its_own_name(tmp_path):
"""
When no package/psys/dram domain exists, the remaining domains are used as a
fallback but must not be renamed into the aggregated "Processor Energy" total.
"""
base = tmp_path
rapl_provider = base / "intel-rapl"
rapl_provider.mkdir()

d_core = rapl_provider / "intel-rapl:0"
d_core.mkdir()
(d_core / "name").write_text("core")
(d_core / "energy_uj").write_text("1000000")
(d_core / "max_energy_range_uj").write_text("262143328850")

rapl = IntelRAPL(rapl_dir=str(base))

names = [f.name for f in rapl._rapl_files]
assert names, "Fallback should still expose the available domain"
assert not any("Processor Energy" in name for name in names), names
Loading