From 6425867bcba99400a9ae3a704f7634513141948f Mon Sep 17 00:00:00 2001 From: Loi Nguyen Date: Sun, 26 Jul 2026 19:29:34 +0700 Subject: [PATCH 1/2] fix: deduplicate mirrored Linux RAPL counters --- codecarbon/core/cpu.py | 23 +++++++++++ docs/explanation/methodology.md | 4 ++ docs/explanation/rapl.md | 2 +- tests/test_rapl_mmio_scanning.py | 68 ++++++++++++++++++++++++++------ 4 files changed, 83 insertions(+), 14 deletions(-) diff --git a/codecarbon/core/cpu.py b/codecarbon/core/cpu.py index e21c39fd8..7fd9e9eec 100644 --- a/codecarbon/core/cpu.py +++ b/codecarbon/core/cpu.py @@ -6,6 +6,7 @@ from __future__ import annotations +import math import os import re import shutil @@ -872,8 +873,30 @@ def start(self) -> None: """ Starts monitoring CPU energy consumption. """ + retained_rapl_files = [] + observed_energies = [] for rapl_file in self._rapl_files: rapl_file.start() + energy = float(rapl_file.last_energy) + is_dram = "dram" in rapl_file.name.lower() + is_mirrored = ( + energy + and not is_dram + and any( + math.isclose(energy, observed_energy, rel_tol=1e-6) + for observed_energy in observed_energies + ) + ) + if is_mirrored: + logger.warning( + "\tRAPL - Ignoring mirrored energy counter at %s to avoid double-counting", + rapl_file.path, + ) + continue + retained_rapl_files.append(rapl_file) + if energy and not is_dram: + observed_energies.append(energy) + self._rapl_files = retained_rapl_files class TDP: diff --git a/docs/explanation/methodology.md b/docs/explanation/methodology.md index 968d55dc1..38e2e53bf 100644 --- a/docs/explanation/methodology.md +++ b/docs/explanation/methodology.md @@ -279,6 +279,10 @@ information. Despite the name "Intel RAPL", it supports AMD processors since Linux kernel 5.8. +On multi-die CPUs, package domains can mirror the same socket-wide energy +counter. CodeCarbon detects matching nonzero counters at startup and keeps one +of them, preventing CPU energy from being counted once per die. + Read more about how we use it in [RAPL Metrics](rapl.md). ## CPU metrics priority diff --git a/docs/explanation/rapl.md b/docs/explanation/rapl.md index eb6d37e21..2b9e7141e 100644 --- a/docs/explanation/rapl.md +++ b/docs/explanation/rapl.md @@ -124,7 +124,7 @@ and consistent measurements: - `core` reports very low energy values - Unclear if `core` is included in `package` (vendor documentation is sparse) -- Multiple dies may report as separate packages (e.g., Threadripper) +- On some multi-die CPUs (e.g., Threadripper), separate package domains mirror the same socket-wide counter. CodeCarbon keeps one mirrored counter so CPU energy is not multiplied by the number of dies. **What RAPL Does NOT Measure**: diff --git a/tests/test_rapl_mmio_scanning.py b/tests/test_rapl_mmio_scanning.py index 61ba0922c..eeeb7dc98 100644 --- a/tests/test_rapl_mmio_scanning.py +++ b/tests/test_rapl_mmio_scanning.py @@ -13,6 +13,46 @@ from codecarbon.core.cpu import IntelRAPL, is_rapl_available +@pytest.mark.parametrize( + ("energies", "expected_count"), + [((1000000, 1000000), 1), ((1000000, 2000000), 2), ((0, 0), 2)], +) +def test_rapl_start_deduplicates_only_nonzero_mirrored_package_counters( + tmp_path, monkeypatch, energies, expected_count +): + monkeypatch.setattr(sys, "platform", "linux") + provider = tmp_path / "intel-rapl" + provider.mkdir() + for index, energy in enumerate(energies): + domain = provider / f"intel-rapl:{index}" + domain.mkdir() + (domain / "name").write_text(f"package-{index}-die-0") + (domain / "energy_uj").write_text(str(energy)) + (domain / "max_energy_range_uj").write_text("262143328850") + + rapl = IntelRAPL(rapl_dir=str(tmp_path)) + rapl.start() + + assert len(rapl._rapl_files) == expected_count + + +def test_rapl_start_keeps_dram_when_it_matches_a_package_counter(tmp_path, monkeypatch): + monkeypatch.setattr(sys, "platform", "linux") + provider = tmp_path / "intel-rapl" + provider.mkdir() + for index, name in enumerate(("package-0-die-0", "package-0-die-1", "dram")): + domain = 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=True) + rapl.start() + + assert len(rapl._rapl_files) == 2 + + @pytest.mark.skipif(not sys.platform.lower().startswith("lin"), reason="requires Linux") def test_multiple_rapl_providers_with_mixed_permissions(tmp_path): """ @@ -188,14 +228,14 @@ def test_rapl_deduplication_prefers_mmio(tmp_path): # Verify package-0 is from MMIO (newer interface) package_files = [f for f in rapl._rapl_files if "Processor Energy" in f.name] - assert ( - len(package_files) == 1 - ), "Should have exactly one package domain after deduplication" + assert len(package_files) == 1, ( + "Should have exactly one package domain after deduplication" + ) # The package file should be from intel-rapl-mmio - assert ( - "intel-rapl-mmio" in package_files[0].path - ), f"Expected MMIO path, got: {package_files[0].path}" + assert "intel-rapl-mmio" in package_files[0].path, ( + f"Expected MMIO path, got: {package_files[0].path}" + ) @pytest.mark.skipif(not sys.platform.lower().startswith("lin"), reason="requires Linux") @@ -247,17 +287,19 @@ def test_psys_not_preferred_when_package_available(tmp_path): # Should have 1 RAPL file: package-0 (not psys) # Package domains are preferred over psys for reliability - assert ( - len(rapl._rapl_files) == 1 - ), f"Expected 1 file (package), got {len(rapl._rapl_files)}" + assert len(rapl._rapl_files) == 1, ( + f"Expected 1 file (package), got {len(rapl._rapl_files)}" + ) # Verify it's the package domain (not psys) assert ( "Processor Energy" in rapl._rapl_files[0].name and "intel-rapl:0" in rapl._rapl_files[0].path - ), f"Expected package-0 domain, got: {rapl._rapl_files[0].name} at {rapl._rapl_files[0].path}" + ), ( + f"Expected package-0 domain, got: {rapl._rapl_files[0].name} at {rapl._rapl_files[0].path}" + ) # Verify psys is NOT used (should be logged as detected but not used) - assert ( - "intel-rapl:1" not in rapl._rapl_files[0].path - ), "psys should not be used when package domains are available" + assert "intel-rapl:1" not in rapl._rapl_files[0].path, ( + "psys should not be used when package domains are available" + ) From 3a116e82d053721ccceed4ee5f0d9342f975e7ca Mon Sep 17 00:00:00 2001 From: Loi Nguyen Date: Sun, 26 Jul 2026 19:35:01 +0700 Subject: [PATCH 2/2] style: format RAPL scanning tests --- tests/test_rapl_mmio_scanning.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/tests/test_rapl_mmio_scanning.py b/tests/test_rapl_mmio_scanning.py index eeeb7dc98..f60d789d8 100644 --- a/tests/test_rapl_mmio_scanning.py +++ b/tests/test_rapl_mmio_scanning.py @@ -228,14 +228,14 @@ def test_rapl_deduplication_prefers_mmio(tmp_path): # Verify package-0 is from MMIO (newer interface) package_files = [f for f in rapl._rapl_files if "Processor Energy" in f.name] - assert len(package_files) == 1, ( - "Should have exactly one package domain after deduplication" - ) + assert ( + len(package_files) == 1 + ), "Should have exactly one package domain after deduplication" # The package file should be from intel-rapl-mmio - assert "intel-rapl-mmio" in package_files[0].path, ( - f"Expected MMIO path, got: {package_files[0].path}" - ) + assert ( + "intel-rapl-mmio" in package_files[0].path + ), f"Expected MMIO path, got: {package_files[0].path}" @pytest.mark.skipif(not sys.platform.lower().startswith("lin"), reason="requires Linux") @@ -287,19 +287,17 @@ def test_psys_not_preferred_when_package_available(tmp_path): # Should have 1 RAPL file: package-0 (not psys) # Package domains are preferred over psys for reliability - assert len(rapl._rapl_files) == 1, ( - f"Expected 1 file (package), got {len(rapl._rapl_files)}" - ) + assert ( + len(rapl._rapl_files) == 1 + ), f"Expected 1 file (package), got {len(rapl._rapl_files)}" # Verify it's the package domain (not psys) assert ( "Processor Energy" in rapl._rapl_files[0].name and "intel-rapl:0" in rapl._rapl_files[0].path - ), ( - f"Expected package-0 domain, got: {rapl._rapl_files[0].name} at {rapl._rapl_files[0].path}" - ) + ), f"Expected package-0 domain, got: {rapl._rapl_files[0].name} at {rapl._rapl_files[0].path}" # Verify psys is NOT used (should be logged as detected but not used) - assert "intel-rapl:1" not in rapl._rapl_files[0].path, ( - "psys should not be used when package domains are available" - ) + assert ( + "intel-rapl:1" not in rapl._rapl_files[0].path + ), "psys should not be used when package domains are available"