Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update master #83

Merged
merged 60 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
f223240
Add test dependencies as optional dependencies, and outphase deprecat…
Snailed Mar 25, 2024
1c925fe
renamed optional dependencies
Snailed Mar 25, 2024
dfec276
fixed location-specific tests
Snailed Mar 25, 2024
b7fb408
added tests to CI
Snailed Mar 25, 2024
e7e6fe8
skip more
Snailed Mar 25, 2024
733c75b
uncomment tests and skip them in CI
Snailed Mar 25, 2024
b1d56a7
Merge pull request #68 from Snailed/master
PedramBakh Mar 25, 2024
abf8193
improve tox setup
Snailed Mar 26, 2024
a99a05a
Make NvidiaGPU.devices() return unicode string on all python versions
Snailed Mar 26, 2024
5d15a7e
make setuptools recognize 'carbontracker' directory as package
Snailed Mar 26, 2024
c3da4b5
made conditional usage of importlib/pkg_resources based on Python ver…
Snailed Mar 26, 2024
470a296
Added Python 3.8-3.12 to CI test matrix
Snailed Mar 26, 2024
9a9912d
Change assertion to test if race condition happens on thread stopping
Snailed Mar 26, 2024
3bb1988
added informative comment
Snailed Mar 26, 2024
d122cce
Merge pull request #69 from Snailed/master
PedramBakh Mar 26, 2024
f4db979
add py3.7 to test pipeline
Snailed May 2, 2024
a8648e9
Fixed syntax error
Snailed May 2, 2024
b0b077e
Merge pull request #71 from Snailed/add-py3.7-to-tests
PedramBakh May 2, 2024
19ce3f0
Change assertion to test if race condition happens on thread stopping
Snailed Mar 26, 2024
75cf8b1
Merge pull request #69 from Snailed/master
PedramBakh Mar 26, 2024
27880b4
add py3.7 to test pipeline
Snailed May 2, 2024
2dd68fb
Merge pull request #71 from Snailed/add-py3.7-to-tests
PedramBakh May 2, 2024
6408d8e
Merge branch 'dev' into docs
Snailed May 2, 2024
505a2d9
Add documentation site, fix tests, add type hints
Snailed Mar 26, 2024
574231d
Merge branch 'lfwa:master' into docs
Snailed May 21, 2024
e2c9b86
Merge branch 'dev' into docs
Snailed May 2, 2024
3ee256a
Merge branch 'docs' of github.com:Snailed/carbontracker into docs
Snailed Jun 25, 2024
ac03b59
fix types
Snailed Jun 25, 2024
f5e0ca8
fix merge conflict
Snailed Jun 25, 2024
8fcb54a
Merge pull request #72 from Snailed/docs
Snailed Jun 25, 2024
2e1d3eb
Fix logging redudancy on multiple instances
Snailed Jun 26, 2024
3c50ae2
add tests
Snailed Jun 26, 2024
9672ee1
fixed energidataservice
Snailed Jun 26, 2024
a24ae4c
fix test and remove warnings
Snailed Jun 26, 2024
405b9d4
replace datetime.UTC with datetime.timezone.utc for backwards compat.
Snailed Jun 26, 2024
26cb0ef
Fix parser error
Snailed Jun 26, 2024
45449f7
fix parser epoch mismatch error
Snailed Jun 26, 2024
c3d7238
Merge pull request #75 from Snailed/fix-parsing-error
Snailed Jul 23, 2024
e404bdc
Merge pull request #74 from Snailed/fix-energidataservice
Snailed Jul 23, 2024
050cc45
Merge pull request #73 from Snailed/fix-multiple-instantiations
Snailed Jul 23, 2024
7e83af8
fix: parser should consider actual file names when listing log files
Snailed Jul 23, 2024
a547f9e
Merge pull request #76 from Snailed/parsing-of-logs-fail
Snailed Aug 6, 2024
1722394
Merge pull request #79 from lfwa/master
Snailed Sep 13, 2024
11f9154
Fix Apple Silicon shutdown bug
PedramBakh Sep 13, 2024
ac639ff
Updated default intensities to 2023 data
Snailed Sep 13, 2024
e19d66b
Updated world default and PUE
Snailed Sep 13, 2024
fd1d2df
Merge pull request #80 from Snailed/feature/update-pue-and-default-em…
Snailed Sep 13, 2024
4efe1ee
Fix test with new defaults
Snailed Sep 13, 2024
18e4b3c
breaking: change default monitor_epochs to -1
Snailed Sep 13, 2024
6b1de16
Merge pull request #81 from Snailed/dev
Snailed Sep 13, 2024
69b4087
Always log prediction consumption first
PedramBakh Sep 13, 2024
adc8969
minor: Deprecated EnergiDataService and CarbonIntensityGB
Snailed Sep 13, 2024
74a4bd0
Merge pull request #82 from Snailed/dev
Snailed Sep 13, 2024
28c14d3
Lower update interval for consumption measurements (components)
PedramBakh Sep 13, 2024
2e4c761
Add parser to CLI
PedramBakh Sep 13, 2024
b509714
Fix parser test
PedramBakh Sep 13, 2024
e140d96
Fix aggregate log parser
PedramBakh Sep 13, 2024
5fb10e0
Add powermetrics guide for apple silicon
PedramBakh Sep 13, 2024
ea4eb04
Update docs for CLI
PedramBakh Sep 13, 2024
42a4a91
fix CD
Snailed Sep 16, 2024
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
16 changes: 16 additions & 0 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: deploy-docs
on:
push:
branches:
- main
- docs
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.x
- run: pip install mkdocs mkdocstrings[python]
- run: mkdocs gh-deploy --force --clean --verbose
6 changes: 3 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8, 3.9, '3.10']
python-version: [3.7, 3.8, 3.9, '3.10', '3.11', '3.12']

steps:
- uses: actions/checkout@v3
Expand All @@ -23,7 +23,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
pip install .\[test,docs\]

- name: Run tests
run: python -m unittest discover
Expand All @@ -45,7 +45,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
pip install .\[test,docs\]
pip install flake8 black

- name: Lint with flake8
Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ on:
- dev

jobs:
build:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8, 3.9, '3.10']
python-version: ['3.7', '3.8','3.9', '3.10', '3.11', '3.12']

steps:
- uses: actions/checkout@v3
Expand All @@ -27,9 +27,11 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install flake8 black
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
pip install .[test]
- name: Lint with flake8
run: |
flake8 carbontracker --count --select=E9,F63,F7,F82 --show-source --statistics
- name: Formatting with Black
run: black --line-length 120 carbontracker
- name: Run tests
run: python -m unittest discover -v
56 changes: 49 additions & 7 deletions carbontracker/cli.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,64 @@
import argparse
import subprocess
from carbontracker.tracker import CarbonTracker
from carbontracker import parser
import ast


def parse_logs(log_dir):
parser.print_aggregate(log_dir=log_dir)


def main():
"""
The **carbontracker** CLI allows the user to track the energy consumption and carbon intensity of any program.
[Make sure that you have relevant permissions before running this.](/#permissions)

Args:
--log_dir (path, optional): Log directory. Defaults to `./logs`.
--api_keys (str, optional): API keys in a dictionary-like format, e.g. `\'{"electricitymaps": "YOUR_KEY"}\'`
--parse (path, optional): Directory containing the log files to parse.

Example:
Tracking the carbon intensity of `script.py`.

$ carbontracker python script.py

With example options

$ carbontracker --log_dir='./logs' --api_keys='{"electricitymaps": "API_KEY_EXAMPLE"}' python script.py

Parsing logs:

$ carbontracker --parse ./internal_logs
"""

# Create a parser for the known arguments
parser = argparse.ArgumentParser(description="CarbonTracker CLI", add_help=True)
parser.add_argument("--log_dir", type=str, default="./logs", help="Log directory")
parser.add_argument("--api_keys", type=str, help="API keys in a dictionary-like format, e.g., "
"'{\"electricitymaps\": \"YOUR_KEY\"}'", default=None)
cli_parser = argparse.ArgumentParser(description="CarbonTracker CLI", add_help=True)
cli_parser.add_argument("--log_dir", type=str, default="./logs", help="Log directory")
cli_parser.add_argument(
"--api_keys",
type=str,
help="API keys in a dictionary-like format, e.g., "
'\'{"electricitymaps": "YOUR_KEY"}\'',
default=None,
)
cli_parser.add_argument("--parse", type=str, help="Directory containing the log files to parse.")

# Parse known arguments only
known_args, remaining_args = parser.parse_known_args()
known_args, remaining_args = cli_parser.parse_known_args()

# Check if the --parse argument is provided
if known_args.parse:
parse_logs(known_args.parse)
return

# Parse the API keys string into a dictionary
api_keys = ast.literal_eval(known_args.api_keys) if known_args.api_keys else None

tracker = CarbonTracker(epochs=1, log_dir=known_args.log_dir, epochs_before_pred=0, api_keys=api_keys)
tracker = CarbonTracker(
epochs=1, log_dir=known_args.log_dir, epochs_before_pred=0, api_keys=api_keys
)
tracker.epoch_start()

# The remaining_args are considered as the command to execute
Expand All @@ -33,4 +75,4 @@ def main():


if __name__ == "__main__":
main()
main()
39 changes: 24 additions & 15 deletions carbontracker/components/apple_silicon/powermetrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,49 @@
import re
import time
from carbontracker.components.handler import Handler
from typing import Union, List, Pattern


class PowerMetricsUnified:
_output = None
_last_updated = None
_output: Union[None, str] = None
_last_updated: Union[None, float] = None

@staticmethod
def get_output():
if PowerMetricsUnified._output is None or time.time() - PowerMetricsUnified._last_updated > 1:
if (
PowerMetricsUnified._output is None
or PowerMetricsUnified._last_updated is None
or time.time() - PowerMetricsUnified._last_updated > 1
):
PowerMetricsUnified._output = subprocess.check_output(
["sudo", "powermetrics", "-n", "1", "-i", "1000", "--samplers", "all"], universal_newlines=True
["sudo", "powermetrics", "-n", "1", "-i", "100", "--samplers", "all"],
universal_newlines=True,
)
PowerMetricsUnified._last_updated = time.time()
return PowerMetricsUnified._output


class AppleSiliconCPU(Handler):
def init(self, pids=None, devices_by_pid=None):
def init(self, pids=None, devices_by_pid=False):
self.devices_list = ["CPU"]
self.cpu_pattern = re.compile(r"CPU Power: (\d+) mW")

def shutdown(self):
pass

def devices(self):
def devices(self) -> List[str]:
"""Returns a list of devices (str) associated with the component."""
return self.devices_list

def available(self):
def available(self) -> bool:
return platform.system() == "Darwin"

def power_usage(self):
def power_usage(self) -> List[float]:
output = PowerMetricsUnified.get_output()
cpu_power = self.parse_power(output, self.cpu_pattern)
return cpu_power
return [cpu_power]

def parse_power(self, output, pattern):
def parse_power(self, output: str, pattern: Pattern[str]) -> float:
match = pattern.search(output)
if match:
power = float(match.group(1)) / 1000 # Convert mW to W
Expand All @@ -49,28 +55,31 @@ def parse_power(self, output, pattern):


class AppleSiliconGPU(Handler):
def init(self, pids=None, devices_by_pid=None):
def init(self, pids=None, devices_by_pid=False):
self.devices_list = ["GPU", "ANE"]
self.gpu_pattern = re.compile(r"GPU Power: (\d+) mW")
self.ane_pattern = re.compile(r"ANE Power: (\d+) mW")

def devices(self):
def devices(self) -> List[str]:
"""Returns a list of devices (str) associated with the component."""
return self.devices_list

def available(self):
def available(self) -> bool:
return platform.system() == "Darwin"

def power_usage(self):
output = PowerMetricsUnified.get_output()
gpu_power = self.parse_power(output, self.gpu_pattern)
ane_power = self.parse_power(output, self.ane_pattern)
return gpu_power + ane_power
return [gpu_power + ane_power]

def parse_power(self, output, pattern):
def parse_power(self, output: str, pattern: Pattern[str]) -> float:
match = pattern.search(output)
if match:
power = float(match.group(1)) / 1000 # Convert mW to W (J/s)
return power
else:
return 0.0

def shutdown(self):
pass
61 changes: 41 additions & 20 deletions carbontracker/components/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
from carbontracker import exceptions
from carbontracker.components.gpu import nvidia
from carbontracker.components.cpu import intel
from carbontracker.components.apple_silicon.powermetrics import AppleSiliconCPU, AppleSiliconGPU
from carbontracker.components.apple_silicon.powermetrics import (
AppleSiliconCPU,
AppleSiliconGPU,
)
from carbontracker.components.handler import Handler
from typing import Iterable, List, Union, Type, Sized

COMPONENTS = [
{
Expand All @@ -19,52 +24,60 @@
]


def component_names():
def component_names() -> List[str]:
return [comp["name"] for comp in COMPONENTS]


def error_by_name(name):
def error_by_name(name) -> Exception:
for comp in COMPONENTS:
if comp["name"] == name:
return comp["error"]
raise exceptions.ComponentNameError()


def handlers_by_name(name):
def handlers_by_name(name) -> List[Type[Handler]]:
for comp in COMPONENTS:
if comp["name"] == name:
return comp["handlers"]
raise exceptions.ComponentNameError()


class Component:
def __init__(self, name, pids, devices_by_pid):
def __init__(self, name: str, pids: Iterable[int], devices_by_pid: bool):
self.name = name
if name not in component_names():
raise exceptions.ComponentNameError(f"No component found with name '{self.name}'.")
self._handler = self._determine_handler(pids=pids, devices_by_pid=devices_by_pid)
self.power_usages = []
self.cur_epoch = -1 # Sentry
raise exceptions.ComponentNameError(
f"No component found with name '{self.name}'."
)
self._handler = self._determine_handler(
pids=pids, devices_by_pid=devices_by_pid
)
self.power_usages: List[List[float]] = []
self.cur_epoch: int = -1 # Sentry

@property
def handler(self):
def handler(self) -> Handler:
if self._handler is None:
raise error_by_name(self.name)
return self._handler

def _determine_handler(self, pids, devices_by_pid):
def _determine_handler(
self, pids: Iterable[int], devices_by_pid: bool
) -> Union[Handler, None]:
handlers = handlers_by_name(self.name)
for h in handlers:
handler = h(pids=pids, devices_by_pid=devices_by_pid)
if handler.available():
return handler
return None

def devices(self):
def devices(self) -> List[str]:
return self.handler.devices()

def available(self):
def available(self) -> bool:
return self._handler is not None

def collect_power_usage(self, epoch):
def collect_power_usage(self, epoch: int):
if epoch < 1:
return

Expand All @@ -77,11 +90,13 @@ def collect_power_usage(self, epoch):
if diff != 0:
for _ in range(diff):
# Copy previous measurement lists.
latest_measurements = self.power_usages[-1] if self.power_usages else []
latest_measurements = (
self.power_usages[-1] if self.power_usages else []
)
self.power_usages.append(latest_measurements)
self.power_usages.append([])
try:
self.power_usages[-1].append(self.handler.power_usage())
self.power_usages[-1] += self.handler.power_usage()
except exceptions.IntelRaplPermissionError:
# Only raise error if no measurements have been collected.
if not self.power_usages[-1]:
Expand All @@ -100,7 +115,7 @@ def collect_power_usage(self, epoch):
# Append zero measurement to avoid further errors.
self.power_usages.append([0])

def energy_usage(self, epoch_times):
def energy_usage(self, epoch_times: List[int]) -> List[int]:
"""Returns energy (mWh) used by component per epoch."""
energy_usages = []
# We have to compute each epoch in a for loop since numpy cannot
Expand Down Expand Up @@ -138,11 +153,17 @@ def shutdown(self):
self.handler.shutdown()


def create_components(components, pids, devices_by_pid):
def create_components(
components: str, pids: Iterable[int], devices_by_pid: bool
) -> List[Component]:
components = components.strip().replace(" ", "").lower()
if components == "all":
return [Component(name=comp_name, pids=pids, devices_by_pid=devices_by_pid) for comp_name in component_names()]
return [
Component(name=comp_name, pids=pids, devices_by_pid=devices_by_pid)
for comp_name in component_names()
]
else:
return [
Component(name=comp_name, pids=pids, devices_by_pid=devices_by_pid) for comp_name in components.split(",")
Component(name=comp_name, pids=pids, devices_by_pid=devices_by_pid)
for comp_name in components.split(",")
]
Loading
Loading