犀牛鸟 2026 研究笔记 estelledc.github.io

Ch16: MACE——深入骨髓的硬件适配

Part 3: 六大框架架构精讲 前置章节:Ch15: Paddle-Lite——万能充电器 本章为 Part 3 最后一章


一句话定位

MACE 是六大引擎中硬件适配深度最极致的一个。其他引擎都是”在 CPU/GPU 上跑算子”的思路——把每个算子翻译成对应后端的指令,MACE 则更进一步:它不仅翻译算子,还要把数据在硬件内存里的物理排布精确到字节,把 OpenCL 的 image2d 纹理对象当成一等公民来管理,把高通 Hexagon DSP 当成一个独立的”黑盒计算机”来整图下推。它就像一个深入到每颗螺丝的定制化装修师——不是简单地”把家具搬进去”,而是要量好每面墙的精确尺寸,把管线、电路、水路全部按照这间房子的具体结构重新走一遍。

GitHub 5K star,小米开源,约 600 个源文件——体量中等,但每个文件的硬件细节密度极高。


类比:定制化装修师

假设你买了一套精装房。普通装修公司的做法是:看看户型图,从标准方案库里挑一个差不多的方案,稍微改改尺寸就动工了。但你找了一个”定制化装修师”。他的做法完全不同:

  1. 先量房:拿激光测距仪把每一面墙、每一根管道、每一个开关的位置精确测量。GPU 有多少纹理单元、每个纹理单元能存多少数据、数据在纹理里怎么排列——MACE 全部要知道。
  2. 管线贴墙走:普通方案是把水管电线走地下(先做格式转换、再计算),定制师会让管线贴着墙的结构走,减少绕行距离。MACE 让数据在 GPU 纹理中的排列方式和卷积计算的访问模式完全吻合,中间不做多余的格式转换。
  3. 针对房型设计家具:普通方案买标准尺寸家具,定制师量好尺寸后现做家具。MACE 针对不同硬件(OpenCL GPU、Hexagon DSP、ARM CPU)设计完全不同的数据布局和计算策略。
  4. 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 是六大引擎中最”硬件至上”的一个。它的核心哲学——不做通用抽象,做精确适配——体现在每一个设计决策上:

这种极致适配的代价是:硬件覆盖面窄(主要是高通平台)、模型格式不通用、开发门槛高、社区活跃度在下降。

如果把六大引擎比作六种木匠:ncnn 是手工木匠(零依赖、手写汇编),MNN 是发明了新刨子的创新木匠(Geometry Compiler),ORT 是承接所有外包的总包商(EP 插件),TNN 是 ncnn 的学徒(架构翻新),Paddle-Lite 是万能充电器(NNAdapter HAL),那 MACE 就是一个只做定制家具的高端木匠——他只接高通家的活,但做出来的家具严丝合缝、连螺丝都是专门定制的。


导读目录:README.md 上一章:Ch15: Paddle-Lite——万能充电器 下一章:Ch17: 数据容器——同一个问题的六种回答