2022年五月昇腾技术讲座:入门篇

Maker 五月昇腾技术讲座

本次讲座讲解华为 MindSpore 与 Atlas 200 DK 的使用

前置知识

必修

  • C++:Linux 环境下的 C++ 语言开发、CMake 工程
  • Python:全部语法、第三方库管理
  • 深度学习原理
  • Linux 基本应用

选修

工具软件

必选

可选

  • 华为官方全流程开发工具链:MindStudio

术语约定

  • 主机:指开发使用的主机
  • Host:运行逻辑代码的处理器
  • Device:运行 AI 代码的处理器
  • Tensor:张量,一个高维矩阵

实验流程

认识昇腾

昇腾计算产业

参阅昇腾计算产业概述

昇腾计算产业是基于昇腾系列(HUAWEI Ascend)处理器和基础软件构建的全栈 AI 计算基础设施、行业应用及服务,包括昇腾系列处理器、系列硬件、CANN(Compute Architecture for Neural Networks,异构计算架构)、AI 计算框架、应用使能、开发工具链、管理运维工具、行业应用及服务等全产业链。

昇腾系列处理器

2018 年 10 月 10 日的 2018 华为全联接大会(HUAWEI CONNECT)上,华为轮值 CEO 徐直军公布了华为全栈全场景 AI 解决方案,并正式宣布了两款 AI 芯片:算力最强的昇腾 910 和最具能效的昇腾 310。https://mp.weixin.qq.com/s?__biz=MzA4MTE5OTQxOQ==&mid=2650027367&idx=3&sn=a295c720f68c3441825f804ffcb765d5&chksm=87983d33b0efb4251fa393af2092b734121c2191ba1aeab13723f601f4544b366bf88e385eca&mpshare=1&

参阅华为海思官网介绍:https://www.hisilicon.com/cn/products/Ascend

昇腾(HUAWEI Ascend) 310 是一款高能效、灵活可编程的人工智能处理器,在典型配置下可以输出 16TOPS@INT8, 8TOPS@FP16,功耗仅为 8W。采用自研华为达芬奇架构,集成丰富的计算单元,提高 AI 计算完备度和效率,进而扩展该芯片的适用性。全 AI 业务流程加速,大幅提高 AI 全系统的性能,有效降低部署成本。

昇腾 310(HUAWEI Ascend)是一款华为专门为图像识别、视频处理、推理计算及机器学习等领域设计的高性能、低功耗 AI 芯片。芯片内置 2 个 AI core,可支持 128 位宽的 LPDDR4X,可实现最大 22TOPS(INT8) 的计算能力。

昇腾(HUAWEI Ascend) 910 是业界算力最强的 AI 处理器,基于自研华为达芬奇架构 3D Cube 技术,实现业界最佳 AI 性能与能效,架构灵活伸缩,支持云边端全栈全场景应用。算力方面,昇腾 910 完全达到设计规格,半精度(FP16)算力达到 320 TFLOPS,整数精度(INT8)算力达到 640 TOPS,功耗 310W。

神经网络计算架构:CANN

简介

AI 场景的异构计算架构,通过提供多层次的编程接口,支持用户快速构建基于昇腾平台的 AI 应用和业务。

CANN 的下载分为社区版商用版,二者都是免费下载的。

  • 社区版更新速度快,可以即时体验最新功能;
  • 商用版更新速度慢,更加稳定

CANN 又分为nnrtnnae两种发行版,其中nnrt只包含离线推理运行时,nnae包含离线推理、在线推理、训练的运行时。

image

image

开发文档

https://support.huaweicloud.com/instg-cann51RC1alpha1/instg_000002.html

AI 计算框架:MindSpore

简介

MindSpore 作为新一代深度学习框架,是源于全产业的最佳实践,最佳匹配昇腾处理器算力,支持终端、边缘、云全场景灵活部署,开创全新的 AI 编程范式,降低 AI 开发门槛。MindSpore 是一种全新的深度学习计算框架,旨在实现易开发、高效执行、全场景覆盖三大目标。为了实现易开发的目标,MindSpore 采用基于源码转换(Source Code Transformation,SCT)的自动微分(Automatic Differentiation,AD)机制,该机制可以用控制流表示复杂的组合。函数被转换成函数中间表达(Intermediate Representation,IR),中间表达构造出一个能够在不同设备上解析和执行的计算图。在执行前,计算图上应用了多种软硬件协同优化技术,以提升端、边、云等不同场景下的性能和效率。MindSpore 支持动态图,更易于检查运行模式。由于采用了基于源码转换的自动微分机制,所以动态图和静态图之间的模式切换非常简单。为了在大型数据集上有效训练大模型,通过高级手动配置策略,MindSpore 可以支持数据并行、模型并行和混合并行训练,具有很强的灵活性。此外,MindSpore 还有“自动并行”能力,它通过在庞大的策略空间中进行高效搜索来找到一种快速的并行策略。

https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/white_paper/MindSpore_white_paperV1.1.pdf

开发文档

https://mindspore.cn/docs/zh-CN/r1.7/index.html

开发工具链:MindStudio

昇腾论坛

Gitee Ascend/samples

应用开发基本流程

确定需求与问题
  • 为什么有需求?
  • 要解决什么问题?
  • 为什么要用 AI?
  • AI 能否解决问题?
  • 有没有其他方案解决同样问题?
  • 别人是如何处理这个问题的?

如果你的需求是区分红色还是黑色,请不要用 AI 解决

如果你的需求是区分三角形和正方形,请不要用 AI 解决

如果你的需求是计算 a + b,请不要用 AI 解决

开发算法
  • 理解问题
  • 论文调研
  • 实验验证
模型训练
模型部署
  • 内存分配与回收
  • 模型装载与卸载
  • 设备能力的调用
测试验收

Atlas外观与接口

Atlas 200 DK 开发者套件(型号 3000)是以 Atlas 200 AI 加速模块(型号 3000)为 核心的开发者板形态的终端类产品。主要功能是将 Atlas 200 AI 加速模块(型号 3000)的接口对外开放,方便用户快速简捷的使用 Atlas 200 AI 加速模块(型号 3000),可以运用于平安城市、无人机、机器人、视频服务器等众多领域的预研开发。

Atlas 200 AI 加速模块(型号 3000)是一款高性能的 AI 智能计算模块,集成了昇腾 310 AI 处理器(Ascend 310 AI 处理器),可以实现图像、视频等多种数据分析与推理计 算,可广泛用于智能监控、机器人、无人机、视频服务器等场景。

目前市面上的AtlasIT21DMDA(旧主板)和IT21VDMB(新主板)两个版本,Maker新到的一批都是新主板。新旧主板可以按照Atlas 200 DK 技术白皮书第 15 页的配图区分。以下以新主板为例讲解,请参阅Atlas 200 DK 技术白皮书

首次启动

镜像烧录

把刚刚的镜像拿下来,把 SD 卡插到电脑上,打开balenaEther,选文件为刚刚的zip压缩包(不必解压),选设备为你的SD卡,点击Flash!,整个过程需要大约半个小时完成。

启动

打开Atlas上盖以观察 LED 状态灯,将烧录好的 SD 卡插入Atlas 200 DK读卡槽,接通电源,观察到主板上 LED 灯从网线口到 GPIO 针依次亮起,四灯全亮说明正常启动,如果十分钟之后还是没有四灯亮起,请参见本文从失败中救赎

连接网络

AtlasHost连接到同一局域网内,使得计算机可以通过网络访问Atlas

在下图中,我将路由器接入公网,Atlas以有线方式接入,主机以无线方式接入。

访问路由器的管理界面(地址可以在路由器的说明书或底部找到),查看davinci-miniIP地址。这里是192.168.3.31,每个局域网都不一样。为讲解方便,以下统一使用192.168.3.31代指AtlasIP

首次登录

打开ssh终端程序,按照以下配置进行连接:

  • 远程主机:上边看到的ip地址
  • 用户名:HwHiAiUser
  • 密码:Mind@123(默认密码)
  • 端口:22ssh协议默认。一般不需要配置)

如果是通过命令行会是这样

ssh HwHiAiUser@192.168.3.31

软件升级

如果按本教程的镜像烧录配置的,无需手动配置,apt已配置为华为云开源镜像站pip已配置为阿里巴巴开源镜像站

使用以下命令更新软件包。如果不更新可能造成一些奇奇怪怪的错误。

sudo apt update && sudo apt upgrade -y

记住,密码是Mind@123

修改密码

如果这台设备所处的局域网会有不受信任的设备接入的话,请修改密码。相信聪明的你一定知道Linux下如何修改用户密码。

passwd

放置ssh公钥

这一节合并到VS Code远程登录中了。

熟悉开发环境

这个镜像里是装好所有所需驱动的,但是为了防止意外,也为了后续开发方便,需要运行样例进行测试以保证安装正确。

巧妇难为无米之炊,我们先搞到一手代码。

Ascend/samples

昇腾官方维护的Gitee仓库中有大量的代码供学习与使用,我们通过这个机会来搭建开发环境。

git clone

可以直接git clone,也可以下载最新release包。

刚刚已经sshAtlas上了,继续用那个终端执行

HwHiAiUser@davinci-mini:~$ git clone https://gitee.com/Ascend/samples.git # 获取存储库
Cloning into 'samples'...
remote: Enumerating objects: 62315, done.
remote: Counting objects: 100% (144/144), done.
remote: Compressing objects: 100% (91/91), done.
remote: Total 62315 (delta 71), reused 79 (delta 48), pack-reused 62171
Receiving objects: 100% (62315/62315), 385.44 MiB | 6.53 MiB/s, done.
Resolving deltas: 100% (40512/40512), done.
Checking out files: 100% (3403/3403), done.
HwHiAiUser@davinci-mini:~$ ls # 看到多了一个 samples 文件夹
Ascend hdc_ppc hdcd ide_daemon samples tdt_ppc var vf0
HwHiAiUser@davinci-mini:~$ cd samples/ # 进入并查看
HwHiAiUser@davinci-mini:~/samples$ ls
CONTRIBUTING_CN.md CONTRIBUTING_EN.md LICENSE NOTICE OWNERS README.md README_CN.md build_run.sh common cplusplus python st
HwHiAiUser@davinci-mini:~/samples$ git checkout v0.6.0 # 切到 v0.6.0 tag
Note: checking out 'v0.6.0'.
HEAD is now at 1a8e9580 !1179 VPC通道任务深度可配 Merge pull request !1179 from wx/master
HwHiAiUser@davinci-mini:~/samples$ ls
CONTRIBUTING_CN.md CONTRIBUTING_EN.md LICENSE NOTICE OWNERS README.md README_CN.md build_run.sh common cplusplus python st

Vim的使用

我们已经在远程开发了,刚刚在终端里敲的每一个字符都是基于ssh协议通过网线和路由器到开发板上的,并piggy back回来的。那让我们用vim写代码吧!

HwHiAiUser@davinci-mini:~/samples$ cd cplusplus/level2_simple_inference/2_object_detection/YOLOV3_coco_detection_picture/src/
HwHiAiUser@davinci-mini:~/samples/cplusplus/level2_simple_inference/2_object_detection/YOLOV3_coco_detection_picture/src$ vim main.cpp

不对吧,都 21 世纪 20 年代了,还有人用vim敲工程?

VS Code远程登录

忆苦思甜,刚才是忆苦,现在是思甜。

首先把这个下载量一百多万的 Extension装上。

或者点击这个链接直接调起 Code:vscode:extension/ms-vscode-remote.remote-ssh

然后点击屏幕右下角的Open a remote window,选择Connect to Host

接着选Add New SSH Host

接着输入刚刚我们已经输入过一次的命令

ssh -A HwHiAiUser@192.168.3.31

其中-A的作用是启用Authentification Forwarding

然后选择写入哪个ssh config,不懂就直接回车。

image

点击Connect之后会打开一个新的VS Code窗口,Code会问你目标主机的系统(选**Linux**),用户的密码(如果你还没改的话是Mind@123)。

每次远程都要输入密码非常烦人,所以采用ssh。没什么好说的,在本机上打开powershellLinux用户打开bash

ssh-keygen

一路回车之后

cat ~/id_rsa.pub # 如果你生成的不是RSA密钥你应该比我明白所以我就不多写了你就自己看着来吧

把这里的内容,打开~/.ssh/authorized_keys,复制进去。

ModelArts

ModelArts提供了企业级环境。

不过我们的第一个任务是手写数字识别,在一般CPU上就可以完成训练,所以我们直接在本机运行,方便直观。

conda create -n ms python=3.9 mindspore-cpu=1.7.0 -c mindspore -c conda-forge -y

进阶应用开发

DVPP 与 AIPP

这两个更是企业级。

总结与展望

更多应用

  • 序列
  • 音频
  • 点云

推荐学习资源

从失败中救赎

启动失败

四灯没有亮起说明启动失败,请尝试重新烧录镜像。

编译失败

运行应用失败

初级应用开发

认识任务:手写数字识别

深度学习模型

认识模型

这次我们用到的模型依然是经典永流传的LeNet-5

图片来源于http://yann.lecun.com/exdb/publis/pdf/lecun-01a.pdf

损失函数采用交叉熵损失函数,算法为带动量的SGD

编写模型代码

编写代码部分,我们将接触昇腾工具链的第一个工具:MindSpore

class LeNet5(nn.Cell):
    def __init__(self):
        super(LeNet5, self).__init__()
        self.conv1 = nn.Conv2d(1, 6, 5, stride=1, pad_mode='valid')
        self.conv2 = nn.Conv2d(6, 16, 5, stride=1, pad_mode='valid')
        self.relu = nn.ReLU()
        self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
        self.flatten = nn.Flatten()
        self.fc1 = nn.Dense(400, 120)
        self.fc2 = nn.Dense(120, 84)
        self.fc3 = nn.Dense(84, 10)

    def construct(self, x):
        x = self.relu(self.conv1(x))
        x = self.pool(x)
        x = self.relu(self.conv2(x))
        x = self.pool(x)
        x = self.flatten(x)
        x = self.fc1(x)
        x = self.fc2(x)
        x = self.fc3(x)

        return x
配置运行环境
import os
# os.environ['DEVICE_ID'] = '0'

import mindspore as ms
import mindspore.context as context
import mindspore.dataset.transforms.c_transforms as C
import mindspore.dataset.vision.c_transforms as CV

from mindspore import nn
from mindspore.train import Model
from mindspore.train.callback import LossMonitor

context.set_context(mode=context.GRAPH_MODE, device_target='CPU') # Ascend, CPU, GPU
获取数据
def create_dataset(data_dir, training=True, batch_size=32, resize=(32, 32),
                   rescale=1/(255*0.3081), shift=-0.1307/0.3081, buffer_size=64):
    data_train = os.path.join(data_dir, 'train') # train set
    data_test = os.path.join(data_dir, 'test') # test set
    ds = ms.dataset.MnistDataset(data_train if training else data_test)

    ds = ds.map(input_columns=["image"], operations=[CV.Resize(resize), CV.Rescale(rescale, shift), CV.HWC2CHW()])
    ds = ds.map(input_columns=["label"], operations=C.TypeCast(ms.int32))
    # When `dataset_sink_mode=True` on Ascend, append `ds = ds.repeat(num_epochs) to the end
    ds = ds.shuffle(buffer_size=buffer_size).batch(batch_size, drop_remainder=True)

    return ds

从中获取几张数据进行可视化。

import matplotlib.pyplot as plt
ds = create_dataset('MNIST', training=False)
data = ds.create_dict_iterator(output_numpy=True).get_next()
images = data['image']
labels = data['label']

for i in range(1, 5):
    plt.subplot(2, 2, i)
    plt.imshow(images[i][0])
    plt.title('Number: %s' % labels[i])
    plt.xticks([])
plt.show()
训练
def train(data_dir, lr=0.01, momentum=0.9, num_epochs=3):
    ds_train = create_dataset(data_dir)
    ds_eval = create_dataset(data_dir, training=False)

    net = LeNet5()
    loss = nn.loss.SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean')
    opt = nn.Momentum(net.trainable_params(), lr, momentum)
    loss_cb = LossMonitor(per_print_times=ds_train.get_dataset_size())

    model = Model(net, loss, opt, metrics={'acc', 'loss'})
    # dataset_sink_mode can be True when using Ascend
    model.train(num_epochs, ds_train, callbacks=[loss_cb], dataset_sink_mode=False)
    metrics = model.eval(ds_eval, dataset_sink_mode=False)
    print('Metrics:', metrics)

train('MNIST/')
导出模型
input_spec = Tensor(np.ones([1, 1, 32, 32]).astype(np.float32))
ms.export(model, mindspore.Tensor(input_spec), file_name='lenet', file_format='ONNX')

模型转换

做到上一步之后,我们已经有了lenet.onnx文件,下面的操作我们将在设备端进行。

在上一步我们进行了模型导出。

模型导出与Checkpoint saving有什么不同呢?

Checkpoint研究中用到的,通过save_checkpointload_checkpoint进行交互;Checkpoint本质上可以看成是一个Pythondict。比如上述LeNet的一个Checkpoint

{
    'conv1.weight': Parameter (name=conv1.weight, shape=(6, 1, 5, 5), dtype=Float32, requires_grad=True),
    'conv2.weight': Parameter (name=conv2.weight, shape=(16, 6, 5, 5), dtype=Float32, requires_grad=True),
    'fc1.weight': Parameter (name=fc1.weight, shape=(120, 400), dtype=Float32, requires_grad=True),
    'fc1.bias': Parameter (name=fc1.bias, shape=(120,), dtype=Float32, requires_grad=True),
    'fc2.weight': Parameter (name=fc2.weight, shape=(84,
    120), dtype=Float32, requires_grad=True),
    'fc2.bias': Parameter (name=fc2.bias, shape=(84,), dtype=Float32, requires_grad=True),
    'fc3.weight': Parameter (name=fc3.weight, shape=(10,
    84), dtype=Float32, requires_grad=True),
    'fc3.bias': Parameter (name=fc3.bias, shape=(10,), dtype=Float32, requires_grad=True)
}

而导出模型则是将模型编译,编译后的模型可以脱离Python语言环境,在专用的运行时中执行,效率也更高。类似于Java代码编译为字节码之后在JVM中执行。

编译模型使用的工具是 Ascend Tensor Compiler,这是我们学到的昇腾工具链中第二个工具。这个工具属于 CANN 的一部分。

原理

img

atc --mode=0 --framework=5 --model=lenet.onnx --output=onnx_lenet --soc_version=Ascend310

https://support.huaweicloud.com/atctool-cann51RC1alpha1/atlasatc_16_0001.html

设备端部署与测试

CMake

CMake is an open-source, cross-platform family of tools designed to build, test and package software. CMake is used to control the software compilation process using simple platform and compiler independent configuration files, and generate native makefiles and workspaces that can be used in the compiler environment of your choice. The suite of CMake tools were created by Kitware in response to the need for a powerful, cross-platform build environment for open-source projects such as ITK and VTK. https://cmake.org/

教程

https://cmake.org/cmake/help/book/mastering-cmake/

原理

CMakeLists –> CMake Software –> configuration file for different toolchains

CMake写一个…Hello World!?

语法

CMake的语法来源于我们数据结构课上都学过也都不知道有什么用的一种数据结构广义表。

推荐工程规范

这里以samples/cplusplus/level2_simple_inference/2_object_detection/YOLOV3_coco_detection_picture工程为例。

目录结构

samples/cplusplus/level2_simple_inference/2_object_detection/YOLOV3_coco_detection_picture
├── CMakeLists.txt                    # CMake 工程
├── README.md
├── README_CN.md
├── YOLOV3_coco_detection_picture.iml # IDEA 的工程配置文件(可忽略)
├── data                              # 模型demo使用的数据
├── inc                               # 头文件
│   ├── dvpp_jpegd.h
│   ├── dvpp_process.h
│   ├── dvpp_resize.h
│   ├── model_process.h
│   ├── object_detect.h
│   └── utils.h
├── model                             # 模型文件
├── scripts                           # 测试/运行所用脚本文件
│   ├── sample_build.sh
│   └── sample_run.sh
└── src                               # 源代码
    ├── CMakeLists.txt
    ├── acl.json
    ├── dvpp_jpegd.cpp
    ├── dvpp_process.cpp
    ├── dvpp_resize.cpp
    ├── main.cpp
    ├── model_process.cpp
    ├── object_detect.cpp
    └── utils.cpp

顶层 CMakeLists.txt

# Copyright (c) Huawei Technologies Co., Ltd. 2019. All rights reserved.

# CMake lowest version requirement
cmake_minimum_required(VERSION 3.5.1)

# project information
project(classification)

add_subdirectory("./src")

src目录下的 CMakeLists.txt

# Copyright (c) Huawei Technologies Co., Ltd. 2019. All rights reserved.

# CMake lowest version requirement
cmake_minimum_required(VERSION 3.5.1)

# project information
project(classification)

# Compile options
add_compile_options(-std=c++11)

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY  "../../../out")
set(CMAKE_CXX_FLAGS_DEBUG "-fPIC -O0 -g -Wall")
set(CMAKE_CXX_FLAGS_RELEASE "-fPIC -O2 -Wall")

add_definitions(-DENABLE_DVPP_INTERFACE)

if (NOT DEFINED ENV{INSTALL_DIR})
    MESSAGE(FATAL_ERROR "Not Defined INSTALL_DIR")
endif()

if (NOT DEFINED ENV{THIRDPART_PATH})
    MESSAGE(FATAL_ERROR "Not Defined THIRDPART_PATH")
endif()

if (NOT DEFINED ENV{CPU_ARCH})
    MESSAGE(FATAL_ERROR "Not Defined CPU_ARCH")
endif()

add_definitions(-DENABLE_DVPP_INTERFACE)
list(APPEND COMMON_DEPEND_LIB avcodec avformat avdevice avutil swresample avfilter swscale)
if ($ENV{CPU_ARCH} MATCHES "aarch64")
    if(EXISTS "$ENV{INSTALL_DIR}/driver/libmedia_mini.so")
        list(APPEND COMMON_DEPEND_LIB media_mini ascend_hal c_sec mmpa)
        add_definitions(-DENABLE_BOARD_CAMARE)
        message(STATUS "arch: arm")
    endif()
endif()

# Header path
include_directories(
    $ENV{INSTALL_DIR}/acllib/include/
    ../inc/
)

if(target STREQUAL "Simulator_Function")
    add_compile_options(-DFUNC_SIM)
endif()

# add host lib path
link_directories(
    $ENV{INSTALL_DIR}/runtime/lib64/stub
)

add_executable(main
        utils.cpp
        model_process.cpp
        object_detect.cpp
        dvpp_process.cpp
        dvpp_resize.cpp
        dvpp_jpegd.cpp
        main.cpp)

if(target STREQUAL "Simulator_Function")
    target_link_libraries(main funcsim)
else()
    target_link_libraries(main ascendcl acl_dvpp stdc++ opencv_core opencv_imgproc opencv_imgcodecs dl rt)
endif()

install(TARGETS main DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})

https://cmake.org/cmake/help/latest/command/target_link_directories.html

AscendCL 编程简明广播体操

提示:本文使用C++进行开发,Python API 与之十分近似,可触类旁通。

AscendCL(Ascend Computing Language)是华为昇腾开发的异构计算架构,提供 Device 管理、Context 管理、Stream 管理、内存管理、模型加载与执行、算子加载与执行、媒体数据处理等 C 语言 API 库供用户开发深度神经网络应用,用于实现目标识别、图像分类等功能。

概念和术语

  • Host指与 Device 相连接的 X86 服务器、ARM 服务器,会利用 Device 提供的 NN(Neural-Network)计算能力,完成业务。
  • Device指安装了芯片的硬件设备,利用 PCIe 接口与 Host 侧连接,提供 NN 计算能力。**
  • Context作为一个容器,管理了所有对象(包括 Stream、Event、设备内存等)的生命周期。不同 Context 的 Stream、不同 Context 的 Event 是完全隔离的,无法建立同步等待关系。
  • 多线程编程场景下,每切换一个线程,都要为该线程指定当前 Context,否则无法获取任何其他运行资源。该部分更多细节我们在“同步等待”实验中深入讨论。

Stream用于维护一些异步操作的执行顺序,确保按照应用程序中的代码调用顺序在 Device 上执行。

以上摘自官网,用大家熟悉的例子作对比,Host 指主机和主机内存,Device 可以类比为显卡和显存,Context 是显存里一个“进程”(类比 CPU 进程)的生命周期,进程中有许多异步操作,异步操作的执行顺序用 Stream 描述。学习过Nvidia CUDA编程的同学对这些概念可能会对号入座。你们是对的,放心大胆地对号入座吧,你们一定比别人学得快。

注意:谨言慎行

因为这是相当Low Level的开发,报错信息不会十分”友好“的给你一个弹窗(因为内核越精简越好)。为了让开发者能获悉错误,ACL内部设置了aclError的一个变量,每次执行ACL API都会对这个变量进行赋值。一些函数会返回这个值的一个copy,如果没有,可以通过aclGetRecentErrMsg()获得一个char*,如果上一个操作成功,则返回nullptr,否则返回一个指向有报错信息的位置。

要我说,华为真是内敛,这么好用的一个API竟然藏得这么深,看到时让我惊喜了一下,用了之后更加兴奋了。

我将错误检查的流程封装为两个宏函数:CHECK对返回aclError的函数进行检查,CHECK_NULLPTR对返回的指针进行检查。

#include <cstdio>
#include <cstdlib>
#define CHECK(call)                                                        \
  do {                                                                     \
    const aclError error_code = call;                                      \
    if (error_code != ACL_SUCCESS) {                                       \
      fprintf(stderr, "\033[1;31mError in %s, file %s, line %d\033[0m\n",  \
              #call, __FILE__, __LINE__);                                  \
      fprintf(stderr, "\033[1;31mError code: %d\033[0m\n", error_code);    \
      fprintf(stderr, "\033[1;31mError message: %s\033[0m\n",              \
              aclGetRecentErrMsg());                                       \
      exit(0);                                                             \
    } else {                                                               \
      fprintf(stderr, "\033[1;32mASCEND API " #call " succeed!\033[0m\n"); \
    }                                                                      \
  } while (0)

#define CHECK_NULLPTR(pointer)                                               \
  do {                                                                       \
    if (pointer == nullptr && aclGetRecentErrMsg() == nullptr) {             \
      fprintf(stderr, "\033[1;31mPointer " #pointer " is nullptr\033[0m\n"); \
      fprintf(stderr, "\033[1;31mError message: %s\033[0m\n",                \
              aclGetRecentErrMsg());                                         \
      exit(0);                                                               \
    } else {                                                                 \
      fprintf(stderr,                                                        \
              "\033[1;32mPointer " #pointer                                  \
              " now is pointing at %#llX!\033[0m\n",                         \
              (unsigned long long)pointer);                                  \
    }                                                                        \
  } while (0)

正身:引用ACL头文件

#include<acl/acl.h>

战歌,起!

这一句表示将AscendCL的所有头文件都包含进来,也就是可以使用ACL的全部功能。

起式:AclInit()

首先,我们要激发昇腾芯片的澎湃算力,就要对其进行初始化操作。

aclError aclInit(const char *configPath)

其中,aclErrorint类型的别名,返回值为$0$时为正常,返回值不为零时,有着各自的意义,可通过查阅官方定义判断错误类型。同时特别提醒:不要以为“不可能在这里出错,一定要对返回值进行判断,在真实的场景中真的有aclInit()都跑不过的情况,比如我有一次返回值是 545000,直接歇菜,重刷系统了。

参数configPath为一个字符数组指针,是配置文件的文件名,置为NULL表示空的配置文件。主要因为这个配置文件我也不会用

第一式:无中生有

您需要按顺序依次申请如下资源:Device、Context、Stream,确保可以使用这些资源执行运算、管理任务。

依照上文的概念和术语,Device 指的是昇腾 310 芯片,Context 指芯片上一个“进程”,Stream 指异步操作的执行顺序(有过网络编程经验用过epoll的同学可能有一些似曾相识?)

那么好,烧火做饭第一步,咱们先把锅(Device)架上!

等等,不急,咱们先摸摸底,看看有几口锅。

aclError aclrtGetDeviceCount(uint32_t *count)

函数的命名采用经典的C语言 OOP 写法,解读一下:acl.rt.GetDeviceCountrt代表 runtime 运行时。

返回值上面仔细讲过,不说了,ACL中的返回值全都是aclError,定义都是一样的,以下再也不提。

值得注意的是,这个函数会修改参数uint32_t*指向的内存地址为可用 Device 数量。建议在下面加一句assert(count>0)判断异常,人人有责。未来的你会感谢现在判断异常的自己。

咱们有锅了~架上!

aclError aclrtSetDevice(int32_t deviceId)

不多说因为我也不知道会有多少个 Device 我只用到过一个

第二式:木牛流马

aclError aclrtCreateContext(aclrtContext *context, int32_t deviceId)

复杂起来了!返回值依然是经典的错误码(不行!程序员怎么能说是“错误码”呢!要说“正确码”!图个吉利)。

注意第二参数deviceId为值传参,是输入,输入的是一个在上一步aclrtSetDevice中成功设置的 device,我一般设为 0;第一参数context是一个aclrtContext指针(老 OOP 了),不需要初始化(良好的编程规范告诉我们初始化指针要置NULL),也千万不要拿到了内容之后给free掉。指针指向区域的具体内容没有文档资料,相当于私有成员了吧。只能通过文档中提供的API进行操作。

第三式:行云流水

硬件资源最多支持 1024 个 Stream……每个 Context 对应一个默认 Stream,该默认 Stream 是调用aclrtSetDevice接口或aclrtCreateContext接口隐式创建的。推荐调用 aclrtCreateStream 接口显式创建 Stream。

aclError aclrtCreateStream(aclrtStream *stream)

函数似乎没有输入,但输入已经确定了是当前活动的 Context,输出自然是 stream 指针指向的内存区域了。

之前的一切都只是铺垫,下面终于搭好花轿子,准备请新娘登轿啦!(刚刚好像在用做饭作比喻这不重要)

第四式:飞龙探云手

???名字很怪大家见怪不怪就好

aclError aclmdlLoadFromFile(const char *modelPath, uint32_t *modelId)

modelPath是输入的 om 文件的路径,请确保执行程序的用户对这个路径中的文件有读权限,输出一个modelId,相当于Linux系统中广泛使用的文件描述符(仅仅是相当于)。

https://support.huaweicloud.com/aclcppdevg-cann51RC1alpha1/aclcppdevg_000021.html

当然这个 API 还有高级版本,比如这个从内存里读模型:

aclError aclmdlLoadFromMem(const void* model, size_t modelSize, uint32_t* modelId)

还有这个,连权值都让你自己分配

aclError aclmdlLoadFromFileWithMem(const char *modelPath, uint32_t *modelId, void *workPtr, size_t workSize, void *weightPtr, size_t weightSize)

这 API 就需要用户自行调用aclmdlQuerySize aclrtMalloc等一系列函数自行管理内存,不到不得已不用这个函数。

第五式:吐息运气

取不起名字了……

创建输入数据集

aclmdlDataset *aclmdlCreateDataset()

注意返回值!是一个指针。

返回的是一个数据集容器,要往里加图片需要

aclError aclmdlAddDatasetBuffer(aclmdlDataset *dataset, aclDataBuffer *dataBuffer)

对,就是把acldataBuffer指向的内存加入dataset之中。

但问题来了!dataBuffer从哪里搞到?

aclDataBuffer *aclCreateDataBuffer(void *data, size_t size)

data是程序员自行用aclrtMalloc创建的内存空间。policy 详情见官方说明

aclError aclrtMalloc(void **devPtr, size_t size, aclrtMemMallocPolicy policy)

输出同时也需要一个数据集来存放!

Atlas 200 DK中,DEVICEHOST共享同一片内存(其他就不是了),所以可以通过对dataBuffer中指向区域的赋值进行修改。

 CHECK(
      aclrtMalloc((void**)&inputBuffer, inputSize, ACL_MEM_MALLOC_NORMAL_ONLY));
  CHECK(aclrtMalloc((void**)&outputBuffer, outputSize,
                    ACL_MEM_MALLOC_NORMAL_ONLY));
  for (int i = 0; i < 28; ++i)
    for (int j = 0; j < 28; ++j) {
      inputBuffer[i * 28 + j] = (float)imageData[i * 28 + j] / 255.0;
    }

第六式:粮草先行

aclDataBuffer* inDataBuffer = aclCreateDataBuffer(inputBuffer, inputSize);
aclDataBuffer* outDataBuffer = aclCreateDataBuffer(outputBuffer, outputSize);

CHECK_NULLPTR(inDataBuffer);
CHECK_NULLPTR(outDataBuffer);

CHECK(aclmdlAddDatasetBuffer(inputDataset, inDataBuffer));
CHECK(aclmdlAddDatasetBuffer(outputDataset, outDataBuffer));

CHECK(aclmdlExecute(modelId, inputDataset, outputDataset));

第七式:正菜!

aclError aclmdlExecute(uint32_t modelId, const aclmdlDataset *input, aclmdlDataset *output)

自由了!就是这么轻松自在,我们的模型开始执行了! 执行之后,结果会放到output里。这是个同步接口,还有一个异步接口叫做aclmdlExecuteAsync,需要指定 stream 才可以使用,并注意使用aclrtSynchronizeStream(stream)等待。

收式:过河拆桥

我的任务完成啦!!!

有始有终,咱们把自己申请的内存、加载的模型、StreamContextDevice按申请的反顺序释放掉,最后释放掉acl

// aclrtFree释放自己申请的内存
CHECK(aclDestroyDataBuffer(inDataBuffer));
CHECK(aclDestroyDataBuffer(outDataBuffer));

CHECK(aclrtFree(inputBuffer));
CHECK(aclrtFree(outputBuffer));

CHECK(aclmdlDestroyDataset(inputDataset));
CHECK(aclmdlDestroyDataset(outputDataset));
CHECK(aclmdlDestroyDesc(modelDesc));

CHECK(aclrtDestroyStream(stream));
CHECK(aclrtDestroyContext(context));
CHECK(aclrtResetDevice(deviceId));
CHECK(aclFinalize());

有一说一,我觉得这一式的题目是这里面最贴切的。

进阶

昇腾芯片还具有更强大的能力:DVPP数字图像硬件级解编码、AIPP预处理等,这些功能都需要进一步学习。

另外,目前该项目的主要目的是为了普及基础知识,更多内容(内存优化、表现评估等)还需要进一步发掘。

完整代码

参考:昇腾CANN社区版(5.1.RC1.alpha001)

昇腾 CANN 社区版 5.0.2.alpha003

https://support.huaweicloud.com/instg-cann51RC1alpha1/instg_000002.html

一些不错的参考:

2 个赞