From 0a24bdb45cefd21ccae2d476abd7ec99be956886 Mon Sep 17 00:00:00 2001 From: David Berenstein Date: Wed, 19 Aug 2026 17:19:55 +0200 Subject: [PATCH 1/2] fix: count DRAM in RAPL energy total Aggregate the dram domain into the reported processor energy alongside package/psys, with tests covering aggregation and the non-power-domain fallback. Co-Authored-By: Claude Opus 5 (1M context) --- codecarbon/core/cpu.py | 5 ++- tests/test_rapl_parameters.py | 80 +++++++++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/codecarbon/core/cpu.py b/codecarbon/core/cpu.py index e21c39fd8..75c0d8f5b 100644 --- a/codecarbon/core/cpu.py +++ b/codecarbon/core/cpu.py @@ -770,8 +770,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 diff --git a/tests/test_rapl_parameters.py b/tests/test_rapl_parameters.py index 4fafe7a65..ac600ccfc 100644 --- a/tests/test_rapl_parameters.py +++ b/tests/test_rapl_parameters.py @@ -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") @@ -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") @@ -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( @@ -475,3 +524,26 @@ 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") +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 From c2cc54b89c69159244ce47851262ce2f649a602b Mon Sep 17 00:00:00 2001 From: David Berenstein Date: Wed, 23 Sep 2026 16:52:09 +0900 Subject: [PATCH 2/2] fix(rapl): exclude DRAM from mirror detection by type and honour rapl_include_dram in fallback Co-Authored-By: Claude Opus 5.5 (1M context) --- codecarbon/core/cpu.py | 15 ++++++++++++--- codecarbon/core/rapl.py | 2 ++ tests/test_rapl_mmio_scanning.py | 4 +++- tests/test_rapl_parameters.py | 21 +++++++++++++++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/codecarbon/core/cpu.py b/codecarbon/core/cpu.py index 5ad13237c..d25d0ca71 100644 --- a/codecarbon/core/cpu.py +++ b/codecarbon/core/cpu.py @@ -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 @@ -790,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", @@ -893,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 diff --git a/codecarbon/core/rapl.py b/codecarbon/core/rapl.py index 46bd6e864..ae2d78889 100644 --- a/codecarbon/core/rapl.py +++ b/codecarbon/core/rapl.py @@ -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() diff --git a/tests/test_rapl_mmio_scanning.py b/tests/test_rapl_mmio_scanning.py index 5175e43d8..4b1cec888 100644 --- a/tests/test_rapl_mmio_scanning.py +++ b/tests/test_rapl_mmio_scanning.py @@ -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): diff --git a/tests/test_rapl_parameters.py b/tests/test_rapl_parameters.py index ac600ccfc..354255d12 100644 --- a/tests/test_rapl_parameters.py +++ b/tests/test_rapl_parameters.py @@ -526,6 +526,27 @@ def test_rapl_parameters_stored_correctly(tmp_path): 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): """