MNN GemvBW:面向 LLM Decode 阶段的 GEMV 带宽基准测试实战
【免费下载链接】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/GemvBWTest.cpp是一个专门针对 LLMdecode 阶段(batch=1)的 GEMV 带宽 microbenchmark,用于衡量不同权重量化位宽(w8 / w4 / w3 / w2)下 kernel 的有效带宽、相对 memcpy 峰值带宽的饱和度(%peak)与算术强度(AI)。读完本文,你将掌握该测试的编译方式、命令行与环境变量用法、测量方法论(冷缓存、best-of-N、W bytes 口径),以及如何在 Android 大核 / Apple Silicon 等真实设备上复现并解读饱和度数据,从而评估 MNN 低 bit GEMV kernel 的优化空间。
测试背景:为什么 decode 阶段要看 GEMV 带宽
LLM 推理分 prefill 与 decode 两个阶段。Prefill 阶段 batch 维度 M 较大,计算是 FLOP bound;而 decode 阶段每次只生成一个 token,M=1,矩阵乘法退化为 GEMV(y = W·x,W 为 M×K,x 为 K 维向量)。此时每生成一个 token 都要把整层权重从 DRAM 完整读一遍,性能瓶颈是内存带宽而非算力——kernel 能跑多快,取决于它把权重从内存拉取的速率有多接近硬件理论峰值。
GemvBW 正是为此设计的 microbenchmark:固定一个 (M, K) 形状,扫不同量化位宽,输出每种量化下的有效带宽(effective GB/s)、相对峰值 memcpy 带宽的饱和度(%peak)及算术强度(AI)。它对标外部参照实现的 GEMV roofline 基准,用于回答一个核心问题:当前 bit 宽的 dequant 开销在多大程度上拖慢了本应被访存延迟完全隐藏的 GEMV?
测试实现:源码结构与关键常量
测试入口是 GemvBWTest.cpp,通过MNNTestSuiteRegister(GemvBWTest, "speed/GemvBW")注册为speed/GemvBW测试项,因此通过统一的 run_test.out 驱动。源码中的关键设计包括:
- 用 1x1 混合量化卷积模拟 GEMV:
benchGemv()通过_HybridConv构造一个 batch=1、oc=M、ic=K 的 1x1 卷积(GemvBWTest.cpp#L84-L129),输入x为{1, K, 1, 1}的 NCHW fp32 tensor,经_Convert(x, NC4HW4)后送入。量化参数nbit直接传入_HybridConv,wScale 为每个输出通道 × block 的 alpha + zp。 - 冷缓存测量:每次迭代前对一块 64 MiB 的
flushBuf做带 stride 的累加读取(GemvBWTest.cpp#L136-L144),强制把权重从 DRAM 重新拉回,避免 L2/L3 命中导致的虚高读数。 - best-of-3 × 200 iters:外层 3 次重复(
outerReps = 3),每次内层对 200 次 cold-cache 迭代取平均,最后取三组中的最优值(GemvBWTest.cpp#L146-L160)。 - memcpy roofline:
measurePeakBwGBs()用std::thread多线程 memcpy 一块 256 MiB 缓冲区、重复 5 次取最优,得到当前线程数下的峰值流式带宽,作为 %peak 的分母(GemvBWTest.cpp#L42-L70)。注意 memcpy 同时计 read+write 流量(2.0 * bytes),其数值与 GEMV 的只读带宽不可直接混同解读。 - W bytes 口径:
weightBytes = oc * ic * nbit / 8 + oc * blockNum * 2.0 * 2.0,即仅计算打包权重 + per-block(alpha + zp,按 fp16 计)元数据,不算输入向量与输出,便于跨实现对比(GemvBWTest.cpp#L168-L176)。 - 可覆盖的默认形状:默认
M = 4096, K = 14336(对应 Llama-3-8B FFN 单层一个投影),blocksize = 64,iters = 200,threads 缺省时取 4(GemvBWTest.cpp#L185-L205)。
编译
mkdir build && cd build cmake .. -DMNN_BUILD_TEST=ON -DMNN_LOW_MEMORY=ON \ -DMNN_BUILD_LLM=ON -DMNN_SUPPORT_TRANSFORMER_FUSE=ON # Metal 测试请加 -DMNN_METAL=ON make -j$(nproc) run_test.out以上选项均在根目录 CMakeLists.txt 中定义:MNN_BUILD_TEST(构建测试)、MNN_LOW_MEMORY(低内存权重量化支持)、MNN_BUILD_LLM、MNN_SUPPORT_TRANSFORMER_FUSE(transformer 融合算子)、MNN_METAL。测试目标run_test.out由 test/CMakeLists.txt 中的add_executable(run_test.out ${Files})构建。
MNN_LOW_MEMORY=ON必开,否则 hybrid 量化路径(即_HybridConv所依赖的低 bit 反量化卷积)不可用,测试无法覆盖 w8 以下位宽。
使用方法与参数说明
./run_test.out speed/GemvBW [forwardType] [precision] [threads]| 位置 | 含义 | 取值 |
|---|---|---|
| 1 | forwardType | 0=CPU, 1=Metal, 3=OpenCL(按 MNNForwardType.h 编号:MNN_FORWARD_CPU = 0、MNN_FORWARD_METAL = 1、MNN_FORWARD_OPENCL = 3) |
| 2 | precision | 0=Normal, 1=High, 2=Low(推荐 2,对应 fp16 累加) |
| 3 | threads | 省略时测试内部按 4 处理;GPU 后端忽略 |
⚠️ 参数顺序是backend 在 precision 之前(见 test/main.cpp 中
argv[2]解析为 forwardType、argv[3]解析为 precision、argv[4]解析为 thread)。传错不会立即失败:越界的 precision 会被打印错误并重置为 Normal(precision = 0),线程数则保持默认,于是本意是"fp16 多线程 sweep"的调用实际跑成了 fp32 固定线程数。校验方法:看输出thr列是否等于你指定的线程数。
说明:早期文档版本中曾把 forwardType 表记为 "3=Metal",与当前源码的
MNNForwardType枚举不符;以仓库当前 include/MNN/MNNForwardType.h 为准,Metal 是 1。
形状覆盖:环境变量免重编
GEMV 效率是强 shape 相关的。默认 M=4096/K=14336 是大形状,而模型内每层还有大量小投影,用默认大形状测出的 %peak 不能代表模型内的实际效率,评估 decode 时务必按真实形状复测。测试支持环境变量覆盖,无需重编(GemvBWTest.cpp#L191-L196):
# Qwen3-0.6B 的 gate/up 投影 MNN_GEMVBW_M=3072 MNN_GEMVBW_K=1024 ./run_test.out speed/GemvBW 0 2 6 # Qwen3-0.6B 的 lm_head MNN_GEMVBW_M=151936 MNN_GEMVBW_K=1024 ./run_test.out speed/GemvBW 0 2 6源码中还额外支持MNN_GEMVBW_BITS环境变量,只跑单个位宽(GemvBWTest.cpp#L224-L226),例如:
MNN_GEMVBW_BITS=4 MNN_GEMVBW_M=151936 MNN_GEMVBW_K=1024 ./run_test.out speed/GemvBW 0 2 6实测中,M4 上 w4 在 lm_head(151936×1024)能到 82% roofline,而每层的小投影(1024×1024)只有 16%——这正是必须按真实形状复测的原因。
输出格式解读
## GemvBW (backend=CPU, precision=2, blocksize=64) ## Peak streaming bandwidth (memcpy, 256 MiB buffer) threads | GB/s -------:|-----: 4 | 109.7 -> peak 109.7 GB/s @ 4 threads (used as roofline) ## GEMV: y = W(4096x14336) * x(14336), block=64 type | thr | us/iter | W MiB | bytes/elem | eff GB/s | %peak | GFLOPS | AI (op/B) -----|----:|----------:|-------:|-----------:|---------:|------:|--------:|----------: w8 | 4 | ... | ... | ... | 109.4 | 99.7 | ... | ... w4 | 4 | ... | ... | ... | 100.7 | 91.8 | ... | ... w3 | 4 | ... | ... | ... | 50.2 | 45.8 | ... | ... w2 | 4 | ... | ... | ... | 64.5 | 58.8 | ... | ...各列含义:
- eff GB/s:
weight_bytes / latency,即每次 decode 实际拉取的权重字节速率。LLM decode 是带宽 bound,这个值越接近 peak 越好。 - %peak:相对 memcpy 峰值的饱和度。kernel 写得好的目标是 80%+;w3/w2 因 dequant ALU 开销大,目前还有较多优化空间。
- AI (op/B):算术强度 = 2 / bytes_per_elem。w8≈0.25,w4≈0.5,w2≈1.0,bit 越低越远离 mem-bound 区。
- GFLOPS:仅作参考,decode 阶段不是 FLOP bound。
实测数据
OnePlus PJZ110(Snapdragon 8 Elite,Oryon,6 mid + 2 big),Android arm64-v8a,precision=Low(fp16)
i8mm + asimddp + fp16 全开。M=4096,K=14336,blocksize=64。
必须用
taskset c0绑双大核 + threads=2 跑,否则 MNN 默认线程池会随机调度到 mid 核,把 GEMV 拖到大核数据的 1/2~1/3。下表是taskset c0 ./run_test.out speed/GemvBW 0 2 2的结果。
| type | threads | us/iter | eff GB/s | GFLOPS | bytes/elem |
|---|---|---|---|---|---|
| w8 | 2 | 1015.5 | 61.4 | 115.7 | 1.0625 |
| w4 | 2 | 782.7 | 42.2 | 150.0 | 0.5625 |
| w3 | 2 | 1367.9 | 18.8 | 85.9 | 0.4375 |
| w2 | 2 | 697.2 | 26.3 | 168.4 | 0.3125 |
memcpy roofline(std::thread,2 big cores):38.8 GB/s。SD8 Elite LPDDR5X-8533 理论峰值 ~68 GB/s/channel,工程估计 single-direction 真实可用 ~55-65 GB/s。
饱和度估计(按真实 DRAM read peak ≈ 60 GB/s 估):w8 ≈102%(实测已撞 LPDDR5X 单方向上限),w4 ≈ 70%,w3 ≈ 31%,w2 ≈ 44%。
注:测试内置的 memcpy 38.8 GB/s 不是 DRAM peak。memcpy 本身计 2× 字节(read+write),且
std::thread在 Android 上不能像 macOS scheduler 一样自动均衡线程,1→8 线程几乎不 scale。GEMV 是 read-only weight,所以 eff GB/s 可以高于 memcpy 数字。
观察:
- w8 已撞墙:61.4 GB/s 已经达到 LPDDR5X 单方向理论上限,说明 i8mm + 寄存器化 accum 链把 dequant 完全隐藏在 mem latency 下。继续优化只能往压缩(w4)走。
- w3 仍是最弱项:18.8 GB/s,饱和度 ~31%,远低于 w2/w4。文档指出这与 M-Mac/SD8G3 上 P3 后的趋势一致——8 Elite 的 sdot/i8mm 调度还有空间(文档引用了内部记录
w2w3_optimization_lessons.md,该文件属团队内部资料,未包含在仓库中)。 - w2 latency 反而比 w4 短:697 vs 782 us,不再是 SD8G3 上"w2≈w4"的现象——8 Elite Oryon 大核的 ALU 吞吐让 w2 dequant 不再卡瓶颈。
- 运行姿势:Android 上跑这个测试必须
taskset c0绑大核,否则数据会被 mid 核噪声污染。
Apple M3 Pro(5P + 6E,36GB LPDDR5-6400),macOS arm64,precision=Low(fp16)
i8mm + asimddp + fp16 全开,36GB 统一内存。M=4096,K=14336,blocksize=64,threads=4。macOS 调度器自动把std::thread分配到 P 核,无需 taskset。
CPU:
| type | threads | us/iter | eff GB/s | %peak | GFLOPS |
|---|---|---|---|---|---|
| w8 | 4 | 605.2 | 103.1 | 87.4 | 194.0 |
| w4 | 4 | 334.0 | 98.9 | 83.9 | 351.6 |
| w3 | 4 | 555.8 | 46.2 | 39.2 | 211.3 |
| w2 | 4 | 294.0 | 62.4 | 52.9 | 399.5 |
memcpy roofline:117.9 GB/s @ 4 threads。
Metal(文档标注仅支持 w8 / w4):
| type | threads | us/iter | eff GB/s | %peak | GFLOPS |
|---|---|---|---|---|---|
| w8 | 4 | 700.4 | 89.1 | 71.3 | 167.7 |
| w4 | 4 | 464.3 | 71.1 | 57.0 | 252.9 |
memcpy roofline:124.9 GB/s。GPU 下 flushCache 失效,eff GB/s 更接近 warm-cache 估计。
观察:
- CPU w8/w4 接近 roofline(87% / 84%),dequant 流水已隐藏在 mem latency 下。
- CPU w3 仍是短板(39%),与 Android SD8 Elite 上的相对位置一致——i8mm 调度还有空间。
- CPU w2 比 w3 快 2.4×(62.4 vs 46.2 GB/s),因为 w2 用 4-IDX unpack 路径无 ext 链。
- Metal w8 vs CPU w8:CPU 反超 Metal(103 vs 89 GB/s)。M3 Pro 上 Metal GEMV 还有 ~15% 的优化空间,主要是 dequant kernel 没充分用 simd-group 寄存器。
- 跨设备对比(w8 eff GB/s):M3 Pro CPU 103 / SD8 Elite(taskset c0,2t)61 / iPad M5 Metal ~114(文档引用了内部记录
ipad_m5_bench.md,该文件同属团队内部资料,未包含在仓库中)。
Metal 后端支持位宽的源码佐证
文档表述 Metal 仅跑 w8 / w4,受 MetalConvolution1x1.mm 中mDequantBits == 4 || mDequantBits == 8分支限制。不过从当前源码结构看,该文件中已存在处理 2/3 bit 的分支(如mDequantBits == 2 || mDequantBits == 3 || mDequantBits == 4 || mDequantBits == 8的判断),且 GemvBWTest.cpp 的注释也写明 Metal 通过 2sg kernel(conv1x1_gemv_g4m1_2sg_wquant_sg)支持 w8 / w4 / w3 / w2 的 decode GEMV。因此 Metal 侧实际可跑的位宽以当前版本源码为准,复测时若 w3/w2 输出异常可先用MNN_GEMVBW_BITS单点定位。
注意事项
flushCache()仅刷 CPU L2/L3。在 Metal 等 GPU/统一内存后端上,权重可能仍驻留在 GPU 缓存里,因此 GPU 下 eff GB/s 更接近 warm-cache 估计。- 测量
WriteMap → readMap总耗时,包含一次 fp16/fp32 输入打包,对小 K 略有误差;K=14336 时影响 < 1%。 - iPad / iOS 真机上设备无 shell,需通过辅助 benchmark 服务脚本间接触发测试(原文档提到的
ios_llm_benchmark_server.py未包含在当前仓库中,相关经验记录同样为团队内部资料)。 - Android 设备务必绑定大核(
taskset c0)并相应调低 threads,避免大小核混跑污染数据。
相关文档
- arm_low_bit_gemm.md — ARM CPU 低 bit GEMM kernel 数据排布与汇编实现
- gemm_speed_benchmark.md — Prefill 阶段(M ≥ 8)的 GEMM 通用基准
- 测试源码:GemvBWTest.cpp;统一测试入口:test/main.cpp
【免费下载链接】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),仅供参考