Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2d99513
feat: :sparkles: create macmon class for Mac monitoring
LuisBlanche Dec 20, 2024
31b8066
style: :art: black formatting
LuisBlanche Dec 20, 2024
f4408f6
Merge branch 'master' into 731-sudoless-power-metrics-on-apple-silicon
LuisBlanche Dec 20, 2024
d4f7fb1
test: :white_check_mark: fix test and add cleanup
LuisBlanche Dec 20, 2024
86df536
fix: :bug: fmacmon to append to log file + energy units
LuisBlanche Dec 21, 2024
a882b50
feat: :sparkles: better handling of "M*" chips and macmon supports
LuisBlanche Dec 21, 2024
29efa24
feat: :sparkles: support of macmon
LuisBlanche Dec 21, 2024
66ee9bb
fix: :bug: split powermetrcis and macmon
LuisBlanche Dec 21, 2024
e8b44ea
fix: :art: fix log after review
LuisBlanche Jan 3, 2025
dc577fb
fix: :art: black formatting
LuisBlanche Jan 3, 2025
59c7157
docs: :memo: update doc regarding mac Silicon chips
LuisBlanche Jan 17, 2025
8a48639
Merge branch 'master' into 731-sudoless-power-metrics-on-apple-silicon
LuisBlanche Jan 17, 2025
64552db
feat: :sparkles: add RAM tracking from Macmon in AppleSilicon Chip
LuisBlanche Jan 18, 2025
6604c37
refactor:
LuisBlanche Jan 18, 2025
120e3ac
test: :white_check_mark: fix macmon test
LuisBlanche Jan 18, 2025
b94ac9b
Merge branch 'master' into 731-sudoless-power-metrics-on-apple-silicon
LuisBlanche Jan 19, 2025
8dd1448
Merge branch 'master' into 731-sudoless-power-metrics-on-apple-silicon
LuisBlanche Mar 18, 2025
b1f3ca8
fix(RAM): :bug: revert to constant mode for RAM power with M1 chips
LuisBlanche Mar 18, 2025
249042d
Merge branch 'master' into 731-sudoless-power-metrics-on-apple-silicon
LuisBlanche Apr 19, 2025
f163530
fix(RAM): :bug: remove default_cpu_power
LuisBlanche Apr 19, 2025
255e77f
Merge branch 'master' into use-ram-load
LuisBlanche May 22, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ __pycache__/
*$py.class
*intel_power_gadget_log.csv
*powermetrics_log.txt
*macmon_log.txt
!tests/test_data/mock*

# C extensions
Expand Down
141 changes: 141 additions & 0 deletions codecarbon/core/macmon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import os
import shutil
import subprocess
import sys
from typing import Dict

import pandas as pd

from codecarbon.core.util import detect_cpu_model
from codecarbon.external.logger import logger


def is_macmon_available() -> bool:
try:
macmon = MacMon()
return macmon._log_file_path is not None
except Exception as e:
msg = "Not using macmon, exception occurred while instantiating"
logger.debug(f"{msg} macmon : {e}")
return False


class MacMon:
"""
A class to interact with and retrieve power metrics on Apple Silicon devices using
the `macmon` command-line tool.

This class implements a singleton pattern for the macmon process to avoid
running multiple instances simultaneously.

Methods:
--------
__init__(output_dir: str = ".", n_points=10, interval=100,
log_file_name="macmon_log.txt"):
Initializes the Applemacmon instance, setting up the log file path,
system info, and other configurations.

get_details() -> Dict:
Parses the log file generated by `macmon` and returns a dictionary
containing the average CPU and GPU power consumption and energy deltas.

start() -> None:
Starts the macmon process if not already running.
"""

_osx_silicon_exec = "macmon"

def __init__(
self,
output_dir: str = ".",
n_points=10,
interval=100,
log_file_name="macmon_log.txt",
) -> None:
# Only initialize once
self._log_file_path = os.path.join(output_dir, log_file_name)
self._system = sys.platform.lower()
self._n_points = n_points
self._interval = interval
self._setup_cli()
self._process = None

def _setup_cli(self) -> None:
"""
Setup cli command to run macmon
"""
if self._system.startswith("darwin"):
cpu_model = detect_cpu_model()
if cpu_model.startswith("Apple"):
if shutil.which(self._osx_silicon_exec):
self._cli = self._osx_silicon_exec
else:
msg = f"Macmon executable not found on {self._system}, install with `brew install macmon` or checkout https://github.com/vladkens/macmon for installation instructions"
raise FileNotFoundError(msg)
else:
raise SystemError("Platform not supported by MacMon")

def _log_values(self) -> None:
"""
Logs output from Macmon to a file. If a macmon process is already
running, it will use the existing process instead of starting a new one.
"""
import multiprocessing

returncode = None
if self._system.startswith("darwin"):
# Run the MacMon command and capture its output
cmd = [
"macmon",
"-i",
str(self._interval),
"pipe",
"-s",
str(self._n_points),
]
lock = multiprocessing.Lock()
with lock:
with open(self._log_file_path, "a") as f: # Open file in append mode
returncode = subprocess.call(
cmd, universal_newlines=True, stdout=f, text=True
)
else:
return None
if returncode != 0:
logger.warning(
"Returncode while logging power values using " + f"Macmon: {returncode}"
)
return

def get_details(self) -> Dict:
"""
Fetches the CPU Power Details by fetching values from a logged csv file
in _log_values function
"""
self._log_values()
details = dict()
try:
data = pd.read_json(self._log_file_path, lines=True)
details["CPU Power"] = data["cpu_power"].mean()
details["CPU Energy Delta"] = (self._interval / 1000) * (
data["cpu_power"].astype(float)
).sum()
details["GPU Power"] = data["gpu_power"].mean()
details["GPU Energy Delta"] = (self._interval / 1000) * (
data["gpu_power"].astype(float)
).sum()
details["RAM Power"] = data["ram_power"].mean()
details["RAM Energy Delta"] = (self._interval / 1000) * (
data["ram_power"].astype(float)
).sum()
except Exception as e:
msg = (
f"Unable to read macmon logged file at {self._log_file_path}\n"
f"Exception occurred {e}"
)
logger.info(msg, exc_info=True)
return details

def start(self) -> None:
# Start is handled in _log_values when needed
pass
7 changes: 7 additions & 0 deletions codecarbon/core/measure.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ def do_measure(self) -> None:
f"Energy consumed for AppleSilicon GPU : {self._total_gpu_energy.kWh:.6f} kWh"
+ f".Apple Silicon GPU Power : {self._gpu_power.W} W"
)
elif hardware.chip_part == "RAM":
self._total_ram_energy += energy
self._ram_power = power
logger.info(
f"Energy consumed for AppleSilicon RAM : {self._total_ram_energy.kWh:.6f} kWh."
+ f"Apple Silicon RAM Power : {self._ram_power.W} W"
)
else:
logger.error(f"Unknown hardware type: {hardware} ({type(hardware)})")
h_time = perf_counter() - h_time
Expand Down
84 changes: 51 additions & 33 deletions codecarbon/core/resource_tracker.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from collections import Counter
from typing import List, Union

from codecarbon.core import cpu, gpu, powermetrics
from codecarbon.core import cpu, gpu, macmon, 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
Expand All @@ -17,19 +17,32 @@ def __init__(self, tracker):

def set_RAM_tracking(self):
logger.info("[setup] RAM Tracking...")
if self.tracker._force_ram_power is not None:
self.ram_tracker = (
f"User specified constant: {self.tracker._force_ram_power} Watts"
)
logger.info(
f"Using user-provided RAM power: {self.tracker._force_ram_power} Watts"
)

if macmon.is_macmon_available() and "M1" not in detect_cpu_model():
logger.info("Tracking Apple RAM via MacMon")
self.ram_tracker = "MacMon"
ram = AppleSiliconChip.from_utils(self.tracker._output_dir, chip_part="RAM")

else:
self.ram_tracker = "RAM power estimation model"
ram = RAM(
tracking_mode=self.tracker._tracking_mode,
force_ram_power=self.tracker._force_ram_power,
)
if macmon.is_macmon_available():
logger.warning(
"MacMon is installed but cannot track RAM power on M1 chips, reverting to constant RAM power"
)

if self.tracker._force_ram_power is not None:
self.ram_tracker = (
f"User specified constant: {self.tracker._force_ram_power} Watts"
)
logger.info(
f"Using user-provided RAM power: {self.tracker._force_ram_power} Watts"
)
else:
self.ram_tracker = "RAM power estimation model"
logger.info("Using RAM power estimation model based on the RAM size")
ram = RAM(
tracking_mode=self.tracker._tracking_mode,
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]

Expand Down Expand Up @@ -87,37 +100,24 @@ def set_CPU_tracking(self):
"The RAPL energy and power reported is divided by 2 for all 'AMD Ryzen Threadripper' as it seems to give better results."
)
# change code to check if powermetrics needs to be installed or just sudo setup
elif (
powermetrics.is_powermetrics_available()
and self.tracker._force_cpu_power is None
):
logger.info("Tracking Apple CPU and GPU via PowerMetrics")
self.gpu_tracker = "PowerMetrics"
elif macmon.is_macmon_available():
self.cpu_tracker = "MacMon"
logger.info(f"Tracking Apple CPU using {self.cpu_tracker}")
elif powermetrics.is_powermetrics_available():
self.cpu_tracker = "PowerMetrics"
logger.info(f"Tracking Apple CPU using {self.cpu_tracker}")
hardware_cpu = AppleSiliconChip.from_utils(
self.tracker._output_dir, chip_part="CPU"
)
self.tracker._hardware.append(hardware_cpu)
self.tracker._conf["cpu_model"] = hardware_cpu.get_model()

hardware_gpu = AppleSiliconChip.from_utils(
self.tracker._output_dir, chip_part="GPU"
)
self.tracker._hardware.append(hardware_gpu)

self.tracker._conf["gpu_model"] = hardware_gpu.get_model()
self.tracker._conf["gpu_count"] = 1
else:
# Explain what to install to increase accuracy
cpu_tracking_install_instructions = ""
if is_mac_os():
if (
"M1" in detect_cpu_model()
or "M2" in detect_cpu_model()
or "M3" in detect_cpu_model()
):
cpu_tracking_install_instructions = ""
cpu_tracking_install_instructions = "Mac OS and ARM processor detected: Please enable PowerMetrics sudo to measure CPU"
if any((m in detect_cpu_model() for m in ["M1", "M2", "M3", "M4"])):
cpu_tracking_install_instructions = "Mac OS and ARM processor detected: Please enable PowerMetrics with sudo or MacMon to measure CPU"
else:
cpu_tracking_install_instructions = "Mac OS detected: Please install Intel Power Gadget or enable PowerMetrics sudo to measure CPU"
elif is_windows_os():
Expand Down Expand Up @@ -209,6 +209,24 @@ def set_GPU_tracking(self):
gpu_devices.devices.get_gpu_static_info()
)
self.gpu_tracker = "pynvml"

elif macmon.is_macmon_available():
logger.info("Tracking Apple GPU via MacMon")
hardware_gpu = AppleSiliconChip.from_utils(
self.tracker._output_dir, chip_part="GPU"
)
self.tracker._hardware.append(hardware_gpu)
self.tracker._conf["gpu_model"] = hardware_gpu.get_model()
self.tracker._conf["gpu_count"] = 1

elif powermetrics.is_powermetrics_available():
logger.info("Tracking Apple GPU via PowerMetrics")
hardware_gpu = AppleSiliconChip.from_utils(
self.tracker._output_dir, chip_part="GPU"
)
self.tracker._hardware.append(hardware_gpu)
self.tracker._conf["gpu_model"] = hardware_gpu.get_model()
self.tracker._conf["gpu_count"] = 1
else:
logger.info("No GPU found.")

Expand Down
7 changes: 7 additions & 0 deletions codecarbon/emissions_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,13 @@ def _do_measurements(self) -> None:
f"Energy consumed for all GPUs : {self._total_gpu_energy.kWh:.6f} kWh"
+ f". Total GPU Power : {self._gpu_power.W} W"
)
elif hardware.chip_part == "RAM":
self._total_ram_energy += energy
self._ram_power = power
logger.info(
f"Energy consumed for RAM : {self._total_ram_energy.kWh:.6f} kWh"
+ f". RAM Power : {self._ram_power.W} W"
)
else:
logger.error(f"Unknown hardware type: {hardware} ({type(hardware)})")
h_time = time.perf_counter() - h_time
Expand Down
20 changes: 18 additions & 2 deletions codecarbon/external/hardware.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@

from codecarbon.core.cpu import IntelPowerGadget, IntelRAPL
from codecarbon.core.gpu import AllGPUDevices
from codecarbon.core.powermetrics import ApplePowermetrics
from codecarbon.core.macmon import MacMon, is_macmon_available
from codecarbon.core.powermetrics import ApplePowermetrics, is_powermetrics_available
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 @@ -344,7 +345,12 @@ def __init__(
):
self._output_dir = output_dir
self._model = model
self._interface = ApplePowermetrics(self._output_dir)
if is_macmon_available():
self._interface = MacMon(self._output_dir)
elif is_powermetrics_available():
self._interface = ApplePowermetrics(self._output_dir)
else:
raise Exception("No supported interface found for Apple Silicon Chip.")
self.chip_part = chip_part

def __repr__(self) -> str:
Expand Down Expand Up @@ -404,3 +410,13 @@ def from_utils(
logger.warning("Could not read AppleSiliconChip model.")

return cls(output_dir=output_dir, model=model, chip_part=chip_part)

@property
def machine_memory_GB(self):
"""
Property to compute the machine's total memory in bytes.

Returns:
float: Total RAM (GB)
"""
return psutil.virtual_memory().total / B_TO_GB
26 changes: 21 additions & 5 deletions docs/_sources/methodology.rst.txt
Original file line number Diff line number Diff line change
Expand Up @@ -143,15 +143,31 @@ CPU

- **On Windows or Mac (Intel)**

Tracks Intel processors energy consumption using the ``Intel Power Gadget``. You need to install it yourself from this `source <https://www.intel.com/content/www/us/en/developer/articles/tool/power-gadget.html>`_ . But has been discontinued. There is a discussion about it on `github issues #457 <https://github.com/mlco2/codecarbon/issues/457>`_.
Tracks Intel processors energy consumption using the ``Intel Power Gadget``. You need to install it yourself from this `source <https://www.intel.com/content/www/us/en/developer/articles/tool/power-gadget.html>`_ .
WARNING : The Intel Power Gadget is not available on Apple Silicon Macs.
WARNING 2 : Intel Power Gadget has been deprecated by Intel, and it is not available for the latest processors. We are looking for alternatives.
There is a discussion about it on `github issues #457 <https://github.com/mlco2/codecarbon/issues/457>`_.

- **Apple Silicon Chips (M1, M2)**
- **Apple Silicon Chips (M1, M2, M3, M4)**

Apple Silicon Chips contain both the CPU and the GPU.

Codecarbon tracks Apple Silicon Chip energy consumption using ``powermetrics``. It should be available natively on any mac.
However, this tool is only usable with ``sudo`` rights and to our current knowledge, there are no other options to track the energy consumption of the Apple Silicon Chip without administrative rights
(if you know of any solution for this do not hesitate and `open an issue with your proposed solution <https://github.com/mlco2/codecarbon/issues/>`_).
There are two options to track the energy consumption of the Apple Silicon Chip:
- `Powermetrics <https://www.unix.com/man-page/osx/1/powermetrics/>`_ The mac native utility to monitor energy consumption on Apple Silicon Macs.
- `macmon <https://github.com/vladkens/macmon>`_ : a utility that allows you to monitor the energy consumption of your Apple Silicon Mac, it has the advantage of not requiring sudo privileges.

In order to provide the easiest for the user, codecarbon uses macmon if it is available, otherwise it falls back to powermetrics, and if none of them are available, it falls back to constant mode using TDP.
Powermetrics should be available natively on any mac, but macmon requires installation.

To use the ``macmon`` utility, you can use the following command:

.. code-block:: bash

brew install macmon

And ``codecarbon`` will automatically detect and use it.

If you prefer to use ``powermetrics`` with ``codecarbon`` then you need to give it ``sudo`` rights.

To give sudo rights without having to enter a password each time, you can modify the sudoers file with the following command:

Expand Down
Loading
Loading