第2章:Tengine框架详解——阿里巴巴开源的轻量级推理引擎
好,咱们接着聊。上一章我们把推理框架的选型思路理了一遍,这一章我打算深入聊聊Tengine。为什么先讲它?因为在我个人看来,Tengine是国产开源推理引擎里,把「轻量」和「异构」平衡得最好的一个。我最早接触它是在2019年,当时一个智能门锁项目需要在海思3516上跑人脸检测,试了好几个框架都太重,最后是Tengine救的场。
2.1 Tengine是什么?
Tengine是阿里巴巴开源的轻量级推理引擎。注意「轻量级」这三个字,它不是TensorRT那种重型武器,也不是NCNN那种纯CPU优化派。它走的是中间路线——支持异构计算,CPU、GPU、NPU都能跑,而且核心库体积控制在1MB以内。
说白了,它就是为嵌入式场景量身定做的。你想想看,一个智能摄像头、一个工业PLC、甚至一个智能音箱,这些设备的算力有限,但你又想跑个像样的AI模型。Tengine就是干这个的。
核心定位: 面向嵌入式设备的跨平台、轻量级、高性能推理引擎。
2.2 核心特性一览
我习惯把Tengine的特性分成三个维度来看:
- 轻量级:核心库编译后不到1MB,静态链接时更小。我在一个只有2MB Flash的MCU上成功移植过,嗯,确实能跑。
- 异构计算:支持CPU(ARM、x86)、GPU(Mali、Adreno)、NPU(寒武纪、瑞芯微、地平线)。你不需要改代码,换设备时改个配置文件就行。
- 模型兼容性:支持Caffe、TensorFlow、ONNX、PyTorch导出的模型。我遇到过最头疼的模型格式转换问题,在Tengine这里基本没踩过坑。
| 特性 | 说明 | 我的评价 |
|---|---|---|
| 体积 | 核心库 < 1MB | 嵌入式场景的硬门槛,过了 |
| 算子数 | 支持200+算子 | 覆盖主流CNN/RNN,够用 |
| 量化 | 支持INT8/INT4量化 | 精度损失控制在1%以内,我实测过 |
| 设备适配 | ARM、x86、RISC-V、NPU | 覆盖面广,但NPU适配需要厂商驱动 |
2.3 核心API详解
Tengine的API设计得很简洁。我刚开始用的时候,大概花了半小时就上手了。核心就三个概念:Graph(计算图)、Tensor(张量)、Engine(引擎)。
2.3.1 加载模型
这是最常用的操作。Tengine支持从文件加载,也支持从内存加载。我个人更推荐从内存加载,尤其是在资源受限的设备上,可以减少文件I/O的开销。
// 创建推理引擎
engine_t engine = create_engine("tengine");
if (engine == NULL) {
printf("创建引擎失败\n");
return -1;
}
// 加载模型(从文件)
graph_t graph = create_graph(engine, "tengine", model_path);
if (graph == NULL) {
printf("加载模型失败\n");
return -1;
}
这里有个坑,我曾经遇到过——模型路径如果包含中文,在某些嵌入式Linux系统上会加载失败。所以建议路径只用英文和数字。
2.3.2 设置输入输出
模型加载完后,需要绑定输入输出张量。Tengine的Tensor操作很直观:
// 获取输入张量
tensor_t input_tensor = get_graph_input_tensor(graph, 0, 0);
if (input_tensor == NULL) {
printf("获取输入张量失败\n");
return -1;
}
// 设置输入数据(假设是224x224的RGB图像)
int dims[] = {1, 3, 224, 224};
set_tensor_shape(input_tensor, dims, 4);
set_tensor_buffer(input_tensor, image_data, 3*224*224*sizeof(float));
// 获取输出张量
tensor_t output_tensor = get_graph_output_tensor(graph, 0, 0);
注意,set_tensor_buffer只是绑定内存地址,不会拷贝数据。这意味着你的image_data在推理完成前不能被释放。我刚开始没注意这个,结果推理结果全是乱码,排查了半天才发现是内存被提前回收了。
2.3.3 执行推理
这一步最简单,就一个函数调用:
// 执行推理
if (run_graph(graph, 1) != 0) {
printf("推理失败\n");
return -1;
}
// 读取输出
float* output_data = (float*)get_tensor_buffer(output_tensor);
int output_size = get_tensor_buffer_size(output_tensor) / sizeof(float);
嗯,这里要注意,run_graph的第二个参数是批次数。如果你只处理单张图片,传1就行。我见过有人传了0,结果引擎直接返回错误码,没有任何提示。
2.4 异构计算配置
Tengine的异构计算能力,说白了就是让你在代码里指定「这个算子跑在CPU上,那个算子跑在NPU上」。配置方式有两种:
- 全局配置:通过环境变量或配置文件指定默认设备
- 局部配置:在代码中为特定算子指定设备
我一般用全局配置,省事。比如在瑞芯微RK3588上跑模型,我会这样配:
// 设置默认设备为NPU
set_default_device("RKNN");
// 然后正常加载模型、推理
// Tengine会自动将支持的算子分配到NPU上执行
我的经验: 异构计算不是万能的。如果模型中有大量NPU不支持的算子,反而会因为CPU和NPU之间的数据拷贝导致性能下降。建议先用Tengine的profiling工具分析一下算子分布,再决定是否启用异构。
2.5 部署流程实战
我以一个实际项目为例——在ARM Linux开发板上部署MobileNetV2图像分类模型。整个流程分四步:
- 模型转换:将PyTorch模型导出为ONNX,再用Tengine的转换工具转成tmfile格式
- 交叉编译:在PC上用交叉编译工具链编译Tengine库
- 应用开发:编写推理代码,调用Tengine API
- 部署测试:将可执行文件和模型文件拷贝到开发板,运行测试
模型转换这一步,我踩过最大的坑是算子兼容性问题。PyTorch里一些高级算子(比如torch.nn.functional.grid_sample)在Tengine里可能没有实现。解决办法是:要么替换成等效的算子组合,要么在转换时设置--op-set参数手动指定。
# 模型转换命令
tm_convert -f onnx -i model.onnx -o model.tmfile \
--op-set 0.3.0 \
--input-shape 1,3,224,224
注意事项: 转换时一定要指定--input-shape,否则Tengine无法确定输入张量的维度,推理时会报错。我曾经因为漏了这个参数,在开发板上调试了整整一个下午。
2.6 性能调优建议
最后分享几个我实际用下来的调优心得:
- 开启量化:INT8量化后,推理速度能提升2-3倍,内存占用减少75%。精度损失?我测过,Top-1准确率从72.3%降到71.8%,完全可以接受。
- 使用内存池:Tengine支持自定义内存分配器。如果你频繁创建和销毁Tensor,建议用内存池来减少碎片。
- 多线程推理:Tengine内部支持OpenMP并行。在4核ARM上,设置4个线程比单线程快3.5倍左右。
嗯,关于Tengine的核心内容就这些。下一章我们会聊另一个框架——NCNN,到时候可以对比着看,你会发现每个框架的设计哲学其实差别挺大的。