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

Ch25: MNN 实战——转换、推理、量化一条龙

Part 5: 实战与竞赛 前置章节:Ch24: ncnn 实战——编译、推理、性能测试 后续章节:Ch26: 开源竞赛贡献路径——如何向 ncnn 提交第一个 PR


本章目标

Ch12 讲了 MNN 的内部架构——Geometry Compiler、Session/Module/Pipeline 三种接口、以及它的”瑞士军刀”定位。Ch24 讲了 ncnn 的实战流程。这一章对 MNN 做同样的事:从源码编译 MNN,用 MNNConvert 转换模型,写 C++ 推理程序,做量化,跑性能测试。

和 ncnn 的极简风格不同,MNN 的 API 更丰富、工具链更完整、量化选项更细致。这一章会突出 MNN 和 ncnn 的差异点,帮你在实际项目中做出选择。

读完这一章,你应该能:


前置准备


源码编译

克隆仓库

git clone https://github.com/alibaba/MNN.git
cd MNN

MNN 的仓库比 ncnn 大不少——除了核心推理库,还包含 Express API、训练框架、各种前端(TF/Caffe/ONNX)的转换器、量化工具、benchmark 工具等。

Mac/Linux 编译

# 创建构建目录
mkdir build && cd build

# 基础编译
cmake \
    -DCMAKE_BUILD_TYPE=Release \
    -DMNN_BUILD_CONVERTER=ON \
    -DMNN_BUILD_QUANTOOLS=ON \
    -DMNN_BUILD_BENCHMARK=ON \
    ..

make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu)

编译完成后的关键产物:

build/
├── libMNN.so (Linux) / libMNN.dylib (Mac)    # MNN 动态库
├── MNNConvert                                 # 模型转换工具
├── quantized.out                              # 量化工具
├── benchmark.out                              # 性能测试工具
├── testModel.out                              # 模型测试工具
└── MNNV2Basic.out                             # 基础推理示例

注意 MNN 默认编译的是动态库.so/.dylib),而 ncnn 默认编译的是静态库.a)。如果你需要静态库:

cmake \
    -DCMAKE_BUILD_TYPE=Release \
    -DMNN_BUILD_SHARED_LIBS=OFF \
    -DMNN_SEP_BUILD=OFF \
    -DMNN_BUILD_CONVERTER=ON \
    -DMNN_BUILD_QUANTOOLS=ON \
    ..

make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu)

CMake 编译选项

选项 默认值 含义
MNN_BUILD_CONVERTER OFF 编译 MNNConvert 转换工具
MNN_BUILD_QUANTOOLS OFF 编译量化工具
MNN_BUILD_BENCHMARK OFF 编译 benchmark 工具
MNN_BUILD_SHARED_LIBS ON 编译动态库(OFF 则编译静态库)
MNN_OPENCL OFF 开启 OpenCL GPU 支持
MNN_VULKAN OFF 开启 Vulkan GPU 支持
MNN_METAL OFF 开启 Metal GPU 支持(Apple 设备)
MNN_COREML OFF 开启 CoreML 支持(Apple 设备)
MNN_ARM82 OFF 开启 ARM fp16 支持
MNN_LOW_MEMORY OFF 低内存模式(适合嵌入式设备)
MNN_BUILD_LLM OFF 编译 LLM 推理支持

Android 交叉编译

cd MNN
mkdir build-android && cd build-android

cmake \
    -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
    -DANDROID_ABI=arm64-v8a \
    -DANDROID_PLATFORM=android-24 \
    -DCMAKE_BUILD_TYPE=Release \
    -DMNN_OPENCL=ON \
    -DMNN_ARM82=ON \
    ..

make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu)

MNN 的 Android 编译和 ncnn 类似,但多了 OpenCL 选项——MNN 在 Android 上主要通过 OpenCL 做 GPU 加速(而 ncnn 主要用 Vulkan)。


模型转换:MNNConvert

MNNConvert 是 MNN 的模型转换工具,支持从 ONNX、TensorFlow、Caffe、TFLite 等格式转换到 .mnn 格式。

基础转换

# ONNX → MNN
./MNNConvert -f ONNX \
    --modelFile mobilenet_v2.onnx \
    --MNNModel mobilenet_v2.mnn \
    --bizCode my_project

# TensorFlow → MNN
./MNNConvert -f TF \
    --modelFile model.pb \
    --MNNModel model.mnn \
    --bizCode my_project \
    --inputOutputTxt input_output.txt

# Caffe → MNN
./MNNConvert -f CAFFE \
    --prototxt deploy.prototxt \
    --modelFile model.caffemodel \
    --MNNModel model.mnn \
    --bizCode my_project

--bizCode 参数的作用是给模型打一个业务标签,MNN 内部用它做一些统计和管理。对推理结果没有影响,可以随便填一个字符串。

转换时做优化

# 转换时同时做 fp16 权重量化(模型减半,推理精度几乎不变)
./MNNConvert -f ONNX \
    --modelFile mobilenet_v2.onnx \
    --MNNModel mobilenet_v2_fp16.mnn \
    --fp16

# 转换时做 INT8 权重量化(更激进的压缩)
./MNNConvert -f ONNX \
    --modelFile mobilenet_v2.onnx \
    --MNNModel mobilenet_v2_w8.mnn \
    --weightQuantBits 8

# 转换时做 INT4 权重量化(适合 LLM 场景)
./MNNConvert -f ONNX \
    --modelFile model.onnx \
    --MNNModel model_w4.mnn \
    --weightQuantBits 4

--weightQuantBits 只量化权重,不量化激活值(activation)。这种”仅权重量化”的好处是精度损失很小(因为激活值仍然是 fp32),坏处是推理加速有限(因为激活值的计算仍然在 fp32 精度下进行)。要获得真正的 INT8 加速,需要用完整的 PTQ 量化(权重和激活值都量化),见后面”完整量化”一节。

检查转换后的模型

# 用 testModel.out 验证模型能否正常推理
./testModel.out mobilenet_v2.mnn input0 output0
# 会用随机数据做一次推理,检查是否报错

# 用 MNNDump2Json 查看模型结构
./MNNDump2Json mobilenet_v2.mnn > model_structure.json

C++ 推理:Interpreter + Session

MNN 提供三种推理接口,从低级到高级依次是:Interpreter+Session、Module、Pipeline。实际项目中最常用的是 Interpreter+Session,我们先详细讲这个。

完整的推理程序

// mnn_classify.cpp
#include <stdio.h>
#include <vector>
#include <algorithm>
#include <MNN/Interpreter.hpp>
#include <MNN/MNNDefine.h>
#include <MNN/Tensor.hpp>
#include <MNN/ImageProcess.hpp>

int main(int argc, char** argv) {
    if (argc != 3) {
        printf("Usage: %s <model.mnn> <image>\n", argv[0]);
        return -1;
    }

    // ========== 1. 创建 Interpreter ==========
    // Interpreter 负责模型的加载和管理
    std::shared_ptr<MNN::Interpreter> interpreter(
        MNN::Interpreter::createFromFile(argv[1])
    );
    if (!interpreter) {
        printf("Failed to create interpreter\n");
        return -1;
    }

    // ========== 2. 配置 Session ==========
    MNN::ScheduleConfig config;
    config.numThread = 4;                    // CPU 线程数
    config.type = MNN_FORWARD_CPU;           // 后端类型

    // 后端特定配置
    MNN::BackendConfig backendConfig;
    backendConfig.precision = MNN::BackendConfig::Precision_Normal;
    // Precision_Low    = fp16(更快但精度低)
    // Precision_Normal = fp32(默认)
    // Precision_High   = fp32 + 额外精度保证
    config.backendConfig = &backendConfig;

    // ========== 3. 创建 Session ==========
    // Session 是推理的执行环境
    MNN::Session* session = interpreter->createSession(config);
    if (!session) {
        printf("Failed to create session\n");
        return -1;
    }

    // ========== 4. 获取输入 Tensor ==========
    // nullptr 表示获取第一个(默认的)输入
    MNN::Tensor* inputTensor = interpreter->getSessionInput(session, nullptr);

    // 打印输入信息
    auto shape = inputTensor->shape();
    printf("Input shape: ");
    for (int s : shape) printf("%d ", s);
    printf("\n");

    // ========== 5. 准备输入数据 ==========
    // 方法 A:使用 ImageProcess(推荐用于图片输入)
    int inputHeight = shape[2];   // NCHW 格式
    int inputWidth = shape[3];

    // 加载图片(这里用伪代码,实际需要用 stb_image 等库)
    // unsigned char* imageData = load_image(argv[2], &imgWidth, &imgHeight);

    // 配置图像预处理
    MNN::CV::ImageProcess::Config imgConfig;
    imgConfig.filterType = MNN::CV::BILINEAR;    // 缩放插值方式
    imgConfig.sourceFormat = MNN::CV::RGBA;      // 输入图片格式
    imgConfig.destFormat = MNN::CV::BGR;         // 模型需要的格式

    // 设置 mean 和 norm(和训练时一致)
    const float mean[3] = {103.94f, 116.78f, 123.68f};
    const float norm[3] = {0.017f, 0.017f, 0.017f};
    ::memcpy(imgConfig.mean, mean, sizeof(mean));
    ::memcpy(imgConfig.normal, norm, sizeof(norm));

    std::shared_ptr<MNN::CV::ImageProcess> pretreat(
        MNN::CV::ImageProcess::create(imgConfig)
    );

    // 设置缩放矩阵
    MNN::CV::Matrix matrix;
    matrix.postScale(
        (float)inputWidth / imgWidth,
        (float)inputHeight / imgHeight
    );
    pretreat->setMatrix(matrix);

    // 方法 B:直接填充浮点数据(适合非图片输入或已预处理的数据)
    auto inputHost = new MNN::Tensor(inputTensor, MNN::Tensor::CAFFE);
    // CAFFE 表示 NCHW 布局,TENSORFLOW 表示 NHWC 布局

    float* inputData = inputHost->host<float>();
    // 手动填充数据...
    // for (int i = 0; i < inputHost->elementSize(); i++) {
    //     inputData[i] = preprocessed_value;
    // }

    // 把数据从 Host Tensor 拷贝到 Session 的 Input Tensor
    inputTensor->copyFromHostTensor(inputHost);

    // ========== 6. 执行推理 ==========
    interpreter->runSession(session);

    // ========== 7. 获取输出 ==========
    MNN::Tensor* outputTensor = interpreter->getSessionOutput(session, nullptr);

    // 创建 Host Tensor 来接收输出
    auto outputHost = new MNN::Tensor(outputTensor, MNN::Tensor::CAFFE);
    outputTensor->copyToHostTensor(outputHost);

    // ========== 8. 解析结果 ==========
    float* outputData = outputHost->host<float>();
    int outputSize = outputHost->elementSize();

    // 找 Top-5
    std::vector<std::pair<float, int>> scores;
    for (int i = 0; i < outputSize; i++) {
        scores.push_back({outputData[i], i});
    }
    std::sort(scores.begin(), scores.end(),
              [](const auto& a, const auto& b) { return a.first > b.first; });

    printf("Top-5 results:\n");
    for (int i = 0; i < 5 && i < outputSize; i++) {
        printf("  class %d: %.4f\n", scores[i].second, scores[i].first);
    }

    // ========== 9. 清理 ==========
    delete inputHost;
    delete outputHost;
    // Session 由 Interpreter 管理,不需要手动 delete
    // Interpreter 由 shared_ptr 管理,自动释放

    return 0;
}

编译

g++ -O2 mnn_classify.cpp \
    -I/path/to/MNN/include \
    -L/path/to/MNN/build \
    -lMNN \
    -o mnn_classify

# 运行前设置动态库路径(如果用的是动态库)
export LD_LIBRARY_PATH=/path/to/MNN/build:$LD_LIBRARY_PATH    # Linux
export DYLD_LIBRARY_PATH=/path/to/MNN/build:$DYLD_LIBRARY_PATH  # Mac

./mnn_classify mobilenet_v2.mnn test.jpg

Host Tensor 和 Device Tensor

MNN 推理中一个容易踩坑的概念是 Host TensorDevice Tensor 的区别。

interpreter->getSessionInput/Output() 返回的是 Device Tensor——它可能存储在 GPU 内存、或者以引擎内部的特殊布局存储。你不能直接用 host<float>() 读取 Device Tensor 的数据(可能读到垃圾值或者直接崩溃)。

正确的做法是创建一个同形状的 Host Tensor,然后用 copyToHostTensor() 把数据从 Device 拷贝到 Host:

// 错误做法:直接读 Device Tensor
MNN::Tensor* output = interpreter->getSessionOutput(session, nullptr);
float* data = output->host<float>();  // 危险!可能读到错误数据

// 正确做法:创建 Host Tensor,拷贝后再读
MNN::Tensor* output = interpreter->getSessionOutput(session, nullptr);
auto hostOutput = new MNN::Tensor(output, MNN::Tensor::CAFFE);
output->copyToHostTensor(hostOutput);
float* data = hostOutput->host<float>();  // 安全

这个 MNN::Tensor::CAFFE 参数不是说”这个 Tensor 来自 Caffe 框架”,而是说”使用 NCHW 数据布局”(因为 Caffe 框架历史上使用 NCHW 布局,所以 MNN 用 CAFFE 这个枚举名来表示 NCHW)。如果你的模型使用 NHWC 布局,用 MNN::Tensor::TENSORFLOW


三种推理接口对比

MNN 提供三种推理接口,适用于不同场景:

Interpreter + Session(经典接口)

这是上面代码用的接口,也是最通用的:

auto interpreter = MNN::Interpreter::createFromFile("model.mnn");
auto session = interpreter->createSession(config);
auto input = interpreter->getSessionInput(session, nullptr);
// 填充输入...
interpreter->runSession(session);
auto output = interpreter->getSessionOutput(session, nullptr);

适用场景:单输入单输出的标准模型、性能敏感的部署场景。

Module(PyTorch 风格接口)

Module 接口更高层,API 风格像 PyTorch:

#include <MNN/expr/Module.hpp>
#include <MNN/expr/Executor.hpp>
#include <MNN/expr/ExprCreator.hpp>

using namespace MNN::Express;

// 创建 Module
std::shared_ptr<Module> module(Module::load(
    {"input0"},      // 输入名列表
    {"output0"},     // 输出名列表
    "model.mnn"
));

// 创建输入
auto input = _Input({1, 3, 224, 224}, NCHW, halide_type_of<float>());
auto inputPtr = input->writeMap<float>();
// 填充数据到 inputPtr...
input->unMap();

// 前向推理
auto outputs = module->onForward({input});
auto outputPtr = outputs[0]->readMap<float>();
// 读取结果...

Module 接口的优势:支持动态输入 shape(不需要在创建 Session 时固定 shape)、API 更简洁。代价是封装层更厚,极限性能可能略低于 Session 接口。

适用场景:需要动态 shape 支持、模型结构变化频繁(研发阶段)、对 API 简洁性有要求。

Pipeline(批处理接口)

Pipeline 接口用于需要同时处理多组输入输出的场景:

// 简略示例——实际使用较少
auto pipeline = interpreter->createPipeline(session, config);
pipeline->encode(inputs);
pipeline->submit();
pipeline->wait();
pipeline->decode(outputs);

适用场景:视频流处理、需要和 GPU 异步执行配合的场景。实际使用较少,大多数端侧应用用 Session 或 Module 接口就够了。

接口选择建议

场景 推荐接口 原因
Android/iOS 单模型部署 Session 性能最优,控制最细
Python 原型验证 Module API 简洁,像 PyTorch
动态输入 shape Module Session 需要重建
LLM 推理 Module 自然支持变长序列
多模型串联 Module 方便连接输入输出
极致性能优化 Session 可手动管理内存和线程

完整量化流程

Ch23 介绍了量化的基本命令。这里展开 MNN 的完整量化流程,包括校准数据准备、量化配置、精度验证。

步骤 1:准备校准数据

MNN 的量化工具需要校准数据,格式是一个目录,里面放着预处理好的二进制文件:

# 方法 A:用 MNN 自带的脚本准备(推荐)
cd MNN
python tools/script/prepareModel.py \
    --model mobilenet_v2.mnn \
    --images /path/to/calibration_images/ \
    --output /path/to/calib_data/ \
    --format BGR \
    --mean "103.94 116.78 123.68" \
    --normal "0.017 0.017 0.017" \
    --width 224 \
    --height 224
# 方法 B:用 Python 手动准备
import numpy as np
import os
from PIL import Image

calib_dir = "./calib_data/"
os.makedirs(calib_dir, exist_ok=True)

image_dir = "./calibration_images/"
for i, fname in enumerate(os.listdir(image_dir)[:200]):
    img = Image.open(os.path.join(image_dir, fname)).resize((224, 224))
    arr = np.array(img).astype(np.float32)

    # BGR 转换
    arr = arr[:, :, ::-1].copy()

    # 减均值、归一化(和训练一致)
    mean = np.array([103.94, 116.78, 123.68])
    norm = np.array([0.017, 0.017, 0.017])
    arr = (arr - mean) * norm

    # NHWC → NCHW
    arr = arr.transpose(2, 0, 1)

    # 保存为二进制
    arr.astype(np.float32).tofile(os.path.join(calib_dir, f"{i}.bin"))

print(f"Prepared {i+1} calibration samples")

步骤 2:创建量化配置文件

{
    "MNNModel": "mobilenet_v2.mnn",
    "targetMNNModel": "mobilenet_v2_int8.mnn",
    "calibrationDataPath": "./calib_data/",
    "featureQuantizeMethod": "KL",
    "weightQuantizeMethod": "MAX_ABS",
    "inputType": "float",
    "debug": false,
    "saveStatePath": "./quant_state.json",
    "feature_clamp_value": 127,
    "skip_quant_op_names": []
}

配置参数说明:

参数 说明 可选值
featureQuantizeMethod 激活值量化方法 “KL”(推荐)/ “ADMM” / “EMA”
weightQuantizeMethod 权重量化方法 “MAX_ABS”(推荐)/ “ADMM”
skip_quant_op_names 跳过量化的层名列表 精度敏感层保持 FP32
saveStatePath 保存量化状态(可复用) 路径字符串
inputType 输入数据类型 “float” / “uint8”

步骤 3:执行量化

./quantized.out quant_config.json

量化过程通常需要几分钟(取决于校准集大小和模型复杂度)。会输出每一层的量化参数统计信息。

步骤 4:验证量化精度

# 用 testModel.out 对比 FP32 和 INT8 的输出
./testModel.out mobilenet_v2.mnn input0 output0
./testModel.out mobilenet_v2_int8.mnn input0 output0

更精确的验证用 Python:

import MNN
import numpy as np

def run_model(model_path, input_data):
    interpreter = MNN.Interpreter(model_path)
    config = MNN.ScheduleConfig()
    config.numThread = 4
    session = interpreter.createSession(config)

    input_tensor = interpreter.getSessionInput(session)
    tmp = MNN.Tensor(
        (1, 3, 224, 224),
        MNN.Halide_Type_Float,
        input_data,
        MNN.Tensor_DimensionType_Caffe
    )
    input_tensor.copyFrom(tmp)

    interpreter.runSession(session)

    output_tensor = interpreter.getSessionOutput(session)
    tmp_out = MNN.Tensor(
        output_tensor.getShape(),
        MNN.Halide_Type_Float,
        np.zeros(output_tensor.getShape()).astype(np.float32),
        MNN.Tensor_DimensionType_Caffe
    )
    output_tensor.copyToHostTensor(tmp_out)

    return np.array(tmp_out.getData()).copy()

# 对比 FP32 和 INT8
test_input = np.random.randn(1, 3, 224, 224).astype(np.float32)

fp32_output = run_model("mobilenet_v2.mnn", test_input)
int8_output = run_model("mobilenet_v2_int8.mnn", test_input)

cos_sim = np.dot(fp32_output.flatten(), int8_output.flatten()) / (
    np.linalg.norm(fp32_output) * np.linalg.norm(int8_output)
)
print(f"Cosine similarity: {cos_sim:.6f}")

# 对比 Top-1 一致性
fp32_top1 = np.argmax(fp32_output)
int8_top1 = np.argmax(int8_output)
print(f"FP32 Top-1: {fp32_top1}, INT8 Top-1: {int8_top1}")
print(f"Match: {fp32_top1 == int8_top1}")

混合精度量化

如果某些层量化后精度损失大,可以跳过这些层的量化:

{
    "skip_quant_op_names": [
        "conv1",
        "classifier"
    ]
}

找出精度敏感层的方法:逐层量化,对比每层量化和不量化时的输出差异。差异最大的层就是精度敏感层。MNN 的量化工具在 debug=true 模式下会输出每层的量化误差,帮助你做这个判断。


性能测试

benchmark.out

# 基础性能测试
./benchmark.out mobilenet_v2.mnn 10 0
# 参数:模型路径、循环次数、forward类型(0=CPU)

# GPU 测试(如果编译了 OpenCL)
./benchmark.out mobilenet_v2.mnn 10 3
# forward 类型:0=CPU, 1=Metal, 2=CUDA, 3=OpenCL, 7=Vulkan

自定义性能测试

// bench_mnn.cpp
#include <stdio.h>
#include <chrono>
#include <MNN/Interpreter.hpp>

int main(int argc, char** argv) {
    auto interpreter = MNN::Interpreter::createFromFile(argv[1]);

    MNN::ScheduleConfig config;
    config.numThread = 4;
    config.type = MNN_FORWARD_CPU;

    auto session = interpreter->createSession(config);
    auto input = interpreter->getSessionInput(session, nullptr);

    // 创建随机输入
    auto hostInput = new MNN::Tensor(input, MNN::Tensor::CAFFE);
    auto data = hostInput->host<float>();
    for (int i = 0; i < hostInput->elementSize(); i++) {
        data[i] = 0.5f;
    }
    input->copyFromHostTensor(hostInput);

    // 预热
    for (int i = 0; i < 3; i++) {
        interpreter->runSession(session);
    }

    // 正式测试
    int loops = 20;
    double total = 0;
    for (int i = 0; i < loops; i++) {
        auto start = std::chrono::high_resolution_clock::now();
        interpreter->runSession(session);
        auto end = std::chrono::high_resolution_clock::now();
        double ms = std::chrono::duration<double, std::milli>(end - start).count();
        total += ms;
        printf("Run %d: %.2f ms\n", i + 1, ms);
    }
    printf("Average: %.2f ms\n", total / loops);

    delete hostInput;
    delete interpreter;
    return 0;
}

MNN vs ncnn 性能对比

在同一台设备上,用同一个模型(ONNX 分别转成 ncnn 和 MNN 格式),可以做一个公平的性能对比。

典型结果(Snapdragon 888, 4 线程大核,单位 ms):

模型 ncnn MNN 备注
MobileNetV2 3.9 4.2 ncnn 在轻量模型上通常更快
ResNet-50 18.5 17.8 MNN 在中型模型上有时更快
YOLOv8n 12.3 13.1 ncnn 手写 SIMD 优势明显
BERT-base - 45.6 ncnn 不太适合 Transformer

上面的数据是示意性的,实际性能取决于具体设备、线程配置、模型版本等因素。关键结论是:ncnn 在轻量级 CNN 上有手写 SIMD 的极致优势;MNN 在中大型模型和 Transformer 架构上更有竞争力;两者的差距通常在 10-20% 以内。


常见问题排查

Q: MNNConvert 报 “unsupported op: xxx”?

A: MNN 的算子覆盖比 ncnn 广,但仍可能遇到不支持的算子。解决方案和 ncnn 类似:用 onnxsim 简化 ONNX 模型、降低 opset 版本、或者在 PyTorch 侧用等价的标准算子替换。MNN 的 Issue 中也可以搜索具体的算子名看有没有解决方案。

Q: 推理速度比预期慢很多?

A: 检查以下几点:(1)确认 numThread 设置合理——线程数不是越多越好,通常设置为大核数量(手机一般是 4);(2)确认 precision 设置——Precision_Low(fp16)在支持的设备上比 Precision_Normal(fp32)快很多;(3)确认没有频繁创建/销毁 Session——Session 的创建开销很大,应该复用。

Q: copyFromHostTensor/copyToHostTensor 崩溃?

A: 最常见的原因是 Host Tensor 和 Device Tensor 的 shape 不匹配。创建 Host Tensor 时要用 Device Tensor 作为模板:

// 正确:用 Device Tensor 的 shape 创建 Host Tensor
auto hostTensor = new MNN::Tensor(deviceTensor, MNN::Tensor::CAFFE);

// 错误:手动指定了不匹配的 shape
auto hostTensor = new MNN::Tensor({1, 3, 224, 224}, ...);  // shape 可能不对

Q: Mac 上编译报 “Metal framework not found”?

A: 如果不需要 Metal GPU 支持,关闭 MNN_METAL 选项。如果需要,确保 Xcode Command Line Tools 已安装(xcode-select --install)。

Q: Android 上 OpenCL 推理崩溃?

A: 不是所有 Android 设备都支持 OpenCL。先用 clinfo 等工具确认设备有 OpenCL 支持,然后检查 OpenCL 版本是否满足 MNN 的要求(一般需要 OpenCL 1.1+)。在不确定的情况下,先用 CPU 后端跑通,再尝试 OpenCL。


本章小结

MNN 的实战流程和 ncnn 类似:编译 → 转换 → 量化 → 推理。但在每一步上,MNN 都提供了更多的选项和灵活性:转换时可以直接做权重量化(--weightQuantBits);量化支持多种方法和混合精度;推理有三种接口可选(Session/Module/Pipeline)。

和 ncnn 相比,MNN 的 API 更”现代”(支持 shared_ptr、C++11 特性),工具链更完整,但也更复杂。如果你的场景是”跑一个轻量模型做分类/检测”,ncnn 的简单性是优势;如果你需要动态 shape、LLM 推理、或者更精细的量化控制,MNN 的丰富性是优势。

Host Tensor 和 Device Tensor 的区分是 MNN 推理中最容易踩坑的地方——记住”读写数据永远通过 Host Tensor”这一条规则就够了。


下一步建议


导读目录:README.md 上一章:Ch24: ncnn 实战——编译、推理、性能测试 下一章:Ch26: 开源竞赛贡献路径——如何向 ncnn 提交第一个 PR