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

644 lines
30 KiB
Python

"""
Utility functions to help perform CSIM inference.
"""
import ctypes
import glob
import re
import subprocess
import struct
import sys
from typing import List, Mapping, Optional, Tuple
import numpy as np
import numpy.typing as npt
from sys_flow.inference import inference_csim_v2 as inference_csim_v2_v1
from sys_flow_v2.inference import inference_csim_v2 as inference_csim_v2_v2
from python_flow.common import constants
from python_flow.common import directory_manager as dm
from python_flow.common import exceptions
from python_flow.utils import utils
# CSIM
def csim_inference(nef: str, inputs: Mapping[str, List[npt.ArrayLike]],
reordering: Optional[List[str]], use_onnx_shape: bool,
model_maps: Mapping[int, Tuple], platform: int = 0,
model_id: Optional[int] = None, data_type: str = "float",
out_dir: Optional[str] = None,
oup_trans: Optional[List[int]] = [0, 2, 3, 1]) -> List[npt.ArrayLike]:
"""Performs inference on the specified NEF file.
Arguments:
nef: String path to the NEF model.
inputs: Mapping of string node names to lists of input NumPy arrays.
reordering: List of string node names specifying the output order. If not provided or
empty, use the output keys as order.
use_onnx_shape: Flag indicating if outputs should be returned in ONNX shape order.
model_maps: Mapping from int model number to a tuple of data. This should come from
the unpack_nefs API call.
platform: Integer platform for NEF model.
model_id: Integer ID of model to perform inference on. Only needed for combined NEF.
data_type: String format of the resulting output, "fixed" or "float".
out_dir: String path to where the output dumps will be saved.
oup_trans: List of integers indicating the axes order to transpose the outputs.
Returns:
A list of NumPy arrays of either floats or integers, depending on 'data_type'. If
data_type is "float", results will be float NumPy arrays. If data_type is "fixed",
results will be fixed NumPy arrays.
"""
if platform in constants.PLATFORMS_MO3:
inference_csim_v2 = inference_csim_v2_v1
else:
inference_csim_v2 = inference_csim_v2_v2
if model_id is None:
model_id = list(model_maps.keys())[0] # single model id
p_nef, ioinfo = model_maps[model_id]
if data_type == "float":
out_dict = inference_csim_v2(
p_nef, ioinfo, inputs, platform, out_fmt="fl", p_working=out_dir)
else:
out_dict = inference_csim_v2(
p_nef, ioinfo, inputs, platform, out_fmt="fx", p_working=out_dir)
# 520 output names will just be integers
if platform == 520:
reordering = [str(index) for index in range(len(out_dict))]
elif reordering is None or not len(reordering):
# if no reordering list, just use provided output dictionary keys
reordering = out_dict.keys()
output_data = []
for output_name in reordering:
output_data.append(out_dict[output_name][0])
if oup_trans is not None: # use axes to transpose if provided
output_data = [data.transpose(oup_trans) if np.ndim(data) == len(oup_trans) else data for data in output_data]
elif not use_onnx_shape:
output_data = [utils.convert_first_to_last(data) for data in output_data]
return output_data
## Rest of these functions are unused after 0.21.0 API change.
# Preparation
def get_node_type(setup: str, data_size: int = 4) -> Optional[int]:
"""Get the node type at the current position.
Only used for 720 setup binaries.
Arguments:
setup: String path to the model setup binary.
data_size: Integer indicating the number of bytes of a single data point.
Returns:
An integer read from the next data_size bytes in the setup binary. Will return
None if the data is empty.
"""
data = setup.read(data_size)
if b'' == data:
return None
return int.from_bytes(data, byteorder="little", signed=False)
def get_input_dims(setup_file: str, index: int, platform: int,
platform_version: int) -> Tuple[int]:
"""Parses the input dimensions from the setup binary.
The returned tuple is in format (row, column, channel).
Argument:
setup_file: String path to the model setup binary.
index: Integer of the input node to get the radix for.
platform: Integer CSIM version the setup binary.
platform_version: Integer version of the specific CSIM platform.
Returns:
A tuple of 3 integers, (row, column, channel), indicating the dimensions of the
specified input node. Default value will be empty tuple.
"""
try:
with open(setup_file, "rb") as setup:
if platform == 520: # 520 input shape is in header
setup.read(7 * 4)
row = int.from_bytes(setup.read(4), byteorder="little", signed=False)
col = int.from_bytes(setup.read(4), byteorder="little", signed=False)
ch = int.from_bytes(setup.read(4), byteorder="little", signed=False)
return (row, col, ch)
elif platform in constants.FLATBUFFERS or (platform == 720 and platform_version == 1):
# 720 version 1 uses flatbuffers
data = setup.read()
inf_data = INFContent.INFContent.GetRootAsINFContent(bytearray(data), 0)
inode = inf_data.Inputs(index)
return (inode.Shape(2), inode.Shape(3), inode.Shape(1)) # row, col, ch
elif platform == 720 and platform_version == 0:
count = 0
setup.read(15 * 4) # cnn header is 15 values for 720
node_type = get_node_type(setup)
while node_type is not None:
if node_type == 5:
setup.read(8) # index, format
if count == index:
row = int.from_bytes(setup.read(4), byteorder="little", signed=False)
col = int.from_bytes(setup.read(4), byteorder="little", signed=False)
ch = int.from_bytes(setup.read(4), byteorder="little", signed=False)
return (row, col, ch)
setup.read(6 * 4) # rest of network info
count += 1
else:
setup.read(constants.NODE_MAP720[node_type] * 4)
node_type = get_node_type(setup)
except FileNotFoundError:
print(f"Could not find setup file, '{setup_file}'. Using default value of ().")
return () # default value
def get_input_format(setup_file: str, index: int, platform: int, platform_version: int) -> str:
"""Parses the input format from the setup binary.
Only needed for non 520 platforms at the moment. The return string will be used as a parameter
to the data converter to generate the input RGBA.
Argument:
setup_file: String path to the model setup binary.
index: Integer of the input node to get the radix for.
platform: Integer CSIM version the setup binary.
platform_version: Integer version of the specific CSIM platform.
Returns:
A string indicating how the specific input node data will be formatted in the RGBA.
Default format will be set to 4W4C8B.
"""
try:
with open(setup_file, "rb") as setup:
if platform in constants.FLATBUFFERS or (platform == 720 and platform_version == 1):
data = setup.read()
inf_data = INFContent.INFContent.GetRootAsINFContent(bytearray(data), 0)
inode = inf_data.Inputs(index)
format_num = inode.Format()
if platform == 720:
return constants.FORMAT_MAP720[format_num]
else:
return constants.FORMAT_MAP530[format_num]
elif platform == 720 and platform_version == 0:
count = 0
setup.read(15 * 4) # cnn header is 15 values for 720
node_type = get_node_type(setup)
while node_type is not None:
if node_type == 5:
setup.read(4) # index
if count == index:
format_num = int.from_bytes(
setup.read(4), byteorder="little", signed=False)
return constants.FORMAT_MAP720[format_num]
setup.read(7 * 4) # rest of network info
count += 1
else:
setup.read(constants.NODE_MAP720[node_type] * 4)
node_type = get_node_type(setup)
except FileNotFoundError:
print(f"Could not find setup file, '{setup_file}'. Using default value of '4W4C8B'.")
return "4W4C8B" # default value
def get_radix(setup_file: str, index: int, platform: int, platform_version: int) -> int:
"""Parses the input radix from the setup binary.
Argument:
setup_file: String path to the model setup binary.
index: Integer of the input node to get the radix for.
platform: Integer CSIM version the setup binary.
platform_version: Integer version of the specific CSIM platform.
Returns:
An integer indicating the radix of the specific input node used to convert between
float and fixed data. Default radix will be set to 8.
"""
try:
with open(setup_file, "rb") as setup:
if platform == 520: # 520 input radix is in header
setup.read(16 * 4)
return int.from_bytes(setup.read(4), byteorder="little", signed=True)
elif platform in constants.FLATBUFFERS or (platform == 720 and platform_version == 1):
# 720 version 1 uses flatbuffers
data = setup.read()
inf_data = INFContent.INFContent.GetRootAsINFContent(bytearray(data), 0)
inode = inf_data.Inputs(index)
return inode.Quantization().FxpInfo(0).Radix()
elif platform == 720 and platform_version == 0:
count = 0
setup.read(15 * 4) # cnn header is 15 values for 720
node_type = get_node_type(setup)
while node_type is not None:
if node_type == 5:
setup.read(7 * 4) # last value
if count == index:
return int.from_bytes(setup.read(4), byteorder="little", signed=True)
setup.read(4) # rest of network info
count += 1
else:
setup.read(constants.NODE_MAP720[node_type] * 4)
node_type = get_node_type(setup)
except FileNotFoundError:
print(f"Could not find setup file, '{setup_file}'. Using default value of 8.")
return 8
def prep_rgba(pre_results: List[npt.ArrayLike], inputs: List[str], setup: str, platform: int,
platform_version: int, use_dongle: bool = False, radix: int = 8,
input_format: int = 0) -> List[str]:
"""Generates the input RGBAs used for CSIM inference.
Inputs generated may be a litte different from the input paths provided in the case the
same model inference is run multiple times for the same image. If using dongle, dimension
checking and radix extraction will be skipped, and the provided radix will be used.
Arguments:
pre_results: List of preprocessed NumPy arrays in channel last format.
inputs: List of string paths to each of the input image RGBA binaries.
setup: String path to the model setup binary.
platform: Integer CSIM version to use.
platform_version: Integer version of the specific CSIM platform that was used.
use_dongle: Flag indicating if RGBAs will be for Dongle use.
radix: Integer radix used for all of the nodes.
input_format: Integer indicating shape format of preprocessed data.
0: channel_last
1: ONNX shape
Returns:
A list of string paths to the newly generated RGBA input binaries.
"""
new_inputs = []
converted_data = []
# convert to channel last
if input_format == 1: # ONNX shape
for pre_data in pre_results:
new_data = utils.convert_first_to_last(pre_data)
converted_data.append(new_data)
else:
converted_data = pre_results
for index, result in enumerate(converted_data):
if not use_dongle:
# verify input shapes are the same
input_shape = result.shape
if len(input_shape) == 4: # cut off batch
input_shape = input_shape[1:]
model_dims = get_input_dims(setup, index, platform, platform_version) # "golden" dims
if input_shape != model_dims:
if input_format == 1:
dims_last = (model_dims[-1], *model_dims[0:-1])
sys.exit("Input dimensions do not match. Please make sure your input data "
f"is of shape {dims_last}")
else:
sys.exit("Input dimensions do not match. Please make sure your input data "
f"is of shape {model_dims}")
radix = get_radix(setup, index, platform, platform_version)
file_name = utils.get_new_dump_name(inputs[index])
utils.convert_pre_numpy_to_rgba(result, file_name, radix, platform, setup,
index, platform_version)
new_inputs.append(file_name)
return new_inputs
# Inference.
def create_ini(base_ini: str, out_ini: str, command: str, weight: str, setup: str,
inputs: List[str], register: str) -> None:
"""Creates INI file used as CSIM input.
Assumes all files except 'out_ini' already exist.
Arguments:
base_ini: String path to the template INI file.
out_ini: String path to the newly generated INI file.
command: String path to the model command binary.
weight: String path to the model weight binary.
setup: String path to the model setup binary.
inputs: List of string paths to each of the input image RGBA binaries.
register: String path to the CSIM file register. Should be related to the CSIM version
used. In most cases, the default register will be valid.
"""
file_inputs = ",".join(inputs)
updated_lines = {
"file_command": "".join(["file_command = ", command, "\n"]),
"file_weight": "".join(["file_weight = ", weight, "\n"]),
"file_setup": "".join(["file_setup = ", setup, "\n"]),
"file_input": "".join(["file_input = ", file_inputs, "\n"]),
"file_register": "".join(["file_register = ", register, "\n"])
}
with open(base_ini, "r") as in_file, open(out_ini, "w") as out_file:
for line in in_file.readlines():
ini_input = line.split(" ", 1)[0]
if ini_input in updated_lines:
out_file.write(updated_lines[ini_input])
else:
out_file.write(line)
def run_csim(output_folder: str, command: str, weight: str, setup: str, platform: int,
inputs: List[str], ini: str = "", dump: int = 0, toolchain: str = "",
platform_version: int = 0, from_toolchain: bool = False) -> None:
"""Performs CSIM inference.
For specific toolchain versions and platform versions, the CSIM executable and register should be
modified prior to calling this function. The ini argument will only be necessary for non-520
CSIM versions. The dump value will only be used for 520 CSIM versions. The toolchain parameter
will only be used for error messages.
Arguments:
output_folder: String path to directory where outputs will be stored.
command: String path to the model command binary.
weight: String path to the model weight binary.
setup: String path to the model setup binary.
platform: Integer CSIM version to use.
inputs: List of string paths to each of the input image RGBA binaries.
ini: String path to the newly generated INI file.
dump: Integer indicating if all intermediate node values should be dumped.
toolchain: String of the toolchain version that was specified.
platform_version: Integer version of the specific CSIM platform that was used. Only used
if from_toolchain is set to True.
from_toolchain: Flag indicating if function call comes from kneron_inference function.
"""
bin_map = {
0: {
530: constants.CSIM530,
540: constants.CSIM540,
630: constants.CSIM630,
720: constants.CSIM720,
730: constants.CSIM730,
},
1: {
720: constants.CSIM720_1
}
}
apb_map = {
0: {
530: constants.APB530,
540: constants.APB540,
630: constants.APB630,
720: constants.APB720,
730: constants.APB730,
},
1: {
720: constants.APB720_1,
}
}
with dm.DirectoryManager(output_folder):
try:
if platform == 520:
subprocess.run([constants.CSIM520, "-d", str(dump), command, weight,
*inputs, setup, "--thread", "1"], check=True)
elif from_toolchain:
# used for kneron_inference, dont really need to worry about platform version
toolchain_map = {
"apb": {
530: constants.APB530,
540: constants.APB540,
630: constants.APB630,
720: constants.APB720_1,
730: constants.APB730,
},
"bin": {
530: constants.CSIM530,
540: constants.CSIM540,
630: constants.CSIM630,
720: constants.CSIM720_1,
730: constants.CSIM730,
}
}
create_ini(constants.TEMPLATE_INI, ini, command, weight, setup,
inputs, toolchain_map["apb"][platform])
subprocess.run([toolchain_map["bin"][platform], ini], check=True)
else:
create_ini(constants.TEMPLATE_INI, ini, command, weight, setup,
inputs, apb_map[platform_version][platform])
subprocess.run([bin_map[platform_version][platform], ini], check=True)
except subprocess.CalledProcessError as error:
sys.exit(f"Hardware CSIM {platform} failed. Please verify that the model is "
f"compatible with the {toolchain} toolchain.\n\n{error}")
# Results.
def convert_results(results: List[npt.ArrayLike], radices: List[int], scales: List[float],
conversion: str) -> List[npt.ArrayLike]:
"""Converts the results using the provided radices, scales, and conversion.
"fx2fl" indicates a conversion from floating point data to fixed point data. "fl2fx"
indicated a conversion from fixed point data to floating point data.
Arguments:
results: List of NumPy arrays to convert.
radices: List of integer radices.
scales: List of float multipliers.
conversion: String indicating conversion type, "fx2fl" or "fl2fx".
Returns:
A list of NumPy arrays of either floats or integers, depending on 'conversion'. If
conversion is 'fx2fl', results will be float arrays. If conversion is 'fl2fx', results
will be fixed arrays.
"""
converted = []
for result, radix, scale in zip(results, radices, scales):
if radix < 0:
value = 1.0 / (1 << -radix)
else:
value = 1 << radix
if conversion == "fx2fl":
data = result.flatten() / float(scale) / value
for i, val in enumerate(data.flatten()): # could not find np function to set precision
data[i] = format(ctypes.c_float(val).value, ".8g") # to match C version of Dynasty
data = data.reshape(result.shape)
else:
data = result * scale * value
data = np.round(data).astype(np.int8)
converted.append(data)
return converted
def convert_csim_data_type(results: List[npt.ArrayLike], output_folder: str, platform: int,
conversion: str, platform_version: int) -> List[npt.ArrayLike]:
"""Converts the data type of the CSIM results.
Assumes the input results are in the same order as in the output folder. The platform_version
parameter currently is only necessary with the new 720 flatbuffers version.
Arguments:
results: List of NumPy arrays to convert.
output_folder: String path to directory where outputs were stored.
platform: Integer CSIM version that was used.
conversion: String indicating conversion type, "fx2fl" or "fl2fx".
platform_version: Integer version of the specific CSIM platform that was used.
Returns:
A list of NumPy arrays of either floats or integers, depending on 'conversion'. If
conversion is 'fx2fl', results will be float arrays. If conversion is 'fl2fx', results
will be fixed arrays.
"""
# get radix/scale information from output folder
if platform == 520:
with open("".join([output_folder, "/radix_scale_info.txt"])) as params:
values = [line.strip().split(" ") for line in params.readlines()]
radices = [struct.unpack("i", struct.pack("I", int(radix)))[0] for _, radix, _ in values]
# convert "int" scale value into float value using its bytes
scales = [struct.unpack("f", struct.pack("I", int(scale)))[0] for _, _, scale in values]
else:
radices = []
scales = []
info = glob.glob(glob.escape(output_folder) + "/dma2seq*.info")
# sort by the integer in the file name, so 10 comes after 9, split on '_' and '.'
info_files = sorted(info, key=lambda x: int(re.split("_|\.", x)[-2]))
for info_file in info_files:
with open(info_file) as info_file_p: # channel/row/column/radix/scale
info = [int(value) for value in info_file_p.read().splitlines()]
if platform == 720 and platform_version == 1:
radices.append(info[3])
else:
if len(info) == 6: # newer version, added batch to element 0
if info[4] < 0: # given as negative value
radices.append(info[4])
else:
radices.append(struct.unpack("i", struct.pack("I", info[4]))[0])
else:
if info[3] < 0: # given as negative value
radices.append(info[3])
else:
radices.append(struct.unpack("i", struct.pack("I", info[3]))[0])
if len(info) == 6:
scales.append(struct.unpack("f", struct.pack("I", info[5]))[0])
else:
scales.append(struct.unpack("f", struct.pack("I", info[4]))[0])
return convert_results(results, radices, scales, conversion)
def csim_520_to_np(output_folder: str, data_type: str, reordering: List[str],
ioinfo: str, first: bool) -> List[npt.ArrayLike]:
"""Converts 520 CSIM output into a list of NumPy arrays.
The resulting NumPy arrays are values in channel first format if first is true (bchw).
Otherwise, it will be in channel last format (bhwc). The type of the resulting arrays
depends on the given data_type. The ordering will depend on the reordering parameter.
Normally, this will be specified based off of the order needed for postprocessing.
Arguments:
output_folder: String path to directory where outputs were stored.
data_type: String specifying type of data to load, "float" or "fixed".
reordering: List of strings correponding to the return node order.
ioinfo: String path to file that maps output node name to node number.
first: Flag indicating if results will be returned in channel first format.
Returns:
A list of NumPy arrays of either floats or integers, depending on 'data_type'. If
data_type is "float", results will be float arrays. If data_type is "fixed", results
will be fixed arrays.
"""
np_output = []
matrices = sorted(glob.glob(glob.escape(output_folder) + "/node*matrix.txt"))
data_files = sorted(glob.glob(glob.escape(output_folder) + "/node*output.txt"))
for matrix, data_file in zip(matrices, data_files):
with open(matrix) as dim_file:
for line in dim_file:
if line.startswith("="):
channel = int("".join(filter(str.isdigit, line)))
elif line.startswith("Rectangle"):
# 520 CSIM will always have two values for this
height, width = [int("".join(filter(str.isdigit, x))) for x in line.split("x")]
data = np.loadtxt(data_file, dtype=np.int8)
data = np.reshape(data, (1, channel, height, width))
if not first:
data = np.transpose(data, (0, 2, 3, 1))
np_output.append(data)
if data_type == "float":
np_output = convert_csim_data_type(np_output, output_folder, 520, "fx2fl", 0)
return utils.reorder_outputs(np_output, reordering, ioinfo)
def csim_to_np(output_folder: str, platform: int, data_type: str, reordering: List[str],
ioinfo: str, first: bool, platform_version: int) -> List[npt.ArrayLike]:
"""Converts non-520 CSIM output into a list of NumPy arrays.
The resulting NumPy arrays are values in channel first format if first is true (bchw).
Otherwise, it will be in channel last format (bhwc). The type of the resulting arrays
depends on the given data_type. The ordering will depend on the reordering parameter.
Normally, this will be specified based off of the order needed for postprocessing. The
platform_version parameter currently is only necessary with the new 720 flatbuffers
version.
Arguments:
output_folder: String path to directory where outputs were stored.
platform: Integer CSIM version that was used.
data_type: String specifying type of data to load, "float" or "fixed".
reordering: List of strings correponding to the return node order.
ioinfo: String path to file that maps output node name to node number.
first: Flag indicating if results will be returned in channel first format.
platform_version: Integer version of the specific CSIM platform that was used.
Returns:
A list of NumPy arrays of either floats or integers, depending on 'data_type'. If
data_type is "float", results will be float arrays. If data_type is "fixed", results
will be fixed arrays.
"""
np_output = []
info = glob.glob(glob.escape(output_folder) + "/dma2seq*.info")
seq = glob.glob(glob.escape(output_folder) + "/dma2seq*.seq")
# sort by the integer in the file name, so 10 comes after 9, split on '_' and '.'
info_files = sorted(info, key=lambda x: int(re.split("_|\.", x)[-2]))
data_files = sorted(seq, key=lambda x: int(re.split("_|\.", x)[-2]))
for data_file, info_file in zip(data_files, info_files):
with open(info_file) as info_file_p: # channel/row/column/radix/scale
info = [int(value) for value in info_file_p.read().splitlines()]
if len(info) == 6: # newer version, added batch to element 0
channel, height, width = info[1:4]
else:
channel, height, width = info[:3]
data = np.loadtxt(data_file, dtype=np.int8)
data = np.reshape(data, (1, channel, height, width))
if not first:
data = np.transpose(data, (0, 2, 3, 1))
np_output.append(data)
if data_type == "float":
np_output = convert_csim_data_type(np_output, output_folder, platform,
"fx2fl", platform_version)
return utils.reorder_outputs(np_output, reordering, ioinfo)
def get_csim_outputs(platform: int, output_folder: str, data_type: str, reordering: List[str],
ioinfo: str, first: bool, platform_version: int = 0) -> List[npt.ArrayLike]:
"""Gets the CSIM inference results as a list of NumPy arrays.
The platform_version parameter currently is only necessary with the new 720 flatbuffers
version.
Arguments:
platform: Integer CSIM version that was used.
output_folder: String path to directory where outputs were stored.
data_type: String specifying type of data to load, "float" or "fixed".
reordering: List of strings correponding to the return node order.
ioinfo: String path to file that maps output node name to node number.
first: Flag indicating if results will be returned in channel first format.
platform_version: Integer version of the specific CSIM platform that was used.
Returns:
A list of NumPy arrays with length n, where n is the number of outputs in the model.
Each array will follow channel first format (bchw) unless the first flag is disabled;
each array will then follow channel last format (bhwc).
"""
try:
if platform == 520:
return csim_520_to_np(output_folder, data_type, reordering, ioinfo, first)
return csim_to_np(output_folder, platform, data_type, reordering,
ioinfo, first, platform_version)
except exceptions.ConfigError as error:
sys.exit(error)