464 lines
17 KiB
Python
464 lines
17 KiB
Python
#! /usr/bin/env python3
|
|
import ctypes
|
|
import os
|
|
import re
|
|
import pathlib
|
|
|
|
from collections import defaultdict
|
|
|
|
import numpy as np
|
|
|
|
# from sys_flow_utils import create_logger
|
|
import sys_flow.flow_constants as fconsts
|
|
import sys_flow.flow_utils as futils
|
|
from sys_flow.exceptions import RegressionError
|
|
|
|
import snoop
|
|
DEBUG = True if os.environ.get("REGRESSION_DEBUG", False) else False
|
|
snoop.install(enabled=DEBUG)
|
|
|
|
|
|
def gen_dynasty_mode_settings(name_mode,
|
|
fn_onnx=None,
|
|
onnx_map=None,
|
|
ioinfo_map={},
|
|
which_onnx="piano_onnx",
|
|
model_id="tc/model_1"):
|
|
"""
|
|
Generate dynasty settings for `one` mode.
|
|
|
|
Args:
|
|
name_mode (str): could be "720", "720wq".
|
|
fn_onnx (pathlib / str): location of onnx file. May skip this if `onnx_map` given.
|
|
onnx_map (dict): a dict of multiple onnx files.
|
|
Will use `name_mode` to find the proper onnx.
|
|
ioinfo_map (dict): a dict of multiple ioinfo json files.
|
|
which_onnx (str): "piano_onnx" / "piano_bie", depends on the picked model.
|
|
model_id (str): default "tc/model_1". it should have the format of "CATEGORY/MODEL_NAME"
|
|
|
|
Returns:
|
|
mode settings for this input.
|
|
"""
|
|
# predefined settings
|
|
select_dynasty_setting = [a for a in fconsts.DYNASTY_MODE_SETTINGS[which_onnx] if a[0] == name_mode]
|
|
assert len(select_dynasty_setting) == 1, "found {} dynasty settings, expect 1".format(len(select_dynasty_setting))
|
|
name_mode, k_onnx, n_mode = select_dynasty_setting[0]
|
|
# k_onnx can be used to select specific onnx in kneron regression for debug
|
|
|
|
i_mode = {}
|
|
i_mode["name_mode"] = name_mode
|
|
i_mode["n_mode"] = n_mode
|
|
i_mode["dir_out"] = f"mode_{name_mode}"
|
|
i_mode["platform"] = "piano_dynasty"
|
|
i_mode["ioinfo_json"] = ioinfo_map.get(n_mode, None)
|
|
i_mode["model_id"] = model_id # will use this in error report
|
|
|
|
if fn_onnx and pathlib.Path(fn_onnx).exists():
|
|
i_mode["fn_onnx"] = fn_onnx
|
|
else:
|
|
assert type(onnx_map) is dict, "must specify at least one parameter 'fn_onnx' or 'onnx_map'"
|
|
i_mode["fn_onnx"] = onnx_map[k_onnx]
|
|
|
|
return i_mode
|
|
|
|
|
|
def gen_dynasty_list(mode_list, input_list, info_in, p_output,
|
|
dump_level=0,
|
|
do_print=False,
|
|
use_cuda=False,
|
|
shape_in="onnx_shape",
|
|
round_mode=1):
|
|
"""Create list of input `x` mode for this model to run.
|
|
|
|
base_dump:
|
|
|
|
Now we support more flexible calling of dynasty which may dump to different folder.
|
|
|
|
Args:
|
|
mode_list: list of dynasty mode to run, e.g., `["520", "520wq"]`
|
|
|
|
input_list: list of input groups, if you want to run batch inference.
|
|
|
|
info_in: list of input node names. To get correct order input from numpy input.
|
|
|
|
p_output: regression will specify "model_name/output/results" by default.
|
|
The results for input `xxx.txt` inference in `dynasty mode` are dumped in
|
|
"`model_name/output/results`/xxx.txt/dynasty_mode_name" folder.
|
|
Change this to "yyy" will cause dump to "`yyy`/xxx.txt/dynasty_mode_name".
|
|
|
|
shape_in: choose `onnx_shape` (current default) / `channel_last` (obsolete).
|
|
|
|
round_mode: Only effective for platform 540/730.
|
|
|
|
- 0: round to inf. 520/530/630/720 will only support this.
|
|
540/730 may set to 0 in case of debug.
|
|
- 1: (default for 540/730). round to even.
|
|
|
|
Returns: tuple.
|
|
- dynasty_list: list of dynasty run parameters, combination of all inputs
|
|
and all mode configs.
|
|
- dir_out_list: path to dynasty dump for each of `dynasty_list` items.
|
|
"""
|
|
# create a list with all input images.
|
|
dynasty_list = []
|
|
dir_out_list = [] # this dir_out corresponding to xxx.txt, not including mode in it
|
|
for i_mode, mode_config in enumerate(mode_list):
|
|
for fn_input in input_list:
|
|
fn_in_base = pathlib.Path(fn_input[0]).name
|
|
this_out = f"""{p_output}/{fn_in_base}"""
|
|
if i_mode == 0: # only save for 0th mode
|
|
dir_out_list.append(this_out)
|
|
pathlib.Path(this_out).mkdir(mode=0o770, parents=True, exist_ok=True)
|
|
|
|
i_para = {
|
|
"model_id": mode_config["model_id"],
|
|
"fn_onnx": mode_config["fn_onnx"],
|
|
"ioinfo_json": mode_config["ioinfo_json"],
|
|
"n_mode": mode_config["n_mode"],
|
|
"name_mode": mode_config["name_mode"],
|
|
"dir_out": "{}/{}".format(this_out, mode_config["dir_out"]),
|
|
"fn_input": fn_input,
|
|
"node_input": info_in,
|
|
"dump_level": dump_level,
|
|
"print_command": do_print,
|
|
"platform": mode_config["platform"],
|
|
"cuda": use_cuda, # only False for now
|
|
"round_mode": round_mode,
|
|
"input_shape": shape_in,
|
|
}
|
|
|
|
dynasty_list.append(i_para)
|
|
|
|
return dynasty_list, dir_out_list
|
|
|
|
|
|
def build_dynasty_cmd_1(dynasty_para, bin_dynasty):
|
|
"""Create command for running dynasty."""
|
|
fn_input = dynasty_para["fn_input"]
|
|
assert type(fn_input) is list
|
|
node_input = dynasty_para["node_input"]
|
|
|
|
dir_out = dynasty_para["dir_out"] + fconsts.DYNASTY_OUT_DIR_APPENDIX["piano"]
|
|
pathlib.Path(dir_out).mkdir(parents=True, exist_ok=True)
|
|
|
|
dump_level = dynasty_para["dump_level"]
|
|
round_mode = dynasty_para["round_mode"]
|
|
|
|
fn_onnx = dynasty_para["fn_onnx"]
|
|
n_mode = dynasty_para["n_mode"]
|
|
ioinfo_json = dynasty_para["ioinfo_json"]
|
|
|
|
# name_mode = dynasty_para["name_mode"]
|
|
# model_name = dynasty_para["model_name"]
|
|
# cat_name = dynasty_para["cat_name"]
|
|
model_id = dynasty_para["model_id"]
|
|
base_name_input = pathlib.PurePath(fn_input[0]).name
|
|
|
|
shape_convert = {"onnx_shape": 1, "channel_last": 0}
|
|
shapeOrder = shape_convert.get(dynasty_para["input_shape"], 1)
|
|
|
|
# -m path_to_model
|
|
# -r path_to_radix_json
|
|
# -d path_to_input_data
|
|
# -n The name of the input node
|
|
# -p platform: kl520, kl720, generic
|
|
# -u dump level: 0: only dump final output, 2: dump all intermediate results
|
|
# -s shapeOrder: 0: hwc, 1: chw
|
|
# -o path to output data dump folder
|
|
# -e if bie, not onnx.
|
|
|
|
inputs = []
|
|
for fn_in, node_in in zip(fn_input, node_input):
|
|
inputs.append("--data")
|
|
inputs.append(str(fn_in))
|
|
inputs.append("--name")
|
|
inputs.append(node_in)
|
|
|
|
# TODO: export LD_LIBRARY_PATH="{lib_dir}:${{LD_LIBRARY_PATH}}" for libonnxruntime parent path
|
|
command = [
|
|
str(bin_dynasty),
|
|
"--model",
|
|
f"\"{fn_onnx}\"",
|
|
"--radix",
|
|
f"\"{fn_onnx}.json\"",
|
|
"--platform",
|
|
fconsts.NUM2MODE[n_mode],
|
|
"--dump",
|
|
str(dump_level),
|
|
"--shapeOrder",
|
|
str(shapeOrder),
|
|
"--output",
|
|
f"\"{dir_out}\"",
|
|
"--roundMode",
|
|
str(round_mode),
|
|
] + inputs
|
|
if pathlib.Path(fn_onnx).name.endswith(".bie"):
|
|
command.append("-e")
|
|
if dynasty_para["cuda"]:
|
|
command.append("--cuda")
|
|
if ioinfo_json and (n_mode not in [520]):
|
|
# NOTE: pass in compiler generated ioinfo.json to dynasty
|
|
command.append(f"-k \"{ioinfo_json}\"")
|
|
|
|
# TODO: remove model_id thing. return only command to make things easier
|
|
return command, fconsts.NUM2PLATFORM[n_mode], model_id, base_name_input
|
|
|
|
|
|
def build_dynasty_cmd(dynasty_list, dynasty_bin, fn_dynasty=None):
|
|
"""Generate commands of ALL dynasty inference."""
|
|
cmds = [" ".join(build_dynasty_cmd_1(d_para, dynasty_bin)[0]) for d_para in dynasty_list]
|
|
if fn_dynasty:
|
|
with open(fn_dynasty, 'w') as f:
|
|
f.write("\n".join(cmds))
|
|
return cmds
|
|
|
|
|
|
def guess_platform(d_cmd):
|
|
"""Guess platform from dynasty command line used onnx or bie."""
|
|
s = re.compile("\.kdp(\d\d\d)\..*?\.(?:onnx|bie)")
|
|
try:
|
|
platform = "kdp{}".format(s.findall(d_cmd)[0])
|
|
except:
|
|
platform = "general"
|
|
|
|
return platform
|
|
|
|
|
|
def run_dynasty_so(lib, model, num_inputs, input_files,
|
|
input_names, output_folder, ort=False):
|
|
"""Call Dynasty C inferencer.
|
|
|
|
Arguments:
|
|
lib: String path to the Dynasty shared library.
|
|
model: String path to the ONNX model.
|
|
num_inputs: Integer number of input nodes.
|
|
input_files: List of pathlib paths to each of the input image text files.
|
|
input_names: List of string names of the input nodes.
|
|
output_folder: String path to directory where outputs will be stored.
|
|
ort: Flag to use onnxruntime as inferencer for Dynasty float.
|
|
"""
|
|
dynasty_lib = ctypes.CDLL(lib)
|
|
c_function = dynasty_lib.inference_wrapper
|
|
c_function.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int,
|
|
ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int,
|
|
ctypes.c_char_p * num_inputs, ctypes.c_char_p * num_inputs,
|
|
ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p]
|
|
c_function.restype = None
|
|
|
|
cuda = "True" # non cuda lib will not use this
|
|
debug = "False"
|
|
dump = 0
|
|
encrypt = "False"
|
|
platform = "Float"
|
|
radix_file = ""
|
|
shape_order = "0"
|
|
|
|
files_arr = (ctypes.c_char_p * num_inputs)()
|
|
files_arr[:] = [str(input_file).encode() for input_file in input_files]
|
|
names_arr = (ctypes.c_char_p * num_inputs)()
|
|
names_arr[:] = [input_name.encode() for input_name in input_names]
|
|
|
|
c_function(platform.encode(), encrypt.encode(), shape_order.encode(), dump, model.encode(),
|
|
radix_file.encode(), num_inputs, files_arr, names_arr, output_folder.encode(),
|
|
debug.encode(), cuda.encode(), str(ort).encode())
|
|
|
|
|
|
def run_dynasty_command_parallel(model_id, fn_cmds, n_thread=None, fn_err=None):
|
|
"""Use gnu parallel to run dynasty in parallel."""
|
|
if n_thread is None:
|
|
n_str = ""
|
|
else:
|
|
n_str = f"--jobs {n_thread}"
|
|
if fn_err is None:
|
|
joblog = ""
|
|
else:
|
|
joblog = f"--joblog {fn_err}"
|
|
|
|
command = f"parallel {n_str} --halt now,fail=1 {joblog} < {fn_cmds}"
|
|
|
|
# TODO: if fn_cmds contains only 1 command, skip parallel
|
|
|
|
cp = futils.run_bash_script(command)
|
|
|
|
# some known errors
|
|
if cp.returncode != 0:
|
|
platform = guess_platform(cp.stderr)
|
|
if cp.returncode == 20:
|
|
raise RegressionError(f"{platform}/knerex", model_id, msg="dynasty: found wrong bw")
|
|
else:
|
|
try:
|
|
with open(fn_err, "a") as f:
|
|
f.write("\n\n")
|
|
f.write("\n\n".join([cp.stderr, cp.stdout]))
|
|
except Exception as e:
|
|
pass
|
|
if DEBUG:
|
|
print(cp.stderr)
|
|
raise RegressionError(f"{platform}/dynasty", model_id, msg=f"Exit code={cp.returncode}")
|
|
|
|
elif fn_err is not None:
|
|
# check fn_err. sometime parallel cannot grab the failed dynasty
|
|
# e.g., if dynasty killed by segmental fault
|
|
msg, e1, e2 = futils.check_parallel_log(fn_err)
|
|
platform = guess_platform(cp.stderr)
|
|
if len(msg) > 3:
|
|
if DEBUG:
|
|
print(f"dynasty parallel failed: {msg}")
|
|
raise RegressionError(f"{platform}/dynasty", model_id, msg=msg)
|
|
|
|
|
|
def np2txt(input_np, input_nodes, p_working, dump_prefix="knerex_input", ch_last=False):
|
|
"""Convert np input to text for knerex / dynasty when do inference from toolchain.
|
|
|
|
NOTE:
|
|
- the numpy array in input_np must be "onnx_shape".
|
|
- move this function to flow_utils.
|
|
"""
|
|
assert set(input_nodes) == set(input_np.keys()), \
|
|
f"ERROR: input name does not match: onnx input ({input_nodes}) vs given np ({input_np.keys()})"
|
|
|
|
# TODO: what if input_np is str or path
|
|
|
|
is_dumped = []
|
|
p_dumped = []
|
|
pair_names = []
|
|
for i_dp, dp_in in enumerate(input_nodes):
|
|
p_dump = pathlib.Path(p_working) / f"{dump_prefix}_{i_dp}"
|
|
p_dumped.append(p_dump)
|
|
is_dumped.append(p_dump.exists())
|
|
|
|
if all(is_dumped):
|
|
# all text exists already. skip for now
|
|
pp(f"{input_nodes} texts are dumped already. continue.")
|
|
|
|
else:
|
|
# save numpy to txt files
|
|
|
|
# TODO: call common function to verify inputs
|
|
# check input_np size, which is dict of list of np (onnx input shape)
|
|
# check pairs of inputs.
|
|
n_pairs = [len(v) for k, v in input_np.items()]
|
|
assert len(set(n_pairs)) == 1, f"input group numbers not match: got len of {n_pairs}"
|
|
|
|
# check input shape
|
|
for i_dp, dp_in in enumerate(input_nodes):
|
|
p_dump = p_dumped[i_dp] # pre-defined above
|
|
p_dump.mkdir(parents=True, exist_ok=True)
|
|
for i_group, np_1 in enumerate(input_np[dp_in]):
|
|
fn_dump = p_dump / f"in_{i_group:06d}.txt"
|
|
if i_dp == 0:
|
|
pair_names.append(fn_dump.name)
|
|
# NOTE: will not support channel last text dump
|
|
# this is only used for dynasty float shared library
|
|
if ch_last:
|
|
input_shape = np_1.shape
|
|
if len(input_shape) > 3:
|
|
axes = range(len(input_shape))
|
|
axes = [axes[0], *axes[2:], axes[1]]
|
|
np_1 = np.transpose(np_1, axes)
|
|
np.savetxt(fn_dump, np_1.flat, fmt="%.8f")
|
|
|
|
# do search to collect all text files
|
|
input_list = list_multiple_input(p_dumped, pair_names, verify_exist=False)
|
|
|
|
return p_dumped, input_list, pair_names
|
|
|
|
|
|
def list_multiple_input(p_txt_inputs, pair_names=None, verify_exist=True):
|
|
"""Check multiple INPUT NODES for this MODEL.
|
|
|
|
Give 1st input image name, give a list with whole input set (might be 1 or more.)
|
|
|
|
TODO:
|
|
need refactor into flow_utils
|
|
|
|
Args:
|
|
p_txt_inputs: where txt files exists.
|
|
paire_names: the txt filenames in the first input folder.
|
|
(should be same in other folder.)
|
|
"""
|
|
# if given txt files then use it otherwise search for it
|
|
fns = pair_names if pair_names else sorted([fn.name for fn in list(p_txt_inputs[0].glob("*.txt"))])
|
|
|
|
pairs = []
|
|
for fn in fns:
|
|
# find a pair of inputs
|
|
assert type(fn) is str, f"Given {fn} must be string."
|
|
pair = [p / fn for p in p_txt_inputs]
|
|
if verify_exist:
|
|
assert all([f.exists() for f in pair])
|
|
pairs.append(pair)
|
|
return pairs
|
|
|
|
|
|
def txt2np_so(out_nodes_shape, output_folder):
|
|
"""Convert the dynasty dumped results from the shared library to np.
|
|
|
|
Args:
|
|
out_nodes_shape:
|
|
output_folder: basically `list(working_dir)`
|
|
|
|
Returns:
|
|
- np_out_fl: dict of list of numpy array, each array is onnx shape, in float format.
|
|
"""
|
|
collect_txt_fl = defaultdict(list)
|
|
for dp_out, shape_out in out_nodes_shape.items():
|
|
fl_output = "layer_output_{}_fl.txt".format(futils.clean_name(dp_out))
|
|
p_fl = output_folder / fl_output
|
|
assert p_fl.exists()
|
|
collect_txt_fl[dp_out].append(futils.txt2np_fl(p_fl, shape_out))
|
|
|
|
return collect_txt_fl
|
|
|
|
|
|
def txt2np(out_nodes_shape, output_list, dmode, load_fl=True, load_fx=False):
|
|
"""Convert the dynasty dumped results to np.
|
|
|
|
Args:
|
|
out_nodes_shape:
|
|
output_list: basically `list(p_dump.glob(f"*.txt/mode_{dmode}_piano"))`
|
|
dmode: which dynasty mode.
|
|
|
|
Returns: tuple
|
|
- np_out_fx: dict of list of numpy array, each array is onnx shape, in fix-point format.
|
|
- np_out_fl: dict of list of numpy array, each array is onnx shape, in float format.
|
|
"""
|
|
collect_txt_fl = defaultdict(list)
|
|
collect_txt_fx = defaultdict(list)
|
|
for dp_out, shape_out in out_nodes_shape.items():
|
|
|
|
fl_output = "layer_output_{}_fl.txt".format(futils.clean_name(dp_out))
|
|
fx_output = "layer_output_{}_fx.txt".format(futils.clean_name(dp_out))
|
|
|
|
for p_dump in output_list:
|
|
if load_fl:
|
|
p_fl = pathlib.Path(p_dump) / f"mode_{dmode}_piano" / fl_output
|
|
assert p_fl.exists()
|
|
collect_txt_fl[dp_out].append(futils.txt2np_fl(p_fl, shape_out))
|
|
if load_fx:
|
|
p_fx = pathlib.Path(p_dump) / f"mode_{dmode}_piano" / fx_output
|
|
assert p_fx.exists()
|
|
collect_txt_fx[dp_out].append(futils.txt2np_fx(p_fx, shape_out))
|
|
|
|
return collect_txt_fl, collect_txt_fx
|
|
|
|
|
|
def np_fx2fl(np_in: dict, d_ch_dim: dict, d_scale: dict, d_radix: dict):
|
|
"""Convert fx data in numpy format to float."""
|
|
np_fl = {}
|
|
for dp_name, dp_arr in np_in.items():
|
|
ch_dim = d_ch_dim[dp_name]
|
|
scale_expand_dim = list(range(len(dp_arr[0].shape)))
|
|
scale_expand_dim.remove(ch_dim)
|
|
|
|
scale1 = np.array(d_scale[dp_name]) * 2.0**np.array(d_radix[dp_name])
|
|
scale = np.expand_dims(scale1, scale_expand_dim)
|
|
|
|
these_fl = []
|
|
for i_dp, dp in enumerate(dp_arr):
|
|
these_fl.append(dp / scale)
|
|
|
|
np_fl[dp_name] = these_fl
|
|
|
|
return np_fl
|