wattameter 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wattameter-0.5.0/LICENSE +26 -0
- wattameter-0.5.0/MANIFEST.in +1 -0
- wattameter-0.5.0/PKG-INFO +127 -0
- wattameter-0.5.0/README.md +115 -0
- wattameter-0.5.0/pyproject.toml +41 -0
- wattameter-0.5.0/setup.cfg +4 -0
- wattameter-0.5.0/src/wattameter/__init__.py +6 -0
- wattameter-0.5.0/src/wattameter/cli/__init__.py +2 -0
- wattameter-0.5.0/src/wattameter/cli/main.py +163 -0
- wattameter-0.5.0/src/wattameter/power_tracker.py +161 -0
- wattameter-0.5.0/src/wattameter/utils/__init__.py +2 -0
- wattameter-0.5.0/src/wattameter/utils/codecarbon.py +56 -0
- wattameter-0.5.0/src/wattameter/utils/wattameter.sh +102 -0
- wattameter-0.5.0/src/wattameter/utils/wattawait.sh +43 -0
- wattameter-0.5.0/src/wattameter.egg-info/PKG-INFO +127 -0
- wattameter-0.5.0/src/wattameter.egg-info/SOURCES.txt +18 -0
- wattameter-0.5.0/src/wattameter.egg-info/dependency_links.txt +1 -0
- wattameter-0.5.0/src/wattameter.egg-info/entry_points.txt +4 -0
- wattameter-0.5.0/src/wattameter.egg-info/requires.txt +1 -0
- wattameter-0.5.0/src/wattameter.egg-info/top_level.txt +1 -0
wattameter-0.5.0/LICENSE
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Copyright (c) 2025, Alliance for Sustainable Energy, LLC. All rights reserved.
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without
|
|
4
|
+
modification, are permitted provided that the following conditions are met:
|
|
5
|
+
|
|
6
|
+
- Redistributions of source code must retain the above copyright notice, this
|
|
7
|
+
list of conditions and the following disclaimer.
|
|
8
|
+
|
|
9
|
+
- Redistributions in binary form must reproduce the above copyright notice,
|
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
|
11
|
+
and/or other materials provided with the distribution.
|
|
12
|
+
|
|
13
|
+
- Neither the name of the copyright holder nor the names of its
|
|
14
|
+
contributors may be used to endorse or promote products derived from
|
|
15
|
+
this software without specific prior written permission.
|
|
16
|
+
|
|
17
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
18
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
19
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
20
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
21
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
22
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
23
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
24
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
25
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
26
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include src/wattameter/utils/*.sh
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wattameter
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Default template for PDM package
|
|
5
|
+
Author-email: Weslley da Silva Pereira <wdasilv@nrel.gov>
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: codecarbon>=3.0.0
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
[](https://github.com/NREL/WattAMeter/actions/workflows/ci.yml)
|
|
14
|
+
|
|
15
|
+
# WattAMeter
|
|
16
|
+
|
|
17
|
+
**wattameter** is a Python package for monitoring and recording power consumption over time, enabling users to collect time series data on CPU, GPU, and RAM power usage. It also estimates energy consumption and CO₂ emissions.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- Track power usage for CPU, GPU, and RAM
|
|
22
|
+
- Periodically log power data to file
|
|
23
|
+
- Estimate energy consumption and CO₂ emissions
|
|
24
|
+
- Customizable logging and output options
|
|
25
|
+
- Command-line interface for easy usage
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
You can install **wattameter** via pip:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install wattameter
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### As a Python module
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from wattameter.power_tracker import PowerTracker
|
|
41
|
+
|
|
42
|
+
tracker = PowerTracker(
|
|
43
|
+
country_iso_code="USA",
|
|
44
|
+
region="colorado",
|
|
45
|
+
measure_power_secs=1,
|
|
46
|
+
api_call_interval=3600,
|
|
47
|
+
output_dir=".",
|
|
48
|
+
output_file="emissions_data.csv",
|
|
49
|
+
output_power_file="power_data.log",
|
|
50
|
+
)
|
|
51
|
+
tracker.start()
|
|
52
|
+
# ... your code ...
|
|
53
|
+
tracker.stop()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Command-line interface
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
wattameter \
|
|
60
|
+
--machine-id id \
|
|
61
|
+
--dt-read 1 \
|
|
62
|
+
--freq-write 3600 \
|
|
63
|
+
--log-level info \
|
|
64
|
+
--country USA \
|
|
65
|
+
--region colorado
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| Option | Short | Default | Description |
|
|
69
|
+
| ------------ | ----- | -------- | -------------------------------------------------------- |
|
|
70
|
+
| --machine-id | -i | None | Unique identifier for the machine (used in output files) |
|
|
71
|
+
| --dt-read | -t | 1 | Time interval (seconds) for reading power data |
|
|
72
|
+
| --freq-write | -f | 3600 | Frequency (reads) for writing power data to file |
|
|
73
|
+
| --log-level | -l | warning | Logging level: debug, info, warning, error, critical |
|
|
74
|
+
| --country | -c | USA | ISO code of the country for CO₂ emissions tracking |
|
|
75
|
+
| --region | -r | colorado | Region for CO₂ emissions tracking |
|
|
76
|
+
| --help | -h | | Show the help message and exit |
|
|
77
|
+
|
|
78
|
+
### Command-line interface with SLURM
|
|
79
|
+
|
|
80
|
+
For asynchronous usage with SLURM, we recommend using our [wattameter.sh](src/wattameter/utils/wattameter.sh) script as in [examples/slurm.sh](examples/slurm.sh), i.e.,
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
#SBATCH --signal=USR1@0 # Send USR1 signal at the end of the job to stop wattameter
|
|
84
|
+
#SBATCH --mem-per-cpu=1G # Should be set according to your needs
|
|
85
|
+
|
|
86
|
+
# Load Python environment with wattameter installed...
|
|
87
|
+
|
|
88
|
+
# Get the path of the wattameter script
|
|
89
|
+
WATTAPATH=$(python -c 'import wattameter; import os; print(os.path.dirname(wattameter.__file__))')
|
|
90
|
+
WATTASCRIPT="${WATTAPATH}/utils/wattameter.sh"
|
|
91
|
+
WATTAWAIT="${WATTAPATH}/utils/wattawait.sh"
|
|
92
|
+
|
|
93
|
+
# Run wattameter on all nodes
|
|
94
|
+
srun --overlap --wait=0 --nodes=$SLURM_JOB_NUM_NODES --ntasks-per-node=1 "${WATTAWAIT}" &
|
|
95
|
+
WAIT_PID=$!
|
|
96
|
+
srun --overlap --wait=0 --output=slurm-$SLURM_JOB_ID-wattameter.txt --nodes=$SLURM_JOB_NUM_NODES --ntasks-per-node=1 "${WATTASCRIPT}" -i $SLURM_JOB_ID \
|
|
97
|
+
[-t dt_read] \
|
|
98
|
+
[-f freq_write] \
|
|
99
|
+
[-l log_level] \
|
|
100
|
+
[-c country] \
|
|
101
|
+
[-r region] \
|
|
102
|
+
&
|
|
103
|
+
wait $WAIT_PID
|
|
104
|
+
|
|
105
|
+
# Run your script here...
|
|
106
|
+
|
|
107
|
+
# Cancel the job to stop wattameter
|
|
108
|
+
scancel $SLURM_JOB_ID
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Almost all options are the same as the regular command-line interface. The exception is that the `--machine-id` option is replaced with `--run-id`, which is used to identify the SLURM job run. The script will automatically handle the output file naming based on the provided run ID and node information.
|
|
112
|
+
|
|
113
|
+
## Contributing
|
|
114
|
+
|
|
115
|
+
Contributions are welcome! Please open issues or submit pull requests.
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
This project is planned to be licensed under the BSD-3-Clause License.
|
|
120
|
+
|
|
121
|
+
## Acknowledgements
|
|
122
|
+
|
|
123
|
+
- [CodeCarbon](https://github.com/mlco2/codecarbon) for emissions tracking and wrappers to energy tracking tools.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
_NREL Software Record number: SWR-25-101_
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
[](https://github.com/NREL/WattAMeter/actions/workflows/ci.yml)
|
|
2
|
+
|
|
3
|
+
# WattAMeter
|
|
4
|
+
|
|
5
|
+
**wattameter** is a Python package for monitoring and recording power consumption over time, enabling users to collect time series data on CPU, GPU, and RAM power usage. It also estimates energy consumption and CO₂ emissions.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Track power usage for CPU, GPU, and RAM
|
|
10
|
+
- Periodically log power data to file
|
|
11
|
+
- Estimate energy consumption and CO₂ emissions
|
|
12
|
+
- Customizable logging and output options
|
|
13
|
+
- Command-line interface for easy usage
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
You can install **wattameter** via pip:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install wattameter
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### As a Python module
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from wattameter.power_tracker import PowerTracker
|
|
29
|
+
|
|
30
|
+
tracker = PowerTracker(
|
|
31
|
+
country_iso_code="USA",
|
|
32
|
+
region="colorado",
|
|
33
|
+
measure_power_secs=1,
|
|
34
|
+
api_call_interval=3600,
|
|
35
|
+
output_dir=".",
|
|
36
|
+
output_file="emissions_data.csv",
|
|
37
|
+
output_power_file="power_data.log",
|
|
38
|
+
)
|
|
39
|
+
tracker.start()
|
|
40
|
+
# ... your code ...
|
|
41
|
+
tracker.stop()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Command-line interface
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
wattameter \
|
|
48
|
+
--machine-id id \
|
|
49
|
+
--dt-read 1 \
|
|
50
|
+
--freq-write 3600 \
|
|
51
|
+
--log-level info \
|
|
52
|
+
--country USA \
|
|
53
|
+
--region colorado
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| Option | Short | Default | Description |
|
|
57
|
+
| ------------ | ----- | -------- | -------------------------------------------------------- |
|
|
58
|
+
| --machine-id | -i | None | Unique identifier for the machine (used in output files) |
|
|
59
|
+
| --dt-read | -t | 1 | Time interval (seconds) for reading power data |
|
|
60
|
+
| --freq-write | -f | 3600 | Frequency (reads) for writing power data to file |
|
|
61
|
+
| --log-level | -l | warning | Logging level: debug, info, warning, error, critical |
|
|
62
|
+
| --country | -c | USA | ISO code of the country for CO₂ emissions tracking |
|
|
63
|
+
| --region | -r | colorado | Region for CO₂ emissions tracking |
|
|
64
|
+
| --help | -h | | Show the help message and exit |
|
|
65
|
+
|
|
66
|
+
### Command-line interface with SLURM
|
|
67
|
+
|
|
68
|
+
For asynchronous usage with SLURM, we recommend using our [wattameter.sh](src/wattameter/utils/wattameter.sh) script as in [examples/slurm.sh](examples/slurm.sh), i.e.,
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
#SBATCH --signal=USR1@0 # Send USR1 signal at the end of the job to stop wattameter
|
|
72
|
+
#SBATCH --mem-per-cpu=1G # Should be set according to your needs
|
|
73
|
+
|
|
74
|
+
# Load Python environment with wattameter installed...
|
|
75
|
+
|
|
76
|
+
# Get the path of the wattameter script
|
|
77
|
+
WATTAPATH=$(python -c 'import wattameter; import os; print(os.path.dirname(wattameter.__file__))')
|
|
78
|
+
WATTASCRIPT="${WATTAPATH}/utils/wattameter.sh"
|
|
79
|
+
WATTAWAIT="${WATTAPATH}/utils/wattawait.sh"
|
|
80
|
+
|
|
81
|
+
# Run wattameter on all nodes
|
|
82
|
+
srun --overlap --wait=0 --nodes=$SLURM_JOB_NUM_NODES --ntasks-per-node=1 "${WATTAWAIT}" &
|
|
83
|
+
WAIT_PID=$!
|
|
84
|
+
srun --overlap --wait=0 --output=slurm-$SLURM_JOB_ID-wattameter.txt --nodes=$SLURM_JOB_NUM_NODES --ntasks-per-node=1 "${WATTASCRIPT}" -i $SLURM_JOB_ID \
|
|
85
|
+
[-t dt_read] \
|
|
86
|
+
[-f freq_write] \
|
|
87
|
+
[-l log_level] \
|
|
88
|
+
[-c country] \
|
|
89
|
+
[-r region] \
|
|
90
|
+
&
|
|
91
|
+
wait $WAIT_PID
|
|
92
|
+
|
|
93
|
+
# Run your script here...
|
|
94
|
+
|
|
95
|
+
# Cancel the job to stop wattameter
|
|
96
|
+
scancel $SLURM_JOB_ID
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Almost all options are the same as the regular command-line interface. The exception is that the `--machine-id` option is replaced with `--run-id`, which is used to identify the SLURM job run. The script will automatically handle the output file naming based on the provided run ID and node information.
|
|
100
|
+
|
|
101
|
+
## Contributing
|
|
102
|
+
|
|
103
|
+
Contributions are welcome! Please open issues or submit pull requests.
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
This project is planned to be licensed under the BSD-3-Clause License.
|
|
108
|
+
|
|
109
|
+
## Acknowledgements
|
|
110
|
+
|
|
111
|
+
- [CodeCarbon](https://github.com/mlco2/codecarbon) for emissions tracking and wrappers to energy tracking tools.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
_NREL Software Record number: SWR-25-101_
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "wattameter"
|
|
3
|
+
version = "0.5.0"
|
|
4
|
+
description = "Default template for PDM package"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Weslley da Silva Pereira", email = "wdasilv@nrel.gov"},
|
|
7
|
+
]
|
|
8
|
+
dependencies = [
|
|
9
|
+
"codecarbon>=3.0.0",
|
|
10
|
+
]
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = {text = "BSD-3-Clause"}
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
wattameter = "wattameter.cli.main:main"
|
|
17
|
+
wattameter_powerlog_filename = "wattameter.cli.main:print_powerlog_filename"
|
|
18
|
+
wattameter_emissions_filename = "wattameter.cli.main:print_emissions_filename"
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
example = [
|
|
22
|
+
"jupyter",
|
|
23
|
+
"matplotlib",
|
|
24
|
+
"py-cpuinfo",
|
|
25
|
+
"nvidia-ml-py",
|
|
26
|
+
]
|
|
27
|
+
test = [
|
|
28
|
+
"pytest",
|
|
29
|
+
"pytest-asyncio",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[tool.pdm]
|
|
33
|
+
distribution = true
|
|
34
|
+
package-dir = "src"
|
|
35
|
+
includes = ["src/wattameter/utils/*.sh"]
|
|
36
|
+
|
|
37
|
+
[tool.pdm.scripts]
|
|
38
|
+
devwattameter = { call = "wattameter.cli.main:main" }
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
# SPDX-FileCopyrightText: 2025, Alliance for Sustainable Energy, LLC
|
|
4
|
+
|
|
5
|
+
from ..power_tracker import PowerTracker
|
|
6
|
+
|
|
7
|
+
import signal
|
|
8
|
+
import time
|
|
9
|
+
import logging
|
|
10
|
+
import argparse
|
|
11
|
+
import threading
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ForcedExit(BaseException):
|
|
15
|
+
"""Exception raised for forced exit signals."""
|
|
16
|
+
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
signal_handled = threading.Event()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def handle_signal(signum, frame):
|
|
24
|
+
"""Handle termination signals."""
|
|
25
|
+
if signal_handled.is_set(): # Thread-safe read
|
|
26
|
+
return # Ignore further signals
|
|
27
|
+
signal_handled.set() # Thread-safe write
|
|
28
|
+
signame = signal.Signals(signum).name
|
|
29
|
+
raise ForcedExit(f"Signal handler called with signal {signame} ({signum})")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _suffix(machine_id=None):
|
|
33
|
+
"""Generate a suffix based on the machine ID."""
|
|
34
|
+
if machine_id is not None:
|
|
35
|
+
return f"_{machine_id}"
|
|
36
|
+
|
|
37
|
+
# If no machine ID is provided, try to parse it from command line arguments
|
|
38
|
+
parser = argparse.ArgumentParser(exit_on_error=False)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--machine-id",
|
|
41
|
+
"-i",
|
|
42
|
+
type=str,
|
|
43
|
+
default=None,
|
|
44
|
+
help="Unique identifier for the machine used as suffix in the output files.",
|
|
45
|
+
)
|
|
46
|
+
machine_id = parser.parse_known_args()[0].machine_id
|
|
47
|
+
|
|
48
|
+
return "" if machine_id is None else f"_{machine_id}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def powerlog_filename(machine_id=None):
|
|
52
|
+
"""Generate a log filename based on the machine ID."""
|
|
53
|
+
suffix = _suffix(machine_id)
|
|
54
|
+
return f"wattameter{suffix}.log"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def print_powerlog_filename(machine_id=None):
|
|
58
|
+
"""Print the power log filename based on the machine ID."""
|
|
59
|
+
print(powerlog_filename(machine_id))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def emissions_filename(machine_id=None):
|
|
63
|
+
"""Generate an emissions filename based on the machine ID."""
|
|
64
|
+
suffix = _suffix(machine_id)
|
|
65
|
+
return f"wattameter_emmisions{suffix}.csv"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def print_emissions_filename(machine_id=None):
|
|
69
|
+
"""Print the emissions filename based on the machine ID."""
|
|
70
|
+
print(emissions_filename(machine_id))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main():
|
|
74
|
+
# Register the signals to handle forced exit
|
|
75
|
+
for sig in (signal.SIGTERM, signal.SIGINT, signal.SIGUSR1, signal.SIGHUP):
|
|
76
|
+
signal.signal(sig, handle_signal)
|
|
77
|
+
|
|
78
|
+
# Parse command line arguments
|
|
79
|
+
parser = argparse.ArgumentParser(
|
|
80
|
+
description="Track power over time, energy and CO2 emissions from your computer."
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--machine-id",
|
|
84
|
+
"-i",
|
|
85
|
+
type=str,
|
|
86
|
+
default=None,
|
|
87
|
+
help="Unique identifier for the machine used as suffix in the output files.",
|
|
88
|
+
)
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
"--dt-read",
|
|
91
|
+
"-t",
|
|
92
|
+
type=float,
|
|
93
|
+
default=1,
|
|
94
|
+
help="Time interval in seconds for reading power data (default: 1 second).",
|
|
95
|
+
)
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"--freq-write",
|
|
98
|
+
"-f",
|
|
99
|
+
type=float,
|
|
100
|
+
default=3600,
|
|
101
|
+
help="Frequence for writing power data to file (default: every 3600 reads).",
|
|
102
|
+
)
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"--log-level",
|
|
105
|
+
"-l",
|
|
106
|
+
choices=["debug", "info", "warning", "error", "critical"],
|
|
107
|
+
default="warning",
|
|
108
|
+
help="Set the logging level (default: warning).",
|
|
109
|
+
)
|
|
110
|
+
parser.add_argument(
|
|
111
|
+
"--country",
|
|
112
|
+
"-c",
|
|
113
|
+
type=str,
|
|
114
|
+
default="USA",
|
|
115
|
+
help="ISO code of the country for CO2 emissions tracking (default: USA).",
|
|
116
|
+
)
|
|
117
|
+
parser.add_argument(
|
|
118
|
+
"--region",
|
|
119
|
+
"-r",
|
|
120
|
+
type=str,
|
|
121
|
+
default="colorado",
|
|
122
|
+
help="Region for CO2 emissions tracking (default: colorado).",
|
|
123
|
+
)
|
|
124
|
+
args = parser.parse_args()
|
|
125
|
+
|
|
126
|
+
# Set up logging
|
|
127
|
+
logging.basicConfig(level=args.log_level.upper())
|
|
128
|
+
|
|
129
|
+
# Initialize the tracker
|
|
130
|
+
tracker = PowerTracker(
|
|
131
|
+
# For CO2 emissions tracking
|
|
132
|
+
country_iso_code=args.country,
|
|
133
|
+
region=args.region,
|
|
134
|
+
# For power tracking
|
|
135
|
+
measure_power_secs=args.dt_read,
|
|
136
|
+
# For recording data
|
|
137
|
+
log_level=args.log_level.upper(),
|
|
138
|
+
# For saving power and energy data to file
|
|
139
|
+
api_call_interval=int(args.freq_write / args.dt_read),
|
|
140
|
+
output_dir=".",
|
|
141
|
+
output_file=emissions_filename(args.machine_id),
|
|
142
|
+
output_power_file=powerlog_filename(args.machine_id),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Start tracking power and energy consumption
|
|
146
|
+
t0 = time.time()
|
|
147
|
+
tracker.start()
|
|
148
|
+
|
|
149
|
+
# Repeat until interrupted
|
|
150
|
+
try:
|
|
151
|
+
logging.info("Tracking power...")
|
|
152
|
+
while True:
|
|
153
|
+
time.sleep(86400) # Sleep for a long time to keep the tracker running
|
|
154
|
+
except ForcedExit:
|
|
155
|
+
logging.info("Forced exit detected. Stopping tracker...")
|
|
156
|
+
finally:
|
|
157
|
+
tracker.stop()
|
|
158
|
+
t1 = time.time()
|
|
159
|
+
logging.info(f"Tracker stopped. Elapsed time: {t1 - t0:.2f} seconds.")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__":
|
|
163
|
+
main() # Call the main function to start the tracker
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
|
+
# SPDX-FileCopyrightText: 2025, Alliance for Sustainable Energy, LLC
|
|
3
|
+
|
|
4
|
+
from codecarbon import OfflineEmissionsTracker
|
|
5
|
+
from codecarbon.external.hardware import CPU, GPU
|
|
6
|
+
from codecarbon.external.ram import RAM
|
|
7
|
+
from codecarbon.core.util import count_physical_cpus
|
|
8
|
+
|
|
9
|
+
from collections import deque
|
|
10
|
+
from typing import Optional
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
import re
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PowerTracker(OfflineEmissionsTracker):
|
|
17
|
+
"""Power tracker based on the CodeCarbon offline emission tracker.
|
|
18
|
+
|
|
19
|
+
:param output_power_file: Optional path to the output file where power data
|
|
20
|
+
will be saved. If not provided, a default file named
|
|
21
|
+
`power_<run_id>.log` will be created.
|
|
22
|
+
:param time_fmt: Format for the time values in the output file.
|
|
23
|
+
:param power_fmt: Format for the power values in the output file.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
*args,
|
|
29
|
+
output_power_file: Optional[str] = None,
|
|
30
|
+
time_fmt: str = ".3e",
|
|
31
|
+
power_fmt: str = ".3e",
|
|
32
|
+
**kwargs,
|
|
33
|
+
):
|
|
34
|
+
super().__init__(*args, **kwargs)
|
|
35
|
+
|
|
36
|
+
# Create queues to store power data
|
|
37
|
+
self._time_series = deque([])
|
|
38
|
+
self._timestamp_series = deque([])
|
|
39
|
+
self._power_series = {key: [] for key in ("cpu", "gpu", "ram")}
|
|
40
|
+
count_cpu = 0
|
|
41
|
+
count_gpu = 0
|
|
42
|
+
count_ram = 0
|
|
43
|
+
for hw in self._hardware:
|
|
44
|
+
if isinstance(hw, CPU):
|
|
45
|
+
assert count_cpu < 1, "Only one CPU object is supported."
|
|
46
|
+
if hw._mode == "intel_rapl":
|
|
47
|
+
self._power_series["cpu"] = [
|
|
48
|
+
deque([]) for _ in range(count_physical_cpus())
|
|
49
|
+
]
|
|
50
|
+
else:
|
|
51
|
+
self._power_series["cpu"] = [deque([])]
|
|
52
|
+
count_cpu += 1
|
|
53
|
+
elif isinstance(hw, GPU):
|
|
54
|
+
assert count_gpu < 1, "Only one GPU object is supported."
|
|
55
|
+
self._power_series["gpu"] = [deque([]) for _ in range(hw.num_gpus)]
|
|
56
|
+
count_gpu += 1
|
|
57
|
+
elif isinstance(hw, RAM):
|
|
58
|
+
assert count_ram < 1, "Only one RAM object is supported."
|
|
59
|
+
self._power_series["ram"] = [deque([])]
|
|
60
|
+
count_ram += 1
|
|
61
|
+
|
|
62
|
+
# Initialize the last time written to file
|
|
63
|
+
if self._api_call_interval != -1:
|
|
64
|
+
self.output_power_file = (
|
|
65
|
+
f"power_{self.run_id}.log"
|
|
66
|
+
if output_power_file is None
|
|
67
|
+
else output_power_file
|
|
68
|
+
)
|
|
69
|
+
self.output_power_file = os.path.join(
|
|
70
|
+
self._output_dir, self.output_power_file
|
|
71
|
+
)
|
|
72
|
+
self._last_time_written = 0
|
|
73
|
+
|
|
74
|
+
# Get the format for time and power values
|
|
75
|
+
self.timestamp_fmt = "%Y-%m-%d_%H:%M:%S.%f"
|
|
76
|
+
self.time_fmt = time_fmt
|
|
77
|
+
self.power_fmt = power_fmt
|
|
78
|
+
|
|
79
|
+
# Write header to the power file
|
|
80
|
+
timestamp_str = datetime.now().strftime(self.timestamp_fmt)[:-3]
|
|
81
|
+
with open(self.output_power_file, "a", encoding="utf-8") as f:
|
|
82
|
+
f.write(f"# {timestamp_str} - Power data for run {self.run_id}\n")
|
|
83
|
+
|
|
84
|
+
def start(self) -> None:
|
|
85
|
+
super().start()
|
|
86
|
+
|
|
87
|
+
if self._api_call_interval != -1:
|
|
88
|
+
timestamp_str = datetime.now().strftime(self.timestamp_fmt)[:-3]
|
|
89
|
+
|
|
90
|
+
timestamp_len = len(timestamp_str)
|
|
91
|
+
time_len = len(f"{0:{self.time_fmt}}")
|
|
92
|
+
power_len = len(f"{0:{self.power_fmt}}")
|
|
93
|
+
|
|
94
|
+
with open(self.output_power_file, "a", encoding="utf-8") as f:
|
|
95
|
+
f.write(f"# {timestamp_str} - Tracking started\n")
|
|
96
|
+
f.write("# timestamp" + " " * (timestamp_len - 9))
|
|
97
|
+
f.write(" time (s)" + " " * (time_len - 8))
|
|
98
|
+
for i in range(len(self._power_series["cpu"])):
|
|
99
|
+
f.write(f"{' cpu ' + str(i):>{power_len - 8}} (W)")
|
|
100
|
+
for i in range(len(self._power_series["gpu"])):
|
|
101
|
+
f.write(f"{' gpu ' + str(i):>{power_len - 8}} (W)")
|
|
102
|
+
for i in range(len(self._power_series["ram"])):
|
|
103
|
+
f.write(f"{' ram ' + str(i):>{power_len - 8}} (W)")
|
|
104
|
+
f.write("\n")
|
|
105
|
+
|
|
106
|
+
def _measure_power_and_energy(self) -> None:
|
|
107
|
+
"""Specialization of the method from the parent class that keeps track
|
|
108
|
+
of the power for each component (CPU, GPU, RAM) and writes it to a file
|
|
109
|
+
periodically."""
|
|
110
|
+
# Measure power and energy consumption
|
|
111
|
+
# Save measure_occurrence before it is modified inside the method
|
|
112
|
+
measure_occurrence = self._measure_occurrence
|
|
113
|
+
super()._measure_power_and_energy()
|
|
114
|
+
timestamp_str = datetime.now().strftime(self.timestamp_fmt)[:-3]
|
|
115
|
+
measure_occurrence += 1
|
|
116
|
+
|
|
117
|
+
# Update power series
|
|
118
|
+
self._time_series.append(self._last_measured_time - self._start_time)
|
|
119
|
+
self._timestamp_series.append(timestamp_str)
|
|
120
|
+
for hw in self._hardware:
|
|
121
|
+
if isinstance(hw, CPU):
|
|
122
|
+
if hw._mode == "intel_rapl":
|
|
123
|
+
all_cpu_details = hw._intel_interface.get_static_cpu_details()
|
|
124
|
+
i = 0
|
|
125
|
+
for metric, value in all_cpu_details.items():
|
|
126
|
+
if re.match(r"^Processor Power", metric):
|
|
127
|
+
self._power_series["cpu"][i].append(value)
|
|
128
|
+
i += 1
|
|
129
|
+
else:
|
|
130
|
+
self._power_series["cpu"][0].append(self._cpu_power.W)
|
|
131
|
+
elif isinstance(hw, GPU):
|
|
132
|
+
for i in range(hw.num_gpus):
|
|
133
|
+
self._power_series["gpu"][i].append(hw.devices.devices[i].power.W)
|
|
134
|
+
elif isinstance(hw, RAM):
|
|
135
|
+
self._power_series["ram"][0].append(self._ram_power.W)
|
|
136
|
+
|
|
137
|
+
# Write power to file if needed
|
|
138
|
+
if (
|
|
139
|
+
self._api_call_interval != -1
|
|
140
|
+
and measure_occurrence >= self._api_call_interval
|
|
141
|
+
) or self._scheduler is None:
|
|
142
|
+
n = len(self._time_series)
|
|
143
|
+
if n == self._last_time_written:
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
buffer = ""
|
|
147
|
+
for i in range(self._last_time_written, n):
|
|
148
|
+
buffer += " " + self._timestamp_series[i]
|
|
149
|
+
buffer += f" {self._time_series[i]:{self.time_fmt}}"
|
|
150
|
+
for k in range(len(self._power_series["cpu"])):
|
|
151
|
+
buffer += f" {self._power_series['cpu'][k][i]:{self.power_fmt}}"
|
|
152
|
+
for k in range(len(self._power_series["gpu"])):
|
|
153
|
+
buffer += f" {self._power_series['gpu'][k][i]:{self.power_fmt}}"
|
|
154
|
+
for k in range(len(self._power_series["ram"])):
|
|
155
|
+
buffer += f" {self._power_series['ram'][k][i]:{self.power_fmt}}"
|
|
156
|
+
buffer += "\n"
|
|
157
|
+
|
|
158
|
+
with open(self.output_power_file, "a", encoding="utf-8") as f:
|
|
159
|
+
f.write(buffer)
|
|
160
|
+
|
|
161
|
+
self._last_time_written = n
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
|
+
# SPDX-FileCopyrightText: 2025, Alliance for Sustainable Energy, LLC
|
|
3
|
+
|
|
4
|
+
import csv
|
|
5
|
+
from importlib import resources
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def add_cpu(cpu_model: str, tdp: float) -> bool:
|
|
9
|
+
"""
|
|
10
|
+
Add a CPU model and its TDP (Thermal Design Power) to codecarbon's CPU power database.
|
|
11
|
+
|
|
12
|
+
This function adds a new CPU entry to codecarbon's hardware database CSV file,
|
|
13
|
+
which is used for power consumption calculations. The function checks if the
|
|
14
|
+
CPU model already exists in the database before adding it to avoid duplicates.
|
|
15
|
+
|
|
16
|
+
Example usage:
|
|
17
|
+
.. code-block:: python
|
|
18
|
+
>>> add_cpu("Intel Xeon Platinum 8470QL", 95.0)
|
|
19
|
+
True
|
|
20
|
+
>>> add_cpu("Intel Xeon Platinum 8470QL", 95.0) # Already exists
|
|
21
|
+
False
|
|
22
|
+
|
|
23
|
+
Note:
|
|
24
|
+
This function modifies the codecarbon package's internal CPU power database.
|
|
25
|
+
The changes will persist across sessions but may be overwritten when
|
|
26
|
+
codecarbon is updated.
|
|
27
|
+
|
|
28
|
+
:param cpu_model: The name/model of the CPU (e.g., "Intel Xeon Platinum 8470QL").
|
|
29
|
+
:param tdp: The Thermal Design Power of the CPU in watts.
|
|
30
|
+
|
|
31
|
+
:return: True if the CPU was successfully added to the database,
|
|
32
|
+
False if the CPU already existed in the database.
|
|
33
|
+
"""
|
|
34
|
+
# Get the path to the CSV file using importlib.resources
|
|
35
|
+
with resources.path(
|
|
36
|
+
"codecarbon.data.hardware", "cpu_power.csv"
|
|
37
|
+
) as cpu_power_csv_path:
|
|
38
|
+
# Check if the line already exists
|
|
39
|
+
line_exists = False
|
|
40
|
+
new_row = [cpu_model, str(tdp)]
|
|
41
|
+
with open(cpu_power_csv_path, "r", encoding="utf-8") as csvfile:
|
|
42
|
+
reader = csv.reader(csvfile)
|
|
43
|
+
for row in reader:
|
|
44
|
+
if row == new_row:
|
|
45
|
+
line_exists = True
|
|
46
|
+
break
|
|
47
|
+
|
|
48
|
+
# Only add the line if it doesn't already exist
|
|
49
|
+
if not line_exists:
|
|
50
|
+
with open(cpu_power_csv_path, "a", newline="", encoding="utf-8") as csvfile:
|
|
51
|
+
writer = csv.writer(csvfile)
|
|
52
|
+
writer.writerow(new_row)
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
not line_exists
|
|
56
|
+
) # Return True if the line was added, False if it already existed
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
# SPDX-FileCopyrightText: 2025, Alliance for Sustainable Energy, LLC
|
|
4
|
+
#
|
|
5
|
+
# This script is used to run the WattAMeter CLI tool for power tracking.
|
|
6
|
+
# It captures the output and PID of the process, allowing for graceful termination on timeout.
|
|
7
|
+
|
|
8
|
+
get_log_file_name() {
|
|
9
|
+
NODE=$(hostname)
|
|
10
|
+
if [ -z "${RUN_ID}" ]; then
|
|
11
|
+
echo "wattameter-${NODE}.txt"
|
|
12
|
+
else
|
|
13
|
+
echo "wattameter-${RUN_ID}-${NODE}.txt"
|
|
14
|
+
fi
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
main() {
|
|
18
|
+
# Default values
|
|
19
|
+
RUN_ID=""
|
|
20
|
+
DT_READ=1
|
|
21
|
+
FREQ_WRITE=3600
|
|
22
|
+
LOG_LEVEL="warning"
|
|
23
|
+
COUNTRY="USA"
|
|
24
|
+
REGION="colorado"
|
|
25
|
+
|
|
26
|
+
# Find the WattAMeter CLI tool
|
|
27
|
+
if ! command -v wattameter 2>&1 >/dev/null; then
|
|
28
|
+
SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
|
|
29
|
+
if [ -f "${SCRIPTPATH}/../cli/main.py" ]; then
|
|
30
|
+
WATTAMETER="python ${SCRIPTPATH}/../cli/main.py"
|
|
31
|
+
else
|
|
32
|
+
echo "WattAMeter CLI not found. Please ensure it is installed and available in the path."
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
else
|
|
36
|
+
WATTAMETER=$(command -v wattameter)
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Usage function to display help
|
|
40
|
+
usage() {
|
|
41
|
+
echo "Usage: $0 [-i run_id] [-t dt_read] [-f freq_write] [-l log_level] [-c country] [-r region]"
|
|
42
|
+
exit 1
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Parse command line options
|
|
46
|
+
while getopts ":i:t:f:l:c:r:" opt; do
|
|
47
|
+
case $opt in
|
|
48
|
+
i) RUN_ID="$OPTARG" ;;
|
|
49
|
+
t) DT_READ="$OPTARG" ;;
|
|
50
|
+
f) FREQ_WRITE="$OPTARG" ;;
|
|
51
|
+
l) LOG_LEVEL="$OPTARG" ;;
|
|
52
|
+
c) COUNTRY="$OPTARG" ;;
|
|
53
|
+
r) REGION="$OPTARG" ;;
|
|
54
|
+
\?) echo "Invalid option: -$OPTARG" >&2; usage ;;
|
|
55
|
+
:) echo "Option -$OPTARG requires an argument." >&2; usage ;;
|
|
56
|
+
esac
|
|
57
|
+
done
|
|
58
|
+
|
|
59
|
+
# Get the hostname of the current node
|
|
60
|
+
NODE=$(hostname)
|
|
61
|
+
|
|
62
|
+
# Use the log identifier name if provided
|
|
63
|
+
log_file=$(get_log_file_name)
|
|
64
|
+
echo "Logging execution on ${NODE} to ${log_file}"
|
|
65
|
+
|
|
66
|
+
# Kill other instances of CodeCarbon that might be running
|
|
67
|
+
find /tmp/ -name ".codecarbon.lock" 2>/dev/null | xargs rm -f
|
|
68
|
+
|
|
69
|
+
# Start the power series tracking and log the output
|
|
70
|
+
${WATTAMETER} \
|
|
71
|
+
--machine-id "${NODE}" \
|
|
72
|
+
--dt-read "${DT_READ}" \
|
|
73
|
+
--freq-write "${FREQ_WRITE}" \
|
|
74
|
+
--log-level "${LOG_LEVEL}" \
|
|
75
|
+
--country "${COUNTRY}" \
|
|
76
|
+
--region "${REGION}" > "${log_file}" 2>&1 &
|
|
77
|
+
WATTAMETER_PID=$!
|
|
78
|
+
|
|
79
|
+
# Gracefully terminates the tracking process on exit.
|
|
80
|
+
on_exit() {
|
|
81
|
+
if [ -n "$EXITING" ]; then
|
|
82
|
+
return
|
|
83
|
+
fi
|
|
84
|
+
EXITING=1
|
|
85
|
+
echo "WattAMeter interrupted on ${NODE}!"
|
|
86
|
+
kill -TERM "$WATTAMETER_PID" 2>/dev/null
|
|
87
|
+
wait "$WATTAMETER_PID" 2>/dev/null
|
|
88
|
+
while kill -0 "$WATTAMETER_PID" 2>/dev/null; do
|
|
89
|
+
sleep 1
|
|
90
|
+
done
|
|
91
|
+
echo "WattAMeter has been terminated on node ${NODE}."
|
|
92
|
+
}
|
|
93
|
+
trap on_exit INT TERM HUP USR1
|
|
94
|
+
trap 'echo "WattAMeter exiting on ${NODE}..."' EXIT
|
|
95
|
+
|
|
96
|
+
# Wait for the WattAMeter process to finish
|
|
97
|
+
wait "$WATTAMETER_PID"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
101
|
+
main "$@"
|
|
102
|
+
fi
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
# SPDX-FileCopyrightText: 2025, Alliance for Sustainable Energy, LLC
|
|
4
|
+
#
|
|
5
|
+
# This script is used to wait for the WattAMeter timer to start before proceeding.
|
|
6
|
+
|
|
7
|
+
# Record the timestamp of the script start
|
|
8
|
+
START_TIME=$(date +%s)
|
|
9
|
+
|
|
10
|
+
# Get the hostname of the current node
|
|
11
|
+
NODE=$(hostname)
|
|
12
|
+
|
|
13
|
+
# Check if an input filename was given
|
|
14
|
+
if [ $# -ge 1 ]; then
|
|
15
|
+
WATTAMETER_FILENAME="$1"
|
|
16
|
+
else
|
|
17
|
+
# Get the WattAMeter powerlog filename for the current node
|
|
18
|
+
WATTAMETER_FILENAME=$(wattameter_powerlog_filename --machine-id "${NODE}")
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Wait for the WattAMeter powerlog file to be created or updated
|
|
22
|
+
if [ -f "${WATTAMETER_FILENAME}" ]; then
|
|
23
|
+
# Check if the file was created after $START_TIME
|
|
24
|
+
FILE_CREATION_TIME=$(stat -c %W "${WATTAMETER_FILENAME}")
|
|
25
|
+
if [ "${FILE_CREATION_TIME}" -ge "${START_TIME}" ]; then
|
|
26
|
+
echo "${WATTAMETER_FILENAME} file created."
|
|
27
|
+
else
|
|
28
|
+
# Wait until the WattAMeter powerlog file is updated
|
|
29
|
+
LAST_MODIFIED="${START_TIME}"
|
|
30
|
+
while [ "${LAST_MODIFIED}" -le "${START_TIME}" ]; do
|
|
31
|
+
# Read the timestamp of the last modification
|
|
32
|
+
LAST_MODIFIED=$(stat -c %Y "${WATTAMETER_FILENAME}")
|
|
33
|
+
sleep 1 # Wait for 1 second before checking again
|
|
34
|
+
done
|
|
35
|
+
echo "${WATTAMETER_FILENAME} file updated."
|
|
36
|
+
fi
|
|
37
|
+
else
|
|
38
|
+
# Wait for the WattAMeter-PowerLog-Filename CLI tool to create the file
|
|
39
|
+
until [ -f "${WATTAMETER_FILENAME}" ]; do
|
|
40
|
+
sleep 1
|
|
41
|
+
done
|
|
42
|
+
echo "${WATTAMETER_FILENAME} file created."
|
|
43
|
+
fi
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wattameter
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Default template for PDM package
|
|
5
|
+
Author-email: Weslley da Silva Pereira <wdasilv@nrel.gov>
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: codecarbon>=3.0.0
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
[](https://github.com/NREL/WattAMeter/actions/workflows/ci.yml)
|
|
14
|
+
|
|
15
|
+
# WattAMeter
|
|
16
|
+
|
|
17
|
+
**wattameter** is a Python package for monitoring and recording power consumption over time, enabling users to collect time series data on CPU, GPU, and RAM power usage. It also estimates energy consumption and CO₂ emissions.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- Track power usage for CPU, GPU, and RAM
|
|
22
|
+
- Periodically log power data to file
|
|
23
|
+
- Estimate energy consumption and CO₂ emissions
|
|
24
|
+
- Customizable logging and output options
|
|
25
|
+
- Command-line interface for easy usage
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
You can install **wattameter** via pip:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install wattameter
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### As a Python module
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from wattameter.power_tracker import PowerTracker
|
|
41
|
+
|
|
42
|
+
tracker = PowerTracker(
|
|
43
|
+
country_iso_code="USA",
|
|
44
|
+
region="colorado",
|
|
45
|
+
measure_power_secs=1,
|
|
46
|
+
api_call_interval=3600,
|
|
47
|
+
output_dir=".",
|
|
48
|
+
output_file="emissions_data.csv",
|
|
49
|
+
output_power_file="power_data.log",
|
|
50
|
+
)
|
|
51
|
+
tracker.start()
|
|
52
|
+
# ... your code ...
|
|
53
|
+
tracker.stop()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Command-line interface
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
wattameter \
|
|
60
|
+
--machine-id id \
|
|
61
|
+
--dt-read 1 \
|
|
62
|
+
--freq-write 3600 \
|
|
63
|
+
--log-level info \
|
|
64
|
+
--country USA \
|
|
65
|
+
--region colorado
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| Option | Short | Default | Description |
|
|
69
|
+
| ------------ | ----- | -------- | -------------------------------------------------------- |
|
|
70
|
+
| --machine-id | -i | None | Unique identifier for the machine (used in output files) |
|
|
71
|
+
| --dt-read | -t | 1 | Time interval (seconds) for reading power data |
|
|
72
|
+
| --freq-write | -f | 3600 | Frequency (reads) for writing power data to file |
|
|
73
|
+
| --log-level | -l | warning | Logging level: debug, info, warning, error, critical |
|
|
74
|
+
| --country | -c | USA | ISO code of the country for CO₂ emissions tracking |
|
|
75
|
+
| --region | -r | colorado | Region for CO₂ emissions tracking |
|
|
76
|
+
| --help | -h | | Show the help message and exit |
|
|
77
|
+
|
|
78
|
+
### Command-line interface with SLURM
|
|
79
|
+
|
|
80
|
+
For asynchronous usage with SLURM, we recommend using our [wattameter.sh](src/wattameter/utils/wattameter.sh) script as in [examples/slurm.sh](examples/slurm.sh), i.e.,
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
#SBATCH --signal=USR1@0 # Send USR1 signal at the end of the job to stop wattameter
|
|
84
|
+
#SBATCH --mem-per-cpu=1G # Should be set according to your needs
|
|
85
|
+
|
|
86
|
+
# Load Python environment with wattameter installed...
|
|
87
|
+
|
|
88
|
+
# Get the path of the wattameter script
|
|
89
|
+
WATTAPATH=$(python -c 'import wattameter; import os; print(os.path.dirname(wattameter.__file__))')
|
|
90
|
+
WATTASCRIPT="${WATTAPATH}/utils/wattameter.sh"
|
|
91
|
+
WATTAWAIT="${WATTAPATH}/utils/wattawait.sh"
|
|
92
|
+
|
|
93
|
+
# Run wattameter on all nodes
|
|
94
|
+
srun --overlap --wait=0 --nodes=$SLURM_JOB_NUM_NODES --ntasks-per-node=1 "${WATTAWAIT}" &
|
|
95
|
+
WAIT_PID=$!
|
|
96
|
+
srun --overlap --wait=0 --output=slurm-$SLURM_JOB_ID-wattameter.txt --nodes=$SLURM_JOB_NUM_NODES --ntasks-per-node=1 "${WATTASCRIPT}" -i $SLURM_JOB_ID \
|
|
97
|
+
[-t dt_read] \
|
|
98
|
+
[-f freq_write] \
|
|
99
|
+
[-l log_level] \
|
|
100
|
+
[-c country] \
|
|
101
|
+
[-r region] \
|
|
102
|
+
&
|
|
103
|
+
wait $WAIT_PID
|
|
104
|
+
|
|
105
|
+
# Run your script here...
|
|
106
|
+
|
|
107
|
+
# Cancel the job to stop wattameter
|
|
108
|
+
scancel $SLURM_JOB_ID
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Almost all options are the same as the regular command-line interface. The exception is that the `--machine-id` option is replaced with `--run-id`, which is used to identify the SLURM job run. The script will automatically handle the output file naming based on the provided run ID and node information.
|
|
112
|
+
|
|
113
|
+
## Contributing
|
|
114
|
+
|
|
115
|
+
Contributions are welcome! Please open issues or submit pull requests.
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
This project is planned to be licensed under the BSD-3-Clause License.
|
|
120
|
+
|
|
121
|
+
## Acknowledgements
|
|
122
|
+
|
|
123
|
+
- [CodeCarbon](https://github.com/mlco2/codecarbon) for emissions tracking and wrappers to energy tracking tools.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
_NREL Software Record number: SWR-25-101_
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
src/wattameter/__init__.py
|
|
6
|
+
src/wattameter/power_tracker.py
|
|
7
|
+
src/wattameter.egg-info/PKG-INFO
|
|
8
|
+
src/wattameter.egg-info/SOURCES.txt
|
|
9
|
+
src/wattameter.egg-info/dependency_links.txt
|
|
10
|
+
src/wattameter.egg-info/entry_points.txt
|
|
11
|
+
src/wattameter.egg-info/requires.txt
|
|
12
|
+
src/wattameter.egg-info/top_level.txt
|
|
13
|
+
src/wattameter/cli/__init__.py
|
|
14
|
+
src/wattameter/cli/main.py
|
|
15
|
+
src/wattameter/utils/__init__.py
|
|
16
|
+
src/wattameter/utils/codecarbon.py
|
|
17
|
+
src/wattameter/utils/wattameter.sh
|
|
18
|
+
src/wattameter/utils/wattawait.sh
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
codecarbon>=3.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
wattameter
|