Skip to content

Commit da97d00

Browse files
luarssvvbandeira
andauthored
[Autotuner] Plotting utility + test (#2394)
* Add plotting utility, test and readme Signed-off-by: Jack Luar <[email protected]> Signed-off-by: luarss <[email protected]> Signed-off-by: Vitor Bandeira <[email protected]> Co-authored-by: Vitor Bandeira <[email protected]>
1 parent c2d463b commit da97d00

File tree

10 files changed

+236
-19
lines changed

10 files changed

+236
-19
lines changed

docs/user/InstructionsForAutoTuner.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,19 @@ python3 -m autotuner.distributed --design gcd --platform sky130hd \
145145
sweep
146146
```
147147

148+
#### Plot images
149+
150+
After running an AutoTuner experiment, you can generate a graph to understand the results better.
151+
The graph will show the progression of one metric (see list below) over the execution of the experiment.
152+
153+
- QoR
154+
- Runtime per trial
155+
- Clock Period
156+
- Worst slack
157+
158+
```shell
159+
python3 utils/plot.py --results_dir <your-autotuner-result-path>
160+
```
148161

149162
### Google Cloud Platform (GCP) distribution with Ray
150163

flow/test/test_autotuner.sh

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,6 @@ cd ../
88
./tools/AutoTuner/installer.sh
99
. ./tools/AutoTuner/setup.sh
1010

11-
# remove dashes and capitalize platform name
12-
PLATFORM=${PLATFORM//-/}
13-
# convert to uppercase
14-
PLATFORM=${PLATFORM^^}
15-
1611
echo "Running Autotuner smoke tune test"
1712
python3 -m unittest tools.AutoTuner.test.smoke_test_tune.${PLATFORM}TuneSmokeTest.test_tune
1813

@@ -30,4 +25,14 @@ if [ "$PLATFORM" == "asap7" ] && [ "$DESIGN_NAME" == "gcd" ]; then
3025
python3 -m unittest tools.AutoTuner.test.resume_check.ResumeCheck.test_tune_resume
3126
fi
3227

28+
echo "Running Autotuner plotting smoke test"
29+
all_experiments=$(ls -d ./flow/logs/${PLATFORM}/${DESIGN_NAME}/smoke-test-tune*)
30+
all_experiments=$(basename -a $all_experiments)
31+
for expt in $all_experiments; do
32+
python3 tools/AutoTuner/src/autotuner/utils/plot.py \
33+
--platform ${PLATFORM} \
34+
--design ${DESIGN_NAME} \
35+
--experiment $expt
36+
done
37+
3338
exit $ret

flow/util/genReport.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
METRICS_CHECK_FMT = "{}/metadata-{}-check.log"
2020
REGEX_ERROR = re.compile(r"^\[error ?(\w+-\d+)?\]", re.IGNORECASE)
2121
REGEX_WARNING = re.compile(r"^\[warning ?(\w+-\d+)?\]", re.IGNORECASE)
22+
SKIPPED_FLOW_VARIANT_KEYWORDS = ["test", "tune"]
2223
STATUS_GREEN = "Passing"
2324
STATUS_RED = "Failing"
2425

@@ -248,7 +249,9 @@ def write_summary():
248249
dir_list = log_dir.split(os.sep)
249250
# Handles autotuner folders, which do not have `report.log` natively.
250251
# TODO: Can we log something for autotuner?
251-
if len(dir_list) != 4 or "test-" in dir_list[-1]:
252+
if len(dir_list) != 4 or any(
253+
word in dir_list[-1] for word in SKIPPED_FLOW_VARIANT_KEYWORDS
254+
):
252255
continue
253256
report_dir = log_dir.replace(LOGS_FOLDER, REPORTS_FOLDER)
254257

tools/AutoTuner/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ tensorboard>=2.14.0,<=2.16.2
99
protobuf==3.20.3
1010
SQLAlchemy==1.4.17
1111
urllib3<=1.26.15
12+
matplotlib==3.10.0
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import glob
2+
import json
3+
import numpy as np
4+
import pandas as pd
5+
import matplotlib.pyplot as plt
6+
import re
7+
import os
8+
import argparse
9+
import sys
10+
11+
# Only does plotting for AutoTunerBase variants
12+
AT_REGEX = r"variant-AutoTunerBase-([\w-]+)-\w+"
13+
14+
# TODO: Make sure the distributed.py METRIC variable is consistent with this, single source of truth.
15+
METRIC = "metric"
16+
17+
cur_dir = os.path.dirname(os.path.abspath(__file__))
18+
root_dir = os.path.join(cur_dir, "../../../../../")
19+
os.chdir(root_dir)
20+
21+
22+
def load_dir(dir: str) -> pd.DataFrame:
23+
"""
24+
Load and merge progress, parameters, and metrics data from a specified directory.
25+
This function searches for `progress.csv`, `params.json`, and `metrics.json` files within the given directory,
26+
concatenates the data, and merges them into a single pandas DataFrame.
27+
Args:
28+
dir (str): The directory path containing the subdirectories with `progress.csv`, `params.json`, and `metrics.json` files.
29+
Returns:
30+
pd.DataFrame: A DataFrame containing the merged data from the progress, parameters, and metrics files.
31+
"""
32+
33+
# Concatenate progress DFs
34+
progress_csvs = glob.glob(f"{dir}/*/progress.csv")
35+
if len(progress_csvs) == 0:
36+
print("No progress.csv files found.")
37+
sys.exit(1)
38+
progress_df = pd.concat([pd.read_csv(f) for f in progress_csvs])
39+
40+
# Concatenate params.json & metrics.json file
41+
params = []
42+
failed = []
43+
for params_fname in glob.glob(f"{dir}/*/params.json"):
44+
metrics_fname = params_fname.replace("params.json", "metrics.json")
45+
try:
46+
with open(params_fname, "r") as f:
47+
_dict = json.load(f)
48+
_dict["trial_id"] = re.search(AT_REGEX, params_fname).group(1)
49+
with open(metrics_fname, "r") as f:
50+
metrics = json.load(f)
51+
ws = metrics["finish"]["timing__setup__ws"]
52+
metrics["worst_slack"] = ws
53+
_dict.update(metrics)
54+
params.append(_dict)
55+
except Exception as e:
56+
failed.append(metrics_fname)
57+
continue
58+
59+
# Merge all dataframe
60+
params_df = pd.DataFrame(params)
61+
try:
62+
progress_df = progress_df.merge(params_df, on="trial_id")
63+
except KeyError:
64+
print(
65+
"Unable to merge DFs due to missing trial_id in params.json (possibly due to failed trials.)"
66+
)
67+
sys.exit(1)
68+
69+
# Print failed, if any
70+
if failed:
71+
failed_files = "\n".join(failed)
72+
print(f"Failed to load {len(failed)} files:\n{failed_files}")
73+
return progress_df
74+
75+
76+
def preprocess(df: pd.DataFrame) -> pd.DataFrame:
77+
"""
78+
Preprocess the input DataFrame by renaming columns, removing unnecessary columns,
79+
filtering out invalid rows, and normalizing the timestamp.
80+
Args:
81+
df (pd.DataFrame): The input DataFrame to preprocess.
82+
Returns:
83+
pd.DataFrame: The preprocessed DataFrame with renamed columns, removed columns,
84+
filtered rows, and normalized timestamp.
85+
"""
86+
87+
cols_to_remove = [
88+
"done",
89+
"training_iteration",
90+
"date",
91+
"pid",
92+
"hostname",
93+
"node_ip",
94+
"time_since_restore",
95+
"time_total_s",
96+
"iterations_since_restore",
97+
]
98+
rename_dict = {
99+
"time_this_iter_s": "runtime",
100+
"_SDC_CLK_PERIOD": "clk_period", # param
101+
}
102+
try:
103+
df = df.rename(columns=rename_dict)
104+
df = df.drop(columns=cols_to_remove)
105+
df = df[df[METRIC] != 9e99]
106+
df["timestamp"] -= df["timestamp"].min()
107+
return df
108+
except KeyError as e:
109+
print(
110+
f"KeyError: {e} in the DataFrame. Dataframe does not contain necessary columns."
111+
)
112+
sys.exit(1)
113+
114+
115+
def plot(df: pd.DataFrame, key: str, dir: str):
116+
"""
117+
Plots a scatter plot with a linear fit and a box plot for a specified key from a DataFrame.
118+
Args:
119+
df (pd.DataFrame): The DataFrame containing the data to plot.
120+
key (str): The column name in the DataFrame to plot.
121+
dir (str): The directory where the plots will be saved. The directory must exist.
122+
Returns:
123+
None
124+
"""
125+
126+
assert os.path.exists(dir), f"Directory {dir} does not exist."
127+
# Plot box plot and time series plot for key
128+
fig, ax = plt.subplots(1, figsize=(15, 10))
129+
ax.scatter(df["timestamp"], df[key])
130+
ax.set_xlabel("Time (s)")
131+
ax.set_ylabel(key)
132+
ax.set_title(f"{key} vs Time")
133+
134+
try:
135+
coeff = np.polyfit(df["timestamp"], df[key], 1)
136+
poly_func = np.poly1d(coeff)
137+
ax.plot(
138+
df["timestamp"],
139+
poly_func(df["timestamp"]),
140+
"r--",
141+
label=f"y={coeff[0]:.2f}x+{coeff[1]:.2f}",
142+
)
143+
ax.legend()
144+
except np.linalg.LinAlgError:
145+
print("Cannot fit a line to the data, plotting only scatter plot.")
146+
147+
fig.savefig(f"{dir}/{key}.png")
148+
149+
plt.figure(figsize=(15, 10))
150+
plt.boxplot(df[key])
151+
plt.ylabel(key)
152+
plt.title(f"{key} Boxplot")
153+
plt.savefig(f"{dir}/{key}-boxplot.png")
154+
155+
156+
def main(platform: str, design: str, experiment: str):
157+
"""
158+
Main function to process results from a specified directory and plot the results.
159+
Args:
160+
platform (str): The platform name.
161+
design (str): The design name.
162+
experiment (str): The experiment name.
163+
Returns:
164+
None
165+
"""
166+
167+
results_dir = os.path.join(
168+
root_dir, f"./flow/logs/{platform}/{design}/{experiment}"
169+
)
170+
img_dir = os.path.join(
171+
root_dir, f"./flow/reports/images/{platform}/{design}/{experiment}"
172+
)
173+
print("Processing results from", results_dir)
174+
os.makedirs(img_dir, exist_ok=True)
175+
df = load_dir(results_dir)
176+
df = preprocess(df)
177+
keys = [METRIC] + ["runtime", "clk_period", "worst_slack"]
178+
179+
# Plot only if more than one entry
180+
if len(df) < 2:
181+
print("Less than 2 entries, skipping plotting.")
182+
sys.exit(0)
183+
for key in keys:
184+
plot(df, key, img_dir)
185+
186+
187+
if __name__ == "__main__":
188+
parser = argparse.ArgumentParser(description="Plot AutoTuner results.")
189+
parser.add_argument("--platform", type=str, help="Platform name.", required=True)
190+
parser.add_argument("--design", type=str, help="Design name.", required=True)
191+
parser.add_argument(
192+
"--experiment", type=str, help="Experiment name.", required=True
193+
)
194+
args = parser.parse_args()
195+
main(platform=args.platform, design=args.design, experiment=args.experiment)

tools/AutoTuner/test/resume_check.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def setUp(self):
5151
f" --platform {self.platform}"
5252
f" --config {self.config}"
5353
f" --jobs {self.jobs}"
54-
f" --experiment test_resume"
54+
f" --experiment test-resume"
5555
f" tune --iterations {self.iterations} --samples {self.samples}"
5656
f" --resources_per_trial {res_per_trial}"
5757
f" {c}"

tools/AutoTuner/test/smoke_test_algo_eval.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,17 @@ def test_algo_eval(self):
5353
self.assertTrue(successful)
5454

5555

56-
class ASAP7AlgoEvalSmokeTest(BaseAlgoEvalSmokeTest):
56+
class asap7AlgoEvalSmokeTest(BaseAlgoEvalSmokeTest):
5757
platform = "asap7"
5858
design = "gcd"
5959

6060

61-
class IHPSG13G2AlgoEvalSmokeTest(BaseAlgoEvalSmokeTest):
61+
class ihpsg13g2AlgoEvalSmokeTest(BaseAlgoEvalSmokeTest):
6262
platform = "ihp-sg13g2"
6363
design = "gcd"
6464

6565

66-
class SKY130HDAlgoEvalSmokeTest(BaseAlgoEvalSmokeTest):
66+
class sky130hdAlgoEvalSmokeTest(BaseAlgoEvalSmokeTest):
6767
platform = "sky130hd"
6868
design = "gcd"
6969

tools/AutoTuner/test/smoke_test_sample_iteration.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,17 @@ def test_sample_iteration(self):
3636
self.assertTrue(successful)
3737

3838

39-
class ASAP7SampleIterationSmokeTest(BaseSampleIterationSmokeTest):
39+
class asap7SampleIterationSmokeTest(BaseSampleIterationSmokeTest):
4040
platform = "asap7"
4141
design = "gcd"
4242

4343

44-
class SKY130HDSampleIterationSmokeTest(BaseSampleIterationSmokeTest):
44+
class sky130hdSampleIterationSmokeTest(BaseSampleIterationSmokeTest):
4545
platform = "sky130hd"
4646
design = "gcd"
4747

4848

49-
class IHPSG13G2SampleIterationSmokeTest(BaseSampleIterationSmokeTest):
49+
class ihpsg13g2SampleIterationSmokeTest(BaseSampleIterationSmokeTest):
5050
platform = "ihp-sg13g2"
5151
design = "gcd"
5252

tools/AutoTuner/test/smoke_test_sweep.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,17 @@ def test_sweep(self):
4848
self.assertTrue(successful)
4949

5050

51-
class ASAP7SweepSmokeTest(BaseSweepSmokeTest):
51+
class asap7SweepSmokeTest(BaseSweepSmokeTest):
5252
platform = "asap7"
5353
design = "gcd"
5454

5555

56-
class SKY130HDSweepSmokeTest(BaseSweepSmokeTest):
56+
class sky130hdSweepSmokeTest(BaseSweepSmokeTest):
5757
platform = "sky130hd"
5858
design = "gcd"
5959

6060

61-
class IHPSG13G2SweepSmokeTest(BaseSweepSmokeTest):
61+
class ihpsg13g2SweepSmokeTest(BaseSweepSmokeTest):
6262
platform = "ihp-sg13g2"
6363
design = "gcd"
6464

tools/AutoTuner/test/smoke_test_tune.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,17 @@ def test_tune(self):
3232
self.assertTrue(successful)
3333

3434

35-
class ASAP7TuneSmokeTest(BaseTuneSmokeTest):
35+
class asap7TuneSmokeTest(BaseTuneSmokeTest):
3636
platform = "asap7"
3737
design = "gcd"
3838

3939

40-
class SKY130HDTuneSmokeTest(BaseTuneSmokeTest):
40+
class sky130hdTuneSmokeTest(BaseTuneSmokeTest):
4141
platform = "sky130hd"
4242
design = "gcd"
4343

4444

45-
class IHPSG13G2TuneSmokeTest(BaseTuneSmokeTest):
45+
class ihpsg13g2TuneSmokeTest(BaseTuneSmokeTest):
4646
platform = "ihp-sg13g2"
4747
design = "gcd"
4848

0 commit comments

Comments
 (0)