File size: 18,208 Bytes
ca4edfb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 | # SPDX-FileCopyrightText: © 2023 Tenstorrent USA, Inc.
# SPDX-License-Identifier: Apache-2.0
import json
import time
from collections import defaultdict
import pandas as pd
from loguru import logger
from tracy.common import clear_profiler_runtime_artifacts
from tracy.process_model_log import (
get_latest_ops_log_filename,
get_samples_per_s,
post_process_ops_log,
run_device_profiler,
)
from models.perf.benchmarking_utils import BenchmarkData, BenchmarkProfiler
from models.perf.perf_utils import process_perf_results
def run_device_perf(
command,
subdir,
num_iterations,
cols,
batch_size,
op_name="",
has_signposts=False,
device_analysis_types=["device_kernel_duration"],
op_support_count=None,
) -> dict:
duration_cols = [col + " DURATION [ns]" for col in cols]
samples_cols = [col + " SAMPLES/S" for col in cols]
clear_profiler_runtime_artifacts()
results = {}
for d_col in duration_cols:
results[f"AVG {d_col}"] = 0
results[f"MIN {d_col}"] = float("inf")
results[f"MAX {d_col}"] = -float("inf")
profiler_kwargs = {}
if op_support_count is not None:
profiler_kwargs["op_support_count"] = op_support_count
for _ in range(num_iterations):
run_device_profiler(command, subdir, device_analysis_types, **profiler_kwargs)
r = post_process_ops_log(subdir, duration_cols, op_name=op_name, has_signposts=has_signposts)
for d_col in duration_cols:
results[f"AVG {d_col}"] += r[d_col]
results[f"MIN {d_col}"] = min(results[f"MIN {d_col}"], r[d_col])
results[f"MAX {d_col}"] = max(results[f"MAX {d_col}"], r[d_col])
post_processed_results = {}
for s_col, d_col in zip(samples_cols, duration_cols):
post_processed_results[f"AVG {s_col}"] = get_samples_per_s(results[f"AVG {d_col}"] / num_iterations, batch_size)
post_processed_results[f"MIN {s_col}"] = get_samples_per_s(results[f"MAX {d_col}"], batch_size)
post_processed_results[f"MAX {s_col}"] = get_samples_per_s(results[f"MIN {d_col}"], batch_size)
post_processed_results[f"AVG {d_col}"] = results[f"AVG {d_col}"] / num_iterations
post_processed_results[f"MIN {d_col}"] = results[f"MIN {d_col}"]
post_processed_results[f"MAX {d_col}"] = results[f"MAX {d_col}"]
logger.info(
f"\nTest: {command}"
f"\nPerformance statistics over {num_iterations} iterations"
f"\n{json.dumps(post_processed_results, indent=4)}"
)
return post_processed_results
# TODO: Move into process_model_log.py (#18698)
def post_process_ops_log_detailed(
output_logs_subdir,
columns,
sum_vals=True,
op_name="",
has_signposts=False,
detailed=False,
warmup_iters=0,
per_op=False,
):
filename = get_latest_ops_log_filename(output_logs_subdir)
df = pd.read_csv(filename)
if has_signposts:
# there are explicit start and stop points in the model we want to measure between
markers = df[df["OP TYPE"] == "signpost"]["OP CODE"]
start = markers[markers == "start"].index[0]
stop = markers[markers == "stop"].index[0]
df = df.iloc[start + 1 : stop]
if op_name != "":
df_filtered = df[df["OP CODE"] == op_name]
if df_filtered.empty:
# Try partial match (in case op_name is just the class name without namespace)
# First try exact suffix match
df_filtered = df[df["OP CODE"].str.endswith(f"::{op_name}", na=False)]
if df_filtered.empty:
# Try matching within template parameters (e.g., MeshDeviceOperationAdapter<...::GroupNormDeviceOperation>)
# This matches both "::GroupNormDeviceOperation" and "GroupNormDeviceOperation>" (end of template)
df_filtered = df[
df["OP CODE"].str.contains(f"::{op_name}", na=False)
| df["OP CODE"].str.contains(f"{op_name}>", na=False)
]
if df_filtered.empty:
# Show what operation names are actually in the CSV
unique_op_codes = df["OP CODE"].unique() if not df.empty else []
error_msg = (
f"No operations found matching op_name='{op_name}' in {filename}. "
f"Found {len(unique_op_codes)} unique operation(s): {list(unique_op_codes)[:10]}"
)
logger.error(error_msg)
raise ValueError(error_msg)
else:
logger.info(f"Found {len(df_filtered)} operation(s) matching '{op_name}' (partial match)")
df = df_filtered
# group by DEVICE ID
df = df.groupby("DEVICE ID")
# now sort the list of df by the DEVICE FW START CYCLE
df = sorted(df, key=lambda x: x[1]["DEVICE FW START CYCLE"].iloc[0])
# Convert list of tuples to list of dataframes
dfs = [group for _, group in df]
# Check if dfs is empty (no matching operations found)
if not dfs:
op_filter_msg = f" matching op_name='{op_name}'" if op_name else ""
error_msg = (
f"No device data found for operations{op_filter_msg} in {filename}. "
f"Operations were matched but no device data is available."
)
logger.error(error_msg)
raise ValueError(error_msg)
# Check if the first dataframe is empty
if len(dfs[0]) == 0:
op_filter_msg = f" matching op_name='{op_name}'" if op_name else ""
error_msg = (
f"No device data found for operations{op_filter_msg} in {filename}. "
f"Operations were matched but the first device group is empty."
)
logger.error(error_msg)
raise ValueError(error_msg)
# concatenate the list of df into a single df by interleaving the rows
df = pd.concat([df.iloc[[i]] for i in range(len(dfs[0])) for df in dfs], ignore_index=True)
if warmup_iters > 0:
df = df.iloc[warmup_iters:]
results = {}
if not per_op:
for col in columns:
df_filtered = df[df[col] != "-"]
if sum_vals:
results[col] = df_filtered[col].astype(float).sum()
else:
results[col] = df_filtered[col].astype(float).to_numpy()
if detailed:
results[f"AVG {col}"] = df_filtered[col].astype(float).mean()
results[f"MIN {col}"] = df_filtered[col].astype(float).min()
results[f"MAX {col}"] = df_filtered[col].astype(float).max()
results[f"STD {col}"] = df_filtered[col].astype(float).std()
else:
for op in df["OP CODE"].unique():
df_op = df[df["OP CODE"] == op]
results[op] = {}
for col in columns:
df_filtered = df_op[df_op[col] != "-"]
if sum_vals:
results[op][col] = df_filtered[col].astype(float).sum()
else:
results[op][col] = df_filtered[col].astype(float).to_numpy()
if detailed:
results[op][f"AVG {col}"] = df_filtered[col].astype(float).mean()
results[op][f"MIN {col}"] = df_filtered[col].astype(float).min()
results[op][f"MAX {col}"] = df_filtered[col].astype(float).max()
results[op][f"STD {col}"] = df_filtered[col].astype(float).std()
return results
def run_device_perf_detailed(
command, subdir, cols, op_name="", has_signposts=False, warmup_iters=0, device_analysis_types=None, per_op=False
):
duration_cols = [col + " DURATION [ns]" for col in cols]
clear_profiler_runtime_artifacts()
results = {}
if device_analysis_types is None:
device_analysis_types = ["device_kernel_duration"]
run_device_profiler(command, subdir, device_analysis_types=device_analysis_types)
r = post_process_ops_log_detailed(
subdir,
duration_cols,
op_name=op_name,
has_signposts=has_signposts,
detailed=True,
warmup_iters=warmup_iters,
per_op=per_op,
)
post_processed_results = defaultdict(dict)
if not per_op:
for d_col in duration_cols:
results[f"AVG {d_col}"] = r[f"AVG {d_col}"]
results[f"MIN {d_col}"] = r[f"MIN {d_col}"]
results[f"MAX {d_col}"] = r[f"MAX {d_col}"]
results[f"STD {d_col}"] = r[f"STD {d_col}"]
for col, d_col in zip(cols, duration_cols):
post_processed_results[col]["AVG"] = results[f"AVG {d_col}"]
post_processed_results[col]["MIN"] = results[f"MIN {d_col}"]
post_processed_results[col]["MAX"] = results[f"MAX {d_col}"]
post_processed_results[col]["STD"] = results[f"STD {d_col}"]
else:
for op in r.keys():
results[op] = {}
for d_col in duration_cols:
results[op][f"AVG {d_col}"] = r[op][f"AVG {d_col}"]
results[op][f"MIN {d_col}"] = r[op][f"MIN {d_col}"]
results[op][f"MAX {d_col}"] = r[op][f"MAX {d_col}"]
results[op][f"STD {d_col}"] = r[op][f"STD {d_col}"]
for col, d_col in zip(cols, duration_cols):
post_processed_results[op][col] = {}
post_processed_results[op][col]["AVG"] = results[op][f"AVG {d_col}"]
post_processed_results[op][col]["MIN"] = results[op][f"MIN {d_col}"]
post_processed_results[op][col]["MAX"] = results[op][f"MAX {d_col}"]
post_processed_results[op][col]["STD"] = results[op][f"STD {d_col}"]
logger.info(
f"\nTest: {command}\nPerformance statistics for op: {op_name}\n{json.dumps(post_processed_results, indent=4)}"
)
return post_processed_results
def check_device_perf(post_processed_results, margin, expected_perf_cols, assert_on_fail=False):
expected_results = {}
failed = False
for col, expected_perf in expected_perf_cols.items():
lower_threshold = (1 - margin) * expected_perf
upper_threshold = (1 + margin) * expected_perf
expected_results.update(
{
f"Lower Threshold {col}": lower_threshold,
f"Upper Threshold {col}": upper_threshold,
}
)
passing = lower_threshold <= post_processed_results[col] <= upper_threshold
if not passing:
failed = True
logger.error(
f"{col} {post_processed_results[col]} is outside of expected range ({lower_threshold}, {upper_threshold})"
)
if assert_on_fail:
assert not failed, "Some performance metrics are outside of expected range, see above for details."
return expected_results
def prep_device_perf_report(
model_name: str,
batch_size: int,
post_processed_results: dict,
expected_results: dict,
comments: str,
) -> None:
"""
Generates a device performance report in CSV format for a given model and batch size.
If run in a CI environment, also saves the report as a pickled PartialBenchmarkRun object
to be uploaded to the benchmarking database.
Args:
model_name (str): The name of the model being evaluated.
batch_size (int): The batch size used during evaluation.
post_processed_results (dict): Dictionary containing the performance metrics after post-processing.
expected_results (dict): Dictionary containing expected lower and upper threshold values for each metric.
comments (str): Settings description to include in the report.
Creates:
- A CSV file named as "device_perf_{model_name}_{comments}_{YYYY_MM_DD}.csv" containing
the model name, settings, batch size, device performance metrics and their corresponding threshold values.
- If run in a CI environment, a PKL file containing the same data.
"""
formatted_results = {}
for metric_name, value in post_processed_results.items():
formatted_results[metric_name] = format(value, ".0f" if value.is_integer() else ".4f")
if (lower := expected_results.get(f"Lower Threshold {metric_name}")) is not None:
formatted_results[f"Lower Threshold {metric_name}"] = format(lower, ".4f")
if (upper := expected_results.get(f"Upper Threshold {metric_name}")) is not None:
formatted_results[f"Upper Threshold {metric_name}"] = format(upper, ".4f")
dict_res = {
"Model": model_name,
"Setting": comments,
"Batch": str(batch_size),
**formatted_results,
}
today = time.strftime("%Y_%m_%d")
csv_file = f"device_perf_{model_name}_{comments}_{today}.csv"
columns = ", ".join(str(d) for d in dict_res.keys())
values = ", ".join(dict_res.values())
with open(csv_file, "w") as csvfile:
csvfile.write(columns)
csvfile.write("\n")
csvfile.write(values)
# Dummy profiler to satisfy BenchmarkData's requirements
profiler = BenchmarkProfiler()
profiler.start("run")
profiler.end("run")
step_name = "device_perf"
profiler.start(step_name)
profiler.end(step_name)
benchmark_data = BenchmarkData()
for metric_name, value in formatted_results.items():
benchmark_data.add_measurement(profiler, 0, step_name, metric_name, float(value))
benchmark_data.save_partial_run_json(
profiler,
run_type="device_perf",
ml_model_name=model_name,
batch_size=batch_size,
)
def check_device_perf_results(fname, expected_cols, check_cols):
cols, merge_res = process_perf_results(fname, expected_cols)
visited_models = []
slow_measured = {col: [] for col in check_cols}
fast_measured = {col: [] for col in check_cols}
for models_info in merge_res:
models_info = [item.strip() for item in models_info]
dict_info = {name: value for name, value in zip(cols, models_info)}
logger.info(dict_info)
model_name = f"{dict_info['Model']}_{dict_info['Setting']}"
visited_models.append(model_name)
for col in check_cols:
model_lower_threshold_col = float(dict_info[f"Lower Threshold {col}"])
model_upper_threshold_col = float(dict_info[f"Upper Threshold {col}"])
model_measured_col = float(dict_info[col])
if model_measured_col > model_upper_threshold_col:
fast_measured[col].append((model_name, model_measured_col, model_upper_threshold_col))
logger.error(
f"{model_name} {col} is faster than expected with {model_measured_col}, max expected {model_upper_threshold_col}. Please update perf targets with latest expected perf"
)
if model_measured_col < model_lower_threshold_col:
slow_measured[col].append((model_name, model_measured_col, model_lower_threshold_col))
logger.error(
f"{model_name} {col} is too slow with {model_measured_col}, min expected {model_lower_threshold_col}."
)
assert any(
len(slow) == 0 for slow in slow_measured.values()
), f"Some model(s) {', '.join(slow_measured.keys())} are too slow, see above for details. {slow_measured}"
for col in slow_measured:
assert (
len(slow_measured[col]) == 0
), f"Some model(s) {col} are too slow, see above for details on slow models: {slow_measured[col]}"
assert any(
len(fast) == 0 for fast in fast_measured.values()
), f"Some model(s) {', '.join(slow_measured.keys())} are faster than expected, see above for details. {fast_measured}"
for col in fast_measured:
assert (
len(fast_measured[col]) == 0
), f"Some model(s) {col} are faster than expected, see above for details on fast models: {slow_measured[col]}"
def run_model_device_perf_test(
command: str,
expected_device_perf_ns_per_iteration: float,
subdir: str,
model_name: str,
num_iterations: int = 1,
batch_size: int = 1,
margin: float = 0.015,
comments: str = "",
op_support_count: int = None,
):
"""
Run device performance test for a model and validate results against expected performance.
This function executes a model performance test, collects device-level metrics,
and validates the results against expected performance thresholds. It also
generates a performance report for tracking and analysis.
Args:
command (str): The command to execute for running the model.
expected_device_perf_ns_per_iteration (float): Expected device kernel duration in nanoseconds.
subdir (str): Subdirectory where performance logs will be stored.
model_name (str): Name of the model being tested.
num_iterations (int, optional): Number of iterations to run. Defaults to 1.
batch_size (int, optional): Batch size for the model. Defaults to 1.
margin (float, optional): Acceptable performance margin as a percentage (e.g., 0.015 = 1.5%). Defaults to 0.015.
comments (str, optional): Additional comments or settings description for the report. Defaults to "".
op_support_count (int, optional): Number of operations to support. Defaults to None.
Raises:
AssertionError: If the measured performance is outside the acceptable margin from expected performance.
"""
cols = ["DEVICE FW", "DEVICE KERNEL", "DEVICE BRISC KERNEL"]
inference_time_key = "AVG DEVICE KERNEL DURATION [ns]"
post_processed_results = run_device_perf(
command,
subdir=subdir,
num_iterations=num_iterations,
cols=cols,
batch_size=batch_size,
op_support_count=op_support_count,
)
expected_perf_cols = {inference_time_key: expected_device_perf_ns_per_iteration}
expected_results = check_device_perf(
post_processed_results, margin=margin, expected_perf_cols=expected_perf_cols, assert_on_fail=True
)
prep_device_perf_report(
model_name=model_name,
batch_size=batch_size,
post_processed_results=post_processed_results,
expected_results=expected_results,
comments=comments,
)
|