# 测试与 CI 本文说明 MNN 当前统一测试入口 `./test.sh` 和测试矩阵配置 `test_stages.json`,并给出新增算子测试的推荐流程。 ## 快速开始 ```bash # 静态检查:文档完整性、cppcheck、PyMNN wrapper 资源校验 ./test.sh static # 本机回归:CPU 构建 + 单元测试 + smoke + LLM smoke ./test.sh local # Android 设备完整矩阵 ./test.sh android # Android 子集。第三个参数是可选 filter: ./test.sh android cpu # CPU + lowmem + llm ./test.sh android opencl-image # 只跑 OpenCL IMAGE 阶段 ./test.sh android vulkan ./test.sh android lowmem ./test.sh android 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。 ## 文件结构 ```text 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++ 源码跑 `cppcheck` - `py_check`:PyMNN wrapper 资源校验,仅在对应 Python 文件变更时运行 `local` 和 `android` 模式使用 `test_stages.json`。 ## Android 模式流程 `./test.sh android [filter]` 的执行流程: 1. 构建 `arm64-v8a`(调用 `project/android/build_64.sh` 后再用本机核心数增量 `make`)。 2. 推送 `ANDROID_BIN_LIST` 中的库和可执行文件到 `/data/local/tmp/MNN`。 3. 在需要 smoke/bench 时下载 Caffe 源模型,并在设备端使用刚构建出的 `MNNConvert` 转成 `.mnn`。 4. 按 `test_stages.json` 运行阶段: `unit/* -> lowmem/* -> smokeA/* -> smokeB/* -> bench/* -> llm/*`。 5. 打印 summary:总数、通过、失败、跳过,以及每个阶段的结果。 每个阶段的 stdout/stderr 会保存到 `logs/test-/` 下。退出码 非 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 前缀,优先级最高。 | ```bash # 已有模型目录,不触发下载 LLM_MODEL_DIR=/path/to/Qwen2.5-0.5B-Instruct-MNN ./test.sh local # HuggingFace 不通时使用 ModelScope LLM_MODEL_SOURCE=modelscope ./test.sh android ``` 内置默认模型在 ModelScope 上会自动把 `taobao-mnn/*` 映射为 `MNN/*`; 如果显式设置了 `LLM_MODEL_REPO`,则按用户指定值使用。 ## `test_stages.json` 结构 ```text { "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-opencl` - `gpu` = OpenCL + Vulkan - `unit` = CPU/OpenCL/Vulkan unit 阶段,不含 lowmem/smoke/llm - `android-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 IMAGE - `unit/opencl/op-buffer`:OpenCL BUFFER - `unit/vulkan/op`:Vulkan 每个 GPU 阶段都可以有自己的 `skip` 列表,用于隔离已知设备或驱动问题。 新增 skip 时必须在 `_documentation.skip_rationale` 里解释原因。 ### `lowmem/*` 低内存模式矩阵,主要覆盖 `op/lowMemory/*` 和 weight-i8/i4 量化卷积。 组合维度包括 precision、线程数、dynamicOption 等。 ### `smokeA//` 用 `MNNV2Basic.out` 对模型做一次前向。主要覆盖模型加载、shape infer 和基础 执行路径,不做数值对比。 ### `smokeB//` 用 `backendTest.out` 做 CPU vs backend 数值对比,默认 tolerance 为 `0.05`。 ### `bench/` 用 `benchmark.out` 做简单性能 sanity。只要程序能跑完并输出时间,阶段就通过; 不根据具体耗时判定失败。 ### `llm/` 用 `llm_demo` 跑配置好的 `config.json` 和 `prompt.txt`。 ## 新增算子测试 新增算子测试通常分两步: 1. 在 `test/op/` 下写 C++ 测试,并用 `MNNTestSuiteRegister` 注册。 2. 如需特定 backend / precision / memory / dynamicOption,再在 `test_stages.json` 中增加专门阶段。 ### 编写测试 最小示例: ```cpp #include #include #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()); x->writeMap()[0] = 1.0f; x->writeMap()[1] = 2.0f; x->writeMap()[2] = 3.0f; x->writeMap()[3] = 4.0f; auto y = _Multiply(x, _Scalar(2.0f)); const std::vector expected = {2.0f, 4.0f, 6.0f, 8.0f}; auto got = y->readMap(); if (!checkVector(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`。 新增文件后可先本地验证: ```bash 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/op` - `op/convolution/myop` 还会进入卷积相关 FP16 窄范围阶段 ### 增加专门阶段 当已有阶段无法覆盖所需配置时,在 `test_stages.json -> android.stages` 增加一项: ```json { "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 } ``` 随后运行: ```bash ./test.sh android cpu ``` ### 跳过已知问题测试 如果某个测试暴露的是已知 backend / 驱动问题,而不是当前变更要解决的问题, 可以把精确测试名加入对应阶段的 `skip`: ```json { "name": "unit/opencl/op-buffer", "skip": [ "op/myop" ] } ``` 同时必须在 `_documentation.skip_rationale` 中写清楚原因,便于后续判断是否 还需要保留 skip。 ## 新增 smoke 模型或 bench 阶段 新增 smoke 模型: 1. 在 `android.smoke_models` 中加入 `.mnn` 相对路径。 2. 在 `test.sh` 的 `SMOKE_SOURCES` 中加入源模型下载信息。 3. 在 `_smoke_pair_for` 中补充 `.mnn` 到 `caffemodel/prototxt` 的映射。 新增 bench 阶段: ```json { "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`。