常用指令

  • 查看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设备上进行部署加速。

自动迁移

  • 操作:查看参考文档,将GPU上训练的代码自动地转为NPU上运行的代码
  • 原理:本质就是自动测代码段进行替换,和手动迁移差不多的
  • 注意:自动迁移对torch-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_namesoutput_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开发参考资料

Netron查看模型结构

Netron可以可视化.pt, .onnx等模型,用于debug。