news 2026/9/14 7:15:57

MNN GemmSpeed 基准测试实战:多后端 LLM GEMM 吞吐评估与 GPU 时间戳剖析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MNN GemmSpeed 基准测试实战:多后端 LLM GEMM 吞吐评估与 GPU 时间戳剖析

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 GEMM0
speed/GemmSpeedInt8Int8-Block0 量化 Conv1x1 GEMM(blockSize=K,全通道量化)2
speed/GemmSpeedInt4Int4-Block64 量化 Conv1x1 GEMM1
speed/GemmSpeedInt4A8Int4-Block0 W4A8:逐通道 INT4 权重,在支持的 Adreno 设备上选择VulkanConv1x1CoopA8,prefill 路径将 W4 解包为 INT8、激活动态量化为 INT8,执行 S8×S8→S32 CoopMat GEMM3
speed/GemmSpeedAllfloat / int8b0 / int4b64 三种模式综合测试(按此顺序逐组输出)0, 2, 1

GemmSpeedAll中 mode 顺序为{0, 2, 1},输出 tag 分别为float-gemmint8b0-gemmint4b64-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_Low0
线程数/GPU选项CPU:线程数;OpenCL:位掩码组合 GPU 内存模式和 tuning 策略1

OpenCL 第三参数:位掩码组合

OpenCL 后端的第三参数是位掩码,各位定义来自 include/MNN/MNNForwardType.h 的MNNGpuMode枚举:

说明
MNN_GPU_TUNING_NONE1 (1<<0)禁止 tuning
MNN_GPU_TUNING_HEAVY2 (1<<1)重度 tuning(一般不建议)
MNN_GPU_TUNING_WIDE4 (1<<2)宽范围 tuning,性能较好,默认策略
MNN_GPU_TUNING_NORMAL8 (1<<3)普通 tuning
MNN_GPU_TUNING_FAST16 (1<<4)快速 tuning,性能可能打折
MNN_GPU_MEMORY_BUFFER64 (1<<6)使用 Buffer 内存模式(OpenCL 默认 Image)
MNN_GPU_MEMORY_IMAGE128 (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 2

Android 设备上运行

# 交叉编译 (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):

  1. 创建执行器Executor::newExecutor((MNNForwardType)forwardType, bnConfig, thread),其中bnConfig.precision取命令行精度参数、bnConfig.memory = Memory_LowExecutorScope将其设为当前作用域执行器;
  2. Warmup 3 轮:对输入x写入全零数据并触发y->readMap<float>()前向,目的是稳定 GPU 时钟、指令缓存和 tuning 缓存,避免首轮编译/调优开销混入测量;
  3. 正式测量 10 轮:每轮执行x->writeMap<float>()+y->readMap<float>()(后者触发图执行),用MNN::Timer累计durationInUs(),取平均得到avgMs
  4. GFLOPS 口径flops = 2.0 × M × K × Ngflops = flops / (avgMs × 1e6),即一次 GEMM 的乘加各计一次浮点运算的等效吞吐;
  5. GPU 时间戳:测量结束后调用ExecutorScope::Current()->getLastGpuTimeMs()。该接口声明于 include/MNN/expr/Executor.hpp,注释明确"measured by GPU timestamps",且在不支持或未开启 profiling 时返回 -1.0f。源码中gpuMs > 0.0f才走双行输出格式,否则回退为纯 CPU 计时格式——这解释了为什么 CPU 后端输出只有avg,而 GPU 后端同时有totalgpu

输出格式解读

CPU 后端输出

float-gemm M=32 K=2560 N=4096 avg=3.768 ms 178.11 GFLOPS

avg是 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}

KNM 范围
256040968, 32, 128, 512
256010248, 32, 128, 512
409625608, 32, 128, 512
256097288, 32, 128, 512
972825608, 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×NM=8M=32M=128M=512
float-gemm2560×40961.029 ms / 163.04 GFLOPS4.026 ms / 166.7112.399 ms / 216.5121.120 ms / 508.41
int8b0-gemm2560×40960.456 ms / 368.241.515 ms / 442.992.442 ms / 1099.3813.529 ms / 793.65
int4b64-gemm2560×40961.038 ms / 161.655.321 ms / 126.1110.988 ms / 244.3017.067 ms / 629.13
int8b0-gemm4096×25600.111 ms / 1508.740.567 ms / 1184.002.709 ms / 990.988.531 ms / 1258.71
float-gemm9728×25600.780 ms / 510.913.247 ms / 490.9216.341 ms / 390.1482.454 ms / 309.28
int8b0-gemm9728×25600.416 ms / 957.141.881 ms / 847.205.266 ms / 1210.5717.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×NM=8 (total/gpu)M=128 (total/gpu)M=512 (total/gpu)
float-gemm2560×40960.893 / 0.318 ms1.870 / 0.499 ms7.234 / 1.999 ms
int8b0-gemm2560×40960.881 / 0.307 ms2.180 / 0.862 ms5.382 / 1.426 ms
int4b64-gemm2560×40961.315 / 0.730 ms2.293 / 0.909 ms7.697 / 2.478 ms
int8b0-gemm9728×25601.540 / 0.686 ms3.242 / 1.426 ms13.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×NM=8 (total/gpu)M=128 (total/gpu)M=512 (total/gpu)
float-gemm2560×40960.985 / 0.593 ms3.138 / 2.184 ms9.844 / 7.226 ms
int4b64-gemm2560×40960.551 / 0.180 ms1.848 / 1.272 ms7.339 / 4.737 ms
int8b0-gemm2560×40960.697 / 0.309 ms2.343 / 1.722 ms8.883 / 5.864 ms
int4b64-gemm2560×97280.827 / 0.432 ms4.089 / 2.894 ms16.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 == 0GemmSpeedInt4GemmSpeedAll中均有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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 7:15:52

长上下文 LLM 如何复用 KV 缓存:LMCache 缓存引擎源码拆解

长上下文 LLM 如何复用 KV 缓存&#xff1a;LMCache 缓存引擎源码拆解 【免费下载链接】LMCache LMCache: Supercharge Your LLM with the Fastest KV Cache Layer 项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache 把一份两万个 token 的 RAG 文档丢给 vLLM&…

作者头像 李华
网站建设 2026/9/14 7:14:16

M12屏蔽连接器选型安装与故障排查:保障工业以太网信号完整性

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 7:12:05

Qwen Code 接入微信:基于 iLink Bot API 的 WeChat 频道完整配置指南

Qwen Code 接入微信&#xff1a;基于 iLink Bot API 的 WeChat 频道完整配置指南 【免费下载链接】qwen-code An open-source AI coding agent that lives in your terminal. 项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code 微信是目前最主流的即时通讯平…

作者头像 李华
网站建设 2026/9/14 7:11:58

deer-flow:一种面向内存边界的沙盒设计思维

1. “deer-flow”不是框架&#xff0c;是内存沙盒的命名隐喻最近在几个技术社区和开源项目讨论区里&#xff0c;频繁看到“deer-flow”这个词——它既不像主流前端框架&#xff08;React/Vue/Svelte&#xff09;那样有官网文档&#xff0c;也不像Node.js或Python那样自带安装器…

作者头像 李华