Skip to content

Commit 34127e5

Browse files
committed
Added support to CodeCarbon.
1 parent 9a15ee5 commit 34127e5

File tree

10 files changed

+492
-11
lines changed

10 files changed

+492
-11
lines changed

poetry.lock

Lines changed: 381 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pydantic = "^2.5.3"
3333
pydantic-settings = "^2.1.0"
3434
scikit-learn = "^1.4.0"
3535
mlflow-skinny = "^2.10.2"
36+
codecarbon = "^2.3.4"
3637

3738
[tool.poetry.group.dev.dependencies]
3839
invoke = "^2.2.0"
@@ -50,6 +51,11 @@ pytest-xdist = "^3.5.0"
5051
pandera = { extras = ["mypy"], version = "^0.18.0" }
5152
ruff = "^0.2.2"
5253

54+
[tool.poetry.group.carbons.dependencies]
55+
dash = "^2.15.0"
56+
dash-bootstrap-components = "^1.5.0"
57+
fire = "^0.5.0"
58+
5359
[tool.poetry.group.notebooks.dependencies]
5460
ipykernel = "^6.29.0"
5561
nbformat = "^5.9.2"
@@ -59,12 +65,16 @@ nbformat = "^5.9.2"
5965
[tool.coverage.run]
6066
branch = true
6167
source = ["src"]
68+
omit = ["__main__.py"]
6269

6370
[tool.mypy]
6471
check_untyped_defs = true
6572
ignore_missing_imports = true
6673
plugins = ["pandera.mypy", "pydantic.mypy"]
6774

75+
[tool.pytest.ini_options]
76+
addopts = "--numprocesses='auto'"
77+
6878
[tool.ruff]
6979
fix = true
7080
line-length = 100

src/bikes/jobs.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class Job(abc.ABC, pdt.BaseModel, strict=True):
3333
KIND: str
3434

3535
logger_service: services.LoggerService = services.LoggerService()
36+
carbon_service: services.CarbonService = services.CarbonService()
3637
mlflow_service: services.MLflowService = services.MLflowService()
3738

3839
def __enter__(self) -> T.Self:
@@ -41,7 +42,10 @@ def __enter__(self) -> T.Self:
4142
Returns:
4243
T.Self: return the current object.
4344
"""
44-
self.logger_service.start()
45+
self.logger_service.start() # start then log
46+
logger.debug("[START] Logger service: {}", self.logger_service)
47+
logger.debug("[START] Carbon service: {}", self.carbon_service)
48+
self.carbon_service.start()
4549
logger.debug("[START] MLflow service: {}", self.mlflow_service)
4650
self.mlflow_service.start()
4751
return self
@@ -59,6 +63,9 @@ def __exit__(self, exc_type, exc_value, traceback) -> T.Literal[False]:
5963
"""
6064
logger.debug("[STOP] MLflow service: {}", self.mlflow_service)
6165
self.mlflow_service.stop()
66+
logger.debug("[STOP] Carbon service: {}", self.carbon_service)
67+
self.carbon_service.stop()
68+
logger.debug("[STOP] Logger service: {}", self.carbon_service)
6269
self.logger_service.stop()
6370
return False
6471

src/bikes/services.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
# %% IMPORTS
44

55
import abc
6+
import os
67
import sys
78
import typing as T
89

10+
import codecarbon as cc
911
import mlflow
1012
import pydantic as pdt
1113
from loguru import logger
@@ -78,6 +80,36 @@ def start(self) -> None:
7880
logger.add(**config)
7981

8082

83+
class CarbonService(Service):
84+
"""Service for tracking carbon emissions."""
85+
86+
# public
87+
# - inputs
88+
log_level: str = "ERROR"
89+
project_name: str = "bikes"
90+
measure_power_secs: int = 5
91+
# - outputs
92+
output_dir: str = "outputs"
93+
output_file: str = "emissions.csv"
94+
on_csv_write: str = "append"
95+
# - offline
96+
country_iso_code: str = "LUX"
97+
# private
98+
_tracker: cc.OfflineEmissionsTracker | None = None
99+
100+
def start(self):
101+
"""Start the carbon service."""
102+
os.makedirs(self.output_dir, exist_ok=True) # create output dir
103+
self._tracker = cc.OfflineEmissionsTracker(**self.model_dump())
104+
self._tracker.start()
105+
106+
def stop(self):
107+
"""Stop the carbon service."""
108+
assert self._tracker, "Carbon tracker should be started!"
109+
self._tracker.flush()
110+
self._tracker.stop()
111+
112+
81113
class MLflowService(Service):
82114
"""Service for MLflow tracking and registry.
83115

tasks/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66

77
from invoke import Collection
88

9-
from . import checks, cleans, containers, dags, docs, formats, installs, packages
9+
from . import carbons, checks, cleans, containers, dags, docs, formats, installs, packages
1010

1111
# %% NAMESPACES
1212

1313
ns = Collection()
1414

1515
# %% COLLECTIONS
1616

17+
ns.add_collection(carbons)
1718
ns.add_collection(checks)
1819
ns.add_collection(cleans)
1920
ns.add_collection(dags, default=True)

tasks/carbons.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""Carbons tasks for pyinvoke."""
2+
3+
# %% IMPORTS
4+
5+
from invoke import task
6+
from invoke.context import Context
7+
8+
# %% TASKS
9+
10+
11+
@task
12+
def board(ctx: Context, filepath: str = "outputs/emissions.csv", port: int = 8050) -> None:
13+
"""Visualize caron emissions data at file path from the carbon board app."""
14+
ctx.run(f"poetry run carbonboard --filepath={filepath} --port={port}")
15+
16+
17+
@task(pre=[board], default=True)
18+
def all(_: Context) -> None:
19+
"""Run all carbon tasks."""

tasks/checks.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
# %% CONFIGS
99

1010
COVERAGE_FAIL_UNDER = 80
11-
PYTEST_N_PROCESSES = "auto"
1211

1312
# %% TASKS
1413

@@ -40,16 +39,13 @@ def code(ctx: Context) -> None:
4039
@task
4140
def test(ctx: Context) -> None:
4241
"""Check the tests with pytest."""
43-
ctx.run("poetry run pytest --numprocesses={PYTEST_N_PROCESSES} tests/")
42+
ctx.run("poetry run pytest tests/")
4443

4544

4645
@task
4746
def coverage(ctx: Context) -> None:
4847
"""Check the coverage with coverage."""
49-
ctx.run(
50-
f"poetry run pytest --numprocesses={PYTEST_N_PROCESSES}"
51-
f" --cov=src/ --cov-fail-under={COVERAGE_FAIL_UNDER} tests/"
52-
)
48+
ctx.run(f"poetry run pytest --cov=src/ --cov-fail-under={COVERAGE_FAIL_UNDER} tests/")
5349

5450

5551
@task(pre=[poetry, format, type, code, coverage], default=True)

tests/conftest.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import mlflow
99
import omegaconf
1010
import pytest
11-
1211
from bikes import datasets, metrics, models, registers, schemas, searchers, services, splitters
1312

1413
# %% CONFIGS
@@ -60,6 +59,12 @@ def outputs_path(data_path: str) -> str:
6059
return os.path.join(data_path, "outputs.parquet")
6160

6261

62+
@pytest.fixture(scope="function")
63+
def tmp_carbon_path(tmp_path: str) -> str:
64+
"""Return a tmp path of the carbon folder."""
65+
return os.path.join(tmp_path, "carbons")
66+
67+
6368
@pytest.fixture(scope="function")
6469
def tmp_outputs_path(tmp_path: str) -> str:
6570
"""Return a tmp path for the outputs dataset."""
@@ -232,13 +237,22 @@ def default_metric() -> metrics.SklearnMetric:
232237

233238

234239
@pytest.fixture(scope="session", autouse=True)
235-
def logger_service():
240+
def logger_service() -> services.LoggerService:
236241
"""Return and start the logger service."""
237242
service = services.LoggerService(colorize=False, diagnose=True)
238243
service.start() # ready to be used
239244
return service
240245

241246

247+
@pytest.fixture(scope="function")
248+
def carbon_service(tmp_carbon_path: str) -> T.Generator[services.CarbonService, None, None]:
249+
"""Return and start the carbon service."""
250+
service = services.CarbonService(output_dir=tmp_carbon_path)
251+
service.start() # ready to be used
252+
yield service
253+
service.stop()
254+
255+
242256
@pytest.fixture(scope="function", autouse=True)
243257
def mlflow_service(tmp_path: str) -> services.MLflowService:
244258
"""Return and start the mlflow service."""

tests/test_jobs.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def test_tuning_job(
1616
default_searcher: searchers.GridCVSearcher,
1717
time_series_splitter: splitters.TimeSeriesSplitter,
1818
logger_service: services.LoggerService,
19+
carbon_service: services.CarbonService,
1920
mlflow_service: services.MLflowService,
2021
):
2122
# given
@@ -30,6 +31,7 @@ def test_tuning_job(
3031
searcher=default_searcher,
3132
splitter=time_series_splitter,
3233
logger_service=logger_service,
34+
carbon_service=carbon_service,
3335
mlflow_service=mlflow_service,
3436
)
3537
mlflow_client = mlflow_service.client()
@@ -81,6 +83,7 @@ def test_training_job(
8183
default_signer: registers.Signer,
8284
train_test_splitter: splitters.TrainTestSplitter,
8385
logger_service: services.LoggerService,
86+
carbon_service: services.CarbonService,
8487
mlflow_service: services.MLflowService,
8588
):
8689
# given
@@ -98,6 +101,7 @@ def test_training_job(
98101
splitter=train_test_splitter,
99102
registry_alias=registry_alias,
100103
logger_service=logger_service,
104+
carbon_service=carbon_service,
101105
mlflow_service=mlflow_service,
102106
)
103107
mlflow_client = mlflow_service.client()
@@ -187,6 +191,7 @@ def test_inference_job(
187191
default_alias: str,
188192
default_mlflow_model_version: registers.Version,
189193
logger_service: services.LoggerService,
194+
carbon_service: services.CarbonService,
190195
mlflow_service: services.MLflowService,
191196
):
192197
# given
@@ -197,6 +202,7 @@ def test_inference_job(
197202
registry_alias=default_alias,
198203
loader=default_loader,
199204
logger_service=logger_service,
205+
carbon_service=carbon_service,
200206
mlflow_service=mlflow_service,
201207
)
202208
# when

tests/test_services.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# %% IMPORTS
22

3+
import os
4+
35
import mlflow
46
from bikes import services
57
from loguru import logger
@@ -21,6 +23,20 @@ def test_logger_service(capsys):
2123
assert "DEBUG" not in capture.out, "Debug should not be logged!"
2224

2325

26+
def test_carbon_service(tmp_carbon_path: str):
27+
# given
28+
output_dir = tmp_carbon_path
29+
output_file = "emissions-test.csv"
30+
output_path = os.path.join(output_dir, output_file)
31+
service = services.CarbonService(output_dir=output_dir, output_file=output_file)
32+
# when
33+
service.start()
34+
service.stop()
35+
# then
36+
assert os.path.exists(output_path), "Output path should be created!"
37+
assert os.stat(output_path).st_size > 0, "Output path should not be empty!"
38+
39+
2440
def test_mlflow_service(mlflow_service: services.MLflowService):
2541
# given
2642
service = mlflow_service

0 commit comments

Comments
 (0)