背景
在端侧推理引擎(如 MNN, NCNN, TFLite)的开发中,算子(Op)的实现往往占据了大部分工作量。传统的做法是为每个算子编写特定的 Kernel,但在面对动态 Shape(如 NLP 任务中长度不一的句子)或复杂的维度变换(如 Transpose, Slice, Reshape)时,这种方式会导致代码膨胀且难以优化。
MNN 引入的几何计算(Geometric Computing)机制改变了这一局面。它将复杂的算子拆解为基础的“计算算子”和“几何变换”,通过统一的内存视图映射(Stride, Offset)实现算子的高度复用。
1. 核心原理:什么是几何计算?
几何计算的核心思想是将张量(Tensor)的坐标变换从数值计算中解耦。对于大多数不改变数值、仅改变数据排布的算子,MNN 不再编写专门的 Kernel,而是通过修改 Tensor 的数据描述符(View)来实现。
一个张量的坐标转换公式可以抽象为:
Index_src = Offset + Sum(Index_dst[i] * Stride[i])
通过这种方式,Slice(切片)、Transpose(转置)、Concat(拼接)等算子都可以转化为对内存地址的重新映射,从而避免了不必要的内存拷贝(Zero-Copy)。
2. 核心组件:Dimension Format
MNN 几何计算主要处理以下三个维度信息:
– Offset: 起始偏移地址。
– Stride: 步长,决定了在各维度移动时跳过的元素个数。
– Extent: 长度,决定了当前维度的范围。
3. 实战示例:使用 MNN Express API 观察几何变换
下面的 C++ 代码演示了如何利用 MNN 的 Express 接口(底层触发几何计算)来实现一个复杂的维度操作,并理解其复用逻辑。
#include <MNN/expr/Expr.hpp>
#include <MNN/expr/ExprCreator.hpp>
#include <iostream>
using namespace MNN::Express;
int main() {
// 1. 创建一个初始 Tensor [1, 3, 4, 4] (NCHW)
auto x = _Input({1, 3, 4, 4}, NCHW, halffloat);
// 2. 执行 Slice 操作:取中间的 2x2 区域
// 在几何计算中,这仅产生一个新的 View,不分配新内存
auto sliced = _Slice(x, {0, 0, 1, 1}, {1, 3, 2, 2});
// 3. 执行 Transpose 操作:交换 C 和 H 维度
// 这将进一步修改 Stride 信息
auto transposed = _Transpose(sliced, {0, 2, 1, 3});
// 4. 重点:当执行具体的计算(如 Add)时,MNN 会合并之前的几何变换
auto y = transposed + _Scalar(1.0f);
// 查看输出形状
auto shape = y->getInfo()->dim;
std::cout << \"Output Shape: \";
for (int d : shape) std::cout << d << \" \";
std::cout << std::endl;
return 0;
}
4. 几何计算如何优化动态 Shape
在动态 Shape 场景下,输入维度频繁变化。传统的引擎需要重新计算 Offset 并重新分配内存。而 MNN 的几何计算通过算子融合(Op Fusion),在推理前的 onResize 阶段将多个变换合并。
例如,如果你执行了 Reshape + Transpose + Slice,MNN 会将其折叠为一个单一的 Raster(光栅化)算子。这个 Raster 算子只需要一次高效的内存拷贝(或在某些后端直接作为输入视图),极大地降低了 CPU/GPU 的调度开销。
5. 开发者如何受益?
- 后端适配简单:如果你正在为国产 NPU 开发适配层,你只需要实现基础的计算算子和 Raster 算子,即可支持几乎所有维度变换相关的算子。
- 性能提升:通过减少内存拷贝,对于轻量级网络(如 MobileNet),性能可提升 10%-20%。
- 灵活性:轻松处理不规则的 Tensor 步长,是支持动态 Shape 的“银弹”。
总结
MNN 的几何计算通过将算子逻辑分解为几何变换与数值计算,实现了代码的极致复用。对于开发者而言,理解 Stride 和 View 的概念,是掌握端侧推理优化的关键步骤。
汤不热吧