测试与 CI
本文说明 MNN 当前统一测试入口 ./test.sh 和测试矩阵配置
test_stages.json,并给出新增算子测试的推荐流程。
快速开始
# 静态检查:文档完整性、cppcheck、PyMNN wrapper 资源校验
./test.sh static
# 本机回归:CPU 构建 + 单元测试 + smoke + LLM smoke
./test.sh local
# Android 设备完整矩阵
./test.sh android <serial>
# Android 子集。第三个参数是可选 filter:
./test.sh android <serial> cpu # CPU + lowmem + llm
./test.sh android <serial> opencl-image # 只跑 OpenCL IMAGE 阶段
./test.sh android <serial> vulkan
./test.sh android <serial> lowmem
./test.sh android <serial> android-ci # bench + smoke + llm,不跑 unit/lowmem
测试矩阵由 test_stages.json 描述。forward type、precision、gpuMode、
线程数、tag、memory mode、动态量化选项、KleidiAI 开关、分阶段 skip
列表、smoke 模型和 benchmark 参数都放在 JSON 中。新增、删除或调整
unit / lowmem / smoke / bench 阶段时,优先改 test_stages.json,不要直接
改 shell。
文件结构
test.sh # 统一测试入口:static / local / android
test_stages.json # 可执行回归阶段的声明式配置
docs/testing.md # 本文档
skills/test-ci/SKILL.md # Agent 使用入口,引用本文档
test/ # C++ 测试框架和测试用例
MNNTestSuite.{h,cpp} # 测试注册、运行、MNN_TEST_SKIP 支持
main.cpp # argv -> BackendConfig / RuntimeHint
op/ # 每个算子一个或一组测试文件
static 模式不使用 test_stages.json。它只做轻量检查:
doc_check:检查 CMake 选项、可执行文件、部分 PyMNN API 是否在文档中出现static_check:对本次变更涉及的 C++ 源码跑cppcheckpy_check:PyMNN wrapper 资源校验,仅在对应 Python 文件变更时运行
local 和 android 模式使用 test_stages.json。
Android 模式流程
./test.sh android <serial> [filter] 的执行流程:
构建
arm64-v8a(调用project/android/build_64.sh后再用本机核心数增量make)。推送
ANDROID_BIN_LIST中的库和可执行文件到/data/local/tmp/MNN。在需要 smoke/bench 时下载 Caffe 源模型,并在设备端使用刚构建出的
MNNConvert转成.mnn。按
test_stages.json运行阶段:unit/* -> lowmem/* -> smokeA/* -> smokeB/* -> bench/* -> llm/*。打印 summary:总数、通过、失败、跳过,以及每个阶段的结果。
每个阶段的 stdout/stderr 会保存到 logs/test-<UTC timestamp>/ 下。退出码
非 0 表示至少有一个阶段失败;SKIP 不算失败。
LLM 模型准备
llm 阶段使用 llm_demo 跑一个 MNN 格式 LLM 模型。模型准备是懒加载的:
只有跑到 llm 阶段时才会检查或下载模型;如果模型不可用,只跳过 llm
阶段,不影响 unit / smoke / bench。
| 环境变量 | 模式 | 含义 |
|---|---|---|
LLM_MODEL_DIR |
local/android | 指向已有 MNN LLM 模型目录。设置后不会下载。 |
LLM_MODEL_REPO |
local/android | 模型仓库,默认 taobao-mnn/Qwen2.5-0.5B-Instruct-MNN。 |
LLM_MODEL_SOURCE |
local/android | 下载源:huggingface 或 modelscope。 |
LLM_MODEL_URL_BASE |
local/android | 直接覆盖下载 URL 前缀,优先级最高。 |
# 已有模型目录,不触发下载
LLM_MODEL_DIR=/path/to/Qwen2.5-0.5B-Instruct-MNN ./test.sh local
# HuggingFace 不通时使用 ModelScope
LLM_MODEL_SOURCE=modelscope ./test.sh android <serial>
内置默认模型在 ModelScope 上会自动把 taobao-mnn/* 映射为 MNN/*;
如果显式设置了 LLM_MODEL_REPO,则按用户指定值使用。
test_stages.json 结构
{
"android": {
"stages": [ ... ], // run_test.out unit + lowmem 阶段
"smoke_models": [ ... ], // smoke/bench 使用的模型路径
"smoke_a_stages": [ ... ], // 每个模型做一次前向
"smoke_b_stages": [ ... ], // CPU vs backend 数值对比
"bench_stages": [ ... ] // benchmark.out 调用
},
"local": {
"stages": [ ... ],
"smoke_a_stages": [ ... ] // 本机 CPU-only,不做 smokeB
},
"llm": {
"model_repo": "...",
"config_file": "config.json",
"prompt_file": "prompt.txt",
"stage": { ... }
},
"_documentation": { ... }
}
stage 字段
| 字段 | 含义 |
|---|---|
name |
阶段名,也用于日志文件名。 |
filter |
过滤标签,和命令行第三个参数匹配。 |
comment |
说明这个阶段为什么存在、覆盖什么。 |
binary |
smoke/bench 使用:run_test、v2basic、backendtest、benchmark。 |
prefix |
传给 run_test.out 的测试名前缀,例如 all、op、op/convolution。 |
type |
forward type:0 CPU,3 OpenCL,7 Vulkan。 |
precision |
BackendConfig::PrecisionMode:0 Normal,1 High,2 Low。 |
threadOrGpuMode |
CPU 下是线程数;GPU 下是 MNN_GPU_TUNING_* 和 MNN_GPU_MEMORY_* bitmask。 |
tag |
传给 run_test.out 的报告标签。 |
memory |
BackendConfig::MemoryMode:0 Normal,1 High,2 Low。可省略。 |
dynamicOption |
RuntimeHint::dynamicQuantOption。可省略。 |
kleidiAi |
argv[8],1 表示启用 KleidiAI。可省略。 |
skip |
精确测试名列表,通过 MNN_TEST_SKIP 跳过。 |
args |
smoke/bench 使用的位置参数。支持 {model} 和 {models_dir} 替换。 |
filter 标签
| 标签 | 含义 |
|---|---|
cpu |
CPU 阶段;cpu filter 也会包含 lowmem 和 llm。 |
opencl-image |
OpenCL IMAGE 内存模式。 |
opencl-buffer |
OpenCL BUFFER 内存模式。 |
vulkan |
Vulkan backend。 |
lowmem |
低内存模式矩阵。 |
smoke-opencl |
OpenCL smokeA/smokeB。 |
smoke-vulkan |
Vulkan smokeA/smokeB。 |
llm |
LLM smoke。 |
快捷 filter:
opencl=opencl-image+opencl-buffer+smoke-openclgpu= OpenCL + Vulkanunit= CPU/OpenCL/Vulkan unit 阶段,不含 lowmem/smoke/llmandroid-ci= bench + smoke + llm,不跑 unit/lowmem
阶段类型
unit/cpu/*
CPU 单元测试,覆盖注册在 MNNTestSuite 中的 C++ 测试。常见阶段:
unit/cpu/all:单线程、Precision_Normal,覆盖面最广unit/cpu/op-mt:4 线程、只跑op前缀,覆盖线程池路径unit/cpu/op-fp16-*:窄范围 FP16 精度回归
unit/opencl/* / unit/vulkan/*
GPU correctness 阶段使用 TUNING_NONE,避免单次正确性测试消耗大量调优时间。
性能相关阶段才使用更宽的 tuning 模式。
unit/opencl/op:OpenCL IMAGEunit/opencl/op-buffer:OpenCL BUFFERunit/vulkan/op:Vulkan
每个 GPU 阶段都可以有自己的 skip 列表,用于隔离已知设备或驱动问题。
新增 skip 时必须在 _documentation.skip_rationale 里解释原因。
lowmem/*
低内存模式矩阵,主要覆盖 op/lowMemory/* 和 weight-i8/i4 量化卷积。
组合维度包括 precision、线程数、dynamicOption 等。
smokeA/<backend>/<model>
用 MNNV2Basic.out 对模型做一次前向。主要覆盖模型加载、shape infer 和基础
执行路径,不做数值对比。
smokeB/<backend>/<model>
用 backendTest.out 做 CPU vs backend 数值对比,默认 tolerance 为 0.05。
bench/<backend>
用 benchmark.out 做简单性能 sanity。只要程序能跑完并输出时间,阶段就通过;
不根据具体耗时判定失败。
llm/<model_name>
用 llm_demo 跑配置好的 config.json 和 prompt.txt。
新增算子测试
新增算子测试通常分两步:
在
test/op/下写 C++ 测试,并用MNNTestSuiteRegister注册。如需特定 backend / precision / memory / dynamicOption,再在
test_stages.json中增加专门阶段。
编写测试
最小示例:
#include <MNN/expr/Expr.hpp>
#include <MNN/expr/ExprCreator.hpp>
#include "MNNTestSuite.h"
#include "TestUtils.h"
using namespace MNN::Express;
class MyOpTest : public MNNTestCase {
public:
virtual ~MyOpTest() = default;
virtual bool run(int precision) override {
auto x = _Input({1, 4}, NCHW, halide_type_of<float>());
x->writeMap<float>()[0] = 1.0f;
x->writeMap<float>()[1] = 2.0f;
x->writeMap<float>()[2] = 3.0f;
x->writeMap<float>()[3] = 4.0f;
auto y = _Multiply(x, _Scalar<float>(2.0f));
const std::vector<float> expected = {2.0f, 4.0f, 6.0f, 8.0f};
auto got = y->readMap<float>();
if (!checkVector<float>(got, expected.data(), 4, 0.0001f)) {
MNN_ERROR("MyOpTest: numerical mismatch\n");
return false;
}
return true;
}
};
MNNTestSuiteRegister(MyOpTest, "op/myop");
约定:
测试名(如
op/myop)会被test_stages.json中的prefix匹配。相关测试尽量放在共同前缀下,例如
op/binary/foo、op/convolution/bar。绝对误差用
checkVector,相对误差用checkVectorByRelativeError。需要读取运行配置时,可使用
MNNTestSuite::get()->pStaus。
新增文件后可先本地验证:
cd build
make -j$(nproc) run_test.out
./run_test.out op/myop
不改 JSON 的情况
如果测试名能被已有阶段覆盖,通常不用改 JSON:
op/myop会进入unit/cpu/all、unit/cpu/op-mt、unit/opencl/op、unit/opencl/op-buffer、unit/vulkan/opop/convolution/myop还会进入卷积相关 FP16 窄范围阶段
增加专门阶段
当已有阶段无法覆盖所需配置时,在 test_stages.json -> android.stages
增加一项:
{
"name": "unit/cpu/myop-fp16-mt",
"filter": "cpu",
"comment": "MyOp FP16 precision, 4 threads.",
"prefix": "op/myop",
"type": 0,
"precision": 2,
"threadOrGpuMode": 4,
"tag": "fp16myop64",
"memory": 0
}
随后运行:
./test.sh android <serial> cpu
跳过已知问题测试
如果某个测试暴露的是已知 backend / 驱动问题,而不是当前变更要解决的问题,
可以把精确测试名加入对应阶段的 skip:
{
"name": "unit/opencl/op-buffer",
"skip": [
"op/myop"
]
}
同时必须在 _documentation.skip_rationale 中写清楚原因,便于后续判断是否
还需要保留 skip。
新增 smoke 模型或 bench 阶段
新增 smoke 模型:
在
android.smoke_models中加入.mnn相对路径。在
test.sh的SMOKE_SOURCES中加入源模型下载信息。在
_smoke_pair_for中补充.mnn到caffemodel/prototxt的映射。
新增 bench 阶段:
{
"name": "bench/cpu-fp16",
"filter": "cpu",
"comment": "CPU FP16 benchmark, 4 threads.",
"binary": "benchmark",
"args": ["{models_dir}", "10", "2", "0", "4", "2"]
}
{models_dir} 在 Android 模式下会替换为
/data/local/tmp/MNN/public_models。