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.
@@ -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
+ [![CI](https://github.com/NREL/WattAMeter/actions/workflows/ci.yml/badge.svg)](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
+ [![CI](https://github.com/NREL/WattAMeter/actions/workflows/ci.yml/badge.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """wattameter package"""
2
+
3
+ __all__ = ["power_tracker", "cli", "utils", "PowerTracker"]
4
+
5
+ from . import power_tracker, cli, utils
6
+ from .power_tracker import PowerTracker
@@ -0,0 +1,2 @@
1
+ __all__ = ["main"]
2
+ from . import main
@@ -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,2 @@
1
+ __all__ = ["codecarbon"]
2
+ from wattameter.utils import codecarbon
@@ -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
+ [![CI](https://github.com/NREL/WattAMeter/actions/workflows/ci.yml/badge.svg)](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,4 @@
1
+ [console_scripts]
2
+ wattameter = wattameter.cli.main:main
3
+ wattameter_emissions_filename = wattameter.cli.main:print_emissions_filename
4
+ wattameter_powerlog_filename = wattameter.cli.main:print_powerlog_filename
@@ -0,0 +1 @@
1
+ codecarbon>=3.0.0
@@ -0,0 +1 @@
1
+ wattameter