MNN GemmSpeed 基准测试实战:多后端 LLM GEMM 吞吐评估与 GPU 时间戳剖析
【免费下载链接】MNNMNN: A blazing-fast, lightweight inference engine battle-tested by Alibaba, powering high-performance on-device LLMs and Edge AI.项目地址: https://gitcode.com/GitHub_Trending/mn/MNN
MNN 通过test/speed/GemmSpeed.cpp提供了一组通用的 GEMM 性能基准测试,用于在 LLM 推理典型矩阵尺寸下评估 CPU / OpenCL / Vulkan / CUDA / Metal 等后端的计算吞吐量。本篇将完整继承并展开该基准测试的使用方法:编译开关(低内存量化、GPU 时间戳剖析)、命令行参数与 OpenCL 位掩码组合、测试用例注册机制、输出格式与 GFLOPS 计算口径,并结合 test/speed/GemmSpeed.cpp 源码剖析其以 Conv1x1 模拟 GEMM 的实现原理、warmup 与测量流程、GPU 硬件时间戳获取链路,以及示例数据的解读方法。
为什么用 Conv1x1 来测 GEMM
LLM 的线性层(QKV 投影、FFN 等)本质上是C = A[M, K] × B[K, N]的矩阵乘。MNN 没有单独实现一套 GEMM 基准接口,而是直接走推理引擎真实路径:用1×1 卷积表达 GEMM。
从源码看,浮点模式通过buildFloatConv1x1()构造:
- 输入张量形状为
{1, K, 1, M},即 batch=1、channel=K、height=1、width=M,数据布局为NC4HW4; - 卷积核大小 1×1、stride 1、VALID 模式,权重形状
{ic, oc}(即{K, N}),于是 1×1 卷积的空间维度逐位置计算N×K内积,等价于一次M×K×N的 GEMM; - 权重用
((i % 127) - 63) / 1000这类伪随机小数值填充,保证各 kernel 路径被真实执行。
量化模式则走buildHybridConv1x1(),通过_HybridConv表达式构造非对称量化卷积:
- Int8-Block0(mode 2):
nbit=8, blockSize=K,即按 K 方向整块量化(全通道),对应文档中的Int8-Block0 量化(blockSize=K,全通道量化); - Int4-Block64(mode 1):
nbit=4, blockSize=64,每 64 个 K 方向元素一组 scale/offset,要求K % 64 == 0(测试中不满足时直接跳过该配置); - scale 布局为
oc × blockNum组的[offset, scale]交替对,模拟真实量化模型中mnnconvert导出后的权值格式。
这里有一个值得注意的实现细节:benchConv1x1()每次运行都为每个 (M, K, N) 组合新建一个 Executor 并将 memory 模式强制设为Memory_Low,因为量化模式只有在低内存配置下才会启用权重压缩路径。这也意味着基准测试覆盖的是"量化权值推理"这一端侧 LLM 最常见的场景。
编译:必需的 CMake 开关
基准测试是 MNN 测试套件(run_test.out)的一部分,需要在构建时开启测试目标:
# 基础编译(CPU 后端) cmake .. -DMNN_BUILD_TEST=ON -DMNN_LOW_MEMORY=ON # 开启 GPU 后端 + 性能 Profile cmake .. -DMNN_BUILD_TEST=ON -DMNN_LOW_MEMORY=ON \ -DMNN_OPENCL=ON -DMNN_VULKAN=ON \ -DMNN_GPU_TIME_PROFILE=ON -DMNN_GPU_PROFILE_SILENT=ON make -j$(nproc) run_test.out关键编译宏说明:
| 选项 | 作用 |
|---|---|
MNN_BUILD_TEST=ON | 构建run_test.out测试主程序,GemmSpeed 系列用例通过 test/MNNTestSuite.h 中的MNNTestSuiteRegister宏静态注册进测试表 |
MNN_LOW_MEMORY=ON | 启用低内存模式,支持 Int4/Int8 权值量化推理;不开启则量化模式无法走压缩权值路径 |
MNN_OPENCL=ON/MNN_VULKAN=ON | 编译对应 GPU 后端,使参数 3(OpenCL)或 7(Vulkan)可用 |
MNN_GPU_TIME_PROFILE=ON | 开启 GPU Kernel 时间统计,CMakeLists.txt 中该选项默认 OFF,描述为 "Enable time profiling for the OpenCL backend and Vulkan backend" |
MNN_GPU_PROFILE_SILENT=ON | 仅累计总耗时、不逐个打印 Kernel 明细,避免 profile 开销污染测量,通过Executor::getLastGpuTimeMs()一次性取回 GPU 总耗时 |
测试用例与注册机制
GemmSpeed.cpp末尾用注册宏将 5 个用例挂入测试套件:
MNNTestSuiteRegister(GemmSpeedFloat, "speed/GemmSpeedFloat"); MNNTestSuiteRegister(GemmSpeedInt8, "speed/GemmSpeedInt8"); MNNTestSuiteRegister(GemmSpeedInt4, "speed/GemmSpeedInt4"); MNNTestSuiteRegister(GemmSpeedInt4A8, "speed/GemmSpeedInt4A8"); MNNTestSuiteRegister(GemmSpeedAll, "speed/GemmSpeedAll");| 测试名 | 说明 | 对应 mode |
|---|---|---|
speed/GemmSpeedFloat | 浮点 Conv1x1 GEMM | 0 |
speed/GemmSpeedInt8 | Int8-Block0 量化 Conv1x1 GEMM(blockSize=K,全通道量化) | 2 |
speed/GemmSpeedInt4 | Int4-Block64 量化 Conv1x1 GEMM | 1 |
speed/GemmSpeedInt4A8 | Int4-Block0 W4A8:逐通道 INT4 权重,在支持的 Adreno 设备上选择VulkanConv1x1CoopA8,prefill 路径将 W4 解包为 INT8、激活动态量化为 INT8,执行 S8×S8→S32 CoopMat GEMM | 3 |
speed/GemmSpeedAll | float / int8b0 / int4b64 三种模式综合测试(按此顺序逐组输出) | 0, 2, 1 |
GemmSpeedAll中 mode 顺序为{0, 2, 1},输出 tag 分别为float-gemm、int8b0-gemm、int4b64-gemm,与后文示例数据一一对应。
命令行参数详解
./run_test.out <测试名> [后端类型] [精度] [线程数/GPU选项]| 参数 | 说明 | 默认值 |
|---|---|---|
| 后端类型 | 0=CPU, 1=Metal, 2=CUDA, 3=OpenCL, 7=Vulkan(取值见 include/MNN/MNNForwardType.h 的MNNForwardType枚举) | 0 |
| 精度 | 0=Normal, 1=High, 2=Low(FP16,对应BackendConfig::Precision_Low) | 0 |
| 线程数/GPU选项 | CPU:线程数;OpenCL:位掩码组合 GPU 内存模式和 tuning 策略 | 1 |
OpenCL 第三参数:位掩码组合
OpenCL 后端的第三参数是位掩码,各位定义来自 include/MNN/MNNForwardType.h 的MNNGpuMode枚举:
| 位 | 值 | 说明 |
|---|---|---|
MNN_GPU_TUNING_NONE | 1 (1<<0) | 禁止 tuning |
MNN_GPU_TUNING_HEAVY | 2 (1<<1) | 重度 tuning(一般不建议) |
MNN_GPU_TUNING_WIDE | 4 (1<<2) | 宽范围 tuning,性能较好,默认策略 |
MNN_GPU_TUNING_NORMAL | 8 (1<<3) | 普通 tuning |
MNN_GPU_TUNING_FAST | 16 (1<<4) | 快速 tuning,性能可能打折 |
MNN_GPU_MEMORY_BUFFER | 64 (1<<6) | 使用 Buffer 内存模式(OpenCL 默认 Image) |
MNN_GPU_MEMORY_IMAGE | 128 (1<<7) | 使用 Image 内存模式 |
组合示例:68 = 64 + 4即 Buffer 模式 + WIDE tuning;若想要 Buffer 模式 + 快速 tuning,则应为64 + 16 = 80。这里需要说明:原文档中写作4 (MNN_GPU_TUNING_FAST),但按当前源码枚举,MNN_GPU_TUNING_FAST的取值为 16(1 << 4),而 4 实际对应默认的MNN_GPU_TUNING_WIDE;68 这一组合本身是合法且常用的(Buffer + 默认 tuning),但引用时应以头文件枚举定义为准。
测试程序自身也会解读该位:printBackendConfig()检测thread & MNN_GPU_MEMORY_BUFFER并在输出中打印OpenCL memory mode: BUFFER/IMAGE,即示例数据里numthread=68旁边那一行OpenCL memory mode: BUFFER。
需要留意的是,MNN_GPU_MEMORY_BUFFER/IMAGE仅对 OpenCL 后端有效;Vulkan 后端的 Image/Buffer 选择由 CMake 选项MNN_VULKAN_IMAGE决定,运行时位掩码对其无效。
常用示例
# CPU 后端,默认精度 ./run_test.out speed/GemmSpeedAll # CPU 后端,FP16 精度,4 线程 ./run_test.out speed/GemmSpeedAll 0 2 4 # OpenCL Buffer 模式 ./run_test.out speed/GemmSpeedAll 3 2 68 # Vulkan 后端,FP16 精度 ./run_test.out speed/GemmSpeedAll 7 2 # 仅测试 Int4 量化 ./run_test.out speed/GemmSpeedInt4 3 2 68 # 仅测试浮点 ./run_test.out speed/GemmSpeedFloat 7 2Android 设备上运行
# 交叉编译 (aarch64) mkdir build_android && cd build_android cmake .. -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \ -DANDROID_ABI=arm64-v8a -DANDROID_NATIVE_API_LEVEL=21 \ -DMNN_BUILD_TEST=ON -DMNN_LOW_MEMORY=ON \ -DMNN_OPENCL=ON -DMNN_VULKAN=ON \ -DMNN_GPU_TIME_PROFILE=ON -DMNN_GPU_PROFILE_SILENT=ON make -j$(nproc) run_test.out # 推送到设备 adb push run_test.out /data/local/tmp/ adb push libMNN.so /data/local/tmp/ # 运行测试 adb shell "cd /data/local/tmp && LD_LIBRARY_PATH=. ./run_test.out speed/GemmSpeedAll 3 2 68"测量流程:warmup、循环计时与 GPU 时间戳
benchConv1x1()的完整测量流程(见 test/speed/GemmSpeed.cpp):
- 创建执行器:
Executor::newExecutor((MNNForwardType)forwardType, bnConfig, thread),其中bnConfig.precision取命令行精度参数、bnConfig.memory = Memory_Low;ExecutorScope将其设为当前作用域执行器; - Warmup 3 轮:对输入
x写入全零数据并触发y->readMap<float>()前向,目的是稳定 GPU 时钟、指令缓存和 tuning 缓存,避免首轮编译/调优开销混入测量; - 正式测量 10 轮:每轮执行
x->writeMap<float>()+y->readMap<float>()(后者触发图执行),用MNN::Timer累计durationInUs(),取平均得到avgMs; - GFLOPS 口径:
flops = 2.0 × M × K × N,gflops = flops / (avgMs × 1e6),即一次 GEMM 的乘加各计一次浮点运算的等效吞吐; - GPU 时间戳:测量结束后调用
ExecutorScope::Current()->getLastGpuTimeMs()。该接口声明于 include/MNN/expr/Executor.hpp,注释明确"measured by GPU timestamps",且在不支持或未开启 profiling 时返回 -1.0f。源码中gpuMs > 0.0f才走双行输出格式,否则回退为纯 CPU 计时格式——这解释了为什么 CPU 后端输出只有avg,而 GPU 后端同时有total和gpu。
输出格式解读
CPU 后端输出
float-gemm M=32 K=2560 N=4096 avg=3.768 ms 178.11 GFLOPSavg是 10 次平均的墙钟耗时,GFLOPS 按2×M×K×N折算。
GPU 后端输出(total 与 gpu 双耗时)
int4b64-gemm M=32 K=2560 N=4096 total=1.633 ms (410.85 GFLOPS) gpu=0.745 ms (900.92 GFLOPS)- total:总运行耗时,包含 CPU↔GPU 数据传输、命令提交、同步等全部开销;
- gpu:GPU Kernel 纯计算耗时,通过 GPU 硬件时间戳测量,不含传输与同步开销。
两者之差就是该尺寸的"框架侧开销"。从示例数据看,Vulkan 小 M(decode 附近)时 total 与 gpu 差距明显(如 K=2560 N=1024、M=8 时 total 0.743 ms vs gpu 0.192 ms,约 3.9 倍),说明此时提交/同步开销占主导;而 M=512 的大 GEMM 下两者趋近(如 K=9728 N=2560 时 19.305 ms vs 4.280 ms 中 gpu 占比随 M 增大而上升的规律在大 shape 下计算占比更高),调优重点也随之从"降低提交开销"转向"kernel 本身吞吐"。
默认测试尺寸与 M 的含义
defaultConfigs()定义的五组 (K, N) 对应常见 LLM 的 hidden/dim 组合,defaultMValues()固定为{8, 32, 128, 512}:
| K | N | M 范围 |
|---|---|---|
| 2560 | 4096 | 8, 32, 128, 512 |
| 2560 | 1024 | 8, 32, 128, 512 |
| 4096 | 2560 | 8, 32, 128, 512 |
| 2560 | 9728 | 8, 32, 128, 512 |
| 9728 | 2560 | 8, 32, 128, 512 |
M 对应 prefill 的不同长度(GEMM 场景):M=8 接近单请求 decode 附近的窄 GEMM,M=512 则对应较长 prefill 或大 batch 的稠密 GEMM。ShapeConfig结构中的maxM字段(0 = 测全部 M,>0 = 仅测 M≤maxM)为超大尺寸预留了只测小 M 的能力,当前默认配置全部为 0。
示例数据(Snapdragon 8 Gen5)
以下为文档中记录在 Snapdragon 8 Gen5 设备上、4 线程、precision=2(FP16)测得的数据摘录。完整数据见 docs/perf/gemm_speed_benchmark.md。
CPU 后端(4 线程 FP16)
| 模式 | K×N | M=8 | M=32 | M=128 | M=512 |
|---|---|---|---|---|---|
| float-gemm | 2560×4096 | 1.029 ms / 163.04 GFLOPS | 4.026 ms / 166.71 | 12.399 ms / 216.51 | 21.120 ms / 508.41 |
| int8b0-gemm | 2560×4096 | 0.456 ms / 368.24 | 1.515 ms / 442.99 | 2.442 ms / 1099.38 | 13.529 ms / 793.65 |
| int4b64-gemm | 2560×4096 | 1.038 ms / 161.65 | 5.321 ms / 126.11 | 10.988 ms / 244.30 | 17.067 ms / 629.13 |
| int8b0-gemm | 4096×2560 | 0.111 ms / 1508.74 | 0.567 ms / 1184.00 | 2.709 ms / 990.98 | 8.531 ms / 1258.71 |
| float-gemm | 9728×2560 | 0.780 ms / 510.91 | 3.247 ms / 490.92 | 16.341 ms / 390.14 | 82.454 ms / 309.28 |
| int8b0-gemm | 9728×2560 | 0.416 ms / 957.14 | 1.881 ms / 847.20 | 5.266 ms / 1210.57 | 17.917 ms / 1423.30 |
几个可以直接读出的规律:
- Int8-Block0 在中大 M 下显著快于 float(如 K=4096 N=2560、M=512:8.531 ms vs 31.386 ms),量化收益在 CPU 上非常明显;
- CPU 上 Int4-Block64 反而慢于 float(如 K=2560 N=4096、M=32:5.321 ms vs 4.026 ms),说明该设备上 Int4 反量化路径的 CPU 收益有限,Int8 是 CPU 端更优的量化档位;
- 同一量化模式下 GFLOPS 随 M 增大先升后降(M=128 附近见顶),符合小 M 受 GEMV 带宽下限、大 M 受缓存/调度效率影响的直觉。
Vulkan 后端(FP16,开启 GPU 时间戳)
| 模式 | K×N | M=8 (total/gpu) | M=128 (total/gpu) | M=512 (total/gpu) |
|---|---|---|---|---|
| float-gemm | 2560×4096 | 0.893 / 0.318 ms | 1.870 / 0.499 ms | 7.234 / 1.999 ms |
| int8b0-gemm | 2560×4096 | 0.881 / 0.307 ms | 2.180 / 0.862 ms | 5.382 / 1.426 ms |
| int4b64-gemm | 2560×4096 | 1.315 / 0.730 ms | 2.293 / 0.909 ms | 7.697 / 2.478 ms |
| int8b0-gemm | 9728×2560 | 1.540 / 0.686 ms | 3.242 / 1.426 ms | 13.072 / 2.789 ms |
GPU 侧 gpu 耗时对应的峰值折算吞吐超过 9000 GFLOPS(K=9728 N=2560、M=512 的 int8b0),但注意这是硬件时间戳下的纯 kernel 折算值,受 FP16 路径影响;total 才是端到端可感知的时延。
OpenCL Buffer 模式(FP16,68 = Buffer + WIDE tuning)
| 模式 | K×N | M=8 (total/gpu) | M=128 (total/gpu) | M=512 (total/gpu) |
|---|---|---|---|---|
| float-gemm | 2560×4096 | 0.985 / 0.593 ms | 3.138 / 2.184 ms | 9.844 / 7.226 ms |
| int4b64-gemm | 2560×4096 | 0.551 / 0.180 ms | 1.848 / 1.272 ms | 7.339 / 4.737 ms |
| int8b0-gemm | 2560×4096 | 0.697 / 0.309 ms | 2.343 / 1.722 ms | 8.883 / 5.864 ms |
| int4b64-gemm | 2560×9728 | 0.827 / 0.432 ms | 4.089 / 2.894 ms | 16.696 / 10.786 ms |
与 CPU 结论相反,OpenCL 上Int4-Block64 明显快于 Int8-Block0(如 K=2560 N=4096、M=32:0.948 ms vs 1.054 ms),且 gpu 折算吞吐更高——说明 GPU 后端存在原生低比特 kernel 路径,量化档位的选择必须按后端分别评估,不能跨后端套用结论。
自定义测试尺寸
如需修改测试尺寸,编辑 test/speed/GemmSpeed.cpp 中的defaultConfigs():
static const std::vector<ShapeConfig>& defaultConfigs() { static std::vector<ShapeConfig> configs = { // {K, N, maxM, "label"} // maxM=0 表示测试所有 M 值,maxM>0 表示仅测试 M<=maxM {2560, 4096, 0, "K=2560 N=4096"}, {2560, 9728, 0, "K=2560 N=9728"}, // 添加更多尺寸... }; return configs; }结构体定义(见 test/speed/GemmSpeed.cpp):
struct ShapeConfig { int K; int N; int maxM; // 0 = all M values, >0 = only test M <= maxM const char* label; // annotation for output };补充两个实操约束:Int4-Block64 模式要求K % 64 == 0(GemmSpeedInt4与GemmSpeedAll中均有cfg.K % 64 != 0的跳过判断);Int8-Block0 与 W4A8 的构造函数断言ic % blockSize == 0,Block0 即 blockSize=K,天然满足。
总结
speed/GemmSpeed*系列用例把"选量化档位、选后端、选内存模式"这三件端侧 LLM 调优中最常见的问题收敛到了一条命令上:
- 用 Conv1x1 走推理引擎真实路径测 GEMM,量化配置与线上
mnnconvert导出格式一致; - 3 次 warmup + 10 次平均消除时钟与缓存抖动,GPU 后端再用硬件时间戳区分 kernel 纯计算与框架开销;
- GFLOPS 统一按
2×M×K×N折算,跨后端可直接横向比较; - M=8/32/128/512 覆盖 decode 附近到长 prefill 的典型区间。
结合同目录下的 gemv_bw_benchmark.md(GEMV 带宽视角)与 arm_low_bit_gemm.md(ARM 低比特 GEMM 原理),可以完整覆盖 LLM 推理中 GEMV 与 GEMM 两类算子的性能评估。
【免费下载链接】MNNMNN: A blazing-fast, lightweight inference engine battle-tested by Alibaba, powering high-performance on-device LLMs and Edge AI.项目地址: https://gitcode.com/GitHub_Trending/mn/MNN
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考