Ch16: MACE——深入骨髓的硬件适配
Part 3: 六大框架架构精讲 前置章节:Ch15: Paddle-Lite——万能充电器 本章为 Part 3 最后一章
一句话定位
MACE 是六大引擎中硬件适配深度最极致的一个。其他引擎都是”在 CPU/GPU 上跑算子”的思路——把每个算子翻译成对应后端的指令,MACE 则更进一步:它不仅翻译算子,还要把数据在硬件内存里的物理排布精确到字节,把 OpenCL 的 image2d 纹理对象当成一等公民来管理,把高通 Hexagon DSP 当成一个独立的”黑盒计算机”来整图下推。它就像一个深入到每颗螺丝的定制化装修师——不是简单地”把家具搬进去”,而是要量好每面墙的精确尺寸,把管线、电路、水路全部按照这间房子的具体结构重新走一遍。
GitHub 5K star,小米开源,约 600 个源文件——体量中等,但每个文件的硬件细节密度极高。
类比:定制化装修师
假设你买了一套精装房。普通装修公司的做法是:看看户型图,从标准方案库里挑一个差不多的方案,稍微改改尺寸就动工了。但你找了一个”定制化装修师”。他的做法完全不同:
- 先量房:拿激光测距仪把每一面墙、每一根管道、每一个开关的位置精确测量。GPU 有多少纹理单元、每个纹理单元能存多少数据、数据在纹理里怎么排列——MACE 全部要知道。
- 管线贴墙走:普通方案是把水管电线走地下(先做格式转换、再计算),定制师会让管线贴着墙的结构走,减少绕行距离。MACE 让数据在 GPU 纹理中的排列方式和卷积计算的访问模式完全吻合,中间不做多余的格式转换。
- 针对房型设计家具:普通方案买标准尺寸家具,定制师量好尺寸后现做家具。MACE 针对不同硬件(OpenCL GPU、Hexagon DSP、ARM CPU)设计完全不同的数据布局和计算策略。
- DSP 整屋外包:如果房子有一间独立的地下室(DSP),定制师不会只把一两件家具搬到地下室,而是把整间地下室的装修全部外包给地下室的专业团队,只需要在楼梯口做好交接(ION 共享内存)。
本章的类比边界:装修师类比强调的是”精确到物理排布”这个特征。但装修是一次性的(装好就不改了),MACE 的内存管理是动态的(运行时分配、复用、释放),这一点后面会专门讲。
设计哲学精讲
MACE 的核心设计理念可以用一句话概括:不做通用抽象,做精确适配。我们逐一展开这句话在三个后端(OpenCL GPU、Hexagon DSP、ARM CPU)上的具体体现。
OpenCL GPU:image2d 纹理是一等公民
大多数支持 OpenCL 的推理引擎(ncnn、MNN、TNN)都使用 OpenCL 的 buffer 对象来存储张量数据——和 CPU 内存差不多,就是一段连续的字节。MACE 则大量使用 OpenCL 的 image2d 纹理对象。
为什么要用纹理?因为移动 GPU 的硬件架构中,纹理单元(Texture Unit)有专门的缓存和访问路径。用纹理存储数据,可以利用 GPU 硬件内建的 2D 空间局部性缓存——对于卷积这种天然具有 2D 空间访问模式的操作,纹理的缓存命中率远高于普通 buffer。
但纹理有一个”麻烦”的地方:它是 2D 的(width × height),而张量是 4D 的(N×C×H×W)。怎么把 4D 数据塞进 2D 纹理?不同的算子有不同的最优映射方式。MACE 为此定义了 8 种 BufferContentType,每一种对应一种从 4D 张量到 2D 纹理的精确映射方案:
// mace/core/types.h (简化)
enum BufferContentType {
IN_OUT_CHANNEL, // 输入输出特征图:width=(W*RoundUp4(C)/4), height=(N*H)
IN_OUT_HEIGHT, // 按高度展开:width=W, height=(N*C*H)
IN_OUT_WIDTH, // 按宽度展开
ARGUMENT, // bias/scale 等 1D 参数
CONV2D_FILTER, // 卷积权重:width=(RoundUp4(Cout)/4), height=(Cin*Kh*Kw)
DW_CONV2D_FILTER, // 深度可分离卷积权重(排列方式不同)
WEIGHT_HEIGHT, // 全连接权重(按行展开)
WEIGHT_WIDTH // 全连接权重(按列展开)
};
每种 BufferContentType 精确规定了张量的每个元素在纹理的哪一行(y)、哪一列(x)、哪一个通道(r/g/b/a)。这样做的好处是:卷积 kernel 读取输入数据时,可以用一条 read_imagef 指令同时读取 4 个通道的数据(因为纹理的每个像素有 rgba 四个通道),实现天然的向量化。
为了在 buffer 和 image2d 之间转换,MACE 实现了一组 BufferToImage / ImageToBuffer 算子,它们根据 BufferContentType 执行精确的重排。这些转换不是”通用的reshape”,而是针对每种数据类型专门写的 OpenCL kernel,确保转换后的纹理布局和后续算子的访问模式完全匹配。
# 伪代码:理解 IN_OUT_CHANNEL 映射
# 张量形状: [N, C, H, W],纹理形状: [W * ceil(C/4), N * H]
# 纹理的每个像素存 rgba 4个值 = 4个通道
def tensor_to_image2d(tensor, N, C, H, W):
image_width = W * math.ceil(C / 4)
image_height = N * H
image = Image2D(image_width, image_height) # 每像素4通道
for n in range(N):
for c in range(C):
for h in range(H):
for w in range(W):
# 计算这个元素在纹理中的位置
x = w + (c // 4) * W # 列:W个位置 × 第几组(4通道一组)
y = n * H + h # 行:第n个样本 × H个高度位置
channel = c % 4 # rgba 中的哪个通道
image[y][x][channel] = tensor[n][c][h][w]
return image
这种精细映射是 MACE 区别于其他引擎最显著的设计特征之一。ncnn 和 MNN 也支持 OpenCL,但它们更多依赖 buffer 或只用少量纹理映射方式;MACE 把”哪种数据用哪种纹理布局”这件事做到了极致。
Hexagon DSP:整图下推 + ION 零拷贝
高通的 Hexagon DSP 是一个独立的处理器,有自己的指令集(HVX 向量扩展)和自己的运行时(Hexagon NN)。大多数框架对 DSP 的支持是”逐算子下推”——把单个算子翻译成 DSP 指令,数据在 CPU 和 DSP 之间来回拷贝。MACE 的做法更激进:整图下推。
整图下推的意思是:把整个计算图(或尽可能大的子图)一次性交给 Hexagon NN 的运行时。CPU 只负责在推理开始前把输入数据放好、推理结束后把输出数据取走,中间的所有计算都在 DSP 上完成,不需要 CPU↔DSP 的中间数据传输。
┌────────────────────────────────────────────────────────┐
│ CPU 端(MACE) │
│ │
│ 1. 构建 Hexagon NN Graph │
│ hexagon_nn_init() │
│ hexagon_nn_append_node(op=Conv2d, ...) │
│ hexagon_nn_append_node(op=Relu, ...) │
│ hexagon_nn_append_node(op=Pool, ...) │
│ hexagon_nn_prepare() │
│ │
│ 2. 设置 ION 共享内存 │
│ ion_fd = ion_alloc(size) // 分配 ION buffer │
│ cpu_ptr = mmap(ion_fd) // CPU 端映射 │
│ // DSP 端也能直接访问同一块物理内存 │
│ │
│ 3. 填充输入 → ION buffer │
│ 4. hexagon_nn_execute() ←── 一次调用,整图在 DSP 执行 │
│ 5. 从 ION buffer 读取输出 │
└────────────────────────────────────────────────────────┘
ION 是 Android 的共享内存机制(现在 Android 12+ 逐渐迁移到 DMA-BUF)。ION 零拷贝的关键在于:CPU 和 DSP 访问的是同一块物理内存,不需要在两个处理器之间做数据拷贝。CPU 往 ION buffer 写入输入数据,DSP 直接从同一块内存读取;DSP 把结果写入 ION buffer,CPU 直接从同一块内存读取结果。
整图下推 + ION 零拷贝的组合,让 MACE 在高通平台的 DSP 性能远超”逐算子下推”的方案。代价是:模型中所有算子都必须有 Hexagon NN 的对应实现,一旦遇到不支持的算子,整图就无法下推,需要回退到 CPU 或拆分子图。
ARM CPU:Delegator 机制
在 CPU 后端,MACE 用 Delegator 机制来分发算子实现。一个算子(比如 Conv2d)可能有多种实现:通用 C++ 实现、NEON fp32 实现、NEON fp16 实现、NEON int8 实现、Winograd 实现、1×1 特化实现等。Delegator 用一个 5 维 key 来选择最合适的实现:
// 5维 key 的组成
struct DelegatorKey {
DeviceType device; // CPU / GPU / DSP
DataType data_type; // FP32 / FP16 / INT8 / BF16
ImplType impl_type; // REF(参考) / NEON / NEON_FP16 / ...
std::string op_type; // Conv2D / DepthwiseConv2D / ...
std::string tag; // 额外标签:kWinograd / k1x1 / kGemm / ...
};
// 注册示例
MACE_REGISTER_DELEGATOR(
device=CPU, dtype=FP32, impl=NEON,
op="Conv2D", tag="kWinograd",
class=Conv2dWinogradNeonFp32
);
MACE_REGISTER_DELEGATOR(
device=CPU, dtype=FP32, impl=NEON,
op="Conv2D", tag="k1x1",
class=Conv2d1x1NeonFp32
);
运行时,框架根据当前硬件能力、数据类型和算子参数(比如 kernel size),构造出一个 5 维 key,在注册表中查找最优实现。如果找不到精确匹配,会按优先级回退(比如 NEON 不支持就回退到 REF)。
这个设计和 ncnn 的 Layer 单继承、MNN 的 Op-Execution 分离都不同。ncnn 是一个 Layer 类包含所有后端实现(#ifdef 切换),MNN 是 Op 和 Execution 分开注册,MACE 是同一个算子的不同实现通过 5 维 key 注册到全局表中。5 维 key 的好处是:粒度更细(可以区分到 Winograd vs 1×1 vs 通用 GEMM),添加新实现不需要修改已有代码。
核心数据结构
Tensor = Buffer + Runtime
MACE 的 Tensor 不像 ncnn 的 Mat 那样自己管理内存分配,也不像 MNN 的 Tensor 那样有 MEMORY_VIRTUAL 标记。MACE 的 Tensor 分为两部分:
Tensor
├── shape_: vector<int64_t> // 逻辑形状 [N, C, H, W]
├── dtype_: DataType // FP32 / FP16 / INT8
├── buffer_: Buffer* // 指向实际存储
│ ├── buf_: void* // 原始指针(CPU)或 cl::Image(GPU)
│ ├── size_: size_t // 字节数
│ └── allocator_: Allocator* // 谁分配的(CPU/GPU/DSP allocator)
├── is_image_: bool // 是否用 OpenCL image2d 存储
├── buffer_content_type_: BufferContentType // 8种映射之一
└── runtime_: Runtime* // 关联的运行时上下文
关键设计点:
Buffer 和 Tensor 分离:Tensor 描述”我是什么形状、什么类型”,Buffer 描述”我的数据存在哪里、谁分配的”。同一个 Buffer 可以被多个 Tensor 共享(aliasing),框架可以在不拷贝数据的情况下让一个 Tensor “重新解释”为另一个形状。
Runtime 关联:每个 Tensor 都知道自己属于哪个 Runtime(CPU Runtime、OpenCL Runtime、Hexagon Runtime)。这意味着当你对一个 Tensor 做操作时,框架知道应该用哪个后端的 API 来分配内存、传输数据。
image vs buffer 双模态:当 is_image_ 为 true 时,底层 buf_ 实际上是一个 cl::Image2D 对象,而不是普通指针。此时 buffer_content_type_ 字段记录了具体使用哪种 4D→2D 映射方案。这是 MACE 特有的设计——其他框架的 Tensor 不需要记录”我在纹理中是怎么排列的”。
Flow:子网络编排
MACE 使用 Flow 来组织计算图的执行。一个 Flow 包含一组 SubFlow,每个 SubFlow 对应一个后端上的一段连续计算:
Flow
├── sub_flows_: vector<SubFlow>
│ ├── SubFlow[0]: CPU 前处理(输入归一化、resize)
│ ├── SubFlow[1]: GPU 主体计算(卷积、池化、...)
│ ├── SubFlow[2]: DSP 后处理(或另一段独立计算)
│ └── SubFlow[3]: CPU 输出拷贝
├── input_info_: vector<TensorInfo>
└── output_info_: vector<TensorInfo>
SubFlow 之间通过共享 Tensor 传递数据。当两个 SubFlow 在不同后端上运行时,框架自动插入数据传输操作(比如 CPU→GPU 的 BufferToImage,或 CPU→DSP 的 ION 映射)。
这个设计和 MNN 的 Pipeline、ORT 的 ExecutionProvider 子图切分类似,但 MACE 的粒度更粗——它不是按算子粒度选后端,而是按”一段连续计算”的粒度切分。这样做的好处是减少后端切换次数(每次切换都有数据传输开销),代价是灵活性不如 ORT 的 per-op 级别分配。
MemoryManager:四种租用类型
MACE 的内存管理器为不同类型的张量提供四种”租用”(lease)方式:
enum MemoryLeaseType {
LEASE_PRIVATE, // 独占:这块内存只给这个张量用,生命周期和张量一致
LEASE_SHARE, // 共享:多个不重叠的张量共享同一块内存(最常见的优化)
LEASE_OUTPUT, // 输出:用户提供的输出缓冲区,框架不管分配和释放
LEASE_SCRATCH // 临时:算子内部的工作区(workspace),用完就还
};
LEASE_SHARE 是最重要的优化。框架在构建计算图时会做生命周期分析:如果张量 A 在算子 3 之后不再被使用,而张量 B 在算子 5 才开始使用,那 A 和 B 可以共享同一块内存。这在 CPU 端是标准的 buffer 复用技术,但在 GPU 端因为涉及 image2d 纹理对象,复用规则更复杂——两个纹理要能复用,不仅大小要兼容,BufferContentType 也需要匹配(否则数据排列方式不一致,复用后读到的值是错的)。
LEASE_SCRATCH 用于算子内部的临时空间。比如 Winograd 卷积需要中间变换的缓冲区,GEMM 需要 packing 缓冲区。这些空间在算子执行完后立即归还,可以被后续算子的 SCRATCH 复用。
推理流程
一个典型的 MACE 推理过程分为三大阶段:
阶段一:模型加载与计算图构建
输入: .pb 模型文件 + 权重数据(.data)
或者 MACE 自定义格式(FlatBuffers 序列化)
1. 反序列化模型定义
- 读取算子列表、拓扑顺序、数据类型
- 读取权重数据(可以 mmap 直接映射到内存,避免拷贝)
2. 创建 Runtime
- 根据用户指定的 DeviceType 创建对应 Runtime
- OpenCLRuntime: 初始化 OpenCL context、command queue
- HexagonRuntime: 加载 Hexagon NN 库(libhexagon_nn_skel.so)
- CpuRuntime: 检测 CPU 能力(NEON/dotprod/fp16/...)
3. 构建 Flow 和 SubFlow
- 把算子序列分段:同一后端的连续算子归入同一 SubFlow
- 在 SubFlow 边界插入数据格式转换算子
- GPU SubFlow: 编译或加载 OpenCL kernel(优先从缓存读取)
4. 内存规划
- 分析每个张量的生命周期
- 按 MemoryLeaseType 分配内存
- GPU 端: 创建 image2d 纹理对象(宽高由 BufferContentType 决定)
- DSP 端: 分配 ION 共享内存
5. 权重预处理
- CPU: 可能做 packing(比如把 Cin 维度按 4/8 对齐)
- GPU: 通过 BufferToImage 把权重从 buffer 转成 image2d
- DSP: 把权重拷贝到 ION buffer
阶段二:推理执行
输入: 用户提供的输入数据(CPU buffer)
1. 输入数据准备
- CPU 后端: 直接使用(或做 resize/归一化)
- GPU 后端: BufferToImage 把输入从 CPU buffer 转成 GPU image2d
- DSP 后端: 拷贝到 ION buffer(或直接用 ION buffer 作为输入)
2. 按 SubFlow 顺序执行
for each sub_flow in flow.sub_flows_:
if sub_flow.device == GPU:
for each op in sub_flow.ops_:
enqueue OpenCL kernel(写入 command queue)
clFinish() // 等待所有 kernel 完成
elif sub_flow.device == DSP:
hexagon_nn_execute() // 整图一次执行
elif sub_flow.device == CPU:
for each op in sub_flow.ops_:
op->Run() // 通过 Delegator 分发到最优实现
3. SubFlow 间数据传输
- GPU→CPU: ImageToBuffer + clEnqueueReadBuffer
- CPU→GPU: clEnqueueWriteBuffer + BufferToImage
- CPU→DSP: 通过 ION buffer 共享(零拷贝)
- DSP→CPU: 通过 ION buffer 共享(零拷贝)
4. 输出数据提取
- GPU 后端: ImageToBuffer 把结果从 image2d 转回 CPU buffer
- DSP 后端: 从 ION buffer 直接读取
- CPU 后端: 直接返回
阶段三:资源释放
1. 释放 OpenCL 对象(image2d、buffer、kernel、program)
2. 释放 ION 共享内存
3. hexagon_nn_teardown() 释放 DSP 资源
4. 释放 CPU 端内存
整个流程中,最耗时的是阶段一的 OpenCL kernel 编译(首次运行可能需要几秒)。MACE 通过 kernel 缓存机制缓解这个问题:首次编译后把编译结果(program binary)缓存到文件,后续加载直接读缓存,跳过编译。
关键差异:和其他五个框架的对比
| 维度 | MACE 的做法 | 其他框架的对比 |
|---|---|---|
| GPU 数据存储 | image2d 纹理为主,8种 BufferContentType | ncnn/MNN/TNN: 以 buffer 为主,少量或不用纹理 |
| DSP 支持 | 整图下推 + ION 零拷贝 | Paddle-Lite: NNAdapter 逐算子适配;ncnn/MNN: 不直接支持 DSP |
| 算子分发 | Delegator 5维 key | ncnn: Layer 单继承 + #ifdef;MNN: Op-Execution 分离 |
| 内存管理 | 4种 LeaseType(含 image2d 复用) | MNN: 循环队列 + MEMORY_VIRTUAL;ncnn: 简单 Allocator |
| 计算图执行 | Flow/SubFlow 粗粒度切分 | ORT: EP 算子级竞标;MNN: Pipeline 4步;Paddle-Lite: MIR pass |
| 模型格式 | 自定义 FlatBuffers(.pb + .data 分离) | ORT: 直接吃 ONNX;ncnn: param+bin;Paddle-Lite: NaiveBuffer |
最根本的差异:MACE 是”自底向上”的设计——从硬件的物理特性出发,倒推出数据布局和计算策略。其他框架更多是”自顶向下”——先定义统一的张量抽象,再在各后端上实现算子。MACE 的做法性能天花板更高,但可移植性和开发效率更低。
功耗管理
移动端不只是跑得快的问题,还有跑得省的问题。MACE 在三个后端都做了功耗管理:
CPU:核绑定(Core Affinity)
ARM 手机的 CPU 通常是大小核架构(big.LITTLE):4 个大核(高性能、高功耗)+ 4 个小核(低性能、低功耗),有的还有超大核。MACE 提供三种 CPU 功耗模式:
enum CPUPowerOption {
DEFAULT, // 系统默认调度
HIGH_PERFORMANCE, // 绑定大核(优先使用 A77/A78/X2 等高性能核心)
LOW_POWER // 绑定小核(优先使用 A55 等高效率核心)
};
核绑定通过 Linux 的 sched_setaffinity 系统调用实现。大核的计算能力可能是小核的 3-4 倍,但功耗也是 3-4 倍。对于实时性要求高的场景(比如人脸检测)绑大核,对于后台任务(比如照片分类)绑小核。
GPU:PerfHint
OpenCL 没有标准的功耗控制 API,但高通的 Adreno GPU 提供了 performance_hint 扩展:
// MACE 在创建 OpenCL command queue 时设置
cl_queue_properties props[] = {
CL_QUEUE_PRIORITY_KHR,
CL_QUEUE_PRIORITY_HIGH_KHR, // 高优先级 = 高频率 = 高功耗
// 或 CL_QUEUE_PRIORITY_LOW_KHR // 低优先级 = 低频率 = 低功耗
0
};
cl_command_queue queue = clCreateCommandQueueWithProperties(
context, device, props, &err
);
这不是标准 OpenCL 的功能,是高通的厂商扩展。在非高通 GPU 上,这个设置会被忽略。MACE 会在运行时检测是否支持该扩展,不支持就跳过。
DSP:SetPower
Hexagon DSP 有自己的功耗控制 API:
// 设置 DSP 的时钟频率和总线带宽
hexagon_nn_set_powersave_level(level);
// level = 0: 最高性能(最高时钟频率)
// level = 1-3: 逐级降低功耗
DSP 功耗控制的粒度比 CPU 粗——你不能精确指定频率,只能选择几个预定义的功耗档位。但 DSP 本身的能效比(每瓦算力)就远高于 CPU 和 GPU,所以即使在低功耗模式下,DSP 的推理延迟也往往优于 CPU。
功耗管理的统一接口
MACE 把三种后端的功耗控制统一到一个接口下:
MaceEngineConfig config;
config.SetCPUPowerOption(CPUPowerOption::HIGH_PERFORMANCE);
config.SetGPUPerfHint(GPUPerfHint::PERF_HIGH);
config.SetGPUPriorityHint(GPUPriorityHint::PRIORITY_HIGH);
用户不需要知道底层是 sched_setaffinity 还是 cl_queue_priority 还是 hexagon_nn_set_powersave_level——框架把这些硬件差异封装起来了。这是 MACE “深入硬件但对用户友好”的一个体现。
适用场景与局限
适用场景
高通平台 + GPU/DSP 混合推理:MACE 在高通 Adreno GPU 和 Hexagon DSP 上的优化最深。如果你的目标设备是高通骁龙芯片,且需要同时利用 GPU 和 DSP,MACE 是首选。image2d 纹理优化在 Adreno 上效果显著(其他厂商的 GPU 未必有同等的纹理缓存优势)。
对延迟极度敏感的场景:实时人脸检测、AR 特效叠加、视频超分辨率等需要在 16ms(60fps)内完成推理的场景。MACE 的 image2d 纹理布局和 DSP 整图下推能把延迟压到其他框架难以企及的水平。
小米生态:MACE 是小米开源的,小米手机内部的 AI 功能(相机美颜、语音唤醒等)使用 MACE。如果你的应用目标用户主要是小米手机,MACE 的适配最有保障。
局限
硬件覆盖面窄:相比 Paddle-Lite 的 13+ 后端和 ORT 的跨平台 EP,MACE 只深度支持 ARM CPU、高通 GPU(Adreno)、高通 DSP(Hexagon)。在联发科、三星 Exynos、苹果 A 系列等非高通芯片上,MACE 只能用 CPU 后端,丧失了核心优势。
模型格式不通用:MACE 使用自定义模型格式,需要通过 mace_tools 把 TensorFlow/Caffe/ONNX 模型转换。转换工具的算子覆盖率不如 ORT 直接吃 ONNX 那么全面——遇到不支持的算子就无法转换。
社区活跃度下降:截至 2024 年底,MACE 的 GitHub 提交频率已明显放缓(最近一年提交量不足 50)。与 ncnn、MNN 的活跃社区相比,遇到问题时获得支持的渠道更有限。
开发复杂度高:要充分发挥 MACE 的性能,开发者需要理解 image2d 纹理映射、ION 共享内存、Hexagon NN 图构建等底层概念。这比使用 ORT(加载 ONNX 就跑)或 ncnn(param+bin 就跑)的门槛高得多。
DSP 整图下推的脆弱性:整图下推要求模型中所有算子都有 Hexagon NN 的实现。一旦模型包含一个不支持的算子(比如某种自定义激活函数),整个 DSP 路径就会失效,需要回退到 CPU/GPU 混合执行,此时性能可能不如一开始就用 GPU 路径。
代码结构
mace/
├── core/ # 核心抽象层
│ ├── tensor.h / tensor.cc # Tensor = Buffer + Runtime
│ ├── buffer.h # Buffer 抽象(CPU / OpenCL / ION)
│ ├── runtime/ # 各后端 Runtime 实现
│ │ ├── cpu/ # CPU Runtime + Allocator
│ │ ├── opencl/ # OpenCL Runtime + image2d 管理
│ │ └── hexagon/ # Hexagon Runtime + ION 管理
│ ├── flow.h / flow.cc # Flow / SubFlow 执行编排
│ ├── memory_manager.h # MemoryManager + 4种 LeaseType
│ ├── types.h # BufferContentType 枚举
│ └── allocator.h # Allocator 接口
│
├── ops/ # 算子定义(框架无关)
│ ├── conv_2d.h # Conv2D 算子接口
│ ├── pooling.h # Pooling 算子接口
│ └── ... # ~80 个算子
│
├── ops/opencl/ # OpenCL 算子实现
│ ├── conv_2d.cc # Conv2D 的 OpenCL 实现
│ ├── buffer_to_image.cc # BufferToImage 转换
│ ├── image_to_buffer.cc # ImageToBuffer 转换
│ └── cl/ # OpenCL kernel 源码(.cl 文件)
│ ├── conv_2d.cl
│ ├── buffer_to_image.cl
│ └── ...
│
├── ops/arm/ # ARM CPU 算子实现
│ ├── fp32/ # FP32 NEON 实现
│ │ ├── conv_2d.cc # Conv2D NEON fp32
│ │ ├── conv_2d_1x1.cc # 1×1 特化
│ │ ├── conv_2d_winograd.cc # Winograd 特化
│ │ └── gemm.cc # GEMM 内核
│ ├── fp16/ # FP16 NEON 实现
│ └── q8/ # INT8 量化实现
│
├── ops/delegator/ # Delegator 注册表
│ ├── delegator_registry.h # 5维 key → 实现 的注册机制
│ └── ...
│
├── libmace/ # 对外 API 封装
│ ├── mace.h # MaceEngine 公开接口
│ ├── mace_engine.cc # 引擎创建、推理、释放
│ └── mace_engine_config.cc # 功耗、后端、线程数等配置
│
├── port/ # 平台适配层
│ ├── android/ # Android 特有(ION、OpenCL loader)
│ ├── linux/ # Linux 通用
│ └── posix/ # POSIX 通用
│
├── proto/ # 模型格式定义
│ └── mace.proto # Protobuf 模型定义
│
├── tools/ # 离线工具
│ ├── converter/ # TF/Caffe/ONNX → MACE 格式转换
│ └── benchmark/ # 性能测试工具
│
└── third_party/ # 第三方依赖
├── opencl-headers/ # OpenCL 头文件
└── hexagon/ # Hexagon NN SDK 头文件
相比 ncnn 的约 500 个文件、Paddle-Lite 的约 3000 个文件,MACE 的约 600 个文件处于中等规模。但注意文件数不代表复杂度——MACE 的 OpenCL kernel(.cl 文件)和 Hexagon 适配代码的硬件细节密度非常高,每个文件都需要对底层硬件有深入理解才能读懂。
小结
MACE 是六大引擎中最”硬件至上”的一个。它的核心哲学——不做通用抽象,做精确适配——体现在每一个设计决策上:
- OpenCL image2d 纹理不是可选优化,而是默认数据存储方式,8 种 BufferContentType 确保每种数据在纹理中的排列和计算的访问模式完全匹配
- Hexagon DSP 整图下推不是逐算子适配,而是把整个计算图交给 DSP 运行时,通过 ION 零拷贝消除 CPU↔DSP 的数据传输
- Delegator 5 维 key 让同一个算子可以有十几种针对不同数据类型、不同硬件指令集、不同计算策略的特化实现
- MemoryManager 4 种租用类型把 image2d 纹理的复用做到了极致
- 功耗管理从 CPU 核绑定到 GPU PerfHint 到 DSP SetPower,覆盖三种后端
这种极致适配的代价是:硬件覆盖面窄(主要是高通平台)、模型格式不通用、开发门槛高、社区活跃度在下降。
如果把六大引擎比作六种木匠:ncnn 是手工木匠(零依赖、手写汇编),MNN 是发明了新刨子的创新木匠(Geometry Compiler),ORT 是承接所有外包的总包商(EP 插件),TNN 是 ncnn 的学徒(架构翻新),Paddle-Lite 是万能充电器(NNAdapter HAL),那 MACE 就是一个只做定制家具的高端木匠——他只接高通家的活,但做出来的家具严丝合缝、连螺丝都是专门定制的。
导读目录:README.md 上一章:Ch15: Paddle-Lite——万能充电器 下一章:Ch17: 数据容器——同一个问题的六种回答