CANN应用开发完整流程
引言
CANN(Compute Architecture for Neural Networks)是一套完整的AI计算架构,为开发者提供了从模型转换、算子开发到应用部署的全栈解决方案。本文将从零开始,详细介绍CANN应用开发的完整流程,帮助开发者快速上手。
CANN应用开发全景图
┌───────────────────────────────────────────────────────────────────┐
│ CANN应用开发完整流程 │
├───────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐│
│ │ 模型开发阶段 │ -> │ 模型转换阶段 │ -> │ 应用开发阶段 ││
│ │ │ │ │ │ ││
│ │ • 框架选择 │ │ • ATC转换 │ │ • AscendCL API ││
│ │ • 模型训练 │ │ • OM模型生成 │ │ • 数据预处理 ││
│ │ • 模型导出 │ │ • 模型优化 │ │ • 推理执行 ││
│ └─────────────────┘ └─────────────────┘ └─────────────────┘│
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 编译部署阶段 │ │
│ │ │ │
│ │ • 交叉编译 │ │
│ │ • 运行时配置 │ │
│ │ • 性能调优 │ │
│ └─────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────────┘
第一阶段:模型准备
1.1 支持的模型框架
CANN支持主流深度学习框架的模型转换:
| 框架 | 版本要求 | 导出格式 | 转换工具 |
|---|---|---|---|
| TensorFlow | 2.x | PB/frozen_graph | ATC |
| PyTorch | 1.8+ | ONNX/PTH | ATC |
| Caffe | - | caffemodel/prototxt | ATC |
| MindSpore | - | MindIR | 内置支持 |
| ONNX | - | onnx | ATC |
1.2 模型导出示例
PyTorch模型导出
python
import torch
import torch.onnx
from torchvision.models import resnet50, ResNet50_Weights
# 1. 加载预训练模型
model = resnet50(weights=ResNet50_Weights.DEFAULT)
model.eval()
# 2. 创建示例输入
dummy_input = torch.randn(1, 3, 224, 224)
# 3. 导出为ONNX格式
torch.onnx.export(
model, # 模型
dummy_input, # 示例输入
"resnet50.onnx", # 输出文件名
export_params=True, # 导出参数
opset_version=11, # ONNX算子集版本
do_constant_folding=True, # 常量折叠优化
input_names=['input'], # 输入节点名
output_names=['output'] # 输出节点名
)
print("ONNX model exported successfully!")
TensorFlow模型导出
python
import tensorflow as tf
from tensorflow.keras.applications import ResNet50
# 1. 加载模型
model = ResNet50(weights='imagenet')
# 2. 保存为SavedModel格式
tf.saved_model.save(model, "resnet50_savedmodel")
print("SavedModel exported successfully!")
# 3. 或者冻结为PB格式
tf.compat.v1.reset_default_graph()
with tf.compat.v1.Session() as sess:
# 加载模型定义
with tf.compat.v1.gfile.GFile("resnet50.pb", "rb") as f:
graph_def = tf.compat.v1.GraphDef()
graph_def.ParseFromString(f.read())
tf.import_graph_def(graph_def, name="")
# 冻结图
output_graph_def = tf.compat.v1.graph_util.convert_variables_to_constants(
sess,
tf.compat.v1.get_default_graph().as_graph_def(),
["output"]
)
with tf.compat.v1.gfile.GFile("resnet50_frozen.pb", "wb") as f:
f.write(output_graph_def.SerializeToString())
print("Frozen PB model exported successfully!")
第二阶段:模型转换
2.1 ATC工具介绍
ATC(Ascend Tensor Compiler)是CANN提供的模型转换工具,用于将第三方框架模型转换为CANN支持的OM(Offline Model)格式。
2.2 基础转换命令
bash
# ATC基础命令格式
atc --framework=<框架类型> \
--model=<模型文件> \
--output=<输出OM文件名> \
--soc_version=<处理器型号> \
[其他选项]
2.3 转换示例
ONNX模型转换
bash
#!/bin/bash
# 设置环境变量
export ASCEND_HOME=/usr/local/Ascend
export PATH=${ASCEND_HOME}/ascend-toolkit/latest/compiler/ccec_compiler/bin:${PATH}
export LD_LIBRARY_PATH=${ASCEND_HOME}/ascend-toolkit/latest/lib64:${LD_LIBRARY_PATH}
# ONNX转OM
atc \
--framework=ONNX \
--model=resnet50.onnx \
--output=resnet50 \
--soc_version=Ascend910A \
--input_format=NCHW \
--input_shape="input:1,3,224,224" \
--out_nodes="output:0" \
--log=error \
--enable_small_channel=1
echo "ONNX to OM conversion completed!"
TensorFlow模型转换
bash
#!/bin/bash
# TensorFlow PB转OM
atc \
--framework=3 \
--model=resnet50_frozen.pb \
--output=resnet50_tf \
--soc_version=Ascend910A \
--input_shape="input:1,224,224,3" \
--out_nodes="output:0" \
--log=error \
--enable_small_channel=1
echo "TensorFlow to OM conversion completed!"
Caffe模型转换
bash
#!/bin/bash
# Caffe模型转OM
atc \
--framework=0 \
--model=resnet50.prototxt \
--weight=resnet50.caffemodel \
--output=resnet50_caffe \
--soc_version=Ascend910A \
--input_format=NCHW \
--input_shape="data:1,3,224,224" \
--out_nodes="prob:0" \
--log=error
echo "Caffe to OM conversion completed!"
2.4 高级转换选项
bash
atc \
--framework=ONNX \
--model=model.onnx \
--output=model \
--soc_version=Ascend910A \
# 输入配置
--input_format=NCHW \
--input_shape="input:1,3,640,640;mask:1,1,80,80" \
# 输出配置
--out_nodes="output:0;cls:0" \
# 精度配置
--precision_mode=allow_fp32_to_fp16 \
# 优化选项
--enable_small_channel=1 \
--op_select_implmode=high_performance \
--optypelist_for_implmode="Gelu" \
# 其他选项
--log=info \
--dump_model=./dump_model
第三阶段:应用开发
3.1 项目结构
cann_application/
├── build/
│ └── CMakeLists.txt
├── src/
│ ├── main.cpp
│ ├── model_loader.cpp
│ ├── data_preprocess.cpp
│ └── postprocess.cpp
├── include/
│ ├── model_loader.h
│ ├── data_preprocess.h
│ └── postprocess.h
├── models/
│ └── resnet50.om
├── data/
│ └── test_images/
├── CMakeLists.txt
└── README.md
3.2 完整代码实现
model_loader.h
cpp
#pragma once
#include "acl/acl.h"
#include "acl/acl_mdl.h"
#include <string>
#include <memory>
#include <vector>
class ModelLoader {
public:
ModelLoader();
~ModelLoader();
// 加载模型
aclError Load(const char* modelPath);
// 创建输入数据集
aclError CreateInput(void* data, size_t size);
// 创建输出数据集
aclError CreateOutput();
// 执行推理
aclError Execute();
// 获取输出数据
aclError GetOutputData(std::vector<void*>& outputs,
std::vector<size_t>& sizes);
// 获取模型信息
size_t GetInputSize() const { return inputSize_; }
size_t GetOutputNum() const { return outputNum_; }
// 卸载模型
void Unload();
private:
uint32_t modelId_;
aclmdlDesc* modelDesc_;
aclmdlDataset* inputDataset_;
aclmdlDataset* outputDataset_;
size_t inputSize_;
size_t outputNum_;
bool loaded_;
};
model_loader.cpp
cpp
#include "model_loader.h"
#include <iostream>
ModelLoader::ModelLoader()
: modelId_(0),
modelDesc_(nullptr),
inputDataset_(nullptr),
outputDataset_(nullptr),
inputSize_(0),
outputNum_(0),
loaded_(false) {
}
ModelLoader::~ModelLoader() {
Unload();
}
aclError ModelLoader::Load(const char* modelPath) {
// 1. 加载模型文件
aclError ret = aclmdlLoadFromFile(modelPath, &modelId_);
if (ret != ACL_ERROR_NONE) {
std::cerr << "aclmdlLoadFromFile failed, ret=" << ret << std::endl;
return ret;
}
// 2. 创建模型描述
modelDesc_ = aclmdlCreateDesc();
if (modelDesc_ == nullptr) {
std::cerr << "aclmdlCreateDesc failed" << std::endl;
return ACL_ERROR_FAILURE;
}
// 3. 获取模型描述信息
ret = aclmdlGetDesc(modelDesc_, modelId_);
if (ret != ACL_ERROR_NONE) {
std::cerr << "aclmdlGetDesc failed, ret=" << ret << std::endl;
return ret;
}
// 4. 获取输入输出信息
inputSize_ = aclmdlGetInputSizeByIndex(modelDesc_, 0);
outputNum_ = aclmdlGetDatasetNumOutputs(modelDesc_);
std::cout << "Model loaded: " << modelPath << std::endl;
std::cout << "Model ID: " << modelId_ << std::endl;
std::cout << "Input size: " << inputSize_ << std::endl;
std::cout << "Output num: " << outputNum_ << std::endl;
loaded_ = true;
return ACL_ERROR_NONE;
}
aclError ModelLoader::CreateInput(void* data, size_t size) {
// 创建输入数据集
inputDataset_ = aclmdlCreateDataset();
if (inputDataset_ == nullptr) {
std::cerr << "aclmdlCreateDataset for input failed" << std::endl;
return ACL_ERROR_FAILURE;
}
// 创建数据Buffer
aclDataBuffer* dataBuffer = aclCreateDataBuffer(data, size);
if (dataBuffer == nullptr) {
std::cerr << "aclCreateDataBuffer failed" << std::endl;
return ACL_ERROR_FAILURE;
}
// 添加到数据集
aclError ret = aclmdlAddDatasetBuffer(inputDataset_, dataBuffer);
if (ret != ACL_ERROR_NONE) {
std::cerr << "aclmdlAddDatasetBuffer failed, ret=" << ret << std::endl;
return ret;
}
return ACL_ERROR_NONE;
}
aclError ModelLoader::CreateOutput() {
// 创建输出数据集
outputDataset_ = aclmdlCreateDataset();
if (outputDataset_ == nullptr) {
std::cerr << "aclmdlCreateDataset for output failed" << std::endl;
return ACL_ERROR_FAILURE;
}
// 为每个输出分配内存
for (size_t i = 0; i < outputNum_; i++) {
size_t outputSize = aclmdlGetOutputSizeByIndex(modelDesc_, i);
void* outputBuffer = nullptr;
aclError ret = aclrtMalloc(&outputBuffer, outputSize,
ACL_MEM_MALLOC_HUGE_FIRST);
if (ret != ACL_ERROR_NONE) {
std::cerr << "aclrtMalloc failed, ret=" << ret << std::endl;
return ret;
}
aclDataBuffer* dataBuffer = aclCreateDataBuffer(outputBuffer, outputSize);
if (dataBuffer == nullptr) {
std::cerr << "aclCreateDataBuffer failed" << std::endl;
return ACL_ERROR_FAILURE;
}
ret = aclmdlAddDatasetBuffer(outputDataset_, dataBuffer);
if (ret != ACL_ERROR_NONE) {
std::cerr << "aclmdlAddDatasetBuffer failed, ret=" << ret << std::endl;
return ret;
}
}
return ACL_ERROR_NONE;
}
aclError ModelLoader::Execute() {
aclError ret = aclmdlExecute(modelId_, inputDataset_, outputDataset_);
if (ret != ACL_ERROR_NONE) {
std::cerr << "aclmdlExecute failed, ret=" << ret << std::endl;
return ret;
}
return ACL_ERROR_NONE;
}
aclError ModelLoader::GetOutputData(std::vector<void*>& outputs,
std::vector<size_t>& sizes) {
outputs.clear();
sizes.clear();
for (size_t i = 0; i < outputNum_; i++) {
aclDataBuffer* dataBuffer = aclmdlGetDatasetBuffer(outputDataset_, i);
if (dataBuffer == nullptr) {
return ACL_ERROR_FAILURE;
}
void* data = aclGetDataBufferAddr(dataBuffer);
size_t size = aclGetDataBufferSizeV2(dataBuffer);
outputs.push_back(data);
sizes.push_back(size);
}
return ACL_ERROR_NONE;
}
void ModelLoader::Unload() {
if (outputDataset_ != nullptr) {
// 释放输出内存
for (size_t i = 0; i < outputNum_; i++) {
aclDataBuffer* dataBuffer = aclmdlGetDatasetBuffer(outputDataset_, i);
if (dataBuffer != nullptr) {
void* data = aclGetDataBufferAddr(dataBuffer);
if (data != nullptr) {
aclrtFree(data);
}
}
}
aclmdlDestroyDataset(outputDataset_);
outputDataset_ = nullptr;
}
if (inputDataset_ != nullptr) {
aclmdlDestroyDataset(inputDataset_);
inputDataset_ = nullptr;
}
if (modelDesc_ != nullptr) {
aclmdlDestroyDesc(modelDesc_);
modelDesc_ = nullptr;
}
if (modelId_ != 0) {
aclmdlUnload(modelId_);
modelId_ = 0;
}
loaded_ = false;
}
main.cpp
cpp
#include "model_loader.h"
#include "data_preprocess.h"
#include "postprocess.h"
#include <iostream>
#include <fstream>
#include <vector>
class ResourceManager {
private:
int32_t deviceId_;
aclrtContext context_;
aclrtStream stream_;
public:
aclError Init(int32_t deviceId = 0) {
deviceId_ = deviceId;
// 设置设备
aclError ret = aclrtSetDevice(deviceId_);
if (ret != ACL_ERROR_NONE) {
std::cerr << "aclrtSetDevice failed, ret=" << ret << std::endl;
return ret;
}
// 创建Context
ret = aclrtCreateContext(&context_, deviceId_);
if (ret != ACL_ERROR_NONE) {
std::cerr << "aclrtCreateContext failed, ret=" << ret << std::endl;
return ret;
}
// 创建Stream
ret = aclrtCreateStream(&stream_);
if (ret != ACL_ERROR_NONE) {
std::cerr << "aclrtCreateStream failed, ret=" << ret << std::endl;
return ret;
}
std::cout << "Resource initialized!" << std::endl;
return ACL_ERROR_NONE;
}
aclrtStream GetStream() const { return stream_; }
void Destroy() {
if (stream_ != nullptr) {
aclrtDestroyStream(stream_);
}
if (context_ != nullptr) {
aclrtDestroyContext(context_);
}
aclrtResetDevice(deviceId_);
}
};
int main(int argc, char* argv[]) {
if (argc < 3) {
std::cout << "Usage: " << argv[0] << " <model_path> <image_path>" << std::endl;
return -1;
}
const char* modelPath = argv[1];
const char* imagePath = argv[2];
// 1. 初始化ACL
aclError ret = aclInit(nullptr);
if (ret != ACL_ERROR_NONE) {
std::cerr << "aclInit failed, ret=" << ret << std::endl;
return -1;
}
// 2. 初始化资源
ResourceManager resourceManager;
ret = resourceManager.Init();
if (ret != ACL_ERROR_NONE) {
aclFinalize();
return -1;
}
// 3. 加载模型
ModelLoader modelLoader;
ret = modelLoader.Load(modelPath);
if (ret != ACL_ERROR_NONE) {
resourceManager.Destroy();
aclFinalize();
return -1;
}
// 4. 创建输出
ret = modelLoader.CreateOutput();
if (ret != ACL_ERROR_NONE) {
modelLoader.Unload();
resourceManager.Destroy();
aclFinalize();
return -1;
}
// 5. 图像预处理
std::vector<float> inputData;
ret = PreprocessImage(imagePath, inputData, modelLoader.GetInputSize());
if (ret != ACL_ERROR_NONE) {
modelLoader.Unload();
resourceManager.Destroy();
aclFinalize();
return -1;
}
// 6. 分配Device内存并拷贝数据
void* deviceInput = nullptr;
ret = aclrtMalloc(&deviceInput, inputData.size() * sizeof(float),
ACL_MEM_MALLOC_HUGE_FIRST);
if (ret != ACL_ERROR_NONE) {
modelLoader.Unload();
resourceManager.Destroy();
aclFinalize();
return -1;
}
ret = aclrtMemcpy(deviceInput, inputData.size() * sizeof(float),
inputData.data(), inputData.size() * sizeof(float),
ACL_MEMCPY_HOST_TO_DEVICE);
if (ret != ACL_ERROR_NONE) {
aclrtFree(deviceInput);
modelLoader.Unload();
resourceManager.Destroy();
aclFinalize();
return -1;
}
// 7. 创建输入并推理
ret = modelLoader.CreateInput(deviceInput, inputData.size() * sizeof(float));
if (ret != ACL_ERROR_NONE) {
aclrtFree(deviceInput);
modelLoader.Unload();
resourceManager.Destroy();
aclFinalize();
return -1;
}
ret = modelLoader.Execute();
if (ret != ACL_ERROR_NONE) {
aclrtFree(deviceInput);
modelLoader.Unload();
resourceManager.Destroy();
aclFinalize();
return -1;
}
// 8. 获取输出并后处理
std::vector<void*> outputs;
std::vector<size_t> outputSizes;
ret = modelLoader.GetOutputData(outputs, outputSizes);
if (ret != ACL_ERROR_NONE) {
aclrtFree(deviceInput);
modelLoader.Unload();
resourceManager.Destroy();
aclFinalize();
return -1;
}
// 拷贝输出到Host
std::vector<float> outputData(outputSizes[0] / sizeof(float));
ret = aclrtMemcpy(outputData.data(), outputSizes[0],
outputs[0], outputSizes[0],
ACL_MEMCPY_DEVICE_TO_HOST);
if (ret == ACL_ERROR_NONE) {
// 后处理
Postprocess(outputData);
}
// 9. 清理资源
aclrtFree(deviceInput);
modelLoader.Unload();
resourceManager.Destroy();
aclFinalize();
std::cout << "\nInference completed!" << std::endl;
return 0;
}
第四阶段:编译部署
4.1 CMakeLists.txt
cmake
cmake_minimum_required(VERSION 3.10)
project(CANN_Application)
# 设置C++标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 设置CANN路径
set(ASCEND_HOME "/usr/local/Ascend")
set(ASCEND_TOOLKIT_ROOT "${ASCEND_HOME}/ascend-toolkit/latest")
# 包含目录
include_directories(
${ASCEND_TOOLKIT_ROOT}/include
${ASCEND_TOOLKIT_ROOT}/compiler/include
${CMAKE_SOURCE_DIR}/include
)
# 链接目录
link_directories(
${ASCEND_TOOLKIT_ROOT}/lib64
)
# 源文件
add_executable(cann_app
src/main.cpp
src/model_loader.cpp
src/data_preprocess.cpp
src/postprocess.cpp
)
# 链接库
target_link_libraries(cann_app
acl
acl_cblas
ge_runner
runtime
pthread
dl
)
# 安装规则
install(TARGETS cann_app DESTINATION bin)
install(FILES models/resnet50.om DESTINATION models)
4.2 编译脚本
bash
#!/bin/bash
# 编译脚本
BUILD_DIR="build"
mkdir -p ${BUILD_DIR}
cd ${BUILD_DIR}
cmake ..
make -j$(nproc)
if [ $? -eq 0 ]; then
echo "Build success!"
echo "Executable: ${BUILD_DIR}/cann_app"
else
echo "Build failed!"
exit 1
fi
4.3 运行脚本
bash
#!/bin/bash
# 设置环境变量
export ASCEND_HOME=/usr/local/Ascend
export LD_LIBRARY_PATH=${ASCEND_HOME}/ascend-toolkit/latest/lib64:${LD_LIBRARY_PATH}
export PATH=${ASCEND_HOME}/ascend-toolkit/latest/compiler/ccec_compiler/bin:${PATH}
# 运行程序
./build/cann_app models/resnet50.om data/test_images/test.jpg
性能优化建议
1. 内存复用
cpp
// 复用输入输出内存,减少分配开销
class MemoryPool {
std::vector<void*> buffers_;
public:
void* Allocate(size_t size) {
for (auto* buf : buffers_) {
// 检查是否可以复用
}
void* buf = nullptr;
aclrtMalloc(&buf, size, ACL_MEM_MALLOC_HUGE_FIRST);
buffers_.push_back(buf);
return buf;
}
};
2. 异步执行
cpp
// 使用Stream实现异步推理
aclError AsyncInference() {
// 异步拷贝数据
aclrtMemcpyAsync(...);
// 异步执行推理
aclmdlExecuteAsync(...);
// 在其他Stream处理下一帧
// ...
}
3. 批处理
cpp
// 批量处理提高吞吐量
aclError BatchInference(const std::vector<void*>& inputs) {
// 将多个输入打包成batch
// 一次推理处理多个样本
}
常见问题排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 模型加载失败 | 路径错误/权限问题 | 检查文件路径和权限 |
| 推理结果异常 | 输入预处理错误 | 验证输入数据格式 |
| 内存分配失败 | 内存不足 | 减少batch size |
| 性能不佳 | 未使用DVPP | 使用硬件加速预处理 |
总结
本文详细介绍了CANN应用开发的完整流程:
- 模型准备:从各框架导出模型
- 模型转换:使用ATC工具转换为OM格式
- 应用开发:完整的推理代码实现
- 编译部署:CMake构建和运行脚本
- 性能优化:内存复用、异步执行等技巧
通过这套完整流程,开发者可以快速将深度学习模型部署到CANN平台上,实现高效的AI推理。