From 26f93e7f56466800787618e45c4ec1355ba38ed2 Mon Sep 17 00:00:00 2001 From: "patrick.lloret@protonmail.com" Date: Sun, 9 Mar 2025 20:46:26 +0100 Subject: [PATCH] feat(codecarbon) add raspberry support --- codecarbon/core/cpu.py | 7 +++ codecarbon/core/resource_tracker.py | 24 +++++++- codecarbon/emissions_tracker.py | 18 +++++- codecarbon/external/hardware.py | 93 +++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 3 deletions(-) diff --git a/codecarbon/core/cpu.py b/codecarbon/core/cpu.py index 4217e2919..5ec1e48b2 100644 --- a/codecarbon/core/cpu.py +++ b/codecarbon/core/cpu.py @@ -81,6 +81,13 @@ def is_psutil_available(): return False +def is_raspberry() -> bool: + """ + Check if raspberry power util is present + """ + return os.path.exists("/usr/bin/vcgencmd") + + class IntelPowerGadget: """ A class to interface with Intel Power Gadget for monitoring CPU power consumption on Windows and (non-Apple Silicon) macOS. diff --git a/codecarbon/core/resource_tracker.py b/codecarbon/core/resource_tracker.py index acd89e72e..5983787b6 100644 --- a/codecarbon/core/resource_tracker.py +++ b/codecarbon/core/resource_tracker.py @@ -4,7 +4,13 @@ from codecarbon.core import cpu, gpu, powermetrics from codecarbon.core.config import parse_gpu_ids from codecarbon.core.util import detect_cpu_model, is_linux_os, is_mac_os, is_windows_os -from codecarbon.external.hardware import CPU, GPU, MODE_CPU_LOAD, AppleSiliconChip +from codecarbon.external.hardware import ( + CPU, + GPU, + MODE_CPU_LOAD, + AppleSiliconChip, + Raspberry, +) from codecarbon.external.logger import logger from codecarbon.external.ram import RAM @@ -31,7 +37,13 @@ def set_RAM_tracking(self): force_ram_power=self.tracker._force_ram_power, ) self.tracker._conf["ram_total_size"] = ram.machine_memory_GB - self.tracker._hardware: List[Union[RAM, CPU, GPU, AppleSiliconChip]] = [ram] + self.tracker._hardware: List[ + Union[RAM, CPU, GPU, AppleSiliconChip, Raspberry] + ] = [ram] + if cpu.is_raspberry(): + self.tracker._hardware = [ + Raspberry.from_utils(self.tracker._output_dir, chip_part="RAM") + ] def set_CPU_tracking(self): logger.info("[setup] CPU Tracking...") @@ -86,6 +98,14 @@ def set_CPU_tracking(self): logger.warning( "The RAPL energy and power reported is divided by 2 for all 'AMD Ryzen Threadripper' as it seems to give better results." ) + elif cpu.is_raspberry(): + logger.info("Tracking CPU via raspberry utils") + self.cpu_tracker = "raspberry" + hardware_cpu = Raspberry.from_utils( + self.tracker._output_dir, chip_part="CPU" + ) + self.tracker._hardware.append(hardware_cpu) + self.tracker._conf["cpu_model"] = hardware_cpu.get_model() # change code to check if powermetrics needs to be installed or just sudo setup elif ( powermetrics.is_powermetrics_available() diff --git a/codecarbon/emissions_tracker.py b/codecarbon/emissions_tracker.py index 9b4294d73..87d1bbe10 100644 --- a/codecarbon/emissions_tracker.py +++ b/codecarbon/emissions_tracker.py @@ -20,7 +20,7 @@ from codecarbon.core.units import Energy, Power, Time from codecarbon.core.util import count_cpus, count_physical_cpus, suppress from codecarbon.external.geography import CloudMetadata, GeoMetadata -from codecarbon.external.hardware import CPU, GPU, AppleSiliconChip +from codecarbon.external.hardware import CPU, GPU, AppleSiliconChip, Raspberry from codecarbon.external.logger import logger, set_logger_format, set_logger_level from codecarbon.external.ram import RAM from codecarbon.external.scheduler import PeriodicScheduler @@ -726,6 +726,7 @@ def _monitor_power(self) -> None: def _do_measurements(self) -> None: for hardware in self._hardware: + logger.info(f"measuring from {hardware=}") h_time = time.perf_counter() # Compute last_duration again for more accuracy last_duration = time.perf_counter() - self._last_measured_time @@ -760,6 +761,21 @@ def _do_measurements(self) -> None: f"Energy consumed for RAM : {self._total_ram_energy.kWh:.6f} kWh" + f". RAM Power : {self._ram_power.W} W" ) + elif isinstance(hardware, Raspberry): + if hardware.chip_part == "CPU": + self._total_cpu_energy += energy + self._cpu_power = power + logger.info( + f"Energy consumed for all CPUs : {self._total_cpu_energy.kWh:.6f} kWh" + + f". Total CPU Power : {self._cpu_power.W} W" + ) + elif hardware.chip_part == "RAM": + self._total_ram_energy += energy + self._ram_power = power + logger.info( + f"Energy consumed for RAMs : {self._total_ram_energy.kWh:.6f} kWh" + + f". Total RAM Power : {self._ram_power.W} W" + ) elif isinstance(hardware, AppleSiliconChip): if hardware.chip_part == "CPU": self._total_cpu_energy += energy diff --git a/codecarbon/external/hardware.py b/codecarbon/external/hardware.py index e2b8abc2f..3a0ea147b 100644 --- a/codecarbon/external/hardware.py +++ b/codecarbon/external/hardware.py @@ -6,6 +6,7 @@ import re from abc import ABC, abstractmethod from dataclasses import dataclass +from subprocess import run from typing import Dict, Iterable, List, Optional, Tuple import psutil @@ -404,3 +405,95 @@ def from_utils( logger.warning("Could not read AppleSiliconChip model.") return cls(output_dir=output_dir, model=model, chip_part=chip_part) + + +@dataclass +class Raspberry(BaseHardware): + def __init__( + self, + output_dir: str, + model: str, + chip_part: str = "CPU", + ): + if chip_part == "CPU": + self.WANTED_COMPONENTS = ( + "3V7_WL_SW", + "3V3_SYS", + "1V8_SYS", + "1V1_SYS", + "0V8_SW", + "VDD_CORE", + "3V3_DAC", + "3V3_ADC", + "0V8_AON", + ) + elif chip_part == "RAM": + self.WANTED_COMPONENTS = ("DDR_VDD2", "DDR_VDD2", "DDR_VDDQ") + else: + raise Exception("Unknown chip part", chip_part) + + self._output_dir = output_dir + self._model = model + self.chip_part = chip_part + + def __repr__(self) -> str: + return f"Raspberry ({self._model} > {self.chip_part})" + + def _get_power(self) -> Power: + """ """ + measure: Dict = self.get_measure() + return Power.from_watts(measure["power"]) + + def _get_energy(self, delay: Time) -> Energy: + """ + Get Chip part energy deltas + Args: + chip_part (str): Chip part to get power from (Processor, GPU, etc.) + :return: energy in kWh + """ + energy = Energy.from_power_and_time( + power=self._get_power(), time=Time.from_seconds(delay) + ) + return energy + + def total_power(self) -> Power: + return self._get_power() + + def get_model(self): + return self._model + + def get_measure(self): + components = {} + res = run(["vcgencmd", "pmic_read_adc"], capture_output=True) + lines = res.stdout.decode("utf-8").splitlines() + for line in lines: + res = re.search( + "([A-Z_0-9]+)_[VA] (current|volt)\(([0-9]+)\)=([0-9.]+)", # noqa: W605 + line, + ) + component_name, measure_type, idx, value = res.groups() + component = components[component_name] = components.get(component_name, {}) + component[measure_type] = float(value) + pi_power = 0 + + for component_name, component in components.items(): + try: + component["power"] = component["volt"] * component["current"] + if component_name in self.WANTED_COMPONENTS: + pi_power += component["power"] + except Exception: + ... + return { + "power": pi_power, + } + + @classmethod + def from_utils( + cls, output_dir: str, model: Optional[str] = None, chip_part: str = "CPU" + ) -> "Raspberry": + if model is None: + model = detect_cpu_model() + if model is None: + logger.warning("Could not read Raspberry model.") + + return cls(output_dir=output_dir, model=model, chip_part=chip_part)