From 28be34c2c25ab6522c8a13fdac3d7f59261a6bb3 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/hardware_cache.py | 17 +++++- codecarbon/core/resource_tracker.py | 28 ++++++++- codecarbon/emissions_tracker.py | 18 +++++- codecarbon/external/hardware.py | 93 +++++++++++++++++++++++++++++ 5 files changed, 159 insertions(+), 4 deletions(-) diff --git a/codecarbon/core/cpu.py b/codecarbon/core/cpu.py index e21c39fd8..a7446078e 100644 --- a/codecarbon/core/cpu.py +++ b/codecarbon/core/cpu.py @@ -248,6 +248,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/hardware_cache.py b/codecarbon/core/hardware_cache.py index 3c661fe07..b960b7a82 100644 --- a/codecarbon/core/hardware_cache.py +++ b/codecarbon/core/hardware_cache.py @@ -36,6 +36,7 @@ class HardwareKind(str, Enum): CPU = "cpu" APPLE_CHIP = "apple_chip" GPU = "gpu" + RASPBERRY = "raspberry" _cache_lock = threading.Lock() @@ -108,6 +109,8 @@ def _hardware_kind(hw) -> HardwareKind: return HardwareKind.APPLE_CHIP if name == "GPU": return HardwareKind.GPU + if name == "Raspberry": + return HardwareKind.RASPBERRY raise TypeError(f"Unsupported hardware type for cache: {type(hw)}") @@ -148,11 +151,17 @@ def _spec_from_hardware(hw) -> Dict[str, Any]: if kind == HardwareKind.GPU: gpu_ids = _canonical_gpu_ids(hw.gpu_ids) return {"kind": kind.value, "gpu_ids": list(gpu_ids) if gpu_ids else None} + if kind == HardwareKind.RASPBERRY: + return { + "kind": kind.value, + "model": hw._model, + "chip_part": hw.chip_part, + } raise TypeError(f"Unsupported hardware type for cache: {type(hw)}") def _hardware_from_spec(spec: Dict[str, Any], output_dir: str): - from codecarbon.external.hardware import CPU, GPU, AppleSiliconChip + from codecarbon.external.hardware import CPU, GPU, AppleSiliconChip, Raspberry from codecarbon.external.ram import RAM try: @@ -185,6 +194,12 @@ def _hardware_from_spec(spec: Dict[str, Any], output_dir: str): if kind == HardwareKind.GPU: gpu_ids = _canonical_gpu_ids(spec.get("gpu_ids")) return GPU.from_utils(gpu_ids=list(gpu_ids) if gpu_ids else None) + if kind == HardwareKind.RASPBERRY: + return Raspberry( + output_dir=output_dir, + model=spec["model"], + chip_part=spec["chip_part"], + ) raise ValueError(f"Unknown hardware spec kind: {kind}") diff --git a/codecarbon/core/resource_tracker.py b/codecarbon/core/resource_tracker.py index e20838718..a121d7986 100644 --- a/codecarbon/core/resource_tracker.py +++ b/codecarbon/core/resource_tracker.py @@ -12,7 +12,13 @@ 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 @@ -39,7 +45,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 _setup_cpu_load_mode(self, tdp, max_power): """Set up CPU tracking in load mode using psutil.""" @@ -102,6 +114,15 @@ def _setup_rapl(self): self.tracker._conf["cpu_model"] = hardware_cpu.get_model() return True + def _setup_raspberry(self): + """Set up CPU tracking using the Raspberry Pi power interface.""" + 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() + return True + def _setup_powermetrics(self): """Set up CPU and GPU tracking using PowerMetrics (Apple Silicon).""" logger.info("Tracking Apple CPU and GPU via PowerMetrics") @@ -223,6 +244,9 @@ def _try_platform_cpu_backend(self) -> bool: if is_linux_os() and cpu.is_rapl_available(): self._setup_rapl() return True + if cpu.is_raspberry(): + self._setup_raspberry() + return True if is_mac_os(): cpu_model = detect_cpu_model() or "" if is_mac_arm(cpu_model): diff --git a/codecarbon/emissions_tracker.py b/codecarbon/emissions_tracker.py index 54e95a9fa..9d88c25ff 100644 --- a/codecarbon/emissions_tracker.py +++ b/codecarbon/emissions_tracker.py @@ -23,7 +23,7 @@ from codecarbon.core.config import get_hierarchical_config, normalize_gpu_ids from codecarbon.core.units import Energy, Power, Time, Water from codecarbon.core.util import count_cpus, count_physical_cpus, suppress -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 @@ -1192,6 +1192,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 @@ -1236,6 +1237,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 9c6d113cc..acf2b3891 100644 --- a/codecarbon/external/hardware.py +++ b/codecarbon/external/hardware.py @@ -7,6 +7,7 @@ import time from abc import ABC, abstractmethod from dataclasses import dataclass +from subprocess import run from typing import Dict, Iterable, List, Optional, Tuple import psutil @@ -548,3 +549,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)