Skip to content

Commit b98e86b

Browse files
authored
Merge branch 'master' into powerletrics_pynvml_integratiion
2 parents 4b2fd9c + c02ffb9 commit b98e86b

File tree

7 files changed

+326
-5
lines changed

7 files changed

+326
-5
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
2+
# Hello World Fibonacci
3+
4+
A simple platform independent example that runs three different fibonacci implementations,
5+
and measures their power consumption, runtime, and memory usage using [EnergiBridge](https://github.com/tdurieux/EnergiBridge).
6+
7+
Note that admin permissions are needed to make use of EnergiBridge.
8+
9+
10+
## Running
11+
12+
From the root directory of the repo, run the following command:
13+
14+
```bash
15+
python experiment-runner/ examples/hello-world-fibonacci/RunnerConfig.py
16+
```
17+
18+
## Results
19+
20+
The results are generated in the `examples/hello-world-fibonacci/experiments` folder.
21+
22+
**!!! WARNING !!!**: COLUMNS IN THE `energibridge.csv` FILES CAN BE DIFFERENT ACROSS MACHINES.
23+
ADJUST THE DATAFRAME COLUMN NAMES ACCORDINGLY.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
from EventManager.Models.RunnerEvents import RunnerEvents
2+
from EventManager.EventSubscriptionController import EventSubscriptionController
3+
from ConfigValidator.Config.Models.RunTableModel import RunTableModel
4+
from ConfigValidator.Config.Models.FactorModel import FactorModel
5+
from ConfigValidator.Config.Models.RunnerContext import RunnerContext
6+
from ConfigValidator.Config.Models.OperationType import OperationType
7+
from ProgressManager.Output.OutputProcedure import OutputProcedure as output
8+
from Plugins.Profilers.EnergiBridge import EnergiBridge
9+
10+
from typing import Dict, List, Any, Optional
11+
from pathlib import Path
12+
from os.path import dirname, realpath
13+
14+
15+
class RunnerConfig:
16+
ROOT_DIR = Path(dirname(realpath(__file__)))
17+
18+
# ================================ USER SPECIFIC CONFIG ================================
19+
"""The name of the experiment."""
20+
name: str = "new_runner_experiment"
21+
22+
"""The path in which Experiment Runner will create a folder with the name `self.name`, in order to store the
23+
results from this experiment. (Path does not need to exist - it will be created if necessary.)
24+
Output path defaults to the config file's path, inside the folder 'experiments'"""
25+
results_output_path: Path = ROOT_DIR / 'experiments'
26+
27+
"""Experiment operation type. Unless you manually want to initiate each run, use `OperationType.AUTO`."""
28+
operation_type: OperationType = OperationType.AUTO
29+
30+
"""The time Experiment Runner will wait after a run completes.
31+
This can be essential to accommodate for cooldown periods on some systems."""
32+
time_between_runs_in_ms: int = 1000
33+
34+
# Dynamic configurations can be one-time satisfied here before the program takes the config as-is
35+
# e.g. Setting some variable based on some criteria
36+
def __init__(self):
37+
"""Executes immediately after program start, on config load"""
38+
39+
EventSubscriptionController.subscribe_to_multiple_events([
40+
(RunnerEvents.BEFORE_EXPERIMENT, self.before_experiment),
41+
(RunnerEvents.BEFORE_RUN , self.before_run ),
42+
(RunnerEvents.START_RUN , self.start_run ),
43+
(RunnerEvents.START_MEASUREMENT, self.start_measurement),
44+
(RunnerEvents.INTERACT , self.interact ),
45+
(RunnerEvents.STOP_MEASUREMENT , self.stop_measurement ),
46+
(RunnerEvents.STOP_RUN , self.stop_run ),
47+
(RunnerEvents.POPULATE_RUN_DATA, self.populate_run_data),
48+
(RunnerEvents.AFTER_EXPERIMENT , self.after_experiment )
49+
])
50+
self.run_table_model = None # Initialized later
51+
52+
output.console_log("Custom config loaded")
53+
54+
def create_run_table_model(self) -> RunTableModel:
55+
"""Create and return the run_table model here. A run_table is a List (rows) of tuples (columns),
56+
representing each run performed"""
57+
factor1 = FactorModel("fib_type", ['iter', 'mem', 'rec'])
58+
factor2 = FactorModel("problem_size", [10, 20, 30])
59+
self.run_table_model = RunTableModel(
60+
factors=[factor1, factor2],
61+
exclude_variations=[
62+
{factor2: [10]}, # all runs having treatment "10" will be excluded
63+
{factor1: ['iter'], factor2: [30]}, # all runs having the combination ("iter", 30) will be excluded
64+
],
65+
repetitions = 3,
66+
data_columns=["total_power (J)", "runtime (sec)", "avg_mem (bytes)"]
67+
)
68+
return self.run_table_model
69+
70+
def before_experiment(self) -> None:
71+
"""Perform any activity required before starting the experiment here
72+
Invoked only once during the lifetime of the program."""
73+
pass
74+
75+
def before_run(self) -> None:
76+
"""Perform any activity required before starting a run.
77+
No context is available here as the run is not yet active (BEFORE RUN)"""
78+
pass
79+
80+
def start_run(self, context: RunnerContext) -> None:
81+
"""Perform any activity required for starting the run here.
82+
For example, starting the target system to measure.
83+
Activities after starting the run should also be performed here."""
84+
pass
85+
86+
def start_measurement(self, context: RunnerContext) -> None:
87+
"""Perform any activity required for starting measurements."""
88+
fib_type = context.run_variation["fib_type"]
89+
problem_size = context.run_variation["problem_size"]
90+
91+
self.profiler = EnergiBridge(target_program=f"python examples/hello-world-fibonacci/fibonacci_{fib_type}.py {problem_size}",
92+
out_file=context.run_dir / "energibridge.csv")
93+
94+
self.profiler.start()
95+
96+
def interact(self, context: RunnerContext) -> None:
97+
"""Perform any interaction with the running target system here, or block here until the target finishes."""
98+
pass
99+
100+
def stop_measurement(self, context: RunnerContext) -> None:
101+
"""Perform any activity here required for stopping measurements."""
102+
stdout = self.profiler.stop(wait=True)
103+
104+
def stop_run(self, context: RunnerContext) -> None:
105+
"""Perform any activity here required for stopping the run.
106+
Activities after stopping the run should also be performed here."""
107+
pass
108+
109+
def populate_run_data(self, context: RunnerContext) -> Optional[Dict[str, Any]]:
110+
"""Parse and process any measurement data here.
111+
You can also store the raw measurement data under `context.run_dir`
112+
Returns a dictionary with keys `self.run_table_model.data_columns` and their values populated"""
113+
114+
eb_log, eb_summary = self.profiler.parse_log(self.profiler.logfile,
115+
self.profiler.summary_logfile)
116+
117+
return {"total_power (J)": eb_summary["total_joules"],
118+
"runtime (sec)": eb_summary["runtime_seconds"],
119+
"total_mem (bytes)": list(eb_log["TOTAL_MEMORY"].values())[-1]}
120+
121+
def after_experiment(self) -> None:
122+
"""Perform any activity required after stopping the experiment here
123+
Invoked only once during the lifetime of the program."""
124+
pass
125+
126+
# ================================ DO NOT ALTER BELOW THIS LINE ================================
127+
experiment_path: Path = None
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Implementation by Mandy Wong (https://realpython.com/fibonacci-sequence-python/)
2+
import sys
3+
4+
def fib(n):
5+
a, b = 0, 1
6+
for i in range(0, n):
7+
a, b = b, a + b
8+
return a
9+
10+
for n in range(int(sys.argv[1])):
11+
print(fib(n))
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Implementation by Mandy Wong (https://realpython.com/fibonacci-sequence-python/)
2+
import sys
3+
4+
cache = {0: 0, 1: 1}
5+
6+
def fib(n):
7+
if n in cache: # Base case
8+
return cache[n]
9+
# Compute and cache the Fibonacci number
10+
cache[n] = fib(n - 1) + fib(n - 2) # Recursive case
11+
return cache[n]
12+
13+
for n in range(int(sys.argv[1])):
14+
print(fib(n))
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Implementation by Mandy Wong (https://realpython.com/fibonacci-sequence-python/)
2+
import sys
3+
4+
def fib(n):
5+
if n in {0, 1}: # Base case
6+
return n
7+
return fib(n - 1) + fib(n - 2) # Recursive case
8+
9+
for n in range(int(sys.argv[1])):
10+
print(fib(n))

experiment-runner/Plugins/Profilers/DataSource.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import ctypes
1010
from enum import StrEnum
1111
import shutil
12+
import ctypes
13+
import os
1214
import subprocess
1315
import threading
1416
import queue
@@ -86,6 +88,12 @@ def is_admin(self):
8688
except:
8789
return ctypes.windll.shell32.IsUserAdmin() == 1
8890

91+
def is_admin(self):
92+
try:
93+
return os.getuid() == 0
94+
except:
95+
return ctypes.windll.shell32.IsUserAdmin() == 1
96+
8997
@property
9098
@abstractmethod
9199
def supported_platforms(self) -> list[str]:
@@ -110,11 +118,15 @@ def stop(self):
110118

111119
@staticmethod
112120
@abstractmethod
113-
def parse_log():
121+
def parse_log(logfile):
114122
pass
115123

116124
class CLISource(DataSource):
117125
def __init__(self):
126+
super().__init__()
127+
128+
self.requires_admin = False
129+
118130
self.process = None
119131
self.args = None
120132
self._logfile = ValueRef(None)
@@ -141,7 +153,8 @@ def parameters(self) -> ParameterDict:
141153
def _validate_platform(self):
142154
super()._validate_platform()
143155

144-
if shutil.which(self.source_name) is None:
156+
if shutil.which(self.source_name) is None \
157+
and not os.access(self.source_name, os.X_OK):
145158
raise RuntimeError(f"The {self.source_name} cli tool is required for this plugin")
146159

147160
def _validate_start(self):
@@ -179,7 +192,11 @@ def _validate_parameters(self, parameters: dict):
179192

180193
def _format_cmd(self):
181194
self._validate_parameters(self.args)
195+
182196
cmd = self.source_name
197+
198+
if self.requires_admin:
199+
cmd = f"sudo {cmd}"
183200

184201
# Transform the parameter dict into string format to be parsed by shlex
185202
for p, v in self.args.items():
@@ -219,13 +236,15 @@ def start(self):
219236

220237
self._validate_start()
221238

222-
def stop(self):
239+
def stop(self, wait=False):
223240
if not self.process:
224241
return
225242

226243
try:
227-
self.process.terminate()
228-
stdout, stderr = self.process.communicate(timeout=5)
244+
if not wait:
245+
self.process.terminate()
246+
247+
stdout, stderr = self.process.communicate(timeout=None if wait else 5)
229248

230249
except Exception as e:
231250
self.process.kill()
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
from pathlib import Path
2+
import pandas as pd
3+
import re
4+
from Plugins.Profilers.DataSource import CLISource, ParameterDict
5+
6+
# Supported Paramters for the PowerJoular metrics plugin
7+
ENERGIBRIDGE_PARAMETERS = {
8+
("-o","--output"): Path,
9+
("-s","--separator"): str,
10+
("-c","--output-command"): str,
11+
("-i","--interval"): int,
12+
("-m","--max-execution"): int,
13+
("-g","--gpu"): None,
14+
("--summary",): None
15+
}
16+
17+
class EnergiBridge(CLISource):
18+
parameters = ParameterDict(ENERGIBRIDGE_PARAMETERS)
19+
source_name = "energibridge"
20+
supported_platforms = ["Linux", "Darwin", "Windows"]
21+
22+
"""An integration of PowerJoular into experiment-runner as a data source plugin"""
23+
def __init__(self,
24+
sample_frequency: int = 200,
25+
out_file: Path = "energibridge.csv",
26+
summary: bool = True,
27+
target_program: str = "sleep 1000000",
28+
additional_args: dict = {}):
29+
30+
super().__init__()
31+
32+
self.requires_admin = True
33+
self.target_program = target_program
34+
self.logfile = out_file
35+
self.args = {
36+
"-o": Path(self.logfile),
37+
"-i": sample_frequency,
38+
}
39+
40+
if summary:
41+
self.update_parameters(add={"--summary": None})
42+
43+
self.update_parameters(add=additional_args)
44+
45+
@property
46+
def summary(self):
47+
return "--summary" in self.args.keys()
48+
49+
@property
50+
def summary_logfile(self):
51+
if not self.logfile \
52+
or not any(map(lambda x: x in self.args.keys(), ["-o", "--output"])):
53+
54+
return None
55+
56+
return self.logfile.parent / Path(self.logfile.name.split(".")[0] + "-summary.txt")
57+
58+
def _stat_delta(self, data, stat):
59+
return list(data[stat].values())[-1] - list(data[stat].values())[0]
60+
61+
# Less accurate than the summary from EB, but better than nothing
62+
# TODO: EnergiBridge calculates this differently in a system dependent way,
63+
# this approximates using available data
64+
def generate_summary(self):
65+
log_data = self.parse_log(self.logfile)
66+
67+
elapsed_time = self._stat_delta(log_data, "Time") / 1000
68+
total_joules = self._stat_delta(log_data, "PACKAGE_ENERGY (J)")
69+
70+
return f"Energy consumption in joules: {total_joules} for {elapsed_time} sec of execution"
71+
72+
# We also want to save the summary of EnergiBridge if present
73+
def stop(self, wait=False):
74+
75+
stdout = super().stop(wait)
76+
77+
if self.summary and self.summary_logfile:
78+
with open(self.summary_logfile, "w") as f:
79+
# The last line is the summary, if present
80+
last_line = stdout.splitlines()[-1]
81+
82+
# If runtime was too short, energibridge doesnt provide a summary
83+
# Approximate this instead
84+
if not last_line.startswith("Energy consumption"):
85+
last_line = self.generate_summary()
86+
87+
f.write(last_line)
88+
89+
return stdout
90+
91+
def _format_cmd(self):
92+
cmd = super()._format_cmd()
93+
94+
return cmd + f" -- {self.target_program}"
95+
96+
@staticmethod
97+
def parse_log(logfile: Path, summary_logfile: Path|None=None):
98+
# Things are already in csv format here, no checks needed
99+
log_data = pd.read_csv(logfile).to_dict()
100+
101+
if not summary_logfile:
102+
return log_data
103+
104+
with open(summary_logfile, "r") as f:
105+
summary_data = f.read()
106+
107+
# Extract the floats from the string, we expect always positive X.X
108+
values = re.findall("[0-9]+[.]?[0-9]*", summary_data)
109+
110+
if len(values) == 2:
111+
summary_data = {
112+
"total_joules": float(values[0]),
113+
"runtime_seconds": float(values[1])
114+
}
115+
116+
return (log_data, summary_data)
117+

0 commit comments

Comments
 (0)