Initial overhaul of Profile mode
Signed-off-by: colramos-amd <colramos@amd.com>
This commit is contained in:
@@ -33,7 +33,8 @@ import getpass
|
||||
from pymongo import MongoClient
|
||||
from tqdm import tqdm
|
||||
import glob
|
||||
from common import resolve_rocprof
|
||||
import re
|
||||
import logging
|
||||
|
||||
cache = dict()
|
||||
|
||||
@@ -129,12 +130,12 @@ def kernel_name_shortener(workload_dir, level):
|
||||
if level < 5:
|
||||
cpp_filt = os.path.join("/usr", "bin", "c++filt")
|
||||
if not os.path.isfile(cpp_filt):
|
||||
print(
|
||||
logging.error(
|
||||
"Error: Could not resolve c++filt in expected directory: {}".format(
|
||||
cpp_filt
|
||||
)
|
||||
)
|
||||
sys.exit(0)
|
||||
sys.exit(1)
|
||||
|
||||
for fpath in glob.glob(workload_dir + "/*.csv"):
|
||||
try:
|
||||
@@ -146,9 +147,9 @@ def kernel_name_shortener(workload_dir, level):
|
||||
modified_df = shorten_file(orig_df, level)
|
||||
modified_df.to_csv(fpath, index=False)
|
||||
except pd.errors.EmptyDataError:
|
||||
print("Skipping empty csv " + str(fpath))
|
||||
logging.debug("[profiling] Skipping shortening on empty csv " + str(fpath))
|
||||
|
||||
print("KernelName shortening complete!")
|
||||
logging.info("[profiling] KernelName shortening complete!")
|
||||
|
||||
|
||||
# Verify target directory and setup connection
|
||||
|
||||
@@ -1,555 +0,0 @@
|
||||
##############################################################################bl
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2021 - 2023 Advanced Micro Devices, Inc. All Rights Reserved.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
##############################################################################el
|
||||
|
||||
import sys, os, shutil, glob, re
|
||||
import numpy as np
|
||||
import math
|
||||
import warnings
|
||||
import pandas as pd
|
||||
|
||||
prog = "omniperf"
|
||||
|
||||
# Per IP block max number of simulutaneous counters
|
||||
# GFX IP Blocks
|
||||
perfmon_config = {
|
||||
"vega10": {
|
||||
"SQ": 8,
|
||||
"TA": 2,
|
||||
"TD": 2,
|
||||
"TCP": 4,
|
||||
"TCC": 4,
|
||||
"CPC": 2,
|
||||
"CPF": 2,
|
||||
"SPI": 2,
|
||||
"GRBM": 2,
|
||||
"GDS": 4,
|
||||
"TCC_channels": 16,
|
||||
},
|
||||
"mi50": {
|
||||
"SQ": 8,
|
||||
"TA": 2,
|
||||
"TD": 2,
|
||||
"TCP": 4,
|
||||
"TCC": 4,
|
||||
"CPC": 2,
|
||||
"CPF": 2,
|
||||
"SPI": 2,
|
||||
"GRBM": 2,
|
||||
"GDS": 4,
|
||||
"TCC_channels": 16,
|
||||
},
|
||||
"mi100": {
|
||||
"SQ": 8,
|
||||
"TA": 2,
|
||||
"TD": 2,
|
||||
"TCP": 4,
|
||||
"TCC": 4,
|
||||
"CPC": 2,
|
||||
"CPF": 2,
|
||||
"SPI": 2,
|
||||
"GRBM": 2,
|
||||
"GDS": 4,
|
||||
"TCC_channels": 32,
|
||||
},
|
||||
"mi200": {
|
||||
"SQ": 8,
|
||||
"TA": 2,
|
||||
"TD": 2,
|
||||
"TCP": 4,
|
||||
"TCC": 4,
|
||||
"CPC": 2,
|
||||
"CPF": 2,
|
||||
"SPI": 2,
|
||||
"GRBM": 2,
|
||||
"GDS": 4,
|
||||
"TCC_channels": 32,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_df_column_equality(df):
|
||||
return df.eq(df.iloc[:, 0], axis=0).all(1).all()
|
||||
|
||||
|
||||
# joins disparate runs less dumbly than rocprof
|
||||
def join_prof(workload_dir, join_type, log_file, verbose, out=None):
|
||||
# Set default output directory if not specified
|
||||
if type(workload_dir) == str:
|
||||
if out is None:
|
||||
out = workload_dir + "/pmc_perf.csv"
|
||||
files = glob.glob(workload_dir + "/" + "pmc_perf_*.csv")
|
||||
elif type(workload_dir) == list:
|
||||
files = workload_dir
|
||||
else:
|
||||
print("ERROR: Invalid workload_dir")
|
||||
sys.exit(1)
|
||||
|
||||
df = None
|
||||
for i, file in enumerate(files):
|
||||
_df = pd.read_csv(file) if type(workload_dir) == str else file
|
||||
if join_type == "kernel":
|
||||
key = _df.groupby("KernelName").cumcount()
|
||||
_df["key"] = _df.KernelName + " - " + key.astype(str)
|
||||
elif join_type == "grid":
|
||||
key = _df.groupby(["KernelName", "grd"]).cumcount()
|
||||
_df["key"] = (
|
||||
_df.KernelName + " - " + _df.grd.astype(str) + " - " + key.astype(str)
|
||||
)
|
||||
else:
|
||||
print("ERROR: Unrecognized --join-type")
|
||||
sys.exit(1)
|
||||
|
||||
if df is None:
|
||||
df = _df
|
||||
else:
|
||||
# join by unique index of kernel
|
||||
df = pd.merge(df, _df, how="inner", on="key", suffixes=("", f"_{i}"))
|
||||
|
||||
# TODO: check for any mismatch in joins
|
||||
duplicate_cols = {
|
||||
"gpu": [col for col in df.columns if "gpu" in col],
|
||||
"grd": [col for col in df.columns if "grd" in col],
|
||||
"wgr": [col for col in df.columns if "wgr" in col],
|
||||
"lds": [col for col in df.columns if "lds" in col],
|
||||
"scr": [col for col in df.columns if "scr" in col],
|
||||
"spgr": [col for col in df.columns if "sgpr" in col],
|
||||
}
|
||||
# Check for vgpr counter in ROCm < 5.3
|
||||
if "vgpr" in df.columns:
|
||||
duplicate_cols["vgpr"] = [col for col in df.columns if "vgpr" in col]
|
||||
# Check for vgpr counter in ROCm >= 5.3
|
||||
else:
|
||||
duplicate_cols["arch_vgpr"] = [col for col in df.columns if "arch_vgpr" in col]
|
||||
duplicate_cols["accum_vgpr"] = [col for col in df.columns if "accum_vgpr" in col]
|
||||
for key, cols in duplicate_cols.items():
|
||||
_df = df[cols]
|
||||
if not test_df_column_equality(_df):
|
||||
msg = (
|
||||
"WARNING: Detected differing {} values while joining pmc_perf.csv".format(
|
||||
key
|
||||
)
|
||||
)
|
||||
warnings.warn(msg)
|
||||
if log_file:
|
||||
log_file.write(msg + "\n")
|
||||
else:
|
||||
msg = "Successfully joined {} in pmc_perf.csv".format(key)
|
||||
if log_file:
|
||||
log_file.write(msg + "\n")
|
||||
if test_df_column_equality(_df) and verbose:
|
||||
print(msg)
|
||||
|
||||
# now, we can:
|
||||
# A) throw away any of the "boring" duplicats
|
||||
df = df[
|
||||
[
|
||||
k
|
||||
for k in df.keys()
|
||||
if not any(
|
||||
check in k
|
||||
for check in [
|
||||
# removed merged counters, keep original
|
||||
"gpu-id_",
|
||||
"grd_",
|
||||
"wgr_",
|
||||
"lds_",
|
||||
"scr_",
|
||||
"vgpr_",
|
||||
"sgpr_",
|
||||
"Index_",
|
||||
# un-mergable, remove all
|
||||
"queue-id",
|
||||
"queue-index",
|
||||
"pid",
|
||||
"tid",
|
||||
"fbar",
|
||||
"sig",
|
||||
"obj",
|
||||
# rocscope specific merged counters, keep original
|
||||
"dispatch_",
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
# B) any timestamps that are _not_ the duration, which is the one we care
|
||||
# about
|
||||
df = df[
|
||||
[
|
||||
k
|
||||
for k in df.keys()
|
||||
if not any(
|
||||
check in k
|
||||
for check in [
|
||||
"DispatchNs",
|
||||
"CompleteNs",
|
||||
# rocscope specific timestamp
|
||||
"HostDuration",
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
# C) sanity check the name and key
|
||||
namekeys = [k for k in df.keys() if "KernelName" in k]
|
||||
assert len(namekeys)
|
||||
for k in namekeys[1:]:
|
||||
assert (df[namekeys[0]] == df[k]).all()
|
||||
df = df.drop(columns=namekeys[1:])
|
||||
# now take the median of the durations
|
||||
bkeys = []
|
||||
ekeys = []
|
||||
for k in df.keys():
|
||||
if "Begin" in k:
|
||||
bkeys.append(k)
|
||||
if "End" in k:
|
||||
ekeys.append(k)
|
||||
# compute mean begin and end timestamps
|
||||
endNs = df[ekeys].mean(axis=1)
|
||||
beginNs = df[bkeys].mean(axis=1)
|
||||
# and replace
|
||||
df = df.drop(columns=bkeys)
|
||||
df = df.drop(columns=ekeys)
|
||||
df["BeginNs"] = beginNs
|
||||
df["EndNs"] = endNs
|
||||
# finally, join the drop key
|
||||
df = df.drop(columns=["key"])
|
||||
# save to file and delete old file(s), skip if we're being called outside of Omniperf
|
||||
if type(workload_dir) == str:
|
||||
df.to_csv(out, index=False)
|
||||
if not verbose:
|
||||
for file in files:
|
||||
os.remove(file)
|
||||
else:
|
||||
return df
|
||||
|
||||
|
||||
def pmc_perf_split(workload_dir):
|
||||
workload_perfmon_dir = workload_dir + "/perfmon"
|
||||
lines = open(workload_perfmon_dir + "/pmc_perf.txt", "r").read().splitlines()
|
||||
|
||||
# Iterate over each line in pmc_perf.txt
|
||||
mpattern = r"^pmc:(.*)"
|
||||
i = 0
|
||||
for line in lines:
|
||||
# Verify no comments
|
||||
stext = line.split("#")[0].strip()
|
||||
if not stext:
|
||||
continue
|
||||
|
||||
# all pmc counters start with "pmc:"
|
||||
m = re.match(mpattern, stext)
|
||||
if m is None:
|
||||
continue
|
||||
|
||||
# Create separate file for each line
|
||||
fd = open(workload_perfmon_dir + "/pmc_perf_" + str(i) + ".txt", "w")
|
||||
fd.write(stext + "\n\n")
|
||||
fd.write("gpu:\n")
|
||||
fd.write("range:\n")
|
||||
fd.write("kernel:\n")
|
||||
fd.close()
|
||||
|
||||
i += 1
|
||||
|
||||
# Remove old pmc_perf.txt input from perfmon dir
|
||||
os.remove(workload_perfmon_dir + "/pmc_perf.txt")
|
||||
|
||||
|
||||
def update_pmc_bucket(
|
||||
counters, save_file, soc, pmc_list=None, stext=None, workload_perfmon_dir=None
|
||||
):
|
||||
# Verify inputs.
|
||||
# If save_file is True, we're being called internally, from perfmon_coalesce
|
||||
# Else we're being called externally, from rocomni
|
||||
detected_external_call = False
|
||||
if save_file and (stext is None or workload_perfmon_dir is None):
|
||||
raise ValueError(
|
||||
"stext and workload_perfmon_dir must be specified if save_file is True"
|
||||
)
|
||||
if pmc_list is None:
|
||||
detected_external_call = True
|
||||
pmc_list = dict(
|
||||
[
|
||||
("SQ", []),
|
||||
("GRBM", []),
|
||||
("TCP", []),
|
||||
("TA", []),
|
||||
("TD", []),
|
||||
("TCC", []),
|
||||
("SPI", []),
|
||||
("CPC", []),
|
||||
("CPF", []),
|
||||
("GDS", []),
|
||||
("TCC2", {}), # per-channel TCC perfmon
|
||||
]
|
||||
)
|
||||
for ch in range(perfmon_config[soc]["TCC_channels"]):
|
||||
pmc_list["TCC2"][str(ch)] = []
|
||||
|
||||
if "SQ_ACCUM_PREV_HIRES" in counters and not detected_external_call:
|
||||
# save all level counters separately
|
||||
nindex = counters.index("SQ_ACCUM_PREV_HIRES")
|
||||
level_counter = counters[nindex - 1]
|
||||
|
||||
if save_file:
|
||||
# Save to level counter file, file name = level counter name
|
||||
fd = open(workload_perfmon_dir + "/" + level_counter + ".txt", "w")
|
||||
fd.write(stext + "\n\n")
|
||||
fd.write("gpu:\n")
|
||||
fd.write("range:\n")
|
||||
fd.write("kernel:\n")
|
||||
fd.close()
|
||||
|
||||
return pmc_list
|
||||
|
||||
# save normal pmc counters in matching buckets
|
||||
for counter in counters:
|
||||
IP_block = counter.split(sep="_")[0].upper()
|
||||
# SQC and SQ belong to the IP block, coalesce them
|
||||
if IP_block == "SQC":
|
||||
IP_block = "SQ"
|
||||
|
||||
if IP_block != "TCC":
|
||||
# Insert unique pmc counters into its bucket
|
||||
if counter not in pmc_list[IP_block]:
|
||||
pmc_list[IP_block].append(counter)
|
||||
|
||||
else:
|
||||
# TCC counters processing
|
||||
m = re.match(r"[\s\S]+\[(\d+)\]", counter)
|
||||
if m is None:
|
||||
# Aggregated TCC counters
|
||||
if counter not in pmc_list[IP_block]:
|
||||
pmc_list[IP_block].append(counter)
|
||||
|
||||
else:
|
||||
# TCC channel ID
|
||||
ch = m.group(1)
|
||||
|
||||
# fake IP block for per channel TCC
|
||||
if str(ch) in pmc_list["TCC2"]:
|
||||
# append unique counter into the channel
|
||||
if counter not in pmc_list["TCC2"][str(ch)]:
|
||||
pmc_list["TCC2"][str(ch)].append(counter)
|
||||
else:
|
||||
# initial counter in this channel
|
||||
pmc_list["TCC2"][str(ch)] = [counter]
|
||||
|
||||
if detected_external_call:
|
||||
# sort the per channel counter, so that same counter in all channels can be aligned
|
||||
for ch in range(perfmon_config[soc]["TCC_channels"]):
|
||||
pmc_list["TCC2"][str(ch)].sort()
|
||||
return pmc_list
|
||||
|
||||
|
||||
def perfmon_coalesce(pmc_files_list, soc, workload_dir):
|
||||
workload_perfmon_dir = workload_dir + "/perfmon"
|
||||
|
||||
# match pattern for pmc counters
|
||||
mpattern = r"^pmc:(.*)"
|
||||
pmc_list = dict(
|
||||
[
|
||||
("SQ", []),
|
||||
("GRBM", []),
|
||||
("TCP", []),
|
||||
("TA", []),
|
||||
("TD", []),
|
||||
("TCC", []),
|
||||
("SPI", []),
|
||||
("CPC", []),
|
||||
("CPF", []),
|
||||
("GDS", []),
|
||||
("TCC2", {}), # per-channel TCC perfmon
|
||||
]
|
||||
)
|
||||
for ch in range(perfmon_config[soc]["TCC_channels"]):
|
||||
pmc_list["TCC2"][str(ch)] = []
|
||||
|
||||
# Extract all PMC counters and store in separate buckets
|
||||
for fname in pmc_files_list:
|
||||
lines = open(fname, "r").read().splitlines()
|
||||
|
||||
for line in lines:
|
||||
# Strip all comements, skip empty lines
|
||||
stext = line.split("#")[0].strip()
|
||||
if not stext:
|
||||
continue
|
||||
|
||||
# all pmc counters start with "pmc:"
|
||||
m = re.match(mpattern, stext)
|
||||
if m is None:
|
||||
continue
|
||||
|
||||
# we have found all the counters, store them in buckets
|
||||
counters = m.group(1).split()
|
||||
|
||||
# Utilitze helper function once a list of counters has be extracted
|
||||
save_file = True
|
||||
pmc_list = update_pmc_bucket(
|
||||
counters, save_file, soc, pmc_list, stext, workload_perfmon_dir
|
||||
)
|
||||
|
||||
# add a timestamp file
|
||||
fd = open(workload_perfmon_dir + "/timestamps.txt", "w")
|
||||
fd.write("pmc:\n\n")
|
||||
fd.write("gpu:\n")
|
||||
fd.write("range:\n")
|
||||
fd.write("kernel:\n")
|
||||
fd.close()
|
||||
|
||||
# sort the per channel counter, so that same counter in all channels can be aligned
|
||||
for ch in range(perfmon_config[soc]["TCC_channels"]):
|
||||
pmc_list["TCC2"][str(ch)].sort()
|
||||
|
||||
return pmc_list
|
||||
|
||||
|
||||
def perfmon_emit(pmc_list, soc, workload_dir=None):
|
||||
# Calculate the minimum number of iteration to save the pmc counters
|
||||
# non-TCC counters
|
||||
pmc_cnt = [
|
||||
len(pmc_list[key]) / perfmon_config[soc][key]
|
||||
for key in pmc_list
|
||||
if key not in ["TCC", "TCC2"]
|
||||
]
|
||||
|
||||
# TCC counters
|
||||
tcc_channels = perfmon_config[soc]["TCC_channels"]
|
||||
|
||||
tcc_cnt = len(pmc_list["TCC"]) / perfmon_config[soc]["TCC"]
|
||||
tcc2_cnt = (
|
||||
np.array([len(pmc_list["TCC2"][str(ch)]) for ch in range(tcc_channels)])
|
||||
/ perfmon_config[soc]["TCC"]
|
||||
)
|
||||
|
||||
# Total number iterations to write pmc: counters line
|
||||
niter = max(math.ceil(max(pmc_cnt)), math.ceil(tcc_cnt) + math.ceil(max(tcc2_cnt)))
|
||||
|
||||
# Emit PMC counters into pmc config file
|
||||
if workload_dir:
|
||||
workload_perfmon_dir = workload_dir + "/perfmon"
|
||||
fd = open(workload_perfmon_dir + "/pmc_perf.txt", "w")
|
||||
else:
|
||||
batches = []
|
||||
|
||||
tcc2_index = 0
|
||||
for iter in range(niter):
|
||||
# Prefix
|
||||
line = "pmc: "
|
||||
|
||||
# Add all non-TCC counters
|
||||
for key in pmc_list:
|
||||
if key not in ["TCC", "TCC2"]:
|
||||
N = perfmon_config[soc][key]
|
||||
ip_counters = pmc_list[key][iter * N : iter * N + N]
|
||||
if ip_counters:
|
||||
line = line + " " + " ".join(ip_counters)
|
||||
|
||||
# Add TCC counters
|
||||
N = perfmon_config[soc]["TCC"]
|
||||
tcc_counters = pmc_list["TCC"][iter * N : iter * N + N]
|
||||
|
||||
if not tcc_counters:
|
||||
# TCC per-channel counters
|
||||
for ch in range(perfmon_config[soc]["TCC_channels"]):
|
||||
tcc_counters += pmc_list["TCC2"][str(ch)][
|
||||
tcc2_index * N : tcc2_index * N + N
|
||||
]
|
||||
|
||||
tcc2_index += 1
|
||||
|
||||
# TCC aggregated counters
|
||||
line = line + " " + " ".join(tcc_counters)
|
||||
if workload_dir:
|
||||
fd.write(line + "\n")
|
||||
else:
|
||||
b = line.split()
|
||||
b.remove("pmc:")
|
||||
batches.append(b)
|
||||
|
||||
if workload_dir:
|
||||
fd.write("\ngpu:\n")
|
||||
fd.write("range:\n")
|
||||
fd.write("kernel:\n")
|
||||
fd.close()
|
||||
else:
|
||||
return batches
|
||||
|
||||
|
||||
def perfmon_filter(workload_dir, perfmon_dir, args):
|
||||
workload_perfmon_dir = workload_dir + "/perfmon"
|
||||
soc = args.target
|
||||
|
||||
# Initialize directories
|
||||
# TODO: Modify this so that data is appended to previous?
|
||||
if not os.path.isdir(workload_dir):
|
||||
os.makedirs(workload_dir)
|
||||
else:
|
||||
shutil.rmtree(workload_dir)
|
||||
|
||||
os.makedirs(workload_perfmon_dir)
|
||||
|
||||
ref_pmc_files_list = glob.glob(perfmon_dir + "/" + "pmc_*perf*.txt")
|
||||
ref_pmc_files_list += glob.glob(perfmon_dir + "/" + soc + "/pmc_*_perf*.txt")
|
||||
|
||||
# Perfmon list filtering
|
||||
if args.ipblocks != None:
|
||||
for i in range(len(args.ipblocks)):
|
||||
args.ipblocks[i] = args.ipblocks[i].lower()
|
||||
mpattern = "pmc_([a-zA-Z0-9_]+)_perf*"
|
||||
|
||||
pmc_files_list = []
|
||||
for fname in ref_pmc_files_list:
|
||||
fbase = os.path.splitext(os.path.basename(fname))[0]
|
||||
ip = re.match(mpattern, fbase).group(1)
|
||||
if ip in args.ipblocks:
|
||||
pmc_files_list.append(fname)
|
||||
print("fname: " + fbase + ": Added")
|
||||
else:
|
||||
print("fname: " + fbase + ": Skipped")
|
||||
|
||||
else:
|
||||
# default: take all perfmons
|
||||
pmc_files_list = ref_pmc_files_list
|
||||
|
||||
# Coalesce and writeback workload specific perfmon
|
||||
pmc_list = perfmon_coalesce(pmc_files_list, soc, workload_dir)
|
||||
perfmon_emit(pmc_list, soc, workload_dir)
|
||||
|
||||
|
||||
def pmc_filter(workload_dir, perfmon_dir, soc):
|
||||
workload_perfmon_dir = workload_dir + "/perfmon"
|
||||
|
||||
if not os.path.isdir(workload_perfmon_dir):
|
||||
os.makedirs(workload_perfmon_dir)
|
||||
else:
|
||||
shutil.rmtree(workload_perfmon_dir)
|
||||
|
||||
ref_pmc_files_list = glob.glob(perfmon_dir + "/roofline/" + "pmc_roof_perf.txt")
|
||||
# ref_pmc_files_list += glob.glob(perfmon_dir + "/" + soc + "/pmc_*_perf*.txt")
|
||||
|
||||
pmc_files_list = ref_pmc_files_list
|
||||
|
||||
# Coalesce and writeback workload specific perfmon
|
||||
pmc_list = perfmon_coalesce(pmc_files_list, soc, workload_dir)
|
||||
perfmon_emit(pmc_list, soc, workload_dir)
|
||||
@@ -0,0 +1,533 @@
|
||||
##############################################################################bl
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2021 - 2023 Advanced Micro Devices, Inc. All Rights Reserved.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
##############################################################################el
|
||||
|
||||
import sys
|
||||
|
||||
from dataclasses import dataclass
|
||||
import csv
|
||||
|
||||
################################################
|
||||
# Global vars
|
||||
################################################
|
||||
|
||||
IMGNAME = "empirRoof"
|
||||
|
||||
L2_BANKS = 32 # default assuming mi200
|
||||
|
||||
XMIN = 0.01
|
||||
XMAX = 1000
|
||||
|
||||
FONT_SIZE = 16
|
||||
FONT_COLOR = "black"
|
||||
FONT_WEIGHT = "bold"
|
||||
|
||||
SUPPORTED_SOC = ["mi200"]
|
||||
|
||||
TOP_N = 10
|
||||
|
||||
|
||||
################################################
|
||||
# Helper funcs
|
||||
################################################
|
||||
@dataclass
|
||||
class AI_Data:
|
||||
KernelName: str
|
||||
numCalls: float
|
||||
|
||||
total_flops: float
|
||||
valu_flops: float
|
||||
mfma_flops_f16: float
|
||||
mfma_flops_bf16: float
|
||||
mfma_flops_f32: float
|
||||
mfma_flops_f64: float
|
||||
mfma_iops_i8: float
|
||||
lds_data: float
|
||||
L1cache_data: float
|
||||
L2cache_data: float
|
||||
hbm_data: float
|
||||
|
||||
totalDuration: float
|
||||
avgDuration: float
|
||||
|
||||
|
||||
def get_font():
|
||||
return {
|
||||
"size": FONT_SIZE,
|
||||
"color": FONT_COLOR,
|
||||
"weight": FONT_WEIGHT,
|
||||
"family": "serif",
|
||||
}
|
||||
|
||||
|
||||
def get_color(catagory):
|
||||
if catagory == "ai_l1":
|
||||
return "green"
|
||||
elif catagory == "ai_l2":
|
||||
return "blue"
|
||||
elif catagory == "ai_hbm":
|
||||
return "red"
|
||||
else:
|
||||
raise RuntimeError("Invalid catagory passed to get_color()")
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------------
|
||||
# Plot BW at each cache level
|
||||
# -------------------------------------------------------------------------------------
|
||||
def calc_ceilings(roof_specs, benchmark_data, targ_mem_level, verbose):
|
||||
"""Given benchmarking data, calculate ceilings (or peak performance) for empirical roofline
|
||||
"""
|
||||
# TODO: This is where filtering by memory level will need to occur for standalone
|
||||
graphPoints = {"hbm": [], "l2": [], "l1": [], "lds": [], "valu": [], "mfma": []}
|
||||
|
||||
if targ_mem_level == "ALL":
|
||||
cacheHierarchy = ["HBM", "L2", "L1", "LDS"]
|
||||
else:
|
||||
cacheHierarchy = targ_mem_level
|
||||
|
||||
x1 = y1 = x2 = y2 = -1
|
||||
x1_mfma = y1_mfma = x2_mfma = y2_mfma = -1
|
||||
target_precision = roof_specs["dtype"][2:]
|
||||
|
||||
if roof_specs["dtype"] != "FP16" and roof_specs["dtype"] != "I8":
|
||||
peakOps = float(
|
||||
benchmark_data[roof_specs["dtype"] + "Flops"][roof_specs["device"]]
|
||||
)
|
||||
for i in range(0, len(cacheHierarchy)):
|
||||
# Plot BW line
|
||||
if verbose >= 3:
|
||||
print("Current cache level is ", cacheHierarchy[i])
|
||||
curr_bw = cacheHierarchy[i] + "Bw"
|
||||
peakBw = float(benchmark_data[curr_bw][roof_specs["device"]])
|
||||
|
||||
if roof_specs["dtype"] == "I8":
|
||||
peakMFMA = float(benchmark_data["MFMAI8Ops"][roof_specs["device"]])
|
||||
else:
|
||||
peakMFMA = float(
|
||||
benchmark_data["MFMAF{}Flops".format(target_precision)][roof_specs["device"]]
|
||||
)
|
||||
|
||||
x1 = float(XMIN)
|
||||
y1 = float(XMIN) * peakBw
|
||||
# Note: No reg peakOps for FP16 or INT8
|
||||
if roof_specs["dtype"] != "FP16" and roof_specs["dtype"] != "I8":
|
||||
x2 = peakOps / peakBw
|
||||
y2 = peakOps
|
||||
|
||||
# Plot MFMA lines (NOTE: Assuming MI200 soc)
|
||||
x1_mfma = peakOps / peakBw
|
||||
y1_mfma = peakOps
|
||||
|
||||
x2_mfma = peakMFMA / peakBw
|
||||
y2_mfma = peakMFMA
|
||||
|
||||
# These are the points to use:
|
||||
if verbose >= 3:
|
||||
print("x = [{}, {}]".format(x1, x2_mfma))
|
||||
print("y = [{}, {}]".format(y1, y2_mfma))
|
||||
|
||||
graphPoints[cacheHierarchy[i].lower()].append([x1, x2_mfma])
|
||||
graphPoints[cacheHierarchy[i].lower()].append([y1, y2_mfma])
|
||||
graphPoints[cacheHierarchy[i].lower()].append(peakBw)
|
||||
|
||||
# -------------------------------------------------------------------------------------
|
||||
# Plot computing roof
|
||||
# -------------------------------------------------------------------------------------
|
||||
# Note: No FMA roof for FP16 or INT8
|
||||
if roof_specs["dtype"] != "FP16" and roof_specs["dtype"] != "I8":
|
||||
# Plot FMA roof
|
||||
x0 = XMAX
|
||||
if x2 < x0:
|
||||
x0 = x2
|
||||
|
||||
if verbose >= 3:
|
||||
print("FMA ROOF [{}, {}], [{},{}]".format(x0, XMAX, peakOps, peakOps))
|
||||
graphPoints["valu"].append([x0, XMAX])
|
||||
graphPoints["valu"].append([peakOps, peakOps])
|
||||
graphPoints["valu"].append(peakOps)
|
||||
|
||||
# Plot MFMA roof
|
||||
if (
|
||||
x1_mfma != -1 or roof_specs["dtype"] == "FP16" or roof_specs["dtype"] == "I8"
|
||||
): # assert that mfma has been assigned
|
||||
x0_mfma = XMAX
|
||||
if x2_mfma < x0_mfma:
|
||||
x0_mfma = x2_mfma
|
||||
|
||||
if verbose >= 3:
|
||||
print("MFMA ROOF [{}, {}], [{},{}]".format(x0_mfma, XMAX, peakMFMA, peakMFMA))
|
||||
graphPoints["mfma"].append([x0_mfma, XMAX])
|
||||
graphPoints["mfma"].append([peakMFMA, peakMFMA])
|
||||
graphPoints["mfma"].append(peakMFMA)
|
||||
|
||||
return graphPoints
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------------
|
||||
# Overlay application performance
|
||||
# -------------------------------------------------------------------------------------
|
||||
# Calculate relevent metrics for ai calculation
|
||||
def calc_ai(sort_type, ret_df, verbose):
|
||||
"""Given counter data, caclulate arithmetic intensity for each kernel in the application.
|
||||
"""
|
||||
df = ret_df["pmc_perf"]
|
||||
# Sort by top kernels or top dispatches?
|
||||
df = df.sort_values(by=["KernelName"])
|
||||
df = df.reset_index(drop=True)
|
||||
|
||||
total_flops = (
|
||||
valu_flops
|
||||
) = (
|
||||
mfma_flops_bf16
|
||||
) = (
|
||||
mfma_flops_f16
|
||||
) = (
|
||||
mfma_iops_i8
|
||||
) = (
|
||||
mfma_flops_f32
|
||||
) = (
|
||||
mfma_flops_f64
|
||||
) = (
|
||||
lds_data
|
||||
) = L1cache_data = L2cache_data = hbm_data = calls = totalDuration = avgDuration = 0.0
|
||||
|
||||
kernelName = ""
|
||||
|
||||
myList = []
|
||||
at_end = False
|
||||
next_kernelName = ""
|
||||
|
||||
for idx in df.index:
|
||||
# CASE: Top kernels
|
||||
# Calculate + append AI data if
|
||||
# a) current KernelName is different than previous OR
|
||||
# b) We've reached the end of list
|
||||
if idx + 1 == df.shape[0]:
|
||||
at_end = True
|
||||
else:
|
||||
next_kernelName = df["KernelName"][idx + 1]
|
||||
|
||||
kernelName = df["KernelName"][idx]
|
||||
try:
|
||||
total_flops += (
|
||||
(
|
||||
64
|
||||
* (
|
||||
df["SQ_INSTS_VALU_ADD_F16"][idx]
|
||||
+ df["SQ_INSTS_VALU_MUL_F16"][idx]
|
||||
+ (2 * df["SQ_INSTS_VALU_FMA_F16"][idx])
|
||||
+ df["SQ_INSTS_VALU_TRANS_F16"][idx]
|
||||
)
|
||||
)
|
||||
+ (
|
||||
64
|
||||
* (
|
||||
df["SQ_INSTS_VALU_ADD_F32"][idx]
|
||||
+ df["SQ_INSTS_VALU_MUL_F32"][idx]
|
||||
+ (2 * df["SQ_INSTS_VALU_FMA_F32"][idx])
|
||||
+ df["SQ_INSTS_VALU_TRANS_F32"][idx]
|
||||
)
|
||||
)
|
||||
+ (
|
||||
64
|
||||
* (
|
||||
df["SQ_INSTS_VALU_ADD_F64"][idx]
|
||||
+ df["SQ_INSTS_VALU_MUL_F64"][idx]
|
||||
+ (2 * df["SQ_INSTS_VALU_FMA_F64"][idx])
|
||||
+ df["SQ_INSTS_VALU_TRANS_F64"][idx]
|
||||
)
|
||||
)
|
||||
+ (df["SQ_INSTS_VALU_MFMA_MOPS_F16"][idx] * 512)
|
||||
+ (df["SQ_INSTS_VALU_MFMA_MOPS_BF16"][idx] * 512)
|
||||
+ (df["SQ_INSTS_VALU_MFMA_MOPS_F32"][idx] * 512)
|
||||
+ (df["SQ_INSTS_VALU_MFMA_MOPS_F64"][idx] * 512)
|
||||
)
|
||||
except KeyError:
|
||||
if verbose >= 3:
|
||||
print("{}: Skipped total_flops at index {}".format(kernelName[:35], idx))
|
||||
pass
|
||||
try:
|
||||
valu_flops += (
|
||||
64
|
||||
* (
|
||||
df["SQ_INSTS_VALU_ADD_F16"][idx]
|
||||
+ df["SQ_INSTS_VALU_MUL_F16"][idx]
|
||||
+ (2 * df["SQ_INSTS_VALU_FMA_F16"][idx])
|
||||
+ df["SQ_INSTS_VALU_TRANS_F16"][idx]
|
||||
)
|
||||
+ 64
|
||||
* (
|
||||
df["SQ_INSTS_VALU_ADD_F32"][idx]
|
||||
+ df["SQ_INSTS_VALU_MUL_F32"][idx]
|
||||
+ (2 * df["SQ_INSTS_VALU_FMA_F32"][idx])
|
||||
+ df["SQ_INSTS_VALU_TRANS_F32"][idx]
|
||||
)
|
||||
+ 64
|
||||
* (
|
||||
df["SQ_INSTS_VALU_ADD_F64"][idx]
|
||||
+ df["SQ_INSTS_VALU_MUL_F64"][idx]
|
||||
+ (2 * df["SQ_INSTS_VALU_FMA_F64"][idx])
|
||||
+ df["SQ_INSTS_VALU_TRANS_F64"][idx]
|
||||
)
|
||||
)
|
||||
except KeyError:
|
||||
if verbose >= 3:
|
||||
print("{}: Skipped valu_flops at index {}".format(kernelName[:35], idx))
|
||||
pass
|
||||
|
||||
try:
|
||||
mfma_flops_f16 += df["SQ_INSTS_VALU_MFMA_MOPS_F16"][idx] * 512
|
||||
mfma_flops_bf16 += df["SQ_INSTS_VALU_MFMA_MOPS_BF16"][idx] * 512
|
||||
mfma_flops_f32 += df["SQ_INSTS_VALU_MFMA_MOPS_F32"][idx] * 512
|
||||
mfma_flops_f64 += df["SQ_INSTS_VALU_MFMA_MOPS_F64"][idx] * 512
|
||||
mfma_iops_i8 += df["SQ_INSTS_VALU_MFMA_MOPS_I8"][idx] * 512
|
||||
except KeyError:
|
||||
if verbose >= 3:
|
||||
print("{}: Skipped mfma ops at index {}".format(kernelName[:35], idx))
|
||||
pass
|
||||
|
||||
try:
|
||||
lds_data += (
|
||||
(df["SQ_LDS_IDX_ACTIVE"][idx] - df["SQ_LDS_BANK_CONFLICT"][idx])
|
||||
* 4
|
||||
* L2_BANKS
|
||||
) # L2_BANKS = 32 (since assuming mi200)
|
||||
except KeyError:
|
||||
if verbose >= 3:
|
||||
print("{}: Skipped lds_data at index {}".format(kernelName[:35], idx))
|
||||
pass
|
||||
|
||||
try:
|
||||
L1cache_data += df["TCP_TOTAL_CACHE_ACCESSES_sum"][idx] * 64
|
||||
except KeyError:
|
||||
if verbose >= 3:
|
||||
print("{}: Skipped L1cache_data at index {}".format(kernelName[:35], idx))
|
||||
pass
|
||||
|
||||
try:
|
||||
L2cache_data += (
|
||||
df["TCP_TCC_WRITE_REQ_sum"][idx] * 64
|
||||
+ df["TCP_TCC_ATOMIC_WITH_RET_REQ_sum"][idx] * 64
|
||||
+ df["TCP_TCC_ATOMIC_WITHOUT_RET_REQ_sum"][idx] * 64
|
||||
+ df["TCP_TCC_READ_REQ_sum"][idx] * 64
|
||||
)
|
||||
except KeyError:
|
||||
if verbose >= 3:
|
||||
print("{}: Skipped L2cache_data at index {}".format(kernelName[:35], idx))
|
||||
pass
|
||||
try:
|
||||
hbm_data += (
|
||||
(df["TCC_EA_RDREQ_32B_sum"][idx] * 32)
|
||||
+ ((df["TCC_EA_RDREQ_sum"][idx] - df["TCC_EA_RDREQ_32B_sum"][idx]) * 64)
|
||||
+ (df["TCC_EA_WRREQ_64B_sum"][idx] * 64)
|
||||
+ ((df["TCC_EA_WRREQ_sum"][idx] - df["TCC_EA_WRREQ_64B_sum"][idx]) * 32)
|
||||
)
|
||||
except KeyError:
|
||||
if verbose >= 3:
|
||||
print("{}: Skipped hbm_data at index {}".format(kernelName[:35], idx))
|
||||
pass
|
||||
|
||||
totalDuration += df["EndNs"][idx] - df["BeginNs"][idx]
|
||||
avgDuration += df["EndNs"][idx] - df["BeginNs"][idx]
|
||||
|
||||
calls += 1
|
||||
|
||||
if sort_type == "kernels" and (at_end == True or (kernelName != next_kernelName)):
|
||||
myList.append(
|
||||
AI_Data(
|
||||
kernelName,
|
||||
calls,
|
||||
total_flops / calls,
|
||||
valu_flops / calls,
|
||||
mfma_flops_f16 / calls,
|
||||
mfma_flops_bf16 / calls,
|
||||
mfma_flops_f32 / calls,
|
||||
mfma_flops_f64 / calls,
|
||||
mfma_iops_i8 / calls,
|
||||
lds_data / calls,
|
||||
L1cache_data / calls,
|
||||
L2cache_data / calls,
|
||||
hbm_data / calls,
|
||||
totalDuration,
|
||||
avgDuration / calls,
|
||||
)
|
||||
)
|
||||
if verbose >= 2:
|
||||
print(
|
||||
"Just added {} to AI_Data at index {}. # of calls: {}".format(
|
||||
kernelName, idx, calls
|
||||
)
|
||||
)
|
||||
total_flops = (
|
||||
valu_flops
|
||||
) = (
|
||||
mfma_flops_bf16
|
||||
) = (
|
||||
mfma_flops_f16
|
||||
) = (
|
||||
mfma_iops_i8
|
||||
) = (
|
||||
mfma_flops_f32
|
||||
) = (
|
||||
mfma_flops_f64
|
||||
) = (
|
||||
lds_data
|
||||
) = (
|
||||
L1cache_data
|
||||
) = L2cache_data = hbm_data = calls = totalDuration = avgDuration = 0.0
|
||||
|
||||
if sort_type == "dispatches":
|
||||
myList.append(
|
||||
AI_Data(
|
||||
kernelName,
|
||||
calls,
|
||||
total_flops,
|
||||
valu_flops,
|
||||
mfma_flops_f16,
|
||||
mfma_flops_bf16,
|
||||
mfma_flops_f32,
|
||||
mfma_flops_f64,
|
||||
mfma_iops_i8,
|
||||
lds_data,
|
||||
L1cache_data,
|
||||
L2cache_data,
|
||||
hbm_data,
|
||||
totalDuration,
|
||||
avgDuration,
|
||||
)
|
||||
)
|
||||
total_flops = (
|
||||
valu_flops
|
||||
) = (
|
||||
mfma_flops_bf16
|
||||
) = (
|
||||
mfma_flops_f16
|
||||
) = (
|
||||
mfma_iops_i8
|
||||
) = (
|
||||
mfma_flops_f32
|
||||
) = (
|
||||
mfma_flops_f64
|
||||
) = (
|
||||
lds_data
|
||||
) = (
|
||||
L1cache_data
|
||||
) = L2cache_data = hbm_data = calls = totalDuration = avgDuration = 0.0
|
||||
|
||||
myList.sort(key=lambda x: x.totalDuration, reverse=True)
|
||||
|
||||
# print("Top 5 intensities ('{}')...".format(roof_details["sort"]))
|
||||
intensities = {"ai_l1": [], "ai_l2": [], "ai_hbm": []}
|
||||
curr_perf = []
|
||||
kernelNames = []
|
||||
i = 0
|
||||
# Create list of top 5 intensities
|
||||
while i < TOP_N and i != len(myList):
|
||||
kernelNames.append(myList[i].KernelName)
|
||||
intensities["ai_l1"].append(
|
||||
myList[i].total_flops / myList[i].L1cache_data
|
||||
) if myList[i].L1cache_data else intensities["ai_l1"].append(0)
|
||||
# print("cur_ai_L1", myList[i].total_flops/myList[i].L1cache_data) if myList[i].L1cache_data else print("null")
|
||||
# print()
|
||||
intensities["ai_l2"].append(
|
||||
myList[i].total_flops / myList[i].L2cache_data
|
||||
) if myList[i].L2cache_data else intensities["ai_l2"].append(0)
|
||||
# print("cur_ai_L2", myList[i].total_flops/myList[i].L2cache_data) if myList[i].L2cache_data else print("null")
|
||||
# print()
|
||||
intensities["ai_hbm"].append(
|
||||
myList[i].total_flops / myList[i].hbm_data
|
||||
) if myList[i].hbm_data else intensities["ai_hbm"].append(0)
|
||||
# print("cur_ai_hbm", myList[i].total_flops/myList[i].hbm_data) if myList[i].hbm_data else print("null")
|
||||
# print()
|
||||
curr_perf.append(myList[i].total_flops / myList[i].avgDuration) if myList[
|
||||
i
|
||||
].avgDuration else curr_perf.append(0)
|
||||
# print("cur_perf", myList[i].total_flops/myList[i].avgDuration) if myList[i].avgDuration else print("null")
|
||||
|
||||
i += 1
|
||||
|
||||
intensityPoints = {"ai_l1": [], "ai_l2": [], "ai_hbm": []}
|
||||
|
||||
for i in intensities:
|
||||
values = intensities[i]
|
||||
|
||||
color = get_color(i)
|
||||
x = []
|
||||
y = []
|
||||
for entryIndx in range(0, len(values)):
|
||||
x.append(values[entryIndx])
|
||||
y.append(curr_perf[entryIndx])
|
||||
|
||||
intensityPoints[i].append(x)
|
||||
intensityPoints[i].append(y)
|
||||
|
||||
# Add an entry for kernel names
|
||||
intensityPoints["kernelNames"] = kernelNames
|
||||
|
||||
return intensityPoints
|
||||
|
||||
|
||||
def constuct_roof(roof_specs, targ_mem_level, verbose):
|
||||
benchmark_results = roof_specs["path"] + "/roofline.csv"
|
||||
# -----------------------------------------------------
|
||||
# Initialize roofline data dictionary from roofline.csv
|
||||
# -----------------------------------------------------
|
||||
benchmark_data = (
|
||||
{}
|
||||
) # TODO: consider changing this to an ordered dict for consistency over py versions
|
||||
headers = []
|
||||
try:
|
||||
with open(benchmark_results, "r") as csvfile:
|
||||
csvReader = csv.reader(csvfile, delimiter=",")
|
||||
rowCount = 0
|
||||
for row in csvReader:
|
||||
row.pop(0) # remove devID
|
||||
if rowCount == 0:
|
||||
headers = row
|
||||
for i in headers:
|
||||
benchmark_data[i] = []
|
||||
else:
|
||||
for i, key in enumerate(headers):
|
||||
benchmark_data[key].append(row[i])
|
||||
|
||||
rowCount += 1
|
||||
csvfile.close()
|
||||
except:
|
||||
graphPoints = {
|
||||
"hbm": [None, None, None],
|
||||
"l2": [None, None, None],
|
||||
"l1": [None, None, None],
|
||||
"lds": [None, None, None],
|
||||
"valu": [None, None, None],
|
||||
"mfma": [None, None, None],
|
||||
}
|
||||
return graphPoints
|
||||
|
||||
# ------------------
|
||||
# Generate Roofline
|
||||
# ------------------
|
||||
results = calc_ceilings(roof_specs, benchmark_data, targ_mem_level, verbose)
|
||||
# for key in results:
|
||||
# print(key, "->", results[key])
|
||||
|
||||
return results
|
||||
+367
-1
@@ -23,6 +23,21 @@
|
||||
##############################################################################el
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import subprocess
|
||||
import shutil
|
||||
import os
|
||||
import io
|
||||
import selectors
|
||||
import pandas as pd
|
||||
import glob
|
||||
from utils import specs
|
||||
from datetime import datetime
|
||||
from pathlib import Path as path
|
||||
import config
|
||||
|
||||
|
||||
rocprof_cmd = ""
|
||||
|
||||
def demarcate(function):
|
||||
def wrap_function(*args, **kwargs):
|
||||
@@ -33,4 +48,355 @@ def demarcate(function):
|
||||
return wrap_function
|
||||
|
||||
def trace_logger(message, *args, **kwargs):
|
||||
logging.log(logging.TRACE, message, *args, **kwargs)
|
||||
logging.log(logging.TRACE, message, *args, **kwargs)
|
||||
|
||||
def get_version(omniperf_home) -> dict:
|
||||
"""Return Omniperf versioning info
|
||||
"""
|
||||
# symantic version info
|
||||
version = os.path.join(omniperf_home.parent, "VERSION")
|
||||
try:
|
||||
with open(version, "r") as file:
|
||||
VER = file.read().replace("\n", "")
|
||||
except EnvironmentError:
|
||||
logging.critical("ERROR: Cannot find VERSION file at {}".format(version))
|
||||
sys.exit(1)
|
||||
|
||||
# git version info
|
||||
gitDir = os.path.join(omniperf_home.parent, ".git")
|
||||
if (shutil.which("git") is not None) and os.path.exists(gitDir):
|
||||
gitQuery = subprocess.run(
|
||||
["git", "log", "--pretty=format:%h", "-n", "1"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
if gitQuery.returncode != 0:
|
||||
SHA = "unknown"
|
||||
MODE = "unknown"
|
||||
else:
|
||||
SHA = gitQuery.stdout.decode("utf-8")
|
||||
MODE = "dev"
|
||||
else:
|
||||
shaFile = os.path.join(omniperf_home.parent, "VERSION.sha")
|
||||
try:
|
||||
with open(shaFile, "r") as file:
|
||||
SHA = file.read().replace("\n", "")
|
||||
except EnvironmentError:
|
||||
logging.error("ERROR: Cannot find VERSION.sha file at {}".format(shaFile))
|
||||
sys.exit(1)
|
||||
|
||||
MODE = "release"
|
||||
|
||||
versionData = {"version": VER, "sha": SHA, "mode": MODE}
|
||||
return versionData
|
||||
|
||||
def get_version_display(version, sha, mode):
|
||||
"""Pretty print versioning info
|
||||
"""
|
||||
buf = io.StringIO()
|
||||
print("-" * 40, file=buf)
|
||||
print("Omniperf version: %s (%s)" % (version, mode), file=buf)
|
||||
print("Git revision: %s" % sha, file=buf)
|
||||
print("-" * 40, file=buf)
|
||||
return buf.getvalue()
|
||||
|
||||
def detect_rocprof():
|
||||
"""Detect loaded rocprof version. Resolve path and set cmd globally.
|
||||
"""
|
||||
global rocprof_cmd
|
||||
# rocprof info
|
||||
if not "ROCPROF" in os.environ.keys():
|
||||
rocprof_cmd = "rocprofv2"
|
||||
else:
|
||||
rocprof_cmd = os.environ["ROCPROF"]
|
||||
rocprof_path = shutil.which(rocprof_cmd)
|
||||
|
||||
# TODO: this could be more elegant, clean code later
|
||||
if not rocprof_path:
|
||||
rocprof_cmd = "rocprof"
|
||||
rocprof_path = shutil.which(rocprof_cmd)
|
||||
|
||||
if not rocprof_path:
|
||||
logging.error("\nError: Unable to resolve path to %s binary" % rocprof_cmd)
|
||||
logging.error(
|
||||
"Please verify installation or set ROCPROF environment variable with full path."
|
||||
)
|
||||
sys.exit(1)
|
||||
else:
|
||||
# Resolve any sym links in file path
|
||||
rocprof_path = os.path.realpath(rocprof_path.rstrip("\n"))
|
||||
logging.info("ROC Profiler: " + str(rocprof_path))
|
||||
return rocprof_cmd #TODO: Do we still need to return this? It's not being used in the function call
|
||||
|
||||
def capture_subprocess_output(subprocess_args):
|
||||
"""Run specified subprocess and concurrently capture output
|
||||
"""
|
||||
# Start subprocess
|
||||
# bufsize = 1 means output is line buffered
|
||||
# universal_newlines = True is required for line buffering
|
||||
process = subprocess.Popen(subprocess_args,
|
||||
bufsize=1,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
universal_newlines=True)
|
||||
|
||||
# Create callback function for process output
|
||||
buf = io.StringIO()
|
||||
def handle_output(stream, mask):
|
||||
# Because the process' output is line buffered, there's only ever one
|
||||
# line to read when this function is called
|
||||
line = stream.readline()
|
||||
buf.write(line)
|
||||
sys.stdout.write(line)
|
||||
|
||||
# Register callback for an "available for read" event from subprocess' stdout stream
|
||||
selector = selectors.DefaultSelector()
|
||||
selector.register(process.stdout, selectors.EVENT_READ, handle_output)
|
||||
|
||||
# Loop until subprocess is terminated
|
||||
while process.poll() is None:
|
||||
# Wait for events and handle them with their registered callbacks
|
||||
events = selector.select()
|
||||
for key, mask in events:
|
||||
callback = key.data
|
||||
callback(key.fileobj, mask)
|
||||
|
||||
# Get process return code
|
||||
return_code = process.wait()
|
||||
selector.close()
|
||||
|
||||
success = (return_code == 0)
|
||||
|
||||
# Store buffered output
|
||||
output = buf.getvalue()
|
||||
buf.close()
|
||||
|
||||
return (success, output)
|
||||
|
||||
def run_prof(fname, workload_dir, perfmon_dir, cmd, target, verbose):
|
||||
|
||||
fbase = os.path.splitext(os.path.basename(fname))[0]
|
||||
|
||||
logging.debug("pmc file:", os.path.basename(fname))
|
||||
|
||||
# profile the app (run w/ custom config files for mi100)
|
||||
if target == "mi100":
|
||||
logging.info("RUNNING WITH CUSTOM METRICS")
|
||||
success, output = capture_subprocess_output(
|
||||
[
|
||||
rocprof_cmd,
|
||||
"-i",
|
||||
fname,
|
||||
"-m",
|
||||
perfmon_dir + "/" + "metrics.xml",
|
||||
"--timestamp",
|
||||
"on",
|
||||
"-o",
|
||||
workload_dir + "/" + fbase + ".csv",
|
||||
'"' + cmd + '"',
|
||||
]
|
||||
)
|
||||
else:
|
||||
success, output = capture_subprocess_output(
|
||||
[
|
||||
rocprof_cmd,
|
||||
"-i",
|
||||
fname,
|
||||
"--timestamp",
|
||||
"on",
|
||||
"-o",
|
||||
workload_dir + "/" + fbase + ".csv",
|
||||
'"' + cmd + '"',
|
||||
]
|
||||
)
|
||||
# write rocprof output to logging
|
||||
logging.info(output)
|
||||
|
||||
def replace_timestamps(workload_dir):
|
||||
df_stamps = pd.read_csv(workload_dir + "/timestamps.csv")
|
||||
if "BeginNs" in df_stamps.columns and "EndNs" in df_stamps.columns:
|
||||
# Update timestamps for all *.csv output files
|
||||
for fname in glob.glob(workload_dir + "/" + "*.csv"):
|
||||
df_pmc_perf = pd.read_csv(fname)
|
||||
|
||||
df_pmc_perf["BeginNs"] = df_stamps["BeginNs"]
|
||||
df_pmc_perf["EndNs"] = df_stamps["EndNs"]
|
||||
df_pmc_perf.to_csv(fname, index=False)
|
||||
else:
|
||||
warning = "WARNING: Incomplete profiling data detected. Unable to update timestamps."
|
||||
logging.warning(warning + "\n")
|
||||
|
||||
def gen_sysinfo(workload_name, workload_dir, ip_blocks, app_cmd, skip_roof):
|
||||
# Record system information
|
||||
mspec = specs.get_machine_specs(0)
|
||||
sysinfo = open(workload_dir + "/" + "sysinfo.csv", "w")
|
||||
|
||||
# write header
|
||||
header = "workload_name,"
|
||||
header += "command,"
|
||||
header += "host_name,host_cpu,host_distro,host_kernel,host_rocmver,date,"
|
||||
header += "gpu_soc,numSE,numCU,numSIMD,waveSize,maxWavesPerCU,maxWorkgroupSize,"
|
||||
header += "L1,L2,sclk,mclk,cur_sclk,cur_mclk,L2Banks,LDSBanks,name,numSQC,hbmBW,"
|
||||
header += "ip_blocks\n"
|
||||
sysinfo.write(header)
|
||||
|
||||
# timestamp
|
||||
now = datetime.now()
|
||||
local_now = now.astimezone()
|
||||
local_tz = local_now.tzinfo
|
||||
local_tzname = local_tz.tzname(local_now)
|
||||
timestamp = now.strftime("%c") + " (" + local_tzname + ")"
|
||||
# host info
|
||||
param = [workload_name]
|
||||
param += ['"' + app_cmd + '"']
|
||||
param += [
|
||||
mspec.hostname,
|
||||
mspec.cpu,
|
||||
mspec.distro,
|
||||
mspec.kernel,
|
||||
mspec.rocmversion,
|
||||
timestamp,
|
||||
]
|
||||
|
||||
# GPU info
|
||||
param += [
|
||||
mspec.GPU,
|
||||
mspec.SE,
|
||||
mspec.CU,
|
||||
mspec.SIMD,
|
||||
mspec.wave_size,
|
||||
mspec.wave_occu,
|
||||
mspec.workgroup_size,
|
||||
]
|
||||
param += [
|
||||
mspec.L1,
|
||||
mspec.L2,
|
||||
mspec.SCLK,
|
||||
mspec.cur_MCLK,
|
||||
mspec.cur_SCLK,
|
||||
mspec.cur_MCLK,
|
||||
]
|
||||
|
||||
blocks = []
|
||||
hbmBW = int(mspec.cur_MCLK) / 1000 * 4096 / 8 * 2
|
||||
if mspec.GPU == "gfx906":
|
||||
param += ["16", "32", "mi50", str(int(mspec.CU) // 4), str(hbmBW)]
|
||||
elif mspec.GPU == "gfx908":
|
||||
param += ["32", "32", "mi100", "48", str(hbmBW)]
|
||||
elif mspec.GPU == "gfx90a":
|
||||
param += ["32", "32", "mi200", "56", str(hbmBW)]
|
||||
if not skip_roof:
|
||||
blocks.append("roofline")
|
||||
|
||||
# ip block info
|
||||
if ip_blocks == None:
|
||||
t = ["SQ", "LDS", "SQC", "TA", "TD", "TCP", "TCC", "SPI", "CPC", "CPF"]
|
||||
blocks += t
|
||||
else:
|
||||
blocks += ip_blocks
|
||||
param.append("|".join(blocks))
|
||||
|
||||
sysinfo.write(",".join(param))
|
||||
sysinfo.close()
|
||||
|
||||
def detect_roofline():
|
||||
mspec = specs.get_machine_specs(0)
|
||||
rocm_ver = mspec.rocmversion[:1]
|
||||
|
||||
os_release = path("/etc/os-release").read_text()
|
||||
ubuntu_distro = specs.search(r'VERSION_ID="(.*?)"', os_release)
|
||||
rhel_distro = specs.search(r'PLATFORM_ID="(.*?)"', os_release)
|
||||
sles_distro = specs.search(r'VERSION_ID="(.*?)"', os_release)
|
||||
|
||||
if "ROOFLINE_BIN" in os.environ.keys():
|
||||
rooflineBinary = os.environ["ROOFLINE_BIN"]
|
||||
if os.path.exists(rooflineBinary):
|
||||
logging._SysExcInfoType("Detected user-supplied binary")
|
||||
return {"rocm_ver": "override", "distro": "override", "path": rooflineBinary}
|
||||
else:
|
||||
logging.error("ROOFLINE ERROR: user-supplied path to binary not accessible")
|
||||
logging.error("--> ROOFLINE_BIN = %s\n" % target_binary)
|
||||
sys.exit(1)
|
||||
elif rhel_distro == "platform:el8":
|
||||
# Must be a valid RHEL machine
|
||||
distro = rhel_distro
|
||||
elif (
|
||||
(type(sles_distro) == str and len(sles_distro) >= 3) and # confirm string and len
|
||||
sles_distro[:2] == "15" and int(sles_distro[3]) >= 3 # SLES15 and SP >= 3
|
||||
):
|
||||
# Must be a valid SLES machine
|
||||
# Use SP3 binary for all forward compatible service pack versions
|
||||
distro = "15.3"
|
||||
elif ubuntu_distro == "20.04":
|
||||
# Must be a valid Ubuntu machine
|
||||
distro = ubuntu_distro
|
||||
else:
|
||||
logging.error("ROOFLINE ERROR: Cannot find a valid binary for your operating system")
|
||||
sys.exit(1)
|
||||
|
||||
target_binary = {"rocm_ver": rocm_ver, "distro": distro}
|
||||
return target_binary
|
||||
|
||||
def run_rocscope(args, fname):
|
||||
# profile the app
|
||||
if args.use_rocscope == True:
|
||||
result = shutil.which("rocscope")
|
||||
if result:
|
||||
rs_cmd = [
|
||||
result.stdout.decode("ascii").strip(),
|
||||
"metrics",
|
||||
"-p",
|
||||
args.path,
|
||||
"-n",
|
||||
args.name,
|
||||
"-t",
|
||||
fname,
|
||||
"--",
|
||||
]
|
||||
for i in args.remaining.split():
|
||||
rs_cmd.append(i)
|
||||
logging.info(rs_cmd)
|
||||
success, output = capture_subprocess_output(
|
||||
rs_cmd
|
||||
)
|
||||
if not success:
|
||||
logging.error(result.stderr.decode("ascii"))
|
||||
sys.exit(1)
|
||||
|
||||
def mibench(args):
|
||||
"""Run roofline microbenchmark to generate peak BW and FLOP measurements.
|
||||
"""
|
||||
logging.info("[roofline] No roofline data found. Generating...")
|
||||
distro_map = {"platform:el8": "rhel8", "15.3": "sle15sp3", "20.04": "ubuntu20_04"}
|
||||
|
||||
target_binary = detect_roofline()
|
||||
if target_binary["rocm_ver"] == "override":
|
||||
path_to_binary = target_binary["path"]
|
||||
else:
|
||||
path_to_binary = (
|
||||
str(config.omniperf_home)
|
||||
+ "/utils/rooflines/roofline"
|
||||
+ "-"
|
||||
+ distro_map[target_binary["distro"]]
|
||||
+ "-"
|
||||
+ args.target.lower()
|
||||
+ "-rocm"
|
||||
+ target_binary["rocm_ver"]
|
||||
)
|
||||
|
||||
# Distro is valid but cant find rocm ver
|
||||
if not os.path.exists(path_to_binary):
|
||||
logging.error("ROOFLINE ERROR: Unable to locate expected binary (%s)." % path_to_binary)
|
||||
sys.exit(1)
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
path_to_binary,
|
||||
"-o",
|
||||
args.path + "/" + "roofline.csv",
|
||||
"-d",
|
||||
str(args.device),
|
||||
],
|
||||
check=True
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user