Skip to content

Commit e43bb52

Browse files
committed
feat(codecarbon) add raspberry support
1 parent 3120a44 commit e43bb52

File tree

4 files changed

+137
-3
lines changed

4 files changed

+137
-3
lines changed

codecarbon/core/cpu.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ def is_psutil_available():
8181
return False
8282

8383

84+
def is_raspberry() -> bool:
85+
"""
86+
Check if raspberry power util is present
87+
"""
88+
return os.path.exists("/usr/bin/vcgencmd")
89+
90+
8491
class IntelPowerGadget:
8592
"""
8693
A class to interface with Intel Power Gadget for monitoring CPU power consumption on Windows and (non-Apple Silicon) macOS.

codecarbon/core/resource_tracker.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from codecarbon.core import cpu, gpu, powermetrics
55
from codecarbon.core.config import parse_gpu_ids
66
from codecarbon.core.util import detect_cpu_model, is_linux_os, is_mac_os, is_windows_os
7-
from codecarbon.external.hardware import CPU, GPU, MODE_CPU_LOAD, AppleSiliconChip
7+
from codecarbon.external.hardware import CPU, GPU, MODE_CPU_LOAD, AppleSiliconChip, Raspberry
88
from codecarbon.external.logger import logger
99
from codecarbon.external.ram import RAM
1010

@@ -31,7 +31,13 @@ def set_RAM_tracking(self):
3131
force_ram_power=self.tracker._force_ram_power,
3232
)
3333
self.tracker._conf["ram_total_size"] = ram.machine_memory_GB
34-
self.tracker._hardware: List[Union[RAM, CPU, GPU, AppleSiliconChip]] = [ram]
34+
self.tracker._hardware: List[
35+
Union[RAM, CPU, GPU, AppleSiliconChip, Raspberry]
36+
] = [ram]
37+
if cpu.is_raspberry():
38+
self.tracker._hardware = [
39+
Raspberry.from_utils(self.tracker._output_dir, chip_part="RAM")
40+
]
3541

3642
def set_CPU_tracking(self):
3743
logger.info("[setup] CPU Tracking...")
@@ -86,6 +92,14 @@ def set_CPU_tracking(self):
8692
logger.warning(
8793
"The RAPL energy and power reported is divided by 2 for all 'AMD Ryzen Threadripper' as it seems to give better results."
8894
)
95+
elif cpu.is_raspberry():
96+
logger.info("Tracking CPU via raspberry utils")
97+
self.cpu_tracker = "raspberry"
98+
hardware_cpu = Raspberry.from_utils(
99+
self.tracker._output_dir, chip_part="CPU"
100+
)
101+
self.tracker._hardware.append(hardware_cpu)
102+
self.tracker._conf["cpu_model"] = hardware_cpu.get_model()
89103
# change code to check if powermetrics needs to be installed or just sudo setup
90104
elif (
91105
powermetrics.is_powermetrics_available()

codecarbon/emissions_tracker.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from codecarbon.core.units import Energy, Power, Time
2121
from codecarbon.core.util import count_cpus, count_physical_cpus, suppress
2222
from codecarbon.external.geography import CloudMetadata, GeoMetadata
23-
from codecarbon.external.hardware import CPU, GPU, AppleSiliconChip
23+
from codecarbon.external.hardware import CPU, GPU, AppleSiliconChip, Raspberry
2424
from codecarbon.external.logger import logger, set_logger_format, set_logger_level
2525
from codecarbon.external.ram import RAM
2626
from codecarbon.external.scheduler import PeriodicScheduler
@@ -722,6 +722,7 @@ def _monitor_power(self) -> None:
722722

723723
def _do_measurements(self) -> None:
724724
for hardware in self._hardware:
725+
logger.info(f"measuring from {hardware=}")
725726
h_time = time.perf_counter()
726727
# Compute last_duration again for more accuracy
727728
last_duration = time.perf_counter() - self._last_measured_time
@@ -756,6 +757,21 @@ def _do_measurements(self) -> None:
756757
f"Energy consumed for RAM : {self._total_ram_energy.kWh:.6f} kWh"
757758
+ f". RAM Power : {self._ram_power.W} W"
758759
)
760+
elif isinstance(hardware, Raspberry):
761+
if hardware.chip_part == "CPU":
762+
self._total_cpu_energy += energy
763+
self._cpu_power = power
764+
logger.info(
765+
f"Energy consumed for all CPUs : {self._total_cpu_energy.kWh:.6f} kWh"
766+
+ f". Total CPU Power : {self._cpu_power.W} W"
767+
)
768+
elif hardware.chip_part == "RAM":
769+
self._total_ram_energy += energy
770+
self._ram_power = power
771+
logger.info(
772+
f"Energy consumed for RAMs : {self._total_ram_energy.kWh:.6f} kWh"
773+
+ f". Total RAM Power : {self._ram_power.W} W"
774+
)
759775
elif isinstance(hardware, AppleSiliconChip):
760776
if hardware.chip_part == "CPU":
761777
self._total_cpu_energy += energy

codecarbon/external/hardware.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import re
77
from abc import ABC, abstractmethod
88
from dataclasses import dataclass
9+
from subprocess import run
910
from typing import Dict, Iterable, List, Optional, Tuple
1011

1112
import psutil
@@ -404,3 +405,99 @@ def from_utils(
404405
logger.warning("Could not read AppleSiliconChip model.")
405406

406407
return cls(output_dir=output_dir, model=model, chip_part=chip_part)
408+
409+
410+
@dataclass
411+
class Raspberry(BaseHardware):
412+
def __init__(
413+
self,
414+
output_dir: str,
415+
model: str,
416+
chip_part: str = "CPU",
417+
):
418+
if chip_part == "CPU":
419+
self.WANTED_COMPONENTS = (
420+
"3V7_WL_SW",
421+
"3V3_SYS",
422+
"1V8_SYS",
423+
"1V1_SYS",
424+
"0V8_SW",
425+
"VDD_CORE",
426+
"3V3_DAC",
427+
"3V3_ADC",
428+
"0V8_AON",
429+
)
430+
elif chip_part == "RAM":
431+
self.WANTED_COMPONENTS = ("DDR_VDD2", "DDR_VDD2", "DDR_VDDQ")
432+
else:
433+
raise Exception("Unknown chip part", chip_part)
434+
435+
self._output_dir = output_dir
436+
self._model = model
437+
self.chip_part = chip_part
438+
439+
def __repr__(self) -> str:
440+
return f"Raspberry ({self._model} > {self.chip_part})"
441+
442+
def _get_power(self) -> Power:
443+
""" """
444+
measure: Dict = self.get_measure()
445+
return Power.from_watts(measure["power"])
446+
447+
def _get_energy(self, delay: Time) -> Energy:
448+
"""
449+
Get Chip part energy deltas
450+
Args:
451+
chip_part (str): Chip part to get power from (Processor, GPU, etc.)
452+
:return: energy in kWh
453+
"""
454+
energy = Energy.from_power_and_time(
455+
power=self._get_power(), time=Time.from_seconds(delay)
456+
)
457+
return energy
458+
459+
def total_power(self) -> Power:
460+
return self._get_power()
461+
462+
def start(self):
463+
# ...
464+
...
465+
466+
def get_model(self):
467+
return self._model
468+
469+
def get_measure(self):
470+
components = {}
471+
res = run(["vcgencmd", "pmic_read_adc"], capture_output=True)
472+
lines = res.stdout.decode("utf-8").splitlines()
473+
for line in lines:
474+
res = re.search(
475+
"([A-Z_0-9]+)_[VA] (current|volt)\(([0-9]+)\)=([0-9.]+)", # noqa: W605
476+
line,
477+
)
478+
component_name, measure_type, idx, value = res.groups()
479+
component = components[component_name] = components.get(component_name, {})
480+
component[measure_type] = float(value)
481+
pi_power = 0
482+
483+
for component_name, component in components.items():
484+
try:
485+
component["power"] = component["volt"] * component["current"]
486+
if component_name in self.WANTED_COMPONENTS:
487+
pi_power += component["power"]
488+
except Exception:
489+
...
490+
return {
491+
"power": pi_power,
492+
}
493+
494+
@classmethod
495+
def from_utils(
496+
cls, output_dir: str, model: Optional[str] = None, chip_part: str = "CPU"
497+
) -> "Raspberry":
498+
if model is None:
499+
model = detect_cpu_model()
500+
if model is None:
501+
logger.warning("Could not read Raspberry model.")
502+
503+
return cls(output_dir=output_dir, model=model, chip_part=chip_part)

0 commit comments

Comments
 (0)