常用指令
- 查看NPU资源:
npu-smi info
- 查看python程序占用:
ps -ef |grep python
- 将日志输出到txt:
python demo.py > log.txt 2>&1
- 杀死python程序清显存:
pkill -9 python
- 查看板卡型号:
ascend-dmi -i -dt
模型迁移部署
将深度学习代码或模型放到NPU设备上进行部署加速。
自动迁移
模型转换
将pytorch权重模型先导出onnx,再导出到昇腾的.om权重,最后改写权重加载调用代码,读取om权重进行推理。该方法较直接进行代码自动迁移路径速度更快,但是bug多,支持的算子有限。模型转om权重主要用到ATC工具,建议仔细阅读其文档,注意避坑。
Torch2ONNX
首先将torch权重文件(pt, ckppt都行)加载,然后用dummy input构造数据流进行推理,采用torch.export到处即可。
以图像配准任务为例,图像配准输入是两个多域image,输出是其匹配的结果,导出的代码段示例如下:
torch.onnx.export(
matcher,
(image0, image1),
output_file,
input_names = input_names,
output_names = output_names,
dynamic_axes = dynamic_axes_input_fixed,
export_params=True,
verbose=False,
opset_version=11)
其中:
matcher
是model;(image0, image1)
是输入的pair,如果单输入则不需要构建tuple;output_file
是输出的onnx文件名;input_names
和output_names
是输入输出节点的名称,和torch的节点名是一样的,建议可以取一下便于后面用netron进行debug的时候好找,例如input_names = ["input1","input2"]
;dynamic_axes
用于定义计算图中的节点维度,用于处理动态维度节点的形状(一般主要是bs,图像尺寸),如果是静态张量固定形状输入,则dynamic_axes=None或者不管就行,如果是动态维度,需要指出哪些维度是动态的,例如dynamic_axes_input_fixed={"output1":{0: 'points1'},"output2":{0: 'points2'} }
允许两个输出节点的第0维(也就是bs维)为动态的,并为其取名;dynamic_axes={ "input1": { 2: 'in_height1', 3: 'in_width1'},"input2": { 2: 'in_height2', 3: 'in_width2'}, "output1":{0: 'points1'}, "output2":{0: 'points2'}}
则进一步允许输入图像的尺寸为动态的。【注意】导出到onnx的时候动态维度没啥毛病,但是进一步到处om时动态维度容易出bug不好找;opset_version=11
这个是大坑,转onnx这里没啥毛病,但是后面转om bug很多。要看看自己的板子支持多少范围的opset算子版本,例如310B就只支持11【参考】。
要校验模型对不对,可以上Netron看看onnx权重能不能打开;如果能打开并可视化,进一步可以加载试试:
``` # Checks try: # import ipdb;ipdb.set_trace() model_onnx = onnx.load(output_file) # load onnx model onnx.checker.check_model(model_onnx) # check onnx model print(‘Support ONNX!’) onnx.save(model_onnx, output_file) except onnx.checker.ValidationError as e: print(“RuntimeError when export ONNX”)
# Simplify
# onnx_model = onnx.load(output_file) # load onnx model
# model_simp, check = simplify(onnx_model)
# assert check, "Simplified ONNX model could not be validated"
# onnx.save(model_simp, output_path)
print('Finished exporting onnx!!!\n\n') ```
如果再不放心,就加载onnx并创建推理,校验一下模型输出对不对: ``` if test_onnx: session = onnxruntime.InferenceSession(output_file) mkpts0_f, mkpts1_f = session.run(None, {“input1”: img0.numpy(), “input2”: img1.numpy()})
各个函数的输入输出流中不要有不支持复杂算子或数据类型(如**字典**),会导致计算图构建失败。
#### ONNX2OM
华子的生态没做起来,全靠自己维护社区做的有限,很多算子不仅不支持甚至还没测试过,导致bug非常多。因此ATC工具使用前务必逐行阅读[文档](https://www.hiascend.com/document/detail/zh/Atlas200IDKA2DeveloperKit/23.0.RC2/Appendices/ttmutat/atctool_000041.html),注意避坑。
##### 常见的坑:
- 不支持onnx中的**动态shape的输入**,模型转换时需指定固定数值;
- 模型中的所有层算子除const算子外,**输入和输出**需要满足dim!=0;
- 310B的套件只支持onnx中v11的opset;
- 各个函数的输入输出流中不要有不支持复杂算子或数据类型(如**字典**);
- 各种数据类型的不匹配。
##### 模型转换指令
atc –model=converter/match.onnx
–framework=5
–output=converter/match_om
–soc_version=Ascend310B1
–input_shape=”input1:1,1,512,512;input2:1,1,512,512”
–input_format=NCHW
–log=error
–op_select_implmode=high_performance
每个参数设置前务必**逐字**阅读[参数文档](https://www.hiascend.com/document/detail/zh/Atlas200IDKA2DeveloperKit/23.0.RC2/Appendices/ttmutat/atctool_000047.html)!
输出文件名不用带.om;具体参数不作赘述,参看文档。
##### OM模型调用方法
其实核心就三四行,这里全贴出来了,以供参考:
import cv2 import numpy as np import os import time from tqdm import tqdm import matplotlib.pyplot as plt from ais_bench.infer.interface import InferSession # 昇腾推理接口
from src.utils.data_io import lower_config from src.config.default import get_cfg_defaults eps = 1e-6
NPU配置参数
NPU_DEVICE_ID = 0
OM_MODEL_PATH = “converter/match.om”
OM_MODEL_PATH = “converter/match_linux_aarch64.om” INPUT_SHAPE = (1, 1, 256, 256) # w = h
folder_path = “sample”
class AscendInferer: def init(self): self.session = InferSession(NPU_DEVICE_ID, OM_MODEL_PATH)
self.input_desc = self.session.get_inputs()
self.output_desc = self.session.get_outputs()
def preprocess(self, img_raw):
if img_raw.shape[:-1] != INPUT_SHAPE[2:]:
img_raw = cv2.resize(img_raw, (INPUT_SHAPE[3], INPUT_SHAPE[2]))
img_gray = cv2.cvtColor(img_raw, cv2.COLOR_BGR2GRAY)
h0, w0 = img_gray.shape
config = get_cfg_defaults(inference=True)
config = lower_config(config)
df = config["test"]["df"]
if h0 % df != 0: # multi-head的分块,要为8 head的整数倍
# 如果no-squre,后面显示的时候还要加上ratio
raiseNotImplementedError("Please keep the input image squre!!\n\n")
# image0_ratio = 1
# new_w0, new_h0 = map(lambda x: int(x * image0_ratio // df * df), [w0, h0])
# img_gray = cv2.resize(img_gray, (new_w0, new_h0))
# scale0 = np.array([w0 / new_w0, h0 / new_h0], dtype=np.float32)
img_gray = img_gray[None][None] / 255.
img_gray_mean = img_gray.mean(axis=(2, 3), keepdims=True)
img_gray_std = img_gray.std(axis=(2, 3), keepdims=True)
img_gray = (img_gray - img_gray_mean) / (img_gray_std + eps)
return img_gray
def infer(self, img0, img1):
## Debug ##
self.input_info = self.session.get_inputs()
self.output_info = self.session.get_outputs()
# print("Input nodes:")
# for idx, info in enumerate(self.input_info):
# print(f" Input {idx}:")
# print(f" Name: {info.name}")
# print(f" Shape: {info.shape}")
# print(f" Format: {info.format}")
# print("Outputs nodes:")
# for idx, info in enumerate(self.output_info):
# print(f" Output {idx}:")
# print(f" Name: {info.name}")
# print(f" Shape: {info.shape}")
# print(f" Format: {info.format}")
# assert img0.shape == tuple(self.input_info[0].shape), "Image size not matched!\n\n"
## Inference ##
inft = time.time()
outputs = self.session.infer([img0.astype(np.float32), img1.astype(np.float32)])
mkpts0 = self._parse_output(outputs[0])
mkpts1 = self._parse_output(outputs[1])
print(time.time()-inft)
return mkpts0.reshape(-1,2), mkpts1.reshape(-1,2)
def _parse_output(self, output_tensor):
return output_tensor.squeeze() # 去除批量维度
def draw_matches(image0, image1, points0, points1): difference = image0.shape[0] - image1.shape[0] if difference < 0: top = abs(difference) // 2 bottom = abs(difference) - top image0 = cv2.copyMakeBorder(image0, top, bottom, 0, 0, cv2.BORDER_CONSTANT) if len(points0) > 0: points0[:, 1] += top elif difference > 0: top = difference // 2 bottom = difference - top image1 = cv2.copyMakeBorder(image1, top, bottom, 0, 0, cv2.BORDER_CONSTANT) if len(points1) > 0: points1[:, 1] += top points0 = [cv2.KeyPoint(point0[0], point0[1], 1) for point0 in points0] points1 = [cv2.KeyPoint(point1[0], point1[1], 1) for point1 in points1] matches = [cv2.DMatch(index, index, 1) for index in range(len(points0))] # noinspection PyTypeChecker matches_visual = cv2.drawMatches(image0, points0, image1, points1, matches, None, (0, 255, 0)) return matches_visual
def post_process(img0_rgb, img1_rgb, mkpts0, mkpts1, draw_flag=True): # inlier_method = “H” # F: Fundamental Matrix, H: Homography # if inlier_method == “F”: # fundamental, mask = cv2.findFundamentalMat(mkpts1, mkpts0, cv2.USAC_MAGSAC, ransacReprojThreshold=1, # maxIters=10000, confidence=0.9999) # elif inlier_method == “H” and len(mkpts0) >= 4: # homography, mask = cv2.findHomography(mkpts1, mkpts0, cv2.USAC_MAGSAC, 5.0) # mkpts0 = mkpts0[mask.flatten() == 1] # mkpts1 = mkpts1[mask.flatten() == 1]
if draw_flag:
matchedVis = draw_matches(img0_rgb, img1_rgb, mkpts0, mkpts1)
plt.figure()
plt.axis("off")
plt.title(f"{len(mkpts0)} matches")
plt.imshow(matchedVis, "gray")
plt.savefig("save_folder/om_matching.png")
if name == “main”: ascend_inferer = AscendInferer()
# 遍历图像对
image_names = sorted(os.listdir(folder_path))
for i in tqdm(range(0, len(image_names), 2)):
start_time = time.time()
img0_raw = cv2.imread(f"{folder_path}/{image_names[i]}")
img1_raw = cv2.imread(f"{folder_path}/{image_names[i+1]}")
img0_rgb = cv2.cvtColor(img0_raw, cv2.COLOR_BGR2RGB)
img1_rgb = cv2.cvtColor(img1_raw, cv2.COLOR_BGR2RGB)
img0 = ascend_inferer.preprocess(img0_raw)
img1 = ascend_inferer.preprocess(img1_raw)
mkpts0, mkpts1 = ascend_inferer.infer(img0, img1)
# 如果输入尺度和INPUT_SHAPE不一致需要缩放
scale0 = 1.0
scale1 = 1.0
# h0, w0 = img0_gray.shape
# scale0 = np.array([w0/INPUT_SHAPE[3], h0/INPUT_SHAPE[2]])
# h1, w1 = img1_gray.shape
# scale1 = np.array([w1/INPUT_SHAPE[3], h1/INPUT_SHAPE[2]])
# 还原原始分辨率坐标
import ipdb;ipdb.set_trace()
mkpts0 = mkpts0 * scale0
mkpts1 = mkpts1 * scale1
if len(mkpts0) == 0:
print("Not matched!\n")
else:
post_process(img0_rgb, img1_rgb, mkpts0, mkpts1)
end_time = time.time()
print(f"Time cost: {time.time()-start_time:.2f}s") ```
常见问题和Debug技巧
Debug
- data预处理放在model外面!!不然转om各种奇怪bug!!
- 索引节点bug的时候可以在netron上搜索name或者shape,实现快速定位;若是搜索shape可能找不到,因为transport等操作是不显示shape的;
- 时刻注意数据流的数据类型,float64还是float32,这涉及om构建推理图时分配的实际运算内存大小;
- 可以export ASCEND_LAUNCH_BLOCKING=1屏蔽掉npu迁移过程乱七八糟的编译东西
转om
- 不支持onnx中的动态shape的输入,模型转换时需指定固定数值;
- 模型中的所有层算子除const算子外,输入和输出需要满足dim!=0;
- 310B的套件只支持onnx中v11的opset;
- 各个函数的输入输出流中不要有不支持复杂算子或数据类型(如字典);
调试环境和参考资料
Zerotier 配置内网穿透组网
安装步骤
- 官网注册:https://my.zerotier.com/
- 组网教程参考:https://zhichao.org/posts/zerotier
-
主机端如windows直接图形界面操作,加入组网就可
- Linux客户端安装zerotier:
curl -s https://install.zerotier.com | sudo bash # Linux系统
- 客户端启动zerotier:
cd /var/lib/zerotier-one/ ; ./zerotier-one -d
如果报错就zerotier-cli info
- 加入局域网组网:
zerotier-cli join <Network ID>
- 网页上刷新一下就能获得IP用于连接。【注:偶尔会慢点,卡个几分钟才联通】
Atlas 200DK 310B开发参考资料
- 开发文档
- 快速模型迁移
- Pytorch/CANN/TorchNPU版本对应关系
- 编译OpenCV NPU
- AIPP加速图像预处理步骤
- 昇腾om模型的推理参考文档以及官方demo
Netron查看模型结构
Netron可以可视化.pt, .onnx等模型,用于debug。