香橙派AIpro NPU初试YOLOv8检测
收藏回复举报
香橙派AIpro NPU初试YOLOv8检测
新人帖
发表于2024-02-15 22:59:08
0 查看

香橙派AIpro NPU初试YOLOv8检测

去年年底下单的AIpro, 当时还以为是20T的NPU, 感觉赚大了, 非常爽快的下单等着到货, 然后就是漫长的等待, 过年前几天到了, 过年一直在走亲访友也没时间试玩一下, 今天休息一天, 正好试一试这个已经晚了这么久的"好饭".

本文所有内容依赖于昇腾论坛和官网, 非常感谢大佬们提供的导航帖子:

https://www.hiascend.com/forum/thread-0285140173361311056-1-1.html

前置工作介绍

我这次初试YOLOv8检测模型的部署, 开发设备为一台笔记本(无NPU), 操作系统为ubuntu23.04, 部署设备就是香橙派AIpro, 按照 https://www.hiascend.com/forum/thread-0260140249549075069-1-1.html 刷机即可, 下面主要讲一下如何给笔记本配置模型转换的环境吧.

不得不吐槽一下华为的教程文档真的很sb*, 给一堆流程图介绍, 也没有下载链接可以跳转, 这里贴一张图:

true

反正我看了这个完全不知道下面我要干什么, 哪怕是操作命令, 下载命令都不知道去哪找去.

进入正题:

将训练好的pytorch模型部署到atlas310b4上的流程大概也许是(我本次走的流程):

pytorch -> onnx -> om

所以我们主要关注的应该是onnx -> om的流程, 这个流程可以在非华为NPU设备上完成, 下面我就介绍一下我是如何走通的这一流程.

先贴一个被我找了很久翻到的下载网站: https://www.hiascend.com/zh/developer/download/community/result?module=cann

true

我的设备是x86_64架构, 可以通过lscpu查看:

lscpu
# Architecture:           x86_64
#   CPU op-mode(s):       32-bit, 64-bit
#   Address sizes:        39 bits physical, 48 bits virtual
#   Byte Order:           Little Endian
# CPU(s):                 32
#   On-line CPU(s) list:  0-31
# Vendor ID:              GenuineIntel
#   Model name:           13th Gen Intel(R) Core(*T*M*) i9-13900HX

然后我们要下载x86平台开发套件软件包, 这个工具包含了转换模型的可执行程序atc, #后的内容是命令显示的日志:

ls
# Ascend-cann-toolkit_8.0.RC1.alpha001_linux-x86_64.run
bash Ascend-cann-toolkit_8.0.RC1.alpha001_linux-x86_64.run
# Verifying archive integrity...  100%   SHA256 checksums are OK. All good.
# Uncompressing ASCEND_RUN_PACKAGE  100%  
# [Toolkit] [20240215-21:36:28] [INFO] LogFile:/home/ubuntu/var/log/ascend_seclog/ascend_toolkit_install.log
# [Toolkit] [20240215-21:36:28] [ERROR] parameter error ! Scene is neither install nor devel, full, uninstall, upgrade.

根据报错日志我们发现需要增加--install命令行参数, 重新执行:

bash Ascend-cann-toolkit_8.0.RC1.alpha001_linux-x86_64.run --install
===========
= Summary =
===========

Driver:   Not installed.
Toolkit:  Ascend-cann-toolkit_8.0.RC1.alpha001_linux-x86_64 install success, installed in /home/ubuntu/Ascend.

Please make sure that the environment variables have been configured.
-  To take effect for current user, you can exec command below: source /home/ubuntu/Ascend/ascend-toolkit/set_env.sh or add "source /home/ubuntu/Ascend/ascend-toolkit/set_env.sh" to ~/.bashrc.

根据日志我们需要把 source /home/ubuntu/Ascend/ascend-toolkit/set_env.sh这句配置环境的命令加入到.bashrc中让系统模型配置好运行环境.

echo "source /home/ubuntu/Ascend/ascend-toolkit/set_env.sh" >> ~/.bashrc
source ~/.bashrc

然后执行一下atc命令看一眼:

a(base) ubuntu@y9000p:~/Downloads/huawei$ atc
# /home/ubuntu/Ascend/ascend-toolkit/8.0.RC1.alpha001/x86_64-linux/bin/atc.bin: error while loading shared libraries: libascend_hal.so: cannot open shared object file: No such file or directory

我们可以看到环境中缺动态库libascend_hal.so, 我们去/home/ubuntu/Ascend/ascend-toolkit/latest中找一找:

cd ~/Ascend/ascend-toolkit/latest
find ./ -name libascend_hal.so
#./x86_64-linux/devlib/x86_64/libascend_hal.so
#./x86_64-linux/devlib/aarch64/libascend_hal.so
#./x86_64-linux/devlib/libascend_hal.so

我们将这个库的目录路径放到set_env.sh中:

sudo su # 先进入root账户, 否则没权限
echo "export LD_LIBRARY_PATH=\${ASCEND_TOOLKIT_HOME}/x86_64-linux/devlib:\$LD_LIBRARY_PATH" >> /home/ubuntu/Ascend/ascend-toolkit/set_env.sh
exit
source ~/.bashrc
atc # 这回应该可以看到atc工具的日志了
(base) ubuntu@y9000p:~/Downloads/huawei$ atc
ATC start working now, please wait for a moment.
...
ATC run failed, Please check the detail log, Try 'atc --help' for more information
E10007: [--framework] is required. The value must be [0(Caffe) or 1(MindSpore) or 3(TensorFlow) or 5(Onnx)].

现在你可以用atc工具转换你的模型了

YOLOv8检测模型裁剪后处理导出ONNX

如果你已经训练了自己的YOLOv8模型了, 记得新开一个conda环境用来执行下面的操作, 避免训练和导出环境不一致, 在新的环境中安装我裁剪模型并修改导出逻辑的triplemu/model-only分支的代码:

git clone https://github.com/triple-Mu/yolov8.git -b triplemu/model-only
cd yolov8
pip install .

写个脚本用于导出ONNX模型:

from ultralytics import YOLO

model = YOLO('yolov8s.pt', 'detect')
model.export(format='onnx', opset=13, simplify=True)

执行后导出的日志:

Ultralytics YOLOv8.1.14 🚀 Python-3.8.18 torch-2.2.0+cu121 CPU (13th Gen Intel Core(*T*M*) i9-13900HX)
YOLOv8s summary (fused): 168 layers, 11156544 parameters, 0 gradients, 28.6 GFLOPs

PyTorch: starting from 'yolov8s.pt' with input shape (1, 3, 640, 640) BCHW and output shape(s) ((1, 80, 80, 80), (1, 80, 80, 64), (1, 40, 40, 80), (1, 40, 40, 64), (1, 20, 20, 80), (1, 20, 20, 64)) (21.5 MB)

ONNX: starting export with onnx 1.15.0 opset 13...
ONNX: simplifying with onnxsim 0.4.35...
ONNX: export success ✅ 2.1s, saved as 'yolov8s.onnx' (42.6 MB)

Export complete (3.5s)
Results saved to /home/ubuntu/workspace/github/yolov8
Predict:         yolo predict task=detect model=yolov8s.onnx imgsz=640  
Validate:        yolo val task=detect model=yolov8s.onnx imgsz=640 data=coco.yaml  
Visualize:       https://netron.app

我把除了head里卷积的部分都裁掉了, 剩下的额外写后处理代码实现吧.

ONNX模型转OM模型

用第一步得到的atc工具即可, 转换的命令我贴在下面了:

atc \
  --mode 0 \
  --model "yolov8s.onnx" \
  --framework 5 \
  --input_format "NCHW" \
  --input_shape "images:1,3,640,640" \
  --output yolov8-det \
  --output_type "FP32" \
  --host_env_os "linux" \
  --host_env_cpu "aarch64" \
  --soc_version "Ascend310B4"

讲一下我理解的参数含义吧:

--mode 设置为0, 含义为离线转换om模型

--model 设置为你需要转换的onnx模型

--framework 0:Caffe; 1:MindSpore; 3:Tensorflow; 5:Onnx, 我用的是onnx所以选5

--input_format 输入的数据格式, onnx的话默认就是NCHW格式

--input_shape 输入的数据形状, 格式是 "输入名:N,C,H,W"

--output 输出om模型的前缀名, 会自动补一个".om"后缀

--output_type 输出的数据类型, 可以选 "FP32" "FP16" 等, 根据你的需求来即可

--host_env_os 执行模型端的环境, 刷机的话应该都是linux

--host_env_cpu 执行模型端的cpu, 是aarch64架构的

--soc_version 这个是需要自己添加, AIpro的npu就是 "Ascend310B4"

如果遇到缺python包的报错可以尝试自己安装一下python包:

pip install decorator attrs

执行成功的话会有一个success的日志:

ATC start working now, please wait for a moment.
[2024-02-15-14:33:34.494.132]157177 This model is irrelevant to the host platform, parameters about host os and host cpu are ignored.
...
ATC run success, welcome to the next use.

我们得到了yolov8-det.om模型, 马上就可以上板子跑了

OM模型上板运行

我们将一张测试图片, om模型放到板子上, 然后根据你的模型自己修改一下推理代码, 我下面贴一下最简单的推理代码, 仅供参考:

import cv2
import time
import random
import numpy as np
from numpy import ndarray
from typing import List, Tuple
from ais_bench.infer.interface import InferSession

CLASS_COLORS = [[random.randint(0, 255) for _ in range(3)] for _ in range(80)]
CLASS_NAMES = ('person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus',
               'train', 'truck', 'boat', 'traffic light', 'fire hydrant',
               'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog',
               'horse', 'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe',
               'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee',
               'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat',
               'baseball glove', 'skateboard', 'surfboard', 'tennis racket',
               'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl',
               'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot',
               'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch',
               'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop',
               'mouse', 'remote', 'keyboard', 'cell phone', 'microwave',
               'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock',
               'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush')


def softmax(x: ndarray, axis: int = -1) -> ndarray:
    e_x = np.exp(x - np.max(x, axis=axis, keepdims=True))
    y = e_x / e_x.sum(axis=axis, keepdims=True)
    return y


def sigmoid(x: ndarray) -> ndarray:
    return 1. / (1. + np.exp(-x))


def postprocess(
        feats: List[ndarray],
        conf_thres: float = 0.25,
        reg_max: int = 16) -> Tuple:
    dfl = np.arange(0, reg_max, dtype=np.float32)
    scores_pro = []
    boxes_pro = []
    labels_pro = []

    for i in range(3):
        stride = 8 << i
        score_feat = feats[i * 2][0]
        boxes_feat = feats[i * 2 + 1][0]
        score_feat = sigmoid(score_feat)
        labels = score_feat.argmax(-1)
        scores = score_feat.max(-1)

        indices = np.where(scores > conf_thres)
        hIdx, wIdx = indices
        num_proposal = hIdx.size
        if not num_proposal:
            continue

        scores = scores[hIdx, wIdx]
        boxes = boxes_feat[hIdx, wIdx].reshape(-1, 4, reg_max)
        boxes = softmax(boxes, -1) @ dfl
        labels = labels[hIdx, wIdx]

        for k in range(num_proposal):
            h, w = hIdx[k], wIdx[k]
            score = scores[k]
            clsid = labels[k]
            x0, y0, x1, y1 = boxes[k]

            x0 = (w + 0.5 - x0) * stride
            y0 = (h + 0.5 - y0) * stride
            x1 = (w + 0.5 + x1) * stride
            y1 = (h + 0.5 + y1) * stride

            scores_pro.append(float(score))
            boxes_pro.append(np.array([x0, y0, x1 - x0, y1 - y0], dtype=np.float32))
            labels_pro.append(clsid)
    boxes_pro = np.array(boxes_pro, dtype=np.float32)
    scores_pro = np.array(scores_pro, dtype=np.float32)
    labels_pro = np.array(labels_pro, dtype=np.int32)

    return boxes_pro, scores_pro, labels_pro


def non_max_suppression(
        boxes: ndarray,
        scores: ndarray,
        labels: ndarray,
        conf_thres: float = 0.25,
        iou_thres: float = 0.65,
) -> Tuple[ndarray, ndarray, ndarray]:
    # indices = cv2.dnn.NMSBoxesBatched(boxes, scores, labels, conf_thres,
    #                                   iou_thres)
    indices = cv2.dnn.NMSBoxes(boxes, scores, conf_thres, iou_thres)
    return boxes[indices], scores[indices], labels[indices]


def main():
    model_path = 'yolov8-det.om'
    img_path = 'bus.jpg'
    model = InferSession(0, model_path)

    ori_img = cv2.imread(img_path)
    img = np.ascontiguousarray(ori_img[:, :, ::-1].transpose(2, 0, 1)[np.newaxis], dtype=np.float32) / 255.

    # warmup 30 times
    for i in range(30):
        tmp = np.random.randn(1, 3, 640, 640).astype(np.float32)
        model.infer([tmp])

    # calculate infer FPS
    start = time.perf_counter()
    outputs = model.infer([img])
    end = time.perf_counter()
    print(f'Inference FPS: {1 / (end - start)}')

    boxes, scores, labels = non_max_suppression(*postprocess(outputs))

    for box, score, label in zip(boxes, scores, labels):
        x1, y1, w, h = box.round().astype(np.int32).tolist()
        x2, y2 = x1 + w, y1 + h
        cv2.rectangle(ori_img, (x1, y1), (x2, y2), CLASS_COLORS[label], 2, cv2.LINE_AA)
        cv2.putText(ori_img, CLASS_NAMES[label], (x1, max(y1 - 5, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255),
                    2, cv2.LINE_AA)

    cv2.imwrite(ori_img, 'result.jpg')


if __name__ == '__main__':
    main()

上面的代码保存到板子上, 如果你的模型是自定义的模型那么就需要修改类别名之类的, 为了简化推理逻辑, 我提前将图片resize和补边了, 如果你应用的话请注意修改对应的框复原逻辑.

下面是检测结果, 看上去还可以, 打印的日志推理部分是32FPS, 这个8T的NPU还是很强的.

true

总结

想要在AIpro上部署自己的模型可以通过atc工具对onnx进行转换, 然后利用板子上的ais_bench库加载模型, 并利用opencv/numpy等实现前后处理等逻辑.

后面可以展望一下用ascendcl 中的c接口实现一下简单的推理demo, 还有就是模型量化等等

参考: 昇腾社区-官网丨昇腾万里 让智能无所不及

大概扫了一眼, 好像跟cuda的接口很像.

本帖最后由 匿名用户2024/02/15 23:04:30 编辑

我要发帖子