Skip to content
Draft
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
89 changes: 54 additions & 35 deletions codecarbon/core/cpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import subprocess
import sys
from functools import lru_cache
from typing import TYPE_CHECKING, Dict, Optional, Tuple
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple

import psutil
from rapidfuzz import fuzz, process, utils
Expand All @@ -23,7 +23,7 @@
counters_match,
find_mirrored_counters,
)
from codecarbon.core.units import Time
from codecarbon.core.units import Energy, Power, Time
from codecarbon.core.util import count_cpus, detect_cpu_model
from codecarbon.external.logger import logger

Expand Down Expand Up @@ -421,15 +421,15 @@ class IntelRAPL:
_rapl_files (List[RAPLFile]): A list of RAPLFile objects representing the files to read energy data from.
_cpu_details (Dict): A dictionary storing the latest CPU energy details.
_last_mesure (int): Placeholder for storing the last measurement time.
rapl_include_dram (bool): Whether to include DRAM power in measurements (default: False for complete hardware measurement).
rapl_include_dram (bool): Whether to read the DRAM domains, reported by get_dram_energy() and never in the CPU details (default: False).
rapl_prefer_psys (bool): Whether to prefer psys domain over package domains (default: False).
When True, uses psys (platform/system) domain which includes CPU + platform components.
When False (default), uses package domains which are more reliable and match CPU TDP specs.

Args:
rapl_dir (str): Path to RAPL directory (default: "/sys/class/powercap/intel-rapl/subsystem")
rapl_include_dram (bool): Include DRAM domain for complete hardware measurement (default: False).
Set to False to measure only CPU package power.
rapl_include_dram (bool): Read the DRAM domains alongside the package ones, for the
RAM energy (default: False). Ignored with psys domains.
rapl_prefer_psys (bool): Prefer psys (platform) domain over package domains (default: False).
Set to True to measure total platform power (CPU + chipset + PCIe).
Note: psys can report higher values than CPU TDP and may be less reliable on older systems.
Expand All @@ -455,6 +455,8 @@ def __init__(
self._lin_rapl_dir = rapl_dir
self._system = sys.platform.lower()
self._rapl_files = []
# DRAM domains, read apart to report the RAM energy (never the CPU's)
self._dram_files: List[RAPLFile] = []
# Files that look like a duplicate of another counter, but whose
# energy deltas have not confirmed it yet. They are left out of the
# measurement while pending. Maps file path -> mirrored file path.
Expand Down Expand Up @@ -664,24 +666,14 @@ def _classify_domains(self, readable_domains: list):
domain_dir,
)
elif "dram" in domain_lower:
parent_dir = os.path.dirname(domain_dir)
if (
parent_dir.endswith(("intel-rapl", "intel-rapl-mmio"))
or os.path.basename(domain_dir).count(":") == 1
):
dram_domains.append(domain_tuple)
logger.debug(
"\tRAPL - Found top-level DRAM domain '%s' at %s",
domain_name,
domain_dir,
)
else:
subdomain_of_package.append(domain_tuple)
logger.debug(
"\tRAPL - Found DRAM subdomain '%s' at %s (will be skipped to avoid double-counting)",
domain_name,
domain_dir,
)
# Top-level or a child zone of its package (the usual layout on
# servers): the package energy never includes the DRAM's
dram_domains.append(domain_tuple)
logger.debug(
"\tRAPL - Found DRAM domain '%s' at %s",
domain_name,
domain_dir,
)
elif any(sub in domain_lower for sub in ["core", "uncore"]):
subdomain_of_package.append(domain_tuple)
logger.debug(
Expand Down Expand Up @@ -716,14 +708,14 @@ def _select_domains_to_use(

if self.rapl_include_dram and dram_domains:
logger.info(
"\tRAPL - Including %d DRAM domain(s) for complete hardware power measurement (CPU+DRAM)",
"\tRAPL - Reading %d DRAM domain(s) as the RAM energy",
len(dram_domains),
)
domains_to_use.extend(dram_domains)
elif dram_domains and not self.rapl_include_dram:
logger.info(
"\tRAPL - Found %d DRAM domain(s) but not including (rapl_include_dram=False). "
"Set rapl_include_dram=True for complete hardware measurement.",
"Set rapl_include_dram=True to measure the RAM energy with it.",
len(dram_domains),
)

Expand All @@ -744,7 +736,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 All @@ -755,7 +751,12 @@ def _deduplicate_domains(self, domains_to_use: list):
name, domain_dir, is_mmio, rapl_file, rapl_file_max, domain_name = (
domain_tuple
)
base_name = domain_name if domain_name else os.path.basename(domain_dir)
zone_id = os.path.basename(domain_dir)
base_name = domain_name if domain_name else zone_id
if "dram" in base_name.lower():
# Every socket's DRAM zone is named "dram": key on the zone id
# (intel-rapl:N:M), the same whatever path it was found through
base_name = zone_id
if base_name not in domain_map or (
is_mmio and not domain_map[base_name][2]
):
Expand All @@ -779,16 +780,19 @@ 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()
is_dram = "dram" in domain_lower
if is_dram:
display_name = f"DRAM Energy Delta_{len(self._dram_files)}(kWh)"
elif "package" in domain_lower or "psys" in domain_lower:
display_name = f"Processor Energy Delta_{domain_index}(kWh)"
domain_index += 1
else:
display_name = name

interface_type = "MMIO" if is_mmio else "MSR"
self._rapl_files.append(
files = self._dram_files if is_dram else self._rapl_files
files.append(
RAPLFile(name=display_name, path=rapl_file, max_path=rapl_file_max)
)
logger.info(
Expand Down Expand Up @@ -821,7 +825,8 @@ def _fetch_rapl_files(self) -> None:
Fetches RAPL files from the RAPL directory.

By default, reads CPU package only
Set rapl_include_dram=True to measure CPU package + DRAM domains
Set rapl_include_dram=True to also read the DRAM domains, which are
reported as the RAM energy by get_dram_energy()
"""
candidate_bases = self._get_rapl_candidate_bases()
domain_dirs = self._collect_domain_dirs(candidate_bases)
Expand Down Expand Up @@ -882,17 +887,31 @@ def get_static_cpu_details(self) -> Dict:
"""
return self._cpu_details

def get_dram_energy(self, duration: Time) -> Optional[Tuple[Power, Energy]]:
"""
Power and energy of the DRAM domains since the previous call, or None
when no DRAM domain is read (rapl_include_dram=False or none found).
"""
if not self._dram_files:
return None
for rapl_file in self._dram_files:
rapl_file.delta(duration)
return (
Power.from_watts(sum(rapl_file.power.W for rapl_file in self._dram_files)),
Energy.from_energy(
sum(rapl_file.energy_delta.kWh for rapl_file in self._dram_files)
),
)

def start(self) -> None:
"""
Starts monitoring CPU energy consumption.
"""
for rapl_file in self._rapl_files:
for rapl_file in self._rapl_files + self._dram_files:
rapl_file.start()
# DRAM domains are a different measurement, never a duplicate
counters = [
(rapl_file.path, float(rapl_file.last_energy))
for rapl_file in self._rapl_files
if "dram" not in rapl_file.name.lower()
]
self._mirrored_candidates = find_mirrored_counters(
counters, SEQUENTIAL_READ_TOLERANCE_KWH
Expand Down
25 changes: 25 additions & 0 deletions codecarbon/core/resource_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,3 +329,28 @@ def set_CPU_GPU_ram_tracking(self):
param tracker: BaseEmissionsTracker object
"""
get_or_run_setup(self, self._run_full_hardware_setup)
self._use_measured_dram()

def _use_measured_dram(self) -> None:
"""
Report the RAPL DRAM energy (rapl_include_dram=True) as the RAM energy
rather than the estimate. Not cached with the hardware plan, as the
RAM has to read the DRAM files of this very CPU instance.
"""
ram = next((hw for hw in self.tracker._hardware if isinstance(hw, RAM)), None)
cpu_hw = next(
(hw for hw in self.tracker._hardware if isinstance(hw, CPU)), None
)
if (
ram is None
or cpu_hw is None
or ram._force_ram_power is not None
# The DRAM counter is machine-wide: keep the per-process estimate
or ram._tracking_mode != "machine"
or cpu_hw._mode != "intel_rapl"
or not cpu_hw._intel_interface._dram_files
):
return
ram._dram_source = cpu_hw._intel_interface
self.ram_tracker = "RAPL DRAM measurement (estimation model as fallback)"
logger.info(f"RAM Tracking Method: {self.ram_tracker}")
17 changes: 9 additions & 8 deletions codecarbon/emissions_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,11 +512,12 @@ def __init__(
:param force_mode_cpu_load: Force the addition of a CPU in MODE_CPU_LOAD
:param allow_multiple_runs: Allow multiple CodeCarbon instances on the same machine.
Defaults to True since v3 (was False in v2).
:param rapl_include_dram: Include DRAM (memory) power in the counter-based CPU
measurements, defaults to False. When True, measures
CPU package + DRAM. Applies to the Linux RAPL interface
and to the Windows Energy Meter Interface, on systems
exposing separate DRAM domains/channels.
:param rapl_include_dram: Read the DRAM (memory) energy counters, defaults
to False. On Linux RAPL, the DRAM domain energy
is reported as the RAM energy instead of its
estimate (machine tracking mode, package domains
only). On the Windows Energy Meter Interface,
the DRAM channels are added to the CPU energy.
:param rapl_prefer_psys: Prefer psys (platform) RAPL domain over package domains on
Linux, defaults to False. When True, uses total platform power
(CPU + chipset + PCIe). When False, uses package domains which
Expand Down Expand Up @@ -1621,9 +1622,9 @@ def track_emissions(
litres of water consumed per kilowatt-hour of electricity consumed.
:param force_carbon_intensity_g_co2e_kwh: Override grid carbon intensity
in gCO2e/kWh for emissions calculations.
:param rapl_include_dram: Include DRAM in the counter-based CPU measurements
(Linux RAPL and Windows EMI, default: False).
When True, measures CPU package + DRAM.
:param rapl_include_dram: Read the DRAM energy counters (default: False).
On Linux RAPL, reported as the RAM energy instead
of its estimate; on Windows EMI, added to the CPU.
:param rapl_prefer_psys: Prefer psys over package domains for RAPL on Linux
(default: False). When True, uses total platform power.

Expand Down
36 changes: 34 additions & 2 deletions codecarbon/external/ram.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@
import re
import subprocess
from dataclasses import dataclass
from typing import Optional
from typing import Optional, Tuple

import psutil

from codecarbon.core.units import Power
from codecarbon.core.units import Energy, Power, Time
from codecarbon.core.util import SLURM_JOB_ID
from codecarbon.external.hardware import B_TO_GB, BaseHardware
from codecarbon.external.logger import logger

RAM_SLOT_POWER_X86 = 5 # Watts
# Measurement intervals a DRAM counter may stay still before it is deemed
# unimplemented: client CPUs often expose a DRAM domain stuck at 0
DRAM_DEAD_AFTER_INTERVALS = 3


@dataclass
Expand All @@ -31,6 +34,11 @@ class RAM(BaseHardware):

memory_size = None
is_arm_cpu = False
# Interface measuring the DRAM energy (IntelRAPL with rapl_include_dram)
_dram_source = None
# Whether the DRAM counter was seen moving, and intervals it has not
_dram_alive = False
_dram_still_intervals = 0

def __init__(
self,
Expand Down Expand Up @@ -339,3 +347,27 @@ def total_power(self) -> Power:
ram_power = Power.from_watts(0)

return ram_power

def measure_power_and_energy(self, last_duration: float) -> Tuple[Power, Energy]:
"""
Use the measured DRAM energy when a DRAM domain is read, otherwise
estimate it. Until the DRAM counter is seen moving, the estimate is
used, and a counter that never moves is dropped for good.
"""
if self._dram_source is not None:
measured = self._dram_source.get_dram_energy(Time(seconds=last_duration))
# A negative delta is a counter wrap we could not correct: estimate
# this interval instead of reporting negative energy.
if measured is not None and measured[1].kWh >= 0:
if self._dram_alive or measured[1].kWh > 0:
self._dram_alive = True
return measured
self._dram_still_intervals += 1
if self._dram_still_intervals >= DRAM_DEAD_AFTER_INTERVALS:
logger.warning(
"The DRAM energy counter did not move over %d measurements, "
"falling back to the RAM power estimation model",
self._dram_still_intervals,
)
self._dram_source = None
return super().measure_power_and_energy(last_duration=last_duration)
8 changes: 7 additions & 1 deletion docs/explanation/methodology.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ Tracks Nvidia GPUs energy consumption using `nvidia-ml-py` library

### RAM

On Linux, with
[`rapl_include_dram`](../how-to/configuration.md#measuring-ram-with-the-dram-energy-counter)
enabled, the RAM energy is measured by the RAPL `dram` domain when it
exists and its counter increases. Otherwise, it is estimated as described
below.

CodeCarbon v2 uses a 3 Watts for 8 GB ratio
[source](https://www.crucial.com/support/articles-faq-memory/how-much-power-does-memory-use)
.
Expand Down Expand Up @@ -225,7 +231,7 @@ twice, so CodeCarbon keeps the package channels only. On multi-die CPUs
where every die mirrors the same socket-wide counter, the duplicates are
detected and dropped as well. The `DRAM` channels are excluded too, unless
the
[`rapl_include_dram`](../how-to/configuration.md#including-dram-in-the-cpu-measurement)
[`rapl_include_dram`](../how-to/configuration.md#measuring-ram-with-the-dram-energy-counter)
option is enabled.

Legacy support for `Intel Power Gadget` is kept for machines where it is
Expand Down
2 changes: 1 addition & 1 deletion docs/explanation/power-estimation.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The most accurate tracking methods rely on built-in hardware energy counters rat
- **NVIDIA GPUs** using `nvmlDeviceGetTotalEnergyConsumption` return accumulated energy in millijoules.
- **AMD GPUs** using `amdsmi_get_energy_count` yield a counter that is multiplied by its resolution and converted into millijoules.
- **CPUs** using the RAPL interface read from files like `energy_uj` to get accumulated microjoules.
- **RAM** using the RAPL interface read from files like `energy_uj` to get accumulated microjoules. See `rapl_include_dram` option. Not used by default.
- **RAM** using the RAPL `dram` domain read from files like `energy_uj` to get accumulated microjoules, on Linux. See `rapl_include_dram` option. Not used by default.

At every measurement interval, CodeCarbon calculates the `energy_delta` by subtracting the previously tracked `last_energy` from the current total energy reading.

Expand Down
20 changes: 12 additions & 8 deletions docs/explanation/rapl.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ and consistent measurements:
- Match CPU TDP specifications
- Provide consistent measurements across different Intel
generations
- Can be supplemented with `dram` domains for complete hardware
measurement (package + DRAM)
- `dram` domains are never added to them: with
`rapl_include_dram=True` they are reported as the RAM energy
2. **Optional psys mode**: Set `prefer_psys=True` to use `psys`
(platform/system) domain instead:
- Provides total platform power (CPU + chipset + PCIe + some other
Expand All @@ -113,9 +113,13 @@ and consistent measurements:
- Falls back to MSR if MMIO is unreadable
4. **Subdomain filtering**: Excludes `core` and `uncore` subdomains
when `package` is available to avoid double-counting
5. **DRAM exclusion**: By default (`include_dram=False`), don't add
DRAM domain to package. As DRAM is supposed to be in RAM power, not
CPU in a future version of CodeCarbon.
5. **DRAM as RAM**: By default (`rapl_include_dram=False`), the DRAM
domains are not read and the RAM power is estimated. With
`rapl_include_dram=True` and `package` domains, the DRAM domains
(top-level, or children of their package as on most servers) are
reported as the RAM energy, never added to the CPU package. With
`psys`, which usually includes the memory, the DRAM domains are not
read and the RAM keeps its estimate.

## Platform-Specific Behavior

Expand Down Expand Up @@ -370,9 +374,9 @@ Analysis:
5. **Interface deduplication**: The same domain may appear in both
`intel-rapl` (MSR) and `intel-rapl-mmio` interfaces. CodeCarbon
automatically deduplicates, preferring MMIO.
6. **DRAM measurement**: CodeCarbon does not include DRAM domains by
default (`include_dram=False`) for CPU hardware measurement. Set
`include_dram=True` to measure CPU package + DRAM domains.
6. **DRAM measurement**: CodeCarbon does not read DRAM domains by
default (`rapl_include_dram=False`). Set `rapl_include_dram=True` to
report the DRAM domains as the RAM energy instead of its estimate.
7. **Platform-specific behavior**:
- Intel modern: package or psys (with prefer_psys=True)
- Intel older: package-0 for CPU only
Expand Down
Loading
Loading