
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/MNNMNN 通过test/speed/GemmSpeed.cpp提供了一组通用的 GEMM 性能基准测试用于在 LLM 推理典型矩阵尺寸下评估 CPU / OpenCL / Vulkan / CUDA / Metal 等后端的计算吞吐量。本篇将完整继承并展开该基准测试的使用方法编译开关低内存量化、GPU 时间戳剖析、命令行参数与 OpenCL 位掩码组合、测试用例注册机制、输出格式与 GFLOPS 计算口径并结合 test/speed/GemmSpeed.cpp 源码剖析其以 Conv1x1 模拟 GEMM 的实现原理、warmup 与测量流程、GPU 硬件时间戳获取链路以及示例数据的解读方法。为什么用 Conv1x1 来测 GEMMLLM 的线性层QKV 投影、FFN 等本质上是C A[M, K] × B[K, N]的矩阵乘。MNN 没有单独实现一套 GEMM 基准接口而是直接走推理引擎真实路径用1×1 卷积表达 GEMM。从源码看浮点模式通过buildFloatConv1x1()构造输入张量形状为{1, K, 1, M}即 batch1、channelK、height1、widthM数据布局为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-Block0mode 2nbit8, blockSizeK即按 K 方向整块量化全通道对应文档中的Int8-Block0 量化blockSizeK全通道量化Int4-Block64mode 1nbit4, blockSize64每 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_TESTON -DMNN_LOW_MEMORYON # 开启 GPU 后端 性能 Profile cmake .. -DMNN_BUILD_TESTON -DMNN_LOW_MEMORYON \ -DMNN_OPENCLON -DMNN_VULKANON \ -DMNN_GPU_TIME_PROFILEON -DMNN_GPU_PROFILE_SILENTON make -j$(nproc) run_test.out关键编译宏说明选项作用MNN_BUILD_TESTON构建run_test.out测试主程序GemmSpeed 系列用例通过 test/MNNTestSuite.h 中的MNNTestSuiteRegister宏静态注册进测试表MNN_LOW_MEMORYON启用低内存模式支持 Int4/Int8 权值量化推理不开启则量化模式无法走压缩权值路径MNN_OPENCLON/MNN_VULKANON编译对应 GPU 后端使参数 3OpenCL或 7Vulkan可用MNN_GPU_TIME_PROFILEON开启 GPU Kernel 时间统计CMakeLists.txt 中该选项默认 OFF描述为 Enable time profiling for the OpenCL backend and Vulkan backendMNN_GPU_PROFILE_SILENTON仅累计总耗时、不逐个打印 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);测试名说明对应 modespeed/GemmSpeedFloat浮点 Conv1x1 GEMM0speed/GemmSpeedInt8Int8-Block0 量化 Conv1x1 GEMMblockSizeK全通道量化2speed/GemmSpeedInt4Int4-Block64 量化 Conv1x1 GEMM1speed/GemmSpeedInt4A8Int4-Block0 W4A8逐通道 INT4 权重在支持的 Adreno 设备上选择VulkanConv1x1CoopA8prefill 路径将 W4 解包为 INT8、激活动态量化为 INT8执行 S8×S8→S32 CoopMat GEMM3speed/GemmSpeedAllfloat / int8b0 / int4b64 三种模式综合测试按此顺序逐组输出0, 2, 1GemmSpeedAll中 mode 顺序为{0, 2, 1}输出 tag 分别为float-gemm、int8b0-gemm、int4b64-gemm与后文示例数据一一对应。命令行参数详解./run_test.out 测试名 [后端类型] [精度] [线程数/GPU选项]参数说明默认值后端类型0CPU, 1Metal, 2CUDA, 3OpenCL, 7Vulkan取值见 include/MNN/MNNForwardType.h 的MNNForwardType枚举0精度0Normal, 1High, 2LowFP16对应BackendConfig::Precision_Low0线程数/GPU选项CPU线程数OpenCL位掩码组合 GPU 内存模式和 tuning 策略1OpenCL 第三参数位掩码组合OpenCL 后端的第三参数是位掩码各位定义来自 include/MNN/MNNForwardType.h 的MNNGpuMode枚举位值说明MNN_GPU_TUNING_NONE1 (10)禁止 tuningMNN_GPU_TUNING_HEAVY2 (11)重度 tuning一般不建议MNN_GPU_TUNING_WIDE4 (12)宽范围 tuning性能较好默认策略MNN_GPU_TUNING_NORMAL8 (13)普通 tuningMNN_GPU_TUNING_FAST16 (14)快速 tuning性能可能打折MNN_GPU_MEMORY_BUFFER64 (16)使用 Buffer 内存模式OpenCL 默认 ImageMNN_GPU_MEMORY_IMAGE128 (17)使用 Image 内存模式组合示例68 64 4即 Buffer 模式 WIDE tuning若想要 Buffer 模式 快速 tuning则应为64 16 80。这里需要说明原文档中写作4 (MNN_GPU_TUNING_FAST)但按当前源码枚举MNN_GPU_TUNING_FAST的取值为 161 4而 4 实际对应默认的MNN_GPU_TUNING_WIDE68 这一组合本身是合法且常用的Buffer 默认 tuning但引用时应以头文件枚举定义为准。测试程序自身也会解读该位printBackendConfig()检测thread MNN_GPU_MEMORY_BUFFER并在输出中打印OpenCL memory mode: BUFFER/IMAGE即示例数据里numthread68旁边那一行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_ABIarm64-v8a -DANDROID_NATIVE_API_LEVEL21 \ -DMNN_BUILD_TESTON -DMNN_LOW_MEMORYON \ -DMNN_OPENCLON -DMNN_VULKANON \ -DMNN_GPU_TIME_PROFILEON -DMNN_GPU_PROFILE_SILENTON 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_LowExecutorScope将其设为当前作用域执行器Warmup 3 轮对输入x写入全零数据并触发y-readMapfloat()前向目的是稳定 GPU 时钟、指令缓存和 tuning 缓存避免首轮编译/调优开销混入测量正式测量 10 轮每轮执行x-writeMapfloat()y-readMapfloat()后者触发图执行用MNN::Timer累计durationInUs()取平均得到avgMsGFLOPS 口径flops 2.0 × M × K × Ngflops 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 M32 K2560 N4096 avg3.768 ms 178.11 GFLOPSavg是 10 次平均的墙钟耗时GFLOPS 按2×M×K×N折算。GPU 后端输出total 与 gpu 双耗时int4b64-gemm M32 K2560 N4096 total1.633 ms (410.85 GFLOPS) gpu0.745 ms (900.92 GFLOPS)total总运行耗时包含 CPU↔GPU 数据传输、命令提交、同步等全部开销gpuGPU Kernel 纯计算耗时通过 GPU 硬件时间戳测量不含传输与同步开销。两者之差就是该尺寸的框架侧开销。从示例数据看Vulkan 小 Mdecode 附近时 total 与 gpu 差距明显如 K2560 N1024、M8 时 total 0.743 ms vs gpu 0.192 ms约 3.9 倍说明此时提交/同步开销占主导而 M512 的大 GEMM 下两者趋近如 K9728 N2560 时 19.305 ms vs 4.280 ms 中 gpu 占比随 M 增大而上升的规律在大 shape 下计算占比更高调优重点也随之从降低提交开销转向kernel 本身吞吐。默认测试尺寸与 M 的含义defaultConfigs()定义的五组 (K, N) 对应常见 LLM 的 hidden/dim 组合defaultMValues()固定为{8, 32, 128, 512}KNM 范围256040968, 32, 128, 512256010248, 32, 128, 512409625608, 32, 128, 512256097288, 32, 128, 512972825608, 32, 128, 512M 对应 prefill 的不同长度GEMM 场景M8 接近单请求 decode 附近的窄 GEMMM512 则对应较长 prefill 或大 batch 的稠密 GEMM。ShapeConfig结构中的maxM字段0 测全部 M0 仅测 M≤maxM为超大尺寸预留了只测小 M 的能力当前默认配置全部为 0。示例数据Snapdragon 8 Gen5以下为文档中记录在 Snapdragon 8 Gen5 设备上、4 线程、precision2FP16测得的数据摘录。完整数据见 docs/perf/gemm_speed_benchmark.md。CPU 后端4 线程 FP16模式K×NM8M32M128M512float-gemm2560×40961.029 ms / 163.04 GFLOPS4.026 ms / 166.7112.399 ms / 216.5121.120 ms / 508.41int8b0-gemm2560×40960.456 ms / 368.241.515 ms / 442.992.442 ms / 1099.3813.529 ms / 793.65int4b64-gemm2560×40961.038 ms / 161.655.321 ms / 126.1110.988 ms / 244.3017.067 ms / 629.13int8b0-gemm4096×25600.111 ms / 1508.740.567 ms / 1184.002.709 ms / 990.988.531 ms / 1258.71float-gemm9728×25600.780 ms / 510.913.247 ms / 490.9216.341 ms / 390.1482.454 ms / 309.28int8b0-gemm9728×25600.416 ms / 957.141.881 ms / 847.205.266 ms / 1210.5717.917 ms / 1423.30几个可以直接读出的规律Int8-Block0 在中大 M 下显著快于 float如 K4096 N2560、M5128.531 ms vs 31.386 ms量化收益在 CPU 上非常明显CPU 上 Int4-Block64 反而慢于 float如 K2560 N4096、M325.321 ms vs 4.026 ms说明该设备上 Int4 反量化路径的 CPU 收益有限Int8 是 CPU 端更优的量化档位同一量化模式下 GFLOPS 随 M 增大先升后降M128 附近见顶符合小 M 受 GEMV 带宽下限、大 M 受缓存/调度效率影响的直觉。Vulkan 后端FP16开启 GPU 时间戳模式K×NM8 (total/gpu)M128 (total/gpu)M512 (total/gpu)float-gemm2560×40960.893 / 0.318 ms1.870 / 0.499 ms7.234 / 1.999 msint8b0-gemm2560×40960.881 / 0.307 ms2.180 / 0.862 ms5.382 / 1.426 msint4b64-gemm2560×40961.315 / 0.730 ms2.293 / 0.909 ms7.697 / 2.478 msint8b0-gemm9728×25601.540 / 0.686 ms3.242 / 1.426 ms13.072 / 2.789 msGPU 侧 gpu 耗时对应的峰值折算吞吐超过 9000 GFLOPSK9728 N2560、M512 的 int8b0但注意这是硬件时间戳下的纯 kernel 折算值受 FP16 路径影响total 才是端到端可感知的时延。OpenCL Buffer 模式FP1668 Buffer WIDE tuning模式K×NM8 (total/gpu)M128 (total/gpu)M512 (total/gpu)float-gemm2560×40960.985 / 0.593 ms3.138 / 2.184 ms9.844 / 7.226 msint4b64-gemm2560×40960.551 / 0.180 ms1.848 / 1.272 ms7.339 / 4.737 msint8b0-gemm2560×40960.697 / 0.309 ms2.343 / 1.722 ms8.883 / 5.864 msint4b64-gemm2560×97280.827 / 0.432 ms4.089 / 2.894 ms16.696 / 10.786 ms与 CPU 结论相反OpenCL 上Int4-Block64 明显快于 Int8-Block0如 K2560 N4096、M320.948 ms vs 1.054 ms且 gpu 折算吞吐更高——说明 GPU 后端存在原生低比特 kernel 路径量化档位的选择必须按后端分别评估不能跨后端套用结论。自定义测试尺寸如需修改测试尺寸编辑 test/speed/GemmSpeed.cpp 中的defaultConfigs()static const std::vectorShapeConfig defaultConfigs() { static std::vectorShapeConfig configs { // {K, N, maxM, label} // maxM0 表示测试所有 M 值maxM0 表示仅测试 MmaxM {2560, 4096, 0, K2560 N4096}, {2560, 9728, 0, K2560 N9728}, // 添加更多尺寸... }; return configs; }结构体定义见 test/speed/GemmSpeed.cppstruct 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 0GemmSpeedInt4与GemmSpeedAll中均有cfg.K % 64 ! 0的跳过判断Int8-Block0 与 W4A8 的构造函数断言ic % blockSize 0Block0 即 blockSizeK天然满足。总结speed/GemmSpeed*系列用例把选量化档位、选后端、选内存模式这三件端侧 LLM 调优中最常见的问题收敛到了一条命令上用 Conv1x1 走推理引擎真实路径测 GEMM量化配置与线上mnnconvert导出格式一致3 次 warmup 10 次平均消除时钟与缓存抖动GPU 后端再用硬件时间戳区分 kernel 纯计算与框架开销GFLOPS 统一按2×M×K×N折算跨后端可直接横向比较M8/32/128/512 覆盖 decode 附近到长 prefill 的典型区间。结合同目录下的 gemv_bw_benchmark.mdGEMV 带宽视角与 arm_low_bit_gemm.mdARM 低比特 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),仅供参考