tf에서 TF-TRT를 지원하지만 jetson 기기에서 사용하기에는 saved-model의 로딩시간이 상당히 길고 inference 속도도 tensorRT에 비해 느리다.
unit: sec | saved-model | TF-TRT | TensorRT |
---|---|---|---|
loading | 350 | 350 | 7.5 |
inference | 0.11 | 0.06 | 0.04 |
https://stackoverflow.com/questions/66087844/jetson-nx-optimize-tensorflow-model-using-tensorrt
tensorRT developer guide에도 tf2.x에 대해서는 별다른 이야기가 없다.
https://github.com/onnx/tensorflow-onnx
python -m tf2onnx.convert --saved-model model/effdet_512x512/saved_model/ --output effdet_origin.onnx --opset 11
onnxruntime을 이용해 테스트하여 inference 작동 확인
nvidia에서 tensorRT를 받아 sample/python에 포함된 코드들을 참고해도 좋지만
잘 정리해주신 분이 계신니 참고하여 코드 작성.
https://blog.si-analytics.ai/33
import tensorrt as trt
# TRT 7.x
print('convert onnx to trt')
TRT_LOGGER = trt.Logger(trt.Logger.WARNING)
trt.init_libnvinfer_plugins(TRT_LOGGER, '')
EXPLICIT_BATCH = 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
with trt.Builder(TRT_LOGGER) as builder, \
builder.create_network(EXPLICIT_BATCH) as network, \
trt.OnnxParser(network, TRT_LOGGER) as parser:
builder.max_workspace_size = (1 << 30)
builder.fp16_mode = True
with open('./effdet_origin.onnx', 'rb') as model:
if not parser.parse(model.read()):
for error in range(parser.num_errors):
print (parser.get_error(error))
engine = builder.build_cuda_engine(network)
buf = engine.serialize()
with open('./effdet_origin.trt', 'wb') as f:
f.write(buf)
사실 한방에 변환되지 않는다.
tensorRT에서 지원하지 않는 연산, 구조를 수정해야한다. 아래 참고
netron을 통해 그래프를 시각화하여 어디를 어떻게 고쳐야할지 찾아야한다.
❗️Unsupported ONNX data type: UINT8
❗️Resize node - transformationMode 변경
❗️[TensorRT] ERROR: INVALID_ARGUMENT: getPluginCreator could not find plugin NonMaxSuppression version 1
❗️[TensorRT] ERROR: Network has dynamic or shape inputs, but no optimization profile has been defined.
❗️[TensorRT] ERROR: ../builder/myelin/codeGenerator.cpp (114) - Myelin Error in addNodeToMyelinGraph: 0 (map/while/TensorArrayV2Read/TensorListGetItem operation not supported within a loop body.)
import onnx
import onnx_graphsurgeon as gs
import numpy as np
graph = gs.import_onnx(onnx.load('effdet_origin.onnx'))
nodes = graph.nodes
tensors = graph.tensors()
# set input_tensor shape & dtype
input_tensor = tensors['input_tensor']
input_tensor.dtype = np.float32
input_tensor.shape = [1, 512, 512, 3]
# # resize mode
# # 전처리 Loop 노드 내부에 서브 그래프가 존재함. - node.attrs['body']로 접근
# preprocessing_node = nodes[2]
# resize_node = [node for node in preprocessing_node.attrs['body'].nodes if node.op == 'Resize'][0]
# resize_node.attrs['coordinate_transformation_mode'] = 'half_pixel'
# replace preprocessing node
# efficientNet 전처리 과정 구현
scale = gs.Constant(name='scale', values=np.array([1./255.], np.float32).reshape(1,))
input_scaled = gs.Variable(name='input_scaled', dtype=np.float32)
node_scale = gs.Node(op='Mul', inputs=[input_tensor, scale], outputs=[input_scaled])
nodes.append(node_scale)
ch_offset = gs.Constant(name='ch_offset', values=np.array([0.485, 0.456, 0.406], np.float32).reshape(1, 1, 3))
input_ch_shifted = gs.Variable(name='input_ch_shifted', dtype=np.float32)
node_ch_shift = gs.Node(op='Sub', inputs=[input_scaled, ch_offset], outputs=[input_ch_shifted])
nodes.append(node_ch_shift)
ch_scale = gs.Constant(name='ch_scale', values=(1./np.array([0.229, 0.224, 0.225], np.float32)).reshape(1, 1, 3))
input_ch_scaled = gs.Variable(name='input_ch_scaled', dtype=np.float32)
node_ch_scale = gs.Node(op='Mul', inputs=[input_ch_shifted, ch_scale], outputs=[input_ch_scaled])
nodes.append(node_ch_scale)
# onnx의 Conv 노드의 입력은 NCHW 포맷이므로 이미지를 transpose한다.
input_transposed = gs.Variable(name='input_transposed', dtype=np.float32)
node_transpose = gs.Node(
op='Transpose',
attrs={'perm': [0, 3, 1, 2]},
inputs=[input_ch_scaled],
outputs=[input_transposed],
)
nodes.append(node_transpose)
# Conv 노드의 입력 중 Loop 노드로부터의 입력을 새로운 전처리 노드의 출력으로 대체한다.
conv_node = [n for n in nodes if n.name == 'StatefulPartitionedCall/EfficientDet-D0/model/stem_conv2d/Conv2D'][0]
conv_node.i(0).outputs.clear()
conv_node.inputs[0] = input_transposed
# raw_detection_boxes에 차원 추가
raw_detection_boxes = tensors['raw_detection_boxes']
raw_detection_scores = tensors['raw_detection_scores']
raw_detection_boxes_unsqueezed = gs.Variable('raw_detection_boxes_unsqueezed', dtype=np.float32)
unsqueeze_node = gs.Node(
op='Unsqueeze',
name='unsqueeze_raw_detection_boxes',
attrs={
'axes': [2]
},
inputs=[raw_detection_boxes],
outputs=[raw_detection_boxes_unsqueezed],
)
graph.nodes.append(unsqueeze_node)
# nms 노드 추가
num_detections = gs.Variable('num_detections', dtype=np.int32, shape=(1, 1))
nmsed_boxes = gs.Variable('nmsed_boxes', dtype=np.float32, shape=(1, 100, 4))
nmsed_scores = gs.Variable('nmsed_scores', dtype=np.float32, shape=(1, 100))
nmsed_classes = gs.Variable('nmsed_classes', dtype=np.float32, shape=(1, 100))
nms_node = gs.Node(
op='BatchedNMS_TRT',
name='nms',
attrs={
"shareLocation": True, # 같은 박스로 모든 클래스에 대해 nms를 수행
"numClasses": 6,
"backgroundLabelId": -1, # 백그라운드 인덱스. 없는 경우 -1로 설정
"topK": 4096, # 스코어 순으로 박스를 정렬하여 상위 4096개만 연산
"keepTopK": 100, # nms 결과 중 스코어순으로 100개만 취함
"scoreThreshold": 1e-8,
"iouThreshold": 0.5,
"isNormalized": True, # 박스가 0~1 범위인 경우 True, 픽셀값이면 False
"clipBoxes": True, # 박스를 0~1 범위로 clip
"scoreBits": 10, # 스코어 비트 수. 높으면 nms 성능이 높은 대신 느려진다.
},
inputs=[raw_detection_boxes_unsqueezed, raw_detection_scores],
outputs=[num_detections, nmsed_boxes, nmsed_scores, nmsed_classes],
)
graph.nodes.append(nms_node)
# 그래프의 아웃풋을 새로 정의
graph.outputs = [num_detections, nmsed_boxes, nmsed_scores, nmsed_classes]
# clearup: 아웃풋에 관여하지 않는 노드를 제거한다.
# toposort: 그래프의 노드들을 순서에 맞게 자동 정렬한다.
graph.cleanup().toposort()
onnx.save_model(gs.export_onnx(graph), 'effdet_modify.onnx')
위의 코드로 수정한 onnx는 tensorRT로 변환이 되었다. 변환 성공!