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..f60d789d8 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): """