在RK3588上使用NCNN和Vulkan加速ResNet50推理全流程
前言:为什么需要已关注移动端AI推理
性能数据
一、环境准备与框架编译
1.1 获取NCNN源码
1.2 安装必要依赖
1.3 编译NCNN
二、模型导出与转换
2.1 下载推理图片
2.2 生成ONNX模型
2.3 转换NCNN格式
三、模型量化加速
3.1 生成校准数据
3.2 执行量化操作
四、性能测试与结果分析
4.1 创建`MSE`计算脚本
4.2 创建推理测试程序
4.3 推理测试
五、最佳实践建议
前言:为什么需要已关注移动端AI推理
在人工智能应用落地的过程中,将训练好的神经网络模型部署到嵌入式设备上面临诸多挑战。相比桌面级GPU,嵌入式平台(如RK3588)的算力和内存资源都更加有限。NCNN作为腾讯开源的轻量级神经网络推理框架,凭借其优异的跨平台性能和极致的优化能力,成为嵌入式AI部署的首选方案之一。本文将以ResNet50图像分类模型为例,完整展示从环境搭建到量化优化的全流程,帮助读者掌握移动端AI部署的核心技术。
性能数据
模型:resnet50 输入:[1,3.224,224 float32] 输出:[1,1000 float32]
| 精度类型 | CPU推理 | Vulkan推理 |
|---|---|---|
| FP16 | 30.9413ms MSE:0.00036 | 43.4555ms MSE:0.00015 |
| INT8 | 24.8097ms MSE:0.01575 | 24.7972ms MSE:0.01575 |
本文基于: RK3588 MNN CPU/Vulkan/OpenCL ResNet50推理测试
一、环境准备与框架编译
1.1 获取NCNN源码
git clone https://github.com/Tencent/ncnn.git
cd ncnn
git submodule update --init # 初始化依赖的子模块
关键解释:
这里通过git获取NCNN的最新代码,git submodule update用于同步依赖的第三方库(如GoogleTest)。完整的源码是后续编译的基础。
1.2 安装必要依赖
apt install libprotobuf-dev -y # Protocol Buffers运行时库
apt install protobuf-compiler -y # Protocol Buffers编译器
pip3 install pnnx # PyTorch模型转换工具
技术背景:
Protocol Buffers是NCNN模型文件的序列化工具,libprotobuf-dev提供C++接口支持。PNNX是专为NCNN设计的PyTorch模型转换器,可将PyTorch模型转换为NCNN支持的格式。
1.3 编译NCNN
mkdir -p build && cd build
cmake -DCMAKE_BUILD_TYPE=Release -DNCNN_VULKAN=ON -DNCNN_BUILD_BENCHMARK=ON ..
make -j1 # 单线程编译避免内存不足
编译选项解析:
DNCNN_VULKAN=ON:启用Vulkan GPU加速支持,利用RK3588的Mali-G610 GPU
DNCNN_BUILD_BENCHMARK=ON:编译性能测试工具
-j1:在内存有限的设备上避免并行编译导致的内存溢出
二、模型导出与转换
2.1 下载推理图片
wget https://raw.githubusercontent.com/hi20240217/csdn_images/refs/heads/main/YellowLabradorLooking_new.jpg
2.2 生成ONNX模型
cat> resnet50.py <<-'EOF'
import requests
from PIL import Image
from io import BytesIO
import torchvision.transforms as transforms
import torch
import numpy as np
import torchvision.models as models
# 读取图片
image = Image.open("YellowLabradorLooking_new.jpg")
# 定义预处理流程
preprocess = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
# 应用预处理
img_t = preprocess(image)
input_tensor = torch.unsqueeze(img_t, 0).float()
input_np=input_tensor.numpy()
# 保存预处理好的输入,用于后面量化和精度对比
np.savetxt('resnet50_input.txt',input_np.reshape(-1), delimiter=' ', fmt='%.6f')
np.save('resnet50_input.npy',input_np[0])
# 加载预训练的ResNet50模型
model = models.resnet50(pretrained=True).float()
model.eval() # 将模型设为评估模式
# 执行前向推理
with torch.no_grad():
output = model(input_tensor).numpy()
# 保存推理的结果,用于后面对比精度
np.savetxt('resnet50_output.txt',output.reshape(-1), delimiter=' ', fmt='%.6f')
# 获取预测类别
predicted = np.argmax(output)
print("Index:",predicted)
input_names = ["input"]
output_names = ["output"]
torch.onnx.export(model, input_tensor, "resnet50.onnx",
verbose=False, input_names=input_names, output_names=output_names)
EOF
python3 resnet50.py
关键步骤说明:
pretrained=False仅为演示模型结构,实际部署需替换为真实权重
opset_version需要与NCNN支持的算子版本匹配
ONNX作为中间格式可实现框架间的模型迁移
2.3 转换NCNN格式
pnnx resnet50.onnx inputshape=[1,3,224,224] # 自动模型转换
./tools/ncnnoptimize resnet50.ncnn.param resnet50.ncnn.bin
resnet50-opt.param resnet50-opt.bin 65536
优化原理:
ncnnoptimize通过以下方式提升推理效率:合并冗余计算图节点
移除训练专用的算子(如Dropout)
内存分配优化
参数65536指定内存池大小,需根据设备内存调整
三、模型量化加速
3.1 生成校准数据
rm -rf calib_data
mkdir calib_data
cp resnet50_input.npy calib_data/0001.npy
量化原理:
量化(Quantization)将FP32权重转换为INT8存储,通过:减小75%模型体积
利用硬件整数指令加速计算
校准数据用于统计各层的数值分布,KL散度方法可最小化量化误差
3.2 执行量化操作
ls calib_data/0001.npy > filelist.txt
./tools/quantize/ncnn2table resnet50-opt.param resnet50-opt.bin
filelist.txt resnet50.table shape=[224,224,3] method=kl type=1
./tools/quantize/ncnn2int8 resnet50-opt.param resnet50-opt.bin
resnet50-int8.param resnet50-int8.bin resnet50.table
参数解析:
method=kl:使用Kullback-Leibler散度优化量化阈值
type=1:选择对称量化策略
shape:指定输入张量维度顺序(HWC格式)
四、性能测试与结果分析
4.1 创建MSE计算脚本
cat> compute_mse.py<<-'EOF'
import numpy as np
import sys
l = np.loadtxt(sys.argv[1], delimiter=' ').reshape(-1)
r = np.loadtxt(sys.argv[2], delimiter=' ').reshape(-1)
#print(l.shape,r.shape)
#print(l[:8])
#print(r[:8])
mse=np.mean((l - r) ** 2)
print(f'[INFO] MSE:{
mse:.5f}')
EOF
4.2 创建推理测试程序
cat> main.cpp <<-'EOF'
#include <net.h>
#include <iostream>
#include <fstream>
#include <vector>
#include <chrono>
#include <sstream>
using namespace std;
// 从文本文件加载浮点数组
vector<float> load_input_from_file(const string& file_path) {
ifstream file(file_path);
vector<float> input;
string line;
if (!file.is_open()) {
cerr << "Failed to open input file: " << file_path << endl;
exit(-1);
}
while (getline(file, line)) {
istringstream iss(line);
float num;
while (iss >> num) {
input.push_back(num);
}
}
file.close();
return input;
}
// 保存结果到文本文件
void save_output_to_file(const string& file_path, const vector<float>& output) {
FILE *f=fopen(file_path.c_str(),"w");
for (const auto& num : output) {
fprintf(f,"%f
",num);
}
fclose(f);
}
int main(int argc, char** argv) {
// 参数解析
if (argc < 5) {
cerr << "Usage: " << argv[0] << " [param] [bin] [input.txt] [output.txt] [--cpu/--gpu] [loop_count]" << endl;
return -1;
}
string param_path = argv[1];
string bin_path = argv[2];
string input_path = argv[3];
string output_path = argv[4];
bool use_gpu = false;
int loop_count = 10;
// 解析参数
for (int i = 5; i < argc; i++) {
string arg = argv[i];
if (arg == "--gpu") {
use_gpu = true;
} else if (arg == "--cpu") {
use_gpu = false;
} else if (arg.find("--loop=") == 0) {
loop_count = stoi(arg.substr(7));
}
}
// 加载输入数据
vector<float> input_data = load_input_from_file(input_path);
// 初始化网络
ncnn::Net net;
net.opt.use_vulkan_compute = use_gpu;
if (use_gpu && !ncnn::get_gpu_count()) {
cerr << "GPU requested but no Vulkan capable device found" << endl;
return -1;
}
if (net.load_param(param_path.c_str())) {
cerr << "Failed to load param file" << endl;
return -1;
}
if (net.load_model(bin_path.c_str())) {
cerr << "Failed to load bin file" << endl;
return -1;
}
// 准备输入
ncnn::Mat in(224,224,3,input_data.data());
ncnn::Mat out;
// 预热
ncnn::Extractor ex = net.create_extractor();
// 性能测试
vector<double> timings;
for (int i = 0; i < loop_count; i++) {
auto start = std::chrono::high_resolution_clock::now();
ncnn::Extractor ex = net.create_extractor();
ex.input("in0", in);
ex.extract("out0", out);
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> elapsed = end - start;
if(i==loop_count-1)
{
std::cout << "[INFO] Execution time: " << elapsed.count() << "ms" << std::endl;
}
}
// 保存结果
vector<float> output_data(out.w);
memcpy(output_data.data(), out.data, out.w * sizeof(float));
save_output_to_file(output_path, output_data);
return 0;
}
EOF
g++ -o infer main.cpp -I ../src -I src/ src/libncnn.a
-lgomp -ldl -lpthread ./glslang/glslang/libglslang.a
4.3 推理测试
cat> run.sh <<-'EOF'
./infer resnet50-opt.param resnet50-opt.bin resnet50_input.txt resnet50_output_pred.txt
--cpu 10 2>&1 | grep "INFO"
python3 compute_mse.py resnet50_output_pred.txt resnet50_output.txt
./infer resnet50-int8.param resnet50-int8.bin resnet50_input.txt resnet50_output_pred.txt
--cpu 10 2>&1 | grep "INFO"
python3 compute_mse.py resnet50_output_pred.txt resnet50_output.txt
./infer resnet50-opt.param resnet50-opt.bin resnet50_input.txt resnet50_output_pred.txt
--gpu 10 2>&1 | grep "INFO"
python3 compute_mse.py resnet50_output_pred.txt resnet50_output.txt
./infer resnet50-int8.param resnet50-int8.bin resnet50_input.txt resnet50_output_pred.txt
--gpu 10 2>&1 | grep "INFO"
python3 compute_mse.py resnet50_output_pred.txt resnet50_output.txt
EOF
bash run.sh
输出
[INFO] Execution time: 30.9413ms
[INFO] MSE:0.00036
[INFO] Execution time: 24.8097ms
[INFO] MSE:0.01575
[INFO] Execution time: 43.4555ms
[INFO] MSE:0.00015
[INFO] Execution time: 24.7972ms
[INFO] MSE:0.01575
五、最佳实践建议
数据准备:校准数据应尽可能接近真实场景,建议采集100-1000张典型样本
混合精度:对敏感层保持FP32精度,其他层使用INT8(需手动调整量化表)
内存优化:通过ncnn::Mat::create重用内存空间,避免频繁分配
多线程:设置gpu_thread参数充分利用多核CPU+GPU异构计算

















暂无评论内容