Skip to content

Commit e43b3e4

Browse files
committed
Add option for simulated CPU/GPU
1 parent 736cd68 commit e43b3e4

File tree

14 files changed

+600
-123
lines changed

14 files changed

+600
-123
lines changed

.github/workflows/test.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ jobs:
2727
run: |
2828
python -m pip install --upgrade pip
2929
pip install flake8 black
30+
if [ "${{ matrix.python-version }}" = "3.9.21" ]; then
31+
pip install pyfakefs==4.5.1
32+
else
33+
pip install pyfakefs==5.8.0
34+
fi
3035
pip install .[test]
3136
- name: Lint with flake8
3237
run: |

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Kindly cite our work if you use **carbontracker** in a scientific publication:
2222
note={arXiv:2007.03051},
2323
year={2020}}
2424
```
25-
25+
_
2626
## Installation
2727
### PyPi
2828
```
@@ -66,6 +66,18 @@ Wrap any of your scripts (python, bash, etc.):
6666
Sets the level of verbosity.
6767
- `decimal_precision` (default=6):
6868
Desired decimal precision of reported values.
69+
- `sim_cpu` (default=None):
70+
Name of the simulated CPU. If set, will use simulated CPU power measurements.
71+
- `sim_cpu_tdp` (default=None):
72+
Thermal Design Power (TDP) in Watts for the simulated CPU. Required if `sim_cpu` is set.
73+
- `sim_cpu_util` (default=None):
74+
CPU utilization factor between 0 and 1. If not set, defaults to 0.5 (50% utilization).
75+
- `sim_gpu` (default=None):
76+
Name of the simulated GPU. If set, will use simulated GPU power measurements.
77+
- `sim_gpu_watts` (default=None):
78+
Power consumption in Watts for the simulated GPU. Required if `sim_gpu` is set.
79+
- `sim_gpu_util` (default=None):
80+
GPU utilization factor between 0 and 1. If not set, defaults to 0.5 (50% utilization).
6981

7082
#### Example usage
7183

carbontracker/cli.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ def main():
3131
--parse (path, optional): Directory containing the log files to parse.
3232
--report (path, optional): Generate a PDF report from a log file.
3333
--output (path, optional): Output path for the generated report. Defaults to 'carbon_report.pdf'
34+
--sim-cpu (str, optional): Simulated CPU name (overrides detection)
35+
--sim-cpu-tdp (float, optional): Simulated CPU TDP in Watts
36+
--sim-cpu-util (float, optional): Simulated CPU utilization (0.0 to 1.0)
37+
--sim-gpu (str, optional): Simulated GPU name (overrides detection)
38+
--sim-gpu-watts (float, optional): Simulated GPU power consumption in Watts
39+
--sim-gpu-util (float, optional): Simulated GPU utilization (0.0 to 1.0)
3440
3541
Example:
3642
Tracking the carbon intensity of `script.py`.
@@ -41,6 +47,10 @@ def main():
4147
4248
$ carbontracker --log_dir='./logs' --api_keys='{"electricitymaps": "API_KEY_EXAMPLE"}' python script.py
4349
50+
Using simulated hardware:
51+
52+
$ carbontracker --sim-cpu "Intel Xeon" --sim-cpu-tdp 150 --sim-gpu "NVIDIA A100" --sim-gpu-watts 400 python script.py
53+
4454
Parsing logs:
4555
4656
$ carbontracker --parse ./internal_logs
@@ -64,6 +74,26 @@ def main():
6474
cli_parser.add_argument("--report", type=str, help="Generate a PDF report from a log file.")
6575
cli_parser.add_argument("--output", type=str, default="carbon_report.pdf",
6676
help="Output path for the generated report.")
77+
78+
# Add simulated hardware arguments
79+
cli_parser.add_argument("--sim-cpu", type=str,
80+
help="Simulated CPU name (overrides detection). REQUIRED with --sim-cpu-tdp",
81+
default=None)
82+
cli_parser.add_argument("--sim-cpu-tdp", type=float,
83+
help="Simulated CPU TDP in Watts. REQUIRED when --sim-cpu is specified",
84+
default=None)
85+
cli_parser.add_argument("--sim-cpu-util", type=float,
86+
help="Simulated CPU utilization (0.0 to 1.0). Defaults to 0.5 if not specified",
87+
default=None)
88+
cli_parser.add_argument("--sim-gpu", type=str,
89+
help="Simulated GPU name (overrides detection). REQUIRED with --sim-gpu-watts",
90+
default=None)
91+
cli_parser.add_argument("--sim-gpu-watts", type=float,
92+
help="Simulated GPU power consumption in Watts. REQUIRED when --sim-gpu is specified",
93+
default=None)
94+
cli_parser.add_argument("--sim-gpu-util", type=float,
95+
help="Simulated GPU utilization (0.0 to 1.0). Defaults to 0.5 if not specified",
96+
default=None)
6797

6898
# Parse known arguments only
6999
known_args, remaining_args = cli_parser.parse_known_args()
@@ -82,7 +112,16 @@ def main():
82112
api_keys = ast.literal_eval(known_args.api_keys) if known_args.api_keys else None
83113

84114
tracker = CarbonTracker(
85-
epochs=1, log_dir=known_args.log_dir, epochs_before_pred=0, api_keys=api_keys
115+
epochs=1,
116+
log_dir=known_args.log_dir,
117+
epochs_before_pred=0,
118+
api_keys=api_keys,
119+
sim_cpu=known_args.sim_cpu,
120+
sim_cpu_tdp=known_args.sim_cpu_tdp,
121+
sim_cpu_util=known_args.sim_cpu_util,
122+
sim_gpu=known_args.sim_gpu,
123+
sim_gpu_watts=known_args.sim_gpu_watts,
124+
sim_gpu_util=known_args.sim_gpu_util
86125
)
87126
tracker.epoch_start()
88127

carbontracker/components/component.py

Lines changed: 98 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from typing import Iterable, List, Union, Type, Sized
1212
from carbontracker.loggerutil import Logger
1313
import os
14+
from carbontracker.components.cpu.sim_cpu import SimulatedCPUHandler
15+
from carbontracker.components.gpu.sim_gpu import SimulatedGPUHandler
1416

1517
COMPONENTS = [
1618
{
@@ -37,25 +39,64 @@ def error_by_name(name) -> Exception:
3739
raise exceptions.ComponentNameError()
3840

3941

40-
def handlers_by_name(name) -> List[Type[Handler]]:
42+
def handlers_by_name(
43+
name,
44+
sim_cpu=None,
45+
sim_cpu_tdp=None,
46+
sim_cpu_util=None,
47+
sim_gpu=None,
48+
sim_gpu_watts=None,
49+
sim_gpu_util=None
50+
):
4151
for comp in COMPONENTS:
4252
if comp["name"] == name:
53+
if name == "cpu" and sim_cpu and sim_cpu_tdp:
54+
return [lambda pids, devices_by_pid: SimulatedCPUHandler(
55+
sim_cpu,
56+
float(sim_cpu_tdp),
57+
float(sim_cpu_util) if sim_cpu_util is not None else 0.5
58+
)]
59+
elif name == "gpu" and sim_gpu and sim_gpu_watts:
60+
return [lambda pids, devices_by_pid: SimulatedGPUHandler(
61+
sim_gpu,
62+
float(sim_gpu_watts),
63+
float(sim_gpu_util) if sim_gpu_util is not None else 0.5
64+
)]
4365
return comp["handlers"]
4466
raise exceptions.ComponentNameError()
4567

4668

4769
class Component:
48-
def __init__(self, name: str, pids: Iterable[int], devices_by_pid: bool, logger: Logger):
70+
def __init__(
71+
self,
72+
name: str,
73+
pids: Iterable[int],
74+
devices_by_pid: bool,
75+
logger: Logger,
76+
sim_cpu=None,
77+
sim_cpu_tdp=None,
78+
sim_cpu_util=None,
79+
sim_gpu=None,
80+
sim_gpu_watts=None,
81+
sim_gpu_util=None
82+
):
4983
self.name = name
5084
if name not in component_names():
5185
raise exceptions.ComponentNameError(
5286
f"No component found with name '{self.name}'."
5387
)
5488
self._handler = self._determine_handler(
55-
pids=pids, devices_by_pid=devices_by_pid
89+
pids=pids,
90+
devices_by_pid=devices_by_pid,
91+
sim_cpu=sim_cpu,
92+
sim_cpu_tdp=sim_cpu_tdp,
93+
sim_cpu_util=sim_cpu_util,
94+
sim_gpu=sim_gpu,
95+
sim_gpu_watts=sim_gpu_watts,
96+
sim_gpu_util=sim_gpu_util
5697
)
5798
self.power_usages: List[List[float]] = []
58-
self.cur_epoch: int = -1 # Sentry
99+
self.cur_epoch: int = -1
59100
self.logger = logger
60101

61102
@property
@@ -65,11 +106,27 @@ def handler(self) -> Handler:
65106
return self._handler
66107

67108
def _determine_handler(
68-
self, pids: Iterable[int], devices_by_pid: bool
109+
self,
110+
pids: Iterable[int],
111+
devices_by_pid: bool,
112+
sim_cpu=None,
113+
sim_cpu_tdp=None,
114+
sim_cpu_util=None,
115+
sim_gpu=None,
116+
sim_gpu_watts=None,
117+
sim_gpu_util=None
69118
) -> Union[Handler, None]:
70-
handlers = handlers_by_name(self.name)
119+
handlers = handlers_by_name(
120+
self.name,
121+
sim_cpu=sim_cpu,
122+
sim_cpu_tdp=sim_cpu_tdp,
123+
sim_cpu_util=sim_cpu_util,
124+
sim_gpu=sim_gpu,
125+
sim_gpu_watts=sim_gpu_watts,
126+
sim_gpu_util=sim_gpu_util
127+
)
71128
for h in handlers:
72-
handler = h(pids=pids, devices_by_pid=devices_by_pid)
129+
handler = h(pids=pids, devices_by_pid=devices_by_pid) if callable(h) else h(pids=pids, devices_by_pid=devices_by_pid)
73130
if handler.available():
74131
return handler
75132
return None
@@ -158,16 +215,47 @@ def shutdown(self):
158215

159216

160217
def create_components(
161-
components: str, pids: Iterable[int], devices_by_pid: bool, logger: Logger
218+
components: str,
219+
pids: Iterable[int],
220+
devices_by_pid: bool,
221+
logger: Logger,
222+
sim_cpu=None,
223+
sim_cpu_tdp=None,
224+
sim_cpu_util=None,
225+
sim_gpu=None,
226+
sim_gpu_watts=None,
227+
sim_gpu_util=None
162228
) -> List[Component]:
163229
components = components.strip().replace(" ", "").lower()
164230
if components == "all":
165231
return [
166-
Component(name=comp_name, pids=pids, devices_by_pid=devices_by_pid, logger=logger)
232+
Component(
233+
name=comp_name,
234+
pids=pids,
235+
devices_by_pid=devices_by_pid,
236+
logger=logger,
237+
sim_cpu=sim_cpu,
238+
sim_cpu_tdp=sim_cpu_tdp,
239+
sim_cpu_util=sim_cpu_util,
240+
sim_gpu=sim_gpu,
241+
sim_gpu_watts=sim_gpu_watts,
242+
sim_gpu_util=sim_gpu_util
243+
)
167244
for comp_name in component_names()
168245
]
169246
else:
170247
return [
171-
Component(name=comp_name, pids=pids, devices_by_pid=devices_by_pid, logger=logger)
248+
Component(
249+
name=comp_name,
250+
pids=pids,
251+
devices_by_pid=devices_by_pid,
252+
logger=logger,
253+
sim_cpu=sim_cpu,
254+
sim_cpu_tdp=sim_cpu_tdp,
255+
sim_cpu_util=sim_cpu_util,
256+
sim_gpu=sim_gpu,
257+
sim_gpu_watts=sim_gpu_watts,
258+
sim_gpu_util=sim_gpu_util
259+
)
172260
for comp_name in components.split(",")
173261
]

carbontracker/components/cpu/generic.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,10 @@ def init(self):
6868

6969
if self.tdp is None:
7070
self.tdp = self.average_tdp
71-
logger.err_warn(f"No matching TDP found for CPU: {self.cpu_brand}. Using average TDP of {self.tdp:.2f}W as fallback.")
71+
logger.err_warn(f"No matching TDP found for CPU: {self.cpu_brand}. Using average TDP of {self.tdp:.2f}W at 50% utilization as fallback.")
7272
else:
7373
self.tdp = self.tdp / 2 # 50% utilization
74-
logger.err_info(f"Using TDP of {self.tdp:.2f}W for {self.cpu_brand}")
74+
logger.err_info(f"Using TDP of {self.tdp:.2f}W for {self.cpu_brand} at 50% utilization")
7575

7676
def find_matching_tdp(self) -> Optional[float]:
7777
# Try direct match
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from carbontracker.components.handler import Handler
2+
from typing import List
3+
4+
class SimulatedCPUHandler(Handler):
5+
def __init__(self, name: str, tdp: float, utilization: float = 0.5):
6+
super().__init__(pids=[], devices_by_pid=False)
7+
if not isinstance(name, str) or not name.strip():
8+
raise ValueError("CPU name must be a non-empty string.")
9+
if tdp is None or not isinstance(tdp, (int, float)) or tdp <= 0:
10+
raise ValueError("CPU TDP must be a positive number.")
11+
if not isinstance(utilization, (int, float)) or not (0.0 <= utilization <= 1.0):
12+
raise ValueError("CPU utilization must be between 0.0 and 1.0.")
13+
self.cpu_brand = name
14+
self.utilization = utilization
15+
self.tdp = tdp * utilization
16+
17+
def devices(self) -> List[str]:
18+
return [self.cpu_brand]
19+
20+
def available(self) -> bool:
21+
return True
22+
23+
def power_usage(self) -> List[float]:
24+
return [self.tdp]
25+
26+
def init(self):
27+
print(f"Using simulated CPU: {self.cpu_brand} with TDP: {self.tdp:.2f}W (at {self.utilization*100:.0f}% utilization)")
28+
29+
def shutdown(self):
30+
pass
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from carbontracker.components.handler import Handler
2+
from typing import List
3+
4+
class SimulatedGPUHandler(Handler):
5+
def __init__(self, name: str, watts: float, utilization: float = 0.5):
6+
super().__init__(pids=[], devices_by_pid=False)
7+
if not isinstance(name, str) or not name.strip():
8+
raise ValueError("GPU name must be a non-empty string.")
9+
if watts is None or not isinstance(watts, (int, float)) or watts <= 0:
10+
raise ValueError("GPU watts must be a positive number.")
11+
if not isinstance(utilization, (int, float)) or not (0.0 <= utilization <= 1.0):
12+
raise ValueError("GPU utilization must be between 0.0 and 1.0.")
13+
self.gpu_brand = name
14+
self.utilization = utilization
15+
self.watts = watts * utilization
16+
17+
def devices(self) -> List[str]:
18+
return [self.gpu_brand]
19+
20+
def available(self) -> bool:
21+
return True
22+
23+
def power_usage(self) -> List[float]:
24+
return [self.watts]
25+
26+
def init(self):
27+
print(f"Using simulated GPU: {self.gpu_brand} with power consumption: {self.watts:.2f}W (at {self.utilization*100:.0f}% utilization)")
28+
29+
def shutdown(self):
30+
pass

0 commit comments

Comments
 (0)