DeAIafdeling's picture
Add files using upload-large-folder tool
3e0ec22 verified
Raw History Blame Contribute Delete
17.2 kB
# SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
# SPDX-License-Identifier: Apache-2.0
import math
from collections import defaultdict
import pandas as pd
import torch
from loguru import logger
from models.tt_transformers.tt.model_config import HfAttentionWrapper, HfDecoderWrapper, HfModelWrapper
def _extract_dtype_from_state_dict(model):
"""Helper to extract dtype from model's state_dict."""
try:
state_dict = model.state_dict()
for key, param in state_dict.items():
if "weight" in key:
print(f"get_ref_model_dype: key={key}, dtype={param.dtype}")
return param.dtype
except Exception as e:
pass
return None
def get_ref_model_dype(ref_model, model_name):
default_dype = torch.float32
if ref_model is None and model_name is None:
return default_dype
try:
models_to_check = []
if isinstance(ref_model, HfAttentionWrapper):
models_to_check.append(ref_model.attention)
elif isinstance(ref_model, HfDecoderWrapper):
models_to_check.append(ref_model.decoder)
elif isinstance(ref_model, HfModelWrapper):
models_to_check.append(ref_model.model)
else:
models_to_check = [ref_model]
# Try all models until one works
for model in models_to_check:
if model is not None:
dtype = _extract_dtype_from_state_dict(model)
if dtype is not None:
return dtype
except Exception as e:
pass
# try hardcoded dtypes
if model_name and isinstance(model_name, str):
model_name_lower = model_name.lower()
if "mistral-7b" in model_name_lower:
return torch.bfloat16
if "llama" in model_name_lower:
return torch.bfloat16
if "phi-3-mini" in model_name_lower or "phi-4" in model_name_lower:
return torch.bfloat16
return default_dype
### UTIL FUNCTIONS FOR DEVICE PERF
def build_duration_dict(raw_dict, column_name):
"""Build a dictionary of op codes to list of durations."""
op_code_dict = {}
for entry in raw_dict:
if column_name not in entry:
logger.warning(f"Warning: {entry} does not have column {column_name}")
op_code = entry["OP CODE"]
duration = entry[column_name]
if op_code not in op_code_dict:
op_code_dict[op_code] = []
op_code_dict[op_code].append(duration)
return op_code_dict
def build_duration_per_instance_dict(input_dict, num_layers):
"""Build a dictionary of op codes to list of durations per instance."""
per_instance_dict = {}
for op_code in input_dict:
num_ops_with_op_code = len(input_dict[op_code])
num_instances = num_ops_with_op_code // num_layers
if num_ops_with_op_code % num_layers != 0:
logger.warning(
f"Warning: {op_code} has {num_ops_with_op_code} ops, not a multiple of {num_layers} layers. Skipping per-instance analysis for this op."
)
continue # Skip this op_code instead of asserting
for iteration_id in range(num_layers):
for instance_id in range(num_instances):
op_code_with_id = f"{op_code}_{instance_id}"
if op_code_with_id not in per_instance_dict:
per_instance_dict[op_code_with_id] = []
per_instance_dict[op_code_with_id].append(
input_dict[op_code][iteration_id * num_instances + instance_id]
)
return per_instance_dict
def merge_device_rows(df):
"""
Merges device rows from a DataFrame into a single row per device.
Args:
df: A DataFrame containing measurements.
Returns:
A DataFrame with merged rows.
"""
block_by_device = defaultdict(list)
for _, row in df.iterrows():
op_name = row["OP CODE"]
op_type = row["OP TYPE"]
if op_type == "tt_dnn_device":
device_id = int(row["DEVICE ID"])
block_by_device[device_id].append((op_name, row.to_dict()))
device_ids = sorted(block_by_device.keys())
merged_blocks = []
global_index = 0
while max(len(block_by_device[device_id]) for device_id in device_ids) > 0:
blocks = []
op_name = None
missing_devices = []
for device_id in device_ids:
if not len(block_by_device[device_id]):
logger.warning(f"Warning: Device {device_id} is missing operation {op_name} at index {global_index}")
continue
if op_name is None:
op_name = block_by_device[device_id][0][0]
elif op_name != block_by_device[device_id][0][0]:
missing_devices.append(device_id)
continue
blocks.append(block_by_device[device_id].pop(0))
if missing_devices:
logger.warning(
f"Warning: {op_name} at index {global_index} not present in CSV for {len(missing_devices)} devices {missing_devices} - do not trust data for this op or directly subsequent ops with the same name"
)
if not blocks:
break
if "AllGather" in op_name or "ReduceScatter" in op_name or "AllReduce" in op_name or "Matmul_RS" in op_name:
# For collective ops, take the average duration over all rows within a block
device_kernel_durations = [
d["DEVICE KERNEL DURATION [ns]"]
for _, d in blocks
if "DEVICE KERNEL DURATION [ns]" in d and not math.isnan(d["DEVICE KERNEL DURATION [ns]"])
]
average_duration = (
sum(device_kernel_durations) / len(device_kernel_durations) if device_kernel_durations else float("nan")
)
# Use the first block's data but update its duration with the average
base_block = blocks[0][1].copy()
base_block["DEVICE KERNEL DURATION [ns]"] = average_duration
merged_blocks.append(base_block)
else:
# For non-collective ops, take the row with maximum duration
max_duration_block = max(blocks, key=lambda x: x[1]["DEVICE KERNEL DURATION [ns]"])
merged_blocks.append(max_duration_block[1])
global_index += 1
return pd.DataFrame(merged_blocks)
def process_measurements(df, num_layers):
"""
Given a Dataframe containing op device perf measurements, return the average, min, and max durations per instance on kerne
dispatch, and first to last start.
Args:
df: A DataFrame containing measurements.
num_layers: The number of layers in the model.
Returns:
A dictionary of aggregated values.
- kernel_duration_per_instance_aggregate_dict: A dictionary of aggregated kernel durations per instance.
- dispatch_duration_per_instance_aggregate_dict: A dictionary of aggregated dispatch durations per instance.
- first_to_last_start_per_instance_aggregate_dict: A dictionary of aggregated first to last start durations per instance.
"""
raw_dict = df[
["OP CODE", "DEVICE KERNEL DURATION [ns]", "OP TO OP LATENCY [ns]", "DEVICE KERNEL FIRST TO LAST START [ns]"]
].to_dict(orient="records")
# Kernel duration
kernel_duration_dict = build_duration_dict(raw_dict, "DEVICE KERNEL DURATION [ns]")
kernel_duration_per_instance_dict = build_duration_per_instance_dict(kernel_duration_dict, num_layers)
kernel_duration_per_instance_aggregate_dict = {
"avg": aggregate_per_instance_dict(kernel_duration_per_instance_dict, lambda v: sum(v) / len(v)),
"min": aggregate_per_instance_dict(kernel_duration_per_instance_dict, min),
"max": aggregate_per_instance_dict(kernel_duration_per_instance_dict, max),
}
# Dispatch duration
dispatch_duration_dict = build_duration_dict(raw_dict, "OP TO OP LATENCY [ns]")
dispatch_duration_per_instance_dict = build_duration_per_instance_dict(dispatch_duration_dict, num_layers)
dispatch_duration_per_instance_aggregate_dict = {
"avg": aggregate_per_instance_dict(dispatch_duration_per_instance_dict, lambda v: sum(v) / len(v)),
"min": aggregate_per_instance_dict(dispatch_duration_per_instance_dict, min),
"max": aggregate_per_instance_dict(dispatch_duration_per_instance_dict, max),
}
# First to last start
first_to_last_start_dict = build_duration_dict(raw_dict, "DEVICE KERNEL FIRST TO LAST START [ns]")
first_to_last_start_per_instance_dict = build_duration_per_instance_dict(first_to_last_start_dict, num_layers)
first_to_last_start_per_instance_aggregate_dict = {
"avg": aggregate_per_instance_dict(first_to_last_start_per_instance_dict, lambda v: sum(v) / len(v)),
"min": aggregate_per_instance_dict(first_to_last_start_per_instance_dict, min),
"max": aggregate_per_instance_dict(first_to_last_start_per_instance_dict, max),
}
return (
kernel_duration_per_instance_aggregate_dict,
dispatch_duration_per_instance_aggregate_dict,
first_to_last_start_per_instance_aggregate_dict,
)
def print_dict(input_dict, dict_name):
# print dict as a readable python dict
logger.info(f"\n{dict_name} = {{")
for op_code_with_id in input_dict:
logger.info(f'"{op_code_with_id}": {input_dict[op_code_with_id]},')
logger.info("}")
def aggregate_per_instance_dict(input_dict, agg_fn, default=0):
"""
Aggregates a dictionary of values by a given function.
Args:
input_dict: A dictionary of values to aggregate.
agg_fn: A function to aggregate the values.
default: The default value to return if the dictionary is empty.
Returns:
A dictionary of aggregated values.
"""
result = {}
for key, values in input_dict.items():
clean_values = [v if v is not None else 0 for v in values]
result[key] = agg_fn(clean_values) if clean_values else default
return result
def find_repeated_runs(ops, num_runs):
"""
Find the starting index of repeated operation runs in a list.
This function scans through a list of operations (`ops`) to find the
first index (`left`) such that the remaining portion of the list,
`ops[left:]`, can be evenly divided into `num_runs` contiguous segments
(runs), all of which are identical.
"""
def check_ops(left):
n = len(ops) - left
if n % num_runs != 0:
return False # Can't evenly split
run_length = n // num_runs
first = ops[left : left + run_length]
for i in range(1, num_runs):
if ops[left + i * run_length : left + (i + 1) * run_length] != first:
return False
return True
left = 0
while left < len(ops):
if check_ops(left):
return left
left += 1
return -1 # return -1 if not found
def find_repeated_block(ops, min_repeat=2):
"""
Detect a repeating block (pattern) of operations within a list.
This function scans through the list of operations `ops` to find a contiguous
sub-sequence (block) that repeats consecutively at least `min_repeat` times.
It returns information about the prefix (head) before the repeated region,
the size and count of the repeated block, and the suffix (tail) after it.
The function assumes that each block represents a "layer" or
repeating structure (e.g., neural network layer operations).
It tries multiple possible block sizes (starting from 10) to identify
the first valid repeated pattern.
"""
n = len(ops)
for block_size in range(10, n // min_repeat + 1): # ignore tiny blocks
for start in range(n - 2 * block_size):
block = ops[start : start + block_size]
next_block = ops[start + block_size : start + 2 * block_size]
if block == next_block:
# Found a repeating pattern
# Extend it as far as it repeats
i = start
while i + block_size <= n and ops[i : i + block_size] == block:
i += block_size
repeat_count = (i - start) // block_size
head = ops[:start]
tail = ops[i:]
return {
"num_head_ops": len(head),
"num_layer_block_ops": len(block),
"num_layers": repeat_count,
"num_tail_ops": len(tail),
}
# No repetition found
return {
"num_head_ops": len(ops),
"num_layer_block_ops": 0,
"num_layers": 0,
"num_tail_ops": len(ops),
}
def split_compile_and_trace(
df: pd.DataFrame,
mode: str = "prefill",
num_runs: int = 1,
num_layers: int = None,
):
"""
Split a concatenated ops DataFrame into compile and runtime-trace segments,
and further partition those into first layer, mid layers, and model tail DataFrames.
The ops CSV typically contains three consecutive phases: compile, capture/trace,
and runtime trace. When an extra sampling compile pass is present (to enable
random sampling), it contributes a fixed number of rows that should not be used
to determine the thirds split.
Parameters:
df: the input DataFrame (all ops)
mode: the mode of the test (prefill or decode)
num_runs: number of runs in the CSV (typically 3: compile, capture, trace)
num_layers: number of core layers to partition (required for further splits)
Returns:
(
df_model_compilation, df_model_trace,
df_first_layer_compilation, df_first_layer_trace,
df_mid_layers_compilation, df_mid_layers_trace,
df_model_tail_compilation, df_model_tail_trace
)
Any of the additional outputs may be None if slicing arguments are not provided.
"""
# Finds the first index such that ops[left:] contains num_runs of identical blocks of ops
first_run_start = find_repeated_runs(df["OP CODE"].tolist(), num_runs)
adjusted_len = (len(df) - first_run_start) // num_runs # The number of ops in each run
first_run_end = first_run_start + adjusted_len
last_run_start = len(df) - adjusted_len
df_model_compilation = df[first_run_start:first_run_end]
df_model_trace = df[last_run_start:]
# Find the head and tail of the repeating region in the model compilation/ trace region of ops
head_tail_ops = find_repeated_block(df_model_compilation["OP CODE"].tolist(), num_layers)
# [op_start_index:op_end_index] = all core layers region
op_start_index = head_tail_ops["num_head_ops"]
op_end_index = len(df_model_compilation) - head_tail_ops["num_tail_ops"]
df_layers_compilation = df_model_compilation[op_start_index:op_end_index]
df_layers_trace = df_model_trace[op_start_index:op_end_index]
# First layer: always first 'len/num_layers'
split_point = int(len(df_layers_compilation) / num_layers)
df_first_layer_compilation = df_layers_compilation[:split_point]
df_first_layer_trace = df_layers_trace[:split_point]
# Mid layers: remainder of layers region
if num_layers > 1:
df_mid_layers_compilation = df_layers_compilation[split_point:]
df_mid_layers_trace = df_layers_trace[split_point:]
else:
df_mid_layers_compilation = None
df_mid_layers_trace = None
# Model tail ops (e.g. lmhead/sampling): [tail_start_index:]
if op_end_index is not None:
df_model_tail_compilation = df_model_compilation[op_end_index:]
df_model_tail_trace = df_model_trace[op_end_index:]
else:
df_model_tail_compilation = None
df_model_tail_trace = None
return (
df_model_compilation,
df_model_trace,
df_first_layer_compilation,
df_first_layer_trace,
df_mid_layers_compilation,
df_mid_layers_trace,
df_model_tail_compilation,
df_model_tail_trace,
)
def verify_value_within_margin(value, target, margin, op_code_with_id, perf_type):
upper_limit = target + margin * target
lower_limit = target - margin * target
passing = True
if value > upper_limit:
passing = False
logger.warning(
f"{op_code_with_id} {perf_type}: {value} ns is larger than target "
f"({target}) ns, difference: "
f"{abs(value - upper_limit)} ns, margin: "
f"{margin}, "
f"relative margin to pass would be: "
f"{(abs(target - value) / target) if target != 0 else -1}"
)
elif value < lower_limit:
passing = False
logger.warning(
f"{op_code_with_id} {perf_type}: {value} ns is smaller than target "
f"({target}) ns, difference: "
f"{abs(value - lower_limit)} ns, margin: "
f"{margin}, "
f"relative margin to pass would be: "
f"{(abs(target - value) / target) if target != 0 else -1}"
)
return passing