Skip to content

Commit 6ba73a1

Browse files
authored
[EP Perf] Extension to post benchmark perf from local devices (microsoft#24236)
### Description <!-- Describe your changes. --> This script can upload local perf log/csv to DB, which can be used as EP Perf Dashboard external data source. (Make sure the csv/log-parsing logic match the targeting DB table's schema ) #### Usage: * To post csv to db: `python parse_post_perf.py --kusto-table="<table_name>" --kusto-conn="<db_link>" --kusto-db="<dashboard_xyz>" --upload-csv="<path\to\data.csv>" ` * To parse log from mobile perf log and post to db: `python parse_post_perf.py --kusto-table="<table_name>" --kusto-conn="<db_link>" --kusto-db="<dashboard_xyz>" --parse_mobile_perf --log-file="<path/to/mobile_model_benchmark.log>" --model="<model_name>" --device-id="<device_name>" --commit-id="<ort_commit_id>" --ep="<test_backend>"` ### Motivation and Context <!-- - Why is this change required? What problem does it solve? - If it fixes an open issue, please link to the issue here. -->
1 parent 5612ce5 commit 6ba73a1

File tree

1 file changed

+183
-0
lines changed

1 file changed

+183
-0
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# -------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License.
4+
# --------------------------------------------------------------------------
5+
"""
6+
Parse and Post customized perf data to DB
7+
"""
8+
9+
import argparse
10+
import csv
11+
import datetime
12+
import logging
13+
import sys
14+
15+
import pandas as pd
16+
from azure.kusto.data import KustoConnectionStringBuilder
17+
from azure.kusto.data.data_format import DataFormat
18+
from azure.kusto.ingest import IngestionProperties, QueuedIngestClient, ReportLevel
19+
20+
# Configure logging
21+
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
22+
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
23+
24+
25+
def parse_mobile_perf(log_data: str, model: str, device_id: str, ep: str, commit_id: str, csv_filename: str):
26+
"""
27+
Parse log data and save metrics to a CSV file.
28+
29+
Args:
30+
log_data (str): The log data to parse.
31+
csv_filename (str): The filename to save the parsed metrics.
32+
"""
33+
metrics = {
34+
"Model": model,
35+
"DeviceId": device_id,
36+
"Ep": ep,
37+
"CommitId": commit_id,
38+
"TTFTAvgTimeSec": None,
39+
"TTFTAvgTokenPerSec": None,
40+
"TokenGenerationAvgTimeSec": None,
41+
"TokenGenerationAvgTokenPerSec": None,
42+
"TokenSamplingAvgTimeSec": None,
43+
"TokenSamplingAvgTokenPerSec": None,
44+
"E2EGenerationAvgTimeSec": None,
45+
"PeakMemoryMB": None,
46+
}
47+
48+
current_section = None
49+
50+
for line in log_data.split("\n").strip():
51+
if "Prompt processing" in line:
52+
current_section = "TTFT"
53+
elif "Token generation" in line:
54+
current_section = "TokenGeneration"
55+
elif "Token sampling" in line:
56+
current_section = "TokenSampling"
57+
elif "E2E generation" in line:
58+
current_section = "E2EGeneration"
59+
elif "Peak working set size" in line:
60+
current_section = "PeakMemory"
61+
62+
if line.startswith("avg (us):"):
63+
value = float(line.split(":")[1].strip()) / 1_000_000
64+
if current_section == "TTFT":
65+
metrics["TTFTAvgTimeSec"] = value
66+
elif current_section == "TokenGeneration":
67+
metrics["TokenGenerationAvgTimeSec"] = value
68+
elif current_section == "TokenSampling":
69+
metrics["TokenSamplingAvgTimeSec"] = value
70+
71+
elif line.startswith("avg (tokens/s):"):
72+
value = float(line.split(":")[1].strip())
73+
if current_section == "TTFT":
74+
metrics["TTFTAvgTokenPerSec"] = value
75+
elif current_section == "TokenGeneration":
76+
metrics["TokenGenerationAvgTokenPerSec"] = value
77+
elif current_section == "TokenSampling":
78+
metrics["TokenSamplingAvgTokenPerSec"] = value
79+
80+
elif line.startswith("avg (ms):"):
81+
value = float(line.split(":")[1].strip()) / 1_000
82+
if current_section == "E2EGeneration":
83+
metrics["E2EGenerationAvgTimeSec"] = value
84+
85+
elif line.startswith("Peak working set size (bytes):"):
86+
value = float(line.split(":")[1].strip()) / (1024 * 1024)
87+
metrics["PeakMemoryMB"] = value
88+
89+
for key in metrics.items():
90+
if metrics[key] is not None and isinstance(metrics[key], (int, float)):
91+
metrics[key] = f"{metrics[key]:.8f}"
92+
93+
save_to_csv(metrics, csv_filename)
94+
95+
96+
def save_to_csv(metrics: dict, csv_filename: str) -> None:
97+
try:
98+
with open(csv_filename, mode="w", newline="") as file:
99+
writer = csv.writer(file)
100+
writer.writerow(metrics.keys())
101+
writer.writerow(metrics.values())
102+
logging.info(f"Metrics saved to {csv_filename}")
103+
except OSError as e:
104+
logging.error(f"Failed to save metrics to {csv_filename}: {e}, abort.")
105+
sys.exit(1)
106+
107+
108+
def post_to_db(csv_file: str, kusto_table: str, kusto_conn: str, kusto_db: str):
109+
"""
110+
Post data to Kusto DB.
111+
112+
Args:
113+
csv_file (str): The path to csv file.
114+
kusto_table (str): The Kusto table name.
115+
kusto_conn (str): The Kusto connection string.
116+
kusto_db (str): The Kusto database name.
117+
"""
118+
try:
119+
table = pd.read_csv(csv_file)
120+
upload_time = datetime.datetime.now(tz=datetime.timezone.utc).replace(microsecond=0).isoformat()
121+
table["UploadTime"] = upload_time
122+
123+
kcsb_ingest = KustoConnectionStringBuilder.with_az_cli_authentication(kusto_conn)
124+
ingest_client = QueuedIngestClient(kcsb_ingest)
125+
126+
ingestion_props = IngestionProperties(
127+
database=kusto_db,
128+
table=kusto_table,
129+
data_format=DataFormat.CSV,
130+
report_level=ReportLevel.FailuresAndSuccesses,
131+
)
132+
133+
ingest_client.ingest_from_dataframe(table, ingestion_properties=ingestion_props)
134+
logging.info(f"Data uploaded to Kusto table {kusto_table} in database {kusto_db}")
135+
except Exception as e:
136+
logging.error(f"Failed to upload data to Kusto DB: {e}")
137+
138+
139+
if __name__ == "__main__":
140+
parser = argparse.ArgumentParser(description="Parse and post perf data to DB")
141+
parser.add_argument("--kusto-table", required=True, help="Kusto table name")
142+
parser.add_argument("--kusto-conn", required=True, help="Kusto connection string")
143+
parser.add_argument("--kusto-db", required=True, help="Kusto database name")
144+
# Post mobile perf data
145+
parser.add_argument("--parse-mobile-perf", action="store_true", help="Parse mobile perf data and post to DB")
146+
parser.add_argument("--log-file", help="Path to log file containing performance data")
147+
parser.add_argument("--model", help="Testing model")
148+
parser.add_argument("--device-id", help="The local mobile device id")
149+
parser.add_argument("--commit-id", help="The ORT commit id")
150+
parser.add_argument("--ep", help="The execution provider running on devices")
151+
parser.add_argument("--output-csv", default="data.csv", help="CSV file to save parsed metrics")
152+
# Post csv data
153+
parser.add_argument("--upload-csv", help="CSV file to upload to DB")
154+
155+
args = parser.parse_args()
156+
157+
if args.parse_mobile_perf:
158+
for arg in [args.log_file, args.model, args.device_id, args.ep, args.commit_id]:
159+
if arg is None:
160+
raise ValueError(f"Missing required parameter {arg} for parsing mobile perf data")
161+
log_data = ""
162+
try:
163+
with open(args.log_file) as f:
164+
log_data = f.read()
165+
logging.info(f"Read mobile perf log from {args.log_file}")
166+
except OSError as e:
167+
logging.error(f"Failed to read log file {args.log_file}: {e}")
168+
sys.exit(1)
169+
# Parse the mobile perf data for further upload
170+
parse_mobile_perf(log_data, args.model, args.device_id, args.ep, args.commit_id, args.output_csv)
171+
post_to_db(args.output_csv, args.kusto_table, args.kusto_conn, args.kusto_db)
172+
elif args.upload_csv:
173+
# Upload an existing CSV to the database
174+
# Need to make sure schema is correct
175+
try:
176+
post_to_db(args.upload_csv, args.kusto_table, args.kusto_conn, args.kusto_db)
177+
except Exception as e:
178+
logging.error(f"Failed to upload CSV {args.upload_csv} to DB: {e}")
179+
sys.exit(1)
180+
else:
181+
parser.print_help()
182+
logging.error("Either --parse-mobile-perf or --upload-csv must be specified")
183+
sys.exit(1)

0 commit comments

Comments
 (0)