2026-01-28 06:16:04 +00:00

365 lines
17 KiB
Python

"""
This module contains classes that store information about the testing flow.
"""
import json
import pathlib
import pprint
import re
import shutil
import sys
import onnxruntime as ort
from sys_flow.compiler_v2 import unpack_nefs as unpack_nefs_v1
from sys_flow.inference import get_model_io as get_model_io_v1
from sys_flow_v2.compiler_v2 import unpack_nefs as unpack_nefs_v2
from sys_flow_v2.inference import get_model_io as get_model_io_v2
import python_flow.common.constants as constants
import python_flow.common.exceptions as exceptions
import python_flow.internal.internal as internal
def clean_string(in_string):
"""Cleans input string to allow API to handle clean directories."""
return re.sub(r'[^a-zA-Z0-9\/_-]', '', in_string)
def check_key_existence(keys, config, process, outer_json):
"""Check if all the keys in the 'keys' list is in the 'config' dictionary."""
for key in keys:
if key not in config:
raise exceptions.RequiredConfigError(
f"[emu][{process}] is missing {key} key from input JSON {outer_json}")
def flatten_emu(config):
"""Flattens the needed dictionary in the input JSON."""
if config["emu_mode"] == "csim":
for key in config["csim"]:
config[key] = config["csim"][key]
elif config["emu_mode"] == "fixed":
for key in config["fixed"]:
config[key] = config["fixed"][key]
elif config["emu_mode"] == "float":
for key in config["float"]:
config[key] = config["float"][key]
elif config["emu_mode"] == "dongle":
for key in config["csim"]:
config[key] = config["csim"][key]
for key in config["dongle"]:
config[key] = config["dongle"][key]
del config["csim"]
del config["fixed"]
del config["float"]
if 'dongle' in config.keys():
del config['dongle']
class TestConfig():
"""Configurations setup from an input JSON.
input_config is given as the data already loaded from the corresponding input JSON file.
Attributes:
config: Dictionary of configurations parsed from an input JSON,
"""
def __init__(self, input_config, options, outer_json, input_image_list):
app_absolute = pathlib.Path(options["app_folder"]).resolve()
input_config["pre"] = PreConfig(input_config["pre"], app_absolute, outer_json).config
input_config["emu"] = EmuConfig(input_config["emu"], app_absolute, outer_json).config
input_config["post"] = PostConfig(input_config["post"], app_absolute, outer_json).config
self.orig_emu = input_config["emu"] # used for updating paths
self.config = input_config
self.input_image_list = input_image_list
self.init_options(options, outer_json)
self.config = internal.set_reordering(self.config)
def __repr__(self):
return ("\n---------------------------Other config--------------------------\n{}"
"\n------------------------Preprocess config------------------------\n{}"
"\n------------------------Inference config--------------------------\n{}"
"\n------------------------Postprocess config------------------------\n{}").format(
pprint.pformat(self.config["flow"]), pprint.pformat(self.config["pre"]),
pprint.pformat(self.config["emu"]), pprint.pformat(self.config["post"]))
def clear_results(self, in_beginning):
"""Clear the inference results folder.
Arguments:
in_beginning: Flag indicating if this function was called at the beginning of flow.
"""
emu = self.config["emu"]
clear_mode = self.config["flow"]["clear"]
# 3 clear modes currently: none, all_x_result, before
if in_beginning:
if clear_mode == "before" or clear_mode == "all_x_result":
if emu["emu_mode"] == "csim":
shutil.rmtree(self.config["flow"]["csim_output"], ignore_errors=True)
elif emu["emu_mode"] == "dongle":
shutil.rmtree(self.config["flow"]["dongle_output"], ignore_errors=True)
elif emu["emu_mode"] == "float" or emu["emu_mode"] == "fixed":
shutil.rmtree(self.config["flow"]["onnx_output"], ignore_errors=True)
shutil.rmtree(self.config["flow"]["out_folder_base"], ignore_errors=True)
else:
if clear_mode == "all_x_result":
if emu["emu_mode"] == "csim":
shutil.rmtree(emu["csim_output"], ignore_errors=True)
elif emu["emu_mode"] == "dongle":
shutil.rmtree(emu["dongle_output"], ignore_errors=True)
elif emu["emu_mode"] == "float" or emu["emu_mode"] == "fixed":
shutil.rmtree(emu["onnx_output"], ignore_errors=True)
# remake out folder for just result.json for specific image folder
# this function is called just before dumping result json in simulator
# out_folder will be set by this point
shutil.rmtree(self.config["flow"]["out_folder"], ignore_errors=True)
pathlib.Path(self.config["flow"]["out_folder"]).mkdir(parents=True, exist_ok=True)
def init_options(self, options, outer_json):
"""Save the command line options into the class dictionary."""
app = pathlib.Path(options["app_folder"]).resolve()
flow_config = options.copy()
flow_config["app_folder"] = app
flow_config["is_binary"] = options["inputs"] != "image"
flow_config["input_json"] = outer_json
flow_config["input_shape"] = options["input_shape"]
try:
relative_dir = pathlib.Path(flow_config["image_directory"]).resolve().relative_to(
pathlib.Path("app").resolve())
except ValueError: # pretend absolute is relative by stripping front '/', for out folder
relative_dir = pathlib.Path(
str(pathlib.Path(flow_config["image_directory"]).resolve()).strip("/"))
# when using image json, update paths will set image_name as part of folder path instead
# of image directory
if self.input_image_list:
flow_config["csim_output"] = "/".join([self.orig_emu["csim_output"], app.name])
flow_config["dongle_output"] = "/".join([self.orig_emu["dongle_output"], app.name])
flow_config["onnx_output"] = "/".join([self.orig_emu["onnx_output"], app.name])
flow_config["out_folder_base"] = (pathlib.Path("bin/out/") / app.name).resolve()
else:
# general directory, specific image directory is updated in update_paths
flow_config["csim_output"] = self.orig_emu["csim_output"] / (app.name / relative_dir)
flow_config["dongle_output"] = self.orig_emu["dongle_output"] / (app.name / relative_dir)
flow_config["onnx_output"] = self.orig_emu["onnx_output"] / (app.name / relative_dir)
flow_config["out_folder_base"] = ("bin/out/" / (app.name / relative_dir)).resolve()
self.config["flow"] = flow_config
def update_paths(self, image_list):
"""Update the class dictionary with the current test image."""
image_list = [pathlib.Path(image) for image in image_list] # list of absolute path strings
self.config["flow"]["image_file"] = image_list
# limit characters in case too long to create folder
combined_name = "+".join([image.name for image in image_list])[:100]
# when using image json, update path with input image path rather than image directory
if self.input_image_list:
try:
relative_dir = pathlib.Path(image_list[0].parent).resolve().relative_to(
pathlib.Path("app").resolve())
except ValueError: # pretend absolute is relative by stripping front '/'
relative_dir = pathlib.Path(str(image_list[0].parent.resolve()).strip("/"))
combined_name = relative_dir / combined_name
clean_out = clean_string(str(self.config["flow"]["out_folder_base"] / combined_name))
self.config["flow"]["out_folder"] = pathlib.Path(clean_out)
self.config["flow"]["out_folder"].mkdir(parents=True, exist_ok=True)
# update dump folders to match current test image, mkdir if necessary
emu = self.orig_emu.copy()
model = emu["model_type"]
if emu["emu_mode"] == "csim":
emu["csim_output"] = clean_string(
str(self.config["flow"]["csim_output"] / combined_name / model))
pathlib.Path(emu["csim_output"]).mkdir(parents=True, exist_ok=True)
if emu["platform"] in constants.INI_PLATFORMS:
ini_name = model + f"_{emu['platform']}.ini"
new_ini = str(self.config["flow"]["out_folder"] / ini_name)
new_ini_path = pathlib.Path(new_ini).resolve()
emu["ini"] = str(new_ini_path)
elif emu["emu_mode"] == "dongle":
emu["dongle_output"] = clean_string(
str(self.config["flow"]["dongle_output"] / combined_name / model))
pathlib.Path(emu["dongle_output"]).mkdir(parents=True, exist_ok=True)
elif emu["emu_mode"] == "float" or emu["emu_mode"] == "fixed":
emu["onnx_output"] = clean_string(
str(self.config["flow"]["onnx_output"] / combined_name / model))
pathlib.Path(emu["onnx_output"]).mkdir(parents=True, exist_ok=True)
self.config["emu"] = emu
class PreConfig():
"""Preprocess configurations from the input JSON.
Attributes:
config: Dictionary of preprocess configurations parsed from an input JSON
"""
defaults = {"dump_onnx_txt": False, "pre_bypass": False, "pre_mode": "",
"pre_type": ""}
def __init__(self, config, app_folder, outer_json):
self.prep(config, app_folder, outer_json)
def __repr__(self):
return ("-------------------------Preprocess config-------------------------\n{}".format(
pprint.pformat(self.config)))
def prep(self, config, app_folder, outer_json):
"""Prepares optional input config values that are needed for preprocessing."""
self.config = {}
self.config.update(self.defaults)
self.config.update(config)
internal.prep_inner(app_folder, self.config, outer_json, "pre")
class EmuConfig():
"""Simulator configurations from the input JSON.
Attributes:
config: Dictionary of configurations parsed from an input JSON
"""
defaults = {"channel_first": True, "debug": False, "data_type": "float", "emu_mode": "csim",
"ioinfo": "", "ini": constants.TEMPLATE_INI, "model_type": "fd",
"platform": 520, "radix": 8, "reordering": [], "runner_mode": "algorithm",
"onnx_output": str(pathlib.Path("bin/dynasty_dump").resolve()), # relative to root
"csim_output": str(pathlib.Path("bin/csim_dump").resolve()),
"dongle_output": str(pathlib.Path("bin/dongle_dump").resolve())}
required_csim = ["nef_file", "input_names"]
required_bie = ["bie_file", "input_names"]
required_onnx_json = ["onnx_file", "json_file", "input_names"]
required_float = ["onnx_file", "input_names"]
required_dongle = ["nef_file", "input_names"]
def __init__(self, config, app_folder, outer_json):
flatten_emu(config)
self.prep(config, app_folder, outer_json)
def __repr__(self):
return ("-------------------------Emulator config-------------------------\n{}".format(
pprint.pformat(self.config)))
def verify(self, config, app_folder, outer_json):
"""Verifies that certain emulator configurations exist and are valid."""
files_to_check = []
mode = config["emu_mode"]
if mode == "csim":
files_to_check.extend(self.required_csim)
check_key_existence(files_to_check, config, "csim", outer_json)
files_to_check.remove("input_names")
elif mode == "fixed":
if config["debug"]: # onnx/json
files_to_check.extend(self.required_onnx_json)
else: # bie
files_to_check.extend(self.required_bie)
check_key_existence(files_to_check, config, "fixed", outer_json)
files_to_check.remove("input_names")
elif mode == "float":
files_to_check.extend(self.required_float)
check_key_existence(files_to_check, config, "float", outer_json)
files_to_check.remove("input_names")
elif mode == "dongle":
files_to_check.extend(self.required_dongle)
check_key_existence(files_to_check, config, "dongle", outer_json)
files_to_check.remove("input_names")
for input_file in files_to_check:
if config[input_file] == "":
raise exceptions.InvalidInputError(f"[emu][{mode}][{input_file}] "
f"from input JSON {outer_json} cannot be empty")
file_name = (app_folder / config[input_file]).resolve()
if not file_name.is_file():
raise exceptions.InvalidInputError(
f"{file_name} does not exist\n\n[emu][{mode}][{input_file}] "
f"from input JSON {outer_json}")
config[input_file] = str(file_name)
# for shape checking for csim/fixed
if config["ioinfo"] != "":
config["ioinfo"] = str((app_folder / config["ioinfo"]).resolve())
def setup_model(self):
"""Prepares the model using the sys_flow API."""
input_nodes = None
out_node_shape = None
d_ioinfo = None
input_shapes = None
if self.config["platform"] in constants.PLATFORMS_MO3:
get_model_io = get_model_io_v1
unpack_nefs = unpack_nefs_v1
else:
get_model_io = get_model_io_v2
unpack_nefs = unpack_nefs_v2
if self.config["emu_mode"] == "csim" or self.config["emu_mode"] == "fixed":
if self.config["ioinfo"] != "":
with open(self.config["ioinfo"]) as ioinfo_file:
data = json.load(ioinfo_file)
input_shapes = [node["onnx_shape"] for node in data["input"]]
if self.config["emu_mode"] == "csim":
model_maps, p_out = unpack_nefs(self.config["nef_file"], self.config["platform"])
self.config["model_maps"] = model_maps
self.config["p_out"] = p_out
elif self.config["emu_mode"] == "fixed":
if self.config["debug"]:
input_nodes, _, out_node_shape, d_ioinfo = get_model_io(
pathlib.Path(self.config["onnx_file"]), self.config["platform"])
else:
input_nodes, _, out_node_shape, d_ioinfo = get_model_io(
pathlib.Path(self.config["bie_file"]), self.config["platform"])
elif self.config["emu_mode"] == "float":
input_nodes, _, out_node_shape, _ = get_model_io_v2(pathlib.Path(self.config["onnx_file"]))
model = ort.InferenceSession(self.config["onnx_file"], providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
input_shapes = [onnx_input.shape for onnx_input in model.get_inputs()]
self.config["input_nodes"] = input_nodes
self.config["out_node_shape"] = out_node_shape
self.config["d_ioinfo"] = d_ioinfo
self.config["input_shapes"] = input_shapes
def prep(self, config, app_folder, outer_json):
"""Prepares optional input config values that are needed for simulation."""
self.config = {}
self.config.update(self.defaults)
self.config.update(config)
# for backward compatibility
if "onnx_input" in self.config:
self.config["input_names"] = self.config["onnx_input"]
if self.config["emu_mode"] != "bypass": # ignore config if bypass
try:
self.verify(self.config, app_folder, outer_json)
except exceptions.ConfigError as error:
sys.exit(error)
self.setup_model()
class PostConfig():
"""Postprocess configurations from the input JSON.
Attributes:
config: Dictionary of configurations parsed from an input JSON
"""
defaults = {"post_bypass": False, "post_mode": "", "post_type": ""}
def __init__(self, config, app_folder, outer_json):
self.prep(config, app_folder, outer_json)
def __repr__(self):
return ("-------------------------Postprocess config-------------------------\n{}".format(
pprint.pformat(self.config)))
def prep(self, config, app_folder, outer_json):
"""Prepares optional input config values that are needed for postprocessing."""
self.config = {}
self.config.update(self.defaults)
self.config.update(config)
internal.prep_inner(app_folder, self.config, outer_json, "post")