Skip to content

Commit 26f93e7

Browse files
committed
feat(codecarbon) add raspberry support
1 parent fc0d5a7 commit 26f93e7

File tree

4 files changed

+139
-3
lines changed

4 files changed

+139
-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: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@
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 (
8+
CPU,
9+
GPU,
10+
MODE_CPU_LOAD,
11+
AppleSiliconChip,
12+
Raspberry,
13+
)
814
from codecarbon.external.logger import logger
915
from codecarbon.external.ram import RAM
1016

@@ -31,7 +37,13 @@ def set_RAM_tracking(self):
3137
force_ram_power=self.tracker._force_ram_power,
3238
)
3339
self.tracker._conf["ram_total_size"] = ram.machine_memory_GB
34-
self.tracker._hardware: List[Union[RAM, CPU, GPU, AppleSiliconChip]] = [ram]
40+
self.tracker._hardware: List[
41+
Union[RAM, CPU, GPU, AppleSiliconChip, Raspberry]
42+
] = [ram]
43+
if cpu.is_raspberry():
44+
self.tracker._hardware = [
45+
Raspberry.from_utils(self.tracker._output_dir, chip_part="RAM")
46+
]
3547

3648
def set_CPU_tracking(self):
3749
logger.info("[setup] CPU Tracking...")
@@ -86,6 +98,14 @@ def set_CPU_tracking(self):
8698
logger.warning(
8799
"The RAPL energy and power reported is divided by 2 for all 'AMD Ryzen Threadripper' as it seems to give better results."
88100
)
101+
elif cpu.is_raspberry():
102+
logger.info("Tracking CPU via raspberry utils")
103+
self.cpu_tracker = "raspberry"
104+
hardware_cpu = Raspberry.from_utils(
105+
self.tracker._output_dir, chip_part="CPU"
106+
)
107+
self.tracker._hardware.append(hardware_cpu)
108+
self.tracker._conf["cpu_model"] = hardware_cpu.get_model()
89109
# change code to check if powermetrics needs to be installed or just sudo setup
90110
elif (
91111
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
@@ -726,6 +726,7 @@ def _monitor_power(self) -> None:
726726

727727
def _do_measurements(self) -> None:
728728
for hardware in self._hardware:
729+
logger.info(f"measuring from {hardware=}")
729730
h_time = time.perf_counter()
730731
# Compute last_duration again for more accuracy
731732
last_duration = time.perf_counter() - self._last_measured_time
@@ -760,6 +761,21 @@ def _do_measurements(self) -> None:
760761
f"Energy consumed for RAM : {self._total_ram_energy.kWh:.6f} kWh"
761762
+ f". RAM Power : {self._ram_power.W} W"
762763
)
764+
elif isinstance(hardware, Raspberry):
765+
if hardware.chip_part == "CPU":
766+
self._total_cpu_energy += energy
767+
self._cpu_power = power
768+
logger.info(
769+
f"Energy consumed for all CPUs : {self._total_cpu_energy.kWh:.6f} kWh"
770+
+ f". Total CPU Power : {self._cpu_power.W} W"
771+
)
772+
elif hardware.chip_part == "RAM":
773+
self._total_ram_energy += energy
774+
self._ram_power = power
775+
logger.info(
776+
f"Energy consumed for RAMs : {self._total_ram_energy.kWh:.6f} kWh"
777+
+ f". Total RAM Power : {self._ram_power.W} W"
778+
)
763779
elif isinstance(hardware, AppleSiliconChip):
764780
if hardware.chip_part == "CPU":
765781
self._total_cpu_energy += energy

codecarbon/external/hardware.py

Lines changed: 93 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,95 @@ 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 get_model(self):
463+
return self._model
464+
465+
def get_measure(self):
466+
components = {}
467+
res = run(["vcgencmd", "pmic_read_adc"], capture_output=True)
468+
lines = res.stdout.decode("utf-8").splitlines()
469+
for line in lines:
470+
res = re.search(
471+
"([A-Z_0-9]+)_[VA] (current|volt)\(([0-9]+)\)=([0-9.]+)", # noqa: W605
472+
line,
473+
)
474+
component_name, measure_type, idx, value = res.groups()
475+
component = components[component_name] = components.get(component_name, {})
476+
component[measure_type] = float(value)
477+
pi_power = 0
478+
479+
for component_name, component in components.items():
480+
try:
481+
component["power"] = component["volt"] * component["current"]
482+
if component_name in self.WANTED_COMPONENTS:
483+
pi_power += component["power"]
484+
except Exception:
485+
...
486+
return {
487+
"power": pi_power,
488+
}
489+
490+
@classmethod
491+
def from_utils(
492+
cls, output_dir: str, model: Optional[str] = None, chip_part: str = "CPU"
493+
) -> "Raspberry":
494+
if model is None:
495+
model = detect_cpu_model()
496+
if model is None:
497+
logger.warning("Could not read Raspberry model.")
498+
499+
return cls(output_dir=output_dir, model=model, chip_part=chip_part)

0 commit comments

Comments
 (0)