news 2026/9/14 2:04:28

MNN GemvBW:面向 LLM Decode 阶段的 GEMV 带宽基准测试实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MNN GemvBW:面向 LLM Decode 阶段的 GEMV 带宽基准测试实战

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 混合量化卷积模拟 GEMVbenchGemv()通过_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 rooflinemeasurePeakBwGBs()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 = 64iters = 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_LLMMNN_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]
位置含义取值
1forwardType0=CPU, 1=Metal, 3=OpenCL(按 MNNForwardType.h 编号:MNN_FORWARD_CPU = 0MNN_FORWARD_METAL = 1MNN_FORWARD_OPENCL = 3
2precision0=Normal, 1=High, 2=Low(推荐 2,对应 fp16 累加)
3threads省略时测试内部按 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/sweight_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的结果。

typethreadsus/itereff GB/sGFLOPSbytes/elem
w821015.561.4115.71.0625
w42782.742.2150.00.5625
w321367.918.885.90.4375
w22697.226.3168.40.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

typethreadsus/itereff GB/s%peakGFLOPS
w84605.2103.187.4194.0
w44334.098.983.9351.6
w34555.846.239.2211.3
w24294.062.452.9399.5

memcpy roofline:117.9 GB/s @ 4 threads。

Metal(文档标注仅支持 w8 / w4):

typethreadsus/itereff GB/s%peakGFLOPS
w84700.489.171.3167.7
w44464.371.157.0252.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),仅供参考

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

ADC与CAN硬件级同步设计:实现时间确定性闭环控制

1. 这不是两个模块的简单拼接&#xff1a;ADC/CAN双结点控制的本质是“感知-决策-执行”闭环的物理层重构你手头那块S32K312或者STM32H7的开发板&#xff0c;上面同时焊着ADC采样电路和CAN收发器&#xff0c;但如果你只是把ADC读电压、CAN发数据这两段代码写在main函数里轮询执…

作者头像 李华
网站建设 2026/9/14 2:03:23

Day 9·3 q8 KV 精度对照——量化进注意力,输出差多少

真机实测通过&#xff1a;本文实验已在 RK3588 板端实测完成&#xff08;2026-09&#xff1b;方法学与原始记录见仓库 docs 与《实验脚本》目录&#xff09; 一句话导读&#xff1a;q8 KV 精度对照实验&#xff1a;fp32 与 q8 两套 KV 走同一注意力公式&#xff0c;板端实测输出…

作者头像 李华
网站建设 2026/9/14 2:02:47

AMS模块跨工艺移植实战:台积电与中芯国际PDK差异及设计要点

1. 为什么要做这份AMS模块清单&#xff1a;跨代工厂项目的真实痛点先讲个背景。我在做一颗数模混合SoC的时候&#xff0c;同时面对台积电和国内代工厂两套PDK&#xff0c;电压域、时钟域、模拟前端混在一起&#xff0c;最头疼的不是某个模块设计不出来&#xff0c;而是等到系统…

作者头像 李华
网站建设 2026/9/14 2:02:18

Maven核心机制与高频问题排查实战指南

"Maven"这三个字母&#xff0c;对Java开发来说几乎是每天都要打交道的存在。但说句实话&#xff0c;绝大多数人对它的理解停留在"点一下刷新&#xff0c;等右下角转完圈"的程度。搜"maven是干嘛的"的&#xff0c;多半是刚接手企业级Java项目的新…

作者头像 李华
网站建设 2026/9/14 2:01:46

AI生成PLC程序能力分级:从L1到L5,工程师的提效指南

1. 为什么AI写PLC这件事&#xff0c;终于有人给出了“判定标尺”最近翻技术群&#xff0c;十条里少说有四五条在聊AI写PLC。有的说“以后PLC工程师要失业了”&#xff0c;有的晒出截图&#xff0c;让AI写了个好几行的梯形图&#xff0c;评论区一片惊叹。我自己也试过不少&#…

作者头像 李华
网站建设 2026/9/14 2:00:42

工业安全PPE检测实战:YOLOv8数据集处理与模型训练全流程

简介&#xff1a;这份数据集面向工业安全领域的PPE检测需求&#xff0c;包含918张真实作业场景图片&#xff0c;其中训练集644张、验证集183张、测试集91张&#xff0c;覆盖安全帽、手套、护目镜、口罩、背心等11类穿戴与未穿戴状态&#xff0c;统一使用YOLO格式的边界框标注。…

作者头像 李华