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,
    )