MNN Vulkan 后端性能优化实战指南:从基准测试到 Kernel 优化的完整工作流
【免费下载链接】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 开源推理引擎 Vulkan 后端(buffer 后端,面向 Adreno / Mali / Apple 移动 GPU)kernel/算子性能优化与新特性集成的完整技术指南,覆盖基准建立、CPU/GPU 瓶颈判定、shader 外科式修改、三层正确性验证、交替 A/B 真机测速,以及 cooperative matrix / subgroup 等 Vulkan 新特性的集成流程。读完你将掌握一套可复现、防踩坑的 Vulkan 优化方法论,并能直接应用于 MNN 的 LLM prefill/decode 场景。
一、优化工作流总览与核心原则
Vulkan 后端的性能优化不能靠"先猜再优化",MNN 将其沉淀为一条可执行的流水线(依据 skills/vulkan-optimize/SKILL.md 与同目录 benchmark/kernel-opt/integrate 三份分步文档):
- 选择优化方向:模型级优化(方向 A)/ 指定算子优化(方向 B)/ 新特性集成(方向 C);
- 建立基准:Profile 全模型,先分 CPU/GPU 瓶颈,再定位 GPU 内瓶颈 kernel;
- 迭代优化:每次只改一个点,至少尝试 3 种技巧,交替 A/B 验证;
- 集成验证:全量回归 + 小/大模型跨规模验证 + 代码质量审查;
- 沉淀经验:把可复用方法论回写到 optimization-handbook.md。
整个流程围绕 11 条核心原则展开,其中最容易"改了不生效"的根因集中在这几条:
- 改
.comp必跑 makeshader 等价流程:GLSL 源不会被构建系统直接编译,运行时读的是AllShader.h/cpp里由makeshader.py生成的 SPIR-V 字节数组(.comp →glslangValidator -V→spirv-opt -O→xxd嵌入)。 - dispatcher 选路要先摸清:同一个 op 常有多条 kernel(CoopMat / subgroup / nosubgroup / 融合 vs 分离),盲改往往根本没被调度;不要把 fallback(nosubgroup)路径性能当 baseline。
- packed weight 必须 packing/unpack 双向镜像:host weight prepare shader 写出的字节布局要与 decode shader 读取逐 bit 匹配。
- 正确性 oracle 先于性能:优化前要有"已知正确"的 baseline,数学等价的改动应与 baseline 逐 token 一致。
- 真机才算数,且面向多个 vendor:Vulkan 同时跑 Adreno(Android)、Mali(Android)、Apple(iOS/MoltenVK),driver 差异极大,Mac MoltenVK ≠ Android Adreno。
- Buffer / Image 是编译期二选一:由
MNN_VULKAN_IMAGE决定,两个后端是完全独立的代码树。 - 换 shader 后必清 pipeline cache:持久化的
VkPipelineCache(tmp/mnn_cachefile.bin)在 shader 变更后会 stale → 直接 segfault。
二、选择优化方向:A/B/C 三轨
| 场景 | 方向 | 说明 |
|---|---|---|
| 提升整个模型推理性能 | A:模型级优化 | Profile 全模型 → 定位瓶颈(先分 CPU/GPU)→ kernel 或算子级优化 → 重新 profile → 迭代 |
| 用户指定优化某个算子 | B:指定算子优化 | 直接进入指定算子,判断 kernel 级或算子级 |
| 集成 Vulkan 新特性(coop matrix / subgroup / 扩展) | C:新特性集成 | 理解示例代码 → 适配 MNN → 特性检测 + fallback → 验证 |
三个方向共享"顺序执行、允许回退、每步验证"的通用规则,每个步骤都有明确的通过标准。
三、方向 A:模型级优化的执行流程
Profile 全模型(op 级 + shader 级耗时) ↓ 先判断瓶颈在 CPU 调度 还是 GPU kernel(handbook §1.4) ├─ CPU 调度瓶颈 → 算子融合减 op / indirect batch / fixResizeCache(handbook §2/§6) └─ GPU kernel 瓶颈 → 定位耗时占比最高的 kernel/op ├─ Kernel 级 → Kernel 优化流程 └─ 算子级 → 算子级优化流程 ↓ 重新 Profile 全模型,验证整体提升 ↓ 定位下一个瓶颈,重复直到满足需求Step A.1:Profile 全模型
用-DMNN_GPU_TIME_PROFILE=ON编译——注意这是编译期宏而非运行时开关。Vulkan profiler 有两块输出:
[Execution Profiling](op 级):按 op 类型(Convolution / Attention / Raster / …)聚合 GPU 时间;[Shader Profileing](shader 级):按具体 shader 名(glsl_..._comp)的 GPU timestamp。
⚠️ 两块输出都跨 execute 累计到程序退出,且含 load 期的 auto-tune forward——分析稳态时只取Prepare for tuning opt End之后的块。通过标准:拿到 op 级 + shader 级耗时排序,确定第一个优化目标,并已判断瓶颈在 CPU 还是 GPU。
Step A.2:判断优化级别
| 信号 | 级别 | 进入流程 |
|---|---|---|
| 端到端 ≫ GPU kernel 累计(GPU 只占几分之一) | CPU 调度瓶颈 | 算子融合 / indirect batch / fixResizeCache |
| 单个 shader 占比高、本身有优化空间 | Kernel 级 | → Kernel 优化流程 |
| 同一算子多个 shader 合计耗时高 / 有大量格式转换 | 算子级 | → 算子级优化流程 |
| 算子计算模式不适合 GPU(单 work-item 串行) | 算子级 | → 算子级优化流程 |
Step A.3:验证并迭代
重新 profile,确认瓶颈耗时下降 + 端到端提升;未满足则回到 A.1 定位下一个瓶颈。
四、方向 B:指定算子优化与算子级优化流程
用户指定算子时,先 profile 算子内各 shader 耗时:最慢的单个 shader 走 Kernel 优化流程;跨 kernel 边界的问题(合并/拆分 dispatch、改中间排布、融合 epilogue)走算子级优化流程。
算子级优化流程(方向 A/B 共用):
定位算子内所有 shader(读 Execution::onEncode/onResize) ↓ Profile 各 shader 耗时占比 ↓ 整体分析策略: - 合并 dispatch(减少命令录制 + 中间 buffer;Vulkan CPU 调度重,收益常更大) - 融合 epilogue(把 unpack/scale 折进 matmul,省一次 dispatch + temp) - 拆分(提高并行度) - 改中间数据排布(消除格式转换 / raster) - 用 cooperative matrix 重写 matmul ↓ 迭代验证,直到算子整体性能满足需求各手段的适用场景与本仓库的真实案例:
| 手段 | 适用场景 | 本仓库案例 |
|---|---|---|
| 融合 epilogue | matmul 后有独立 unpack/转置 pass | conv1x1 coop 把 COOP_to_C4 折进 matmul epilogue(小 N 收益,大 N 反伤 occupancy → 按 N 门控) |
| coop matrix 重写 | GEMM/QKV,Adreno 支持 coop | attention QK·V 用 coop(scalar qkv_acc 102ms → coop 54ms) |
| subgroup 归约 | 有 tree reduction + barrier | prefill softmax 用 subgroupMax/Add 替代树形(37→23ms) |
| 合并 dispatch / indirect batch | 命令录制占大头 | MNN_GPU_RECORD_BATCH让多 op 共享 command buffer(prefill +56%) |
Vulkan 关键提醒:算子级优化前先确认瓶颈是 GPU 还是 CPU 调度。若是 CPU 调度,GPU kernel 再快端到端也不动。
五、Kernel 优化流程:从瓶颈分析到交替 A/B
分析 shader 的计算强度 / BW 利用率(handbook §1) ↓ 判断瓶颈类型(compute / memory / occupancy) ↓ 从 §2 技巧 + §5 速查表选匹配方法 ↓ 实施 → 验证正确性 → 测量性能(交替 A/B) ↓ 未达预期?换一种技巧重试| 阶段 | 文档 | 目标 |
|---|---|---|
| 基准 | benchmark.md | 建立性能基准,分析瓶颈类型 |
| 优化 | kernel-opt.md | 至少尝试 3 种技术,迭代提升 |
| 集成 | integrate.md | 全量回归、代码审查、性能报告 |
迭代至少 3 种技术,按难度递进:先手(低难度)push_constant、indirect batch、epilogue 合并写;再上(中难度)subgroup 归约、融合 epilogue + 按规模门控;最后(高难度)cooperative matrix 重写、算子整体重写。停止条件(须全满足):已试 ≥3 种 / 达标或连续 3 次 <5% 提升 / 每次都有 A/B 数据记录。
六、编译与真机运行(Android,唯一来源)
编译
cd project/android/build_64 # 首次配置(确认 buffer 后端 + LLM) cmake .. -DMNN_VULKAN=ON -DMNN_VULKAN_IMAGE=OFF -DMNN_BUILD_LLM=ON \ -DMNN_SUPPORT_TRANSFORMER_FUSE=ON -DMNN_LOW_MEMORY=ON -DMNN_ARM82=ON make llm_demo -j8 # SEP_BUILD=OFF → 会重编 libMNN.so # 需要 GPU per-op/shader 耗时:cmake -DMNN_GPU_TIME_PROFILE=ON . #(测干净 tok/s 时务必 =OFF)要点:
- 参数
MNN_VULKAN_IMAGE=OFF明确走buffer 后端(LLM 全链路走 buffer);=ON则是source/backend/vulkan/image/*的独立代码树,两边改动互不影响; make MNN不会连带重编 Vulkan 静态库,必须用make llm_demo;改 shader 后AllShader.cpp变了也会重编;- 该宏在 source/backend/vulkan/CMakeLists.txt 中定义,并分别驱动 buffer/image 两棵代码树的构建。
推送 + 运行
adb push libMNN.so llm_demo /data/local/tmp/MNN/ adb -s <serial> shell "cd /data/local/tmp/MNN && rm -f tmp/mnn_cachefile.bin && \ LD_LIBRARY_PATH=. ./llm_demo <model>/config_vk.json <prompt.txt> <ndecode>"换 shader 后必清
tmp/mnn_cachefile.bin;否则 stale pipeline cache 会 segfault。profile 命令示例(benchmark.md §0.3):adb -s <serial> shell "cd /data/local/tmp/MNN && rm -f tmp/mnn_cachefile.bin && LD_LIBRARY_PATH=. ./llm_demo <model>/config_vk.json 512.txt 2 2>&1" > prof.txt。
入口定位
grep -rn "OpType_<MyOp>" source/backend/vulkan/buffer/execution/ # buffer 后端 grep -rn "OpType_<MyOp>" source/backend/vulkan/image/execution/ # image 后端conv1x1 低 bit 选路(VulkanConvolution.cpp 的onCreate):
useInt8Conv && is1x1 ├─ coopMat supported && Adreno │ ├─ perChannelAsym + S8S8S32 → VulkanConv1x1CoopA8 │ └─ else → VulkanConv1x1Coop (CoopMat 只支持 int4/int8) └─ → VulkanConv1x1General (native int8/int4/int2/int3) else → VulkanConvolutionSlideWindowsInt8attention prefill 走 VulkanAttention.cpp:rearrange_q → init_state → (per k-block: qk → softmax → qkv) → finalize,coop QKV / subgroup softmax 由设备能力选路。每个onEncode决定本次 dispatch 哪些 shader,把目标 shape 代入确认,再改对应.comp。仓库中已存在attention_prefill_coop_qk.comp、attention_prefill_coop_qkv.comp、dynamic_w8a8_coop_gemm.comp等 coop 路径 shader(见 source/backend/vulkan/buffer/execution/glsl)。
七、Shader 修改流程(含防污染)
# 1) 编辑 .comp(确认 buffer 还是 image 后端) vi source/backend/vulkan/buffer/execution/glsl/<my_kernel>.comp # 2) 新文件在 glsl/macro.json 登记 useFP16(决定是否生成 _FP16 变体) # 3) 重新生成 SPIR-V 嵌入数组⚠️不要直接跑全量makeshader.py——本机 glslang/spirv-opt 与仓库版本不同,会把所有 shader 数组重编/重排,AllShader.cpp出现几万行无关 diff(污染)。正确做法是只重生成改动的那几个数组,外科式替换进AllShader.cpp:
# 对每个改动的 shader(fp32 + fp16 变体),用与 makeshader 相同的管线单独编译: # header(FP32/FP16) + body → glslangValidator -V [--target-env vulkan1.1(若含 coop/subgroup/memory_scope)] → spirv-opt -O → xxd -i # 得到 const unsigned char glsl_<name>_comp[]={...}; unsigned int glsl_<name>_comp_len=N; # 再用脚本精确替换 AllShader.cpp 里对应的那一段(正则匹配数组头到 _len 行)。 # 新增 shader 还要在 AllShader.h(extern 声明)+ VulkanShaderMap.cpp(name→数组 map)追加。改完确认:grep -c 'glsl_<name>_comp_len' AllShader.cpp且用git diff --stat确认只有目标数组变了。SPIR-V 合法性可用spirv-val验证。
新加 kernel 同步检查清单:.comp主路径 +_nosubgroup变体都加 /macro.json登记 / host pipeline 选择分支 /AllShader.{cpp,h}+VulkanShaderMap.cpp三处注册 / weight stride & buffer size 重算 / dispatcher 显式选路或 fallback。
八、正确性验证:三层 Oracle
三层 oracle(数值层 dump tensor → op 层MNNV2Basic.out单层 → 端到端跑模型)。端到端关 sampler 随机性(temperature:0.0,greedy),CPU/Vulkan 同 prompt 前 N token 应一致(fp16 误差内)。
- CPU oracle 不可用时的兜底(低 bit 路径 CPU 本身就乱):用冻结 baseline 二进制做 GPU-baseline vs GPU-opt 的逐 token 贪心对比(数学等价改动应完全一致)。改 kernel 前先
git stash存一份 baselinelibMNN.so。 - 模型本身可能就坏:小模型极低 bit 量化 CPU 跑也乱(如 Qwen3-0.6B 的 w2/w3),用 4B/8B 才是有效验证样本。
- Mac MoltenVK 行为不代表 Android,最终验收必须 Android 真机。
- 数值容忍:fp16 vs fp16 abs<1e-2;量化 dequant+fp16 abs<1e-1。
集成阶段的回归命令(integrate.md):Vulkan 的 forward type 为 7,单测用adb -s <serial> shell "cd /data/local/tmp/MNN && LD_LIBRARY_PATH=. ./run_test.out op/XxxTest 7 <precision> <numThread>",全量 op 把op/XxxTest换成op/,通过标准是all tests passed。
九、性能测量:Android 真机上的防噪声方法
小收益最容易被噪声骗,手机 GPU 有两个陷阱:
- 冷启动首跑不算数:首跑含 auto-tune + pipeline 编译。清了
mnn_cachefile.bin后第一次也是冷的。先 warm 再测。 - 跨时段热漂移 ~±8–10%:设备温度随时间变,"先测完 base 再测 opt"不可比。必须交替 A/B——base 和 opt 两份二进制(或同一二进制不同 env)背靠背配对:
base→opt→base→opt,看每轮配对里 opt 是否稳定胜出,而非比两组绝对值。
换 shader 做 A/B 时,每次切库 pipeline cache 会 stale;每轮跑前
rm tmp/mnn_cachefile.bin(tok/s 不含 load,清 cache 只影响 load 时间,A/B 仍公平)。
带MNN_GPU_TIME_PROFILE=ON的 build 只看相对占比(放大绝对耗时且改变调度);最终收益以不带 profile 的干净 build 的端到端 tok/s 为准。
十、优化技巧速查与瓶颈分析方法论
先做 roofline 分析
- 计算强度
AI = FLOPs / Bytes,与ridge_point = peak_FLOPS / peak_BW比:AI < ridge→ memory-bound;> ridge→ compute-bound。典型:GEMV(decode) memory-bound;GEMM(prefill) 随 batch 增长;elementwise/raster ≈ memory-bound。移动 GPU LPDDR 理论带宽打 6–8 折为实测可达。 - BW 利用率
BW_util = actual_bytes / kernel_time / peak_BW:<50% 访存模式/launch 有问题;50–70% cache miss/WG 大小可调;>70% 只能减数据量(packed 存储)。 - occupancy 墙:roofline 说 memory-bound 但减流量/减 ALU 都不涨 → 大概率是 occupancy 墙。Vulkan compute shader 的 occupancy 受寄存器和**共享内存(
shared数组)**双重限制——本仓库 conv1x1 融合 epilogue 引入shared[64*64](8KB)使大 N conv occupancy 下降、+148ms。
⚠️ Vulkan 第一诊断:CPU 调度 vs GPU kernel
这是 Vulkan 区别于 OpenCL/Metal 的最关键一步。MNN Vulkan 的 prefill 端到端常常受 CPU 侧命令录制/调度瓶颈,而非 GPU kernel。实测数据(Adreno,Qwen3-0.6B,514-token prefill):
| 阶段 | 耗时 | 占比 |
|---|---|---|
| GPU 计算 | ~263ms | 60% |
| 命令录制(op onResize/onEncode ×1129) | ~63ms | 14% ← CPU 侧最大 |
| submit / 等待 | ~52ms | 12% |
| allocMemory 其余 + geometry + shape + 真显存分配 | ~47ms | 11% |
结论:CPU 调度开销(~187ms)> GPU kernel 中可优化的部分。判断方法:用MNN_GPU_TIME_PROFILE=ON拿 GPU kernel 累计,与干净 build 的端到端 wall 时间比——GPU 累计 ≈ wall → GPU-bound,优化 kernel 有效;GPU 累计 ≪ wall → CPU 调度 bound,优化 kernel 无效,改去攻调度。模型规模不同瓶颈会翻转:小模型(0.6B)CPU-bound,大 int4 模型(4B)conv GPU 占比大反而 GPU-bound。
技巧速查表(摘自 optimization-handbook.md §5)
| # | 技巧 | 针对瓶颈 | 适用场景 | 难度 | 收益参考 |
|---|---|---|---|---|---|
| 1 | Cooperative matrix 重写 matmul | compute/访存(GEMM) | Adreno + coop,K 维够大 | 高 | QKV −47% kernel |
| 2 | Subgroup 归约替代 tree reduction | 归约 barrier | softmax/reduce,支持 subgroup | 中 | softmax −38% kernel |
| 3 | Epilogue 合并写(coalesced store) | memory(scatter 写) | NC4HW4/转置 epilogue 非合并 | 中 | 大 N conv 回收约一半 regression |
| 4 | 融合 epilogue(unpack/scale 折进 matmul) | dispatch + temp 往返 | matmul 后紧跟独立逐元素 pass | 中 | 小模型 prefill +8% |
| 5 | 按输出规模门控融合 vs 分离 | occupancy | 融合用 shared、大 N 反伤 | 中 | 4B −6%→+2.6% |
| 6 | push_constant 替代小 uniform | CPU 命令录制 | uniform ≤128B 的 op | 低 | raster onResize −15~25%(e2e 小) |
| 7 | indirect batch(RECORD_BATCH) | submit + 命令录制 | 多 op 推理 | 低 | prefill +56% |
先分清 CPU 调度 vs GPU kernel:CPU-bound 选 6/7 + §6 候选;GPU-bound 选 1–5。
关键技巧要点
- Coop matrix 重写 matmul(技巧 1):用
coopmat<...>+coopMatLoad/coopMatMulAdd/coopMatStore替代 scalar 累加;COOP_M/N/K 用设备selectedFP16CoopMatShape(Adreno 常 16×16×16 或 64×64×16)经 spec constant 传入;local_size_x = subgroup size。coop matrix 只对 int8/int4 有硬件定义;K 维太小时 shared staging 开销盖过收益(headDim=128 做 coop-QK 实测 e2e −3%,负收益,已弃)。 - Subgroup 归约(技巧 2):一个 workgroup = 一个 subgroup(
local_size_x_id=0设为 subgroup size),用subgroupMax/subgroupAdd替代"shared 数组 + barrier + stride 折半"的树形归约;需#extension GL_KHR_shader_subgroup_arithmetic : require+--target-env vulkan1.1;host 按getSubgroupSize()动态设 local size,别 hardcode。⚠️ softmax kernel −38% 但 prefill e2e 0%(CPU-bound),收益体现在散热/长文脉/大模型。 - Epilogue 合并写(技巧 3):让输出的连续维在相邻线程间最快变化。NC4HW4 输出
[N/4, M, 4],epilogue 循环里让m = idx % TILE_M(gm 最快)而非m = idx / n4PerTile——后者相邻线程写地址跨步M(=token 数),完全非合并。 - 融合 epilogue(技巧 4):matmul 把 tile 存进
shared,barrier()后直接按目标布局写出(含 bias/激活),省掉"写 temp → 独立 pass 读 temp"的一次 dispatch。⚠️ 引入的sharedstaging会吃 occupancy——必须配合技巧 5 按规模门控。 - 按规模门控(技巧 5):保留两条路径——小 N 走融合(零 temp,省 dispatch),大 N 走分离(matmul 写 row-major temp,不用 shared → 高 occupancy+ 一个独立 unpack pass)。阈值靠逐 conv dump 出真实 N 分布确定(2B 最大 conv N=6144 偏好融合、4B N=9728 偏好分离,阈值取 8192),并做成 env 可调(
MNN_VK_CONV_FUSE_MAXN)。 - push_constant(技巧 6):
layout(binding=N) uniform→layout(push_constant) uniform,host 侧allocUniform+writeBuffer→vkCmdPushConstants。收益比预期小(raster onResize 只降 15–25%),因为vkAllocateDescriptorSets才是大头。 - indirect batch(技巧 7):开
MNN_GPU_RECORD_BATCH(ScheduleConfig::mode位0x200),让mDirect=false——1000+ 个 op 打包到少数 command buffer segment,submit 从 ~1129 次降到少数次。LLM 里通过 config"vulkan_record_batch": true开启。这是攻 CPU 瓶颈的最大单一杠杆。
十一、常见陷阱速查
| # | 陷阱 | 后果 | 规避 |
|---|---|---|---|
| A | 全量 makeshader 污染 AllShader.cpp | 几万行无关 diff | 只外科式重生成改动数组 |
| B | 持久化 pipeline cache 换 shader 后 stale | 直接 segfault | 每轮rm tmp/mnn_cachefile.bin |
| C | buffer/image 编译期二选一,改错树 | 白改 | grep MNN_VULKAN_IMAGE CMakeCache.txt |
| D | coop/subgroup shader 缺 target-env vulkan1.1 | spirv-opt 拒绝/静默回退 | 手动重生成时必带--target-env vulkan1.1 |
| E | descriptor set 池化在 Adreno 反变慢 | decode −12%(净负) | 目标 driver 实测后再动 |
| F | attention GPU kernel 微优化对 prefill e2e 常无效 | e2e 0% | 动 kernel 前先确认 GPU-bound |
| G | 跨 session 顺序测 A/B 被热漂移欺骗 | 噪声当收益 | 必须交替 A/B |
| H | cooperative matrix 只支持 int8/int4 | 数值乱/driver crash | dispatcher 显式 gate,w2/w3 走 General |
| I | shared 数组吃 occupancy | 大 N 净 +148ms | 评估 occupancy,大规模走无-shared 分离路径 |
| J | CPU 侧大头是 vkAllocateDescriptorSets(82%) | push_constant 只省小头 | 攻 descriptor set 或跨 forward 复用命令 |
| K | subgroup 与 nosubgroup 双 shader 必须同步 | 老 Mali 挂或走错逻辑 | 两个变体都改 |
| L | coop 端数 tile 越界读靠 robustBufferAccess 兜底(UB) | 输出对但是 UB | host 侧把 workspace 的 M 维 padding 到 COOP_M 倍数 |
| M | 未扩展路径吃错 buffer 大小 | OOB 读到下个 op 数据 | 搜所有以mIsInt4分流处同步加处理 |
| N | driver OOM / 设备重启 | 手机重启 | 先小模型验通路,崩了让设备恢复 |
十二、Packed weight 设计与新特性集成
Packed weight 设计(§4)
新加 quant bit / 调 tile 排布时先固定 5 个量:tile(最小访问区块,Vulkan conv1x1 常见行主[N, padK/W]每 word 装 W 个 weight)/ word 内 weight 数(w2:16, w4:8, w8:4 per uint32)/ 多 word split(w3 用 lo2+hi1 双 word,[N, padK/16, 2])/ bit 顺序(低 bit 先)/ signed 存储。
- signed/unsigned 与 originOffset:导出器写出的 alpha
b = min + offset_signed*scale,originOffset 已折进 bias。Vulkan int8 path 用bitfieldExtract(packedW,0,8)会 sign-extend,signed bytes 直接当 signed 解,无需再减 offset(这点和 OpenCL/Metal 从 unsigned 解不同)。 - w3 split:
[N, padK/16, 2]uint pairs,pair[0] 低 16bit 装 16 个 2bit,pair[1] 低 16bit 装 16 个 1bit;decodeq = (low2>>(i*2))&3 | ((hi1>>i)&1)<<2,减 4 得 signed[-4,3]。host buffer size 乘wordsPerGroup=2,shader 内 stride 也 ×2。
方向 C:新特性集成流程
触发条件是用户说"用这个 Vulkan 特性/coop matrix/subgroup 扩展"并提供示例代码;没提供示例时主动要求(.comp + host 或完整可运行 demo、特性说明、目标算子、目标平台、预期收益)。
集成步骤:特性检测 → shader 适配(用FLOAT宏、NC4HW4、spec constant、扩展 require + 头部注释)→ 外科式重生成 + 三处注册 → host 选路(if (feature supported) 用新 pipeline else 原路径,两条都 createSet + 绑定)→ 编译验证。Fallback 必须设计:coop 目前一般 gate 在gpuType()==ADRENO && supportCoopMat。
设备能力查询在VulkanDevice(runtime 目录)中:
auto coop = vkBn->getDevice().getCoopMatInfo(); // supportCoopMat, selectedFP16CoopMatShape const auto& sg = vkBn->getDevice().getSubgroupInfo(); // size, stages, ops uint32_t sgSize = vkBn->getDevice().getSubgroupSize();新特性用 spec constant(layout(constant_id=N),host 经getPipeline(name, types, localSize, spec)传入)或 build 宏控制路径 + runtime 检测 fallback。coop/subgroup shader 需--target-env vulkan1.1(makeshader 已按扩展关键字判断)。仓库中 coop 相关 host 实现可参考 VulkanConv1x1CoopA8.cpp、VulkanConv1x1CoopAFP16.cpp 与 VulkanAttention.cpp。
十三、集成验证与经验沉淀
优化进入 MNN 主体前必须完成:全量回归(run_test.out op/ 7 ...全过)+跨模型规模验证(至少小模型 + 大模型各测一遍,如 0.6B + 4B,确认没有一个规模退步)+代码质量审查(shader 三处注册、subgroup/nosubgroup 双变体、occupancy 评估、fallback 完善、阈值 env 可调)+性能报告(实测交替 A/B 数据,不写"预期")。
提交信息格式建议:
[Vulkan:Perf] Optimize Xxx (coalesce epilogue / gate fusion by N / ...) - 技术1 - 技术2 Performance: 0.6B +x% / 2B +x% / 4B +x% prefill (Adreno, interleaved A/B) All op/ tests passedMNN 有 clang-format-diff pre-commit 钩子,手写 .cpp/.hpp 被拦时用
git clang-format HEAD只格式化改动行,但不要让它重排 AllShader.cpp 的字节数组。
沉淀经验:完成一个较复杂的优化任务后(重写算子、新技巧奏效、踩了非显而易见的坑、验证/排除了某方向),把可复用的方法论回写到 optimization-handbook.md——它是 Vulkan kernel/访存/调度级技巧与陷阱的唯一来源:新技巧进 §2 + 登记 §5 速查表;新陷阱进 §3;新瓶颈定位/测量方法论进 §1;验证通过某候选方向从 §6 移入 §2;实测排除某方向在 §6 标注"已排除 + 原因"。只写方法论,不写单次任务流水账;技巧编号是稳定 ID(只追加不复用)。某具体 shape/kernel 的一次性发现属于 agent memory,不进手册。
十四、候选优化方向(尚未落地 / 部分已排除)
有依据但未在 MNN 落地实测,或已实测排除,想用先做 spike 验证:
- 候选 A:fixResizeCache 跨 forward 复用命令——同 shape 的第二次 forward 整段跳过 resize/encode,预期省整个
_allocForTensor(~59ms)。风险中、收益结构性,未落地。 - 候选 B:算子融合减 op 数——在 GeometryComputer 层合并 raster,或 Vulkan 后端把连续 raster 打包成一个 dispatch。工作量大。
- 候选 C:region 合并——已排除(LLM 场景):实测 LLM raster 只有 1% 可合并(
max_run=2),省 ~0.4ms,噪声内。 - 候选 D:descriptor set 池化——已排除(Adreno):decode −12% 净负,其它 driver 可重试。
- 候选 E:coop matrix 用于 QK——已排除(headDim=128):K 维太小,e2e −3%,更大 headDim 可重试。
结语
MNN Vulkan 后端的性能优化是一个"先分清 CPU/GPU 瓶颈 → 建立可对比基线 → 单点突破 → 交替 A/B 验证 → 集成回归 → 沉淀方法论"的闭环。核心心法总结为三点:改.comp必走外科式 makeshader 流程、真机交替 A/B 才是唯一可信的测量方式、先量化瓶颈再选优化杠杆。按本文工作流操作,即可在 Adreno/Mali/Apple 多 vendor 真机上稳定推进 conv/gemm/attention 等算子的 kernel 与调度级优化。
【免费下载链接】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),仅供参考