第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图像分类模型。整个流程分四步:

  1. 模型转换:将PyTorch模型导出为ONNX,再用Tengine的转换工具转成tmfile格式
  2. 交叉编译:在PC上用交叉编译工具链编译Tengine库
  3. 应用开发:编写推理代码,调用Tengine API
  4. 部署测试:将可执行文件和模型文件拷贝到开发板,运行测试

模型转换这一步,我踩过最大的坑是算子兼容性问题。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,到时候可以对比着看,你会发现每个框架的设计哲学其实差别挺大的。