MNN Metal LLM 构建与测试实战:编译、模型导出、正确性对拍与性能基准全流程

发布时间:2026/9/15 1:43:03
MNN Metal LLM 构建与测试实战:编译、模型导出、正确性对拍与性能基准全流程 MNN Metal LLM 构建与测试实战编译、模型导出、正确性对拍与性能基准全流程【免费下载链接】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 仓库skills/metal-optimize/技能体系中的构建与测试分册对应 skills/metal-optimize/build-and-test.md聚焦 Metal 后端 LLM 场景下“改完 kernel 后如何验证”从强制重编译、二进制新鲜度断言、pipeline cache 清理到 cmake 编译选项、llmexport.py模型导出、llm_bench/llm_demo的正确性与性能对拍以及加载非标准 causal 模型时必须了解的 attention 假设边界。读完本文你将能建立一套“先证正确、再比性能”的 Metal LLM 开发闭环避免把过期产物、随机采样或错误 mask 语义误判成真实回归。一、每次 Metal 改动之后的强制验证流程最重要在 MNN 的 Metal 后端里shader 并不是独立的.metal文件而是以 C 字符串形式嵌入在*Shader.hpp中见 source/backend/metal/MetalAttentionShader.hpp公共头通过字符串拼接共享。这套机制决定了任何 Metal 改动都必须走完下面的验证流程再评估性能否则大概率是假信号。这不是建议是硬性规则。Step 0. 强制重编译别相信 make 增量改完.hpp后make有时只重编产物但不重新 linklibMNN.dylib时间戳判断问题运行时仍加载旧 shader。因此# 改完 shader 或 .mm 后touch 强制标记 dirty或 -B 全量重编 touch source/backend/metal/MetalAttentionShader.hpp # 或改动的 shader/mm cd build make -j8 llm_demo # 或强力版 cd build make -j8 -B llm_demo如果测试结果诡异比如“改了 kernel body 但行为没变”第一步先ls -l build/libMNN.dylib看链接时间是否最新不是就make -B。新增源文件后必须重跑cmake ..。build 目录的源文件列表是 configure 时 GLOB 出来的新增的.mm/.hpp不会被自动纳入表现为链接期Undefined symbols更坏的情况是 link 失败后libMNN.dylib已被删掉而你以为“构建成功过”。正确的重建命令cd build cmake .. make -j10 llm_demo llm_benchStep 0.5 测量前的二进制新鲜度断言必做过期 build 不会报错只会静默给出错误的性能数字。文档记录了真实的教训一整轮 MNN-vs-对手基准对比4 模型 × 3 prompt 长度跑在隔了一天的libMNN.dylib上decode 数字偏低 6~8%并据此立项排查了一个根本不存在的“回归”建 worktree、编译 baseline、正反序配对、二分两轮全部白做。# 1) 时间戳必须晚于你最后一次改动 ls -l build/libMNN.dylib # 2) 关键符号断言改动引入的新 kernel/函数名必须出现在产物里 strings build/libMNN.dylib | grep -c prefill_flash_attn_tc # 应为非 0判据只要“当前 HEAD 引入的某个新符号”在libMNN.dylib里找不到这一轮所有数字作废。反向也成立——排查“疑似回归”时先做这个断言再去建 worktree 二分。⚠️ 顺带一个易踩的输出格式口径HEAD 的llm_bench -pg pp,tg分列报告 prefill / decode 两个速度-kv已废弃-p A -n B -kv true-pg A,B。历史 WIP 二进制的-pg输出格式与 HEAD 不同——发现输出列数/口径和预期不符本身就是“跑的不是当前产物”的信号回到本节做新鲜度断言。Step 1. 清 Metal pipeline binary cacheMetal 会把 pipeline JIT 结果缓存到tmp/mnn_cachefile.binlaunch 目录相对路径。改 shader 后 pipeline key 可能没变宏组合相同Metal 会加载旧 binary → 观察到“改了 shader 但完全没生效”。find . -name mnn_cachefile.bin -delete # 常见位置: build/tmp/mnn_cachefile.bin, ./tmp/mnn_cachefile.bin (llm_demo launch dir)Step 1.5 新导出模型先造 greedy config禁止用导出默认 config 对拍llmexport.py产出的默认config.json是backend_type: cpusampler_type: mixedtemperature 0.8——拿它跑llm_demo做“对拍/自拍”得到的是CPU 上的随机采样自拍必然 DIFFERS、从第 1 个 token 就分叉且任何 Metal env 开关都“看似无效”。在 sampler 侧sampler_type: mixed会走temperature/topK/topP等随机采样链路见 transformers/llm/engine/src/sampler.cpp 的configSampler因此对拍必须换成确定性的 greedy# 新模型落地第一步llm_bench 因有 -a metal 不受影响llm_demo 全部用这份 python3 - EOF import json, sys p /path/to/model/config.json d json.load(open(p)) d[backend_type] metal; d[sampler_type] greedy; d[temperature] 0.0 json.dump(d, open(p.replace(config.json, config_mtl_greedy.json), w), indent4) EOF判据自拍同 config 连跑两次DIFFERS 时第一反应先grep sampler_type config再怀疑代码。Step 2. 正确性验证矩阵必须全部跑只测速度不测正确性 假信号。强制使用sampler_type: greedy, temperature: 0.0, top_k: 1的 config跨 run byte-identical 是黄金标准。对每一次改动跑满这套矩阵维度覆盖点为什么Prompt 长度短 (~50 tok) 中 (~512 tok) 长 (~2048 tok)触发不同 kernel 路径mShortSeq / mQkSimdMatrix / mQkTensorMatrix / mFlashAttnPrefillFA on / offMNN_ENABLE_FLASH_ATTN_PREFILL1和0决定走 flash-attn 还是三段 pipelineprefill_qk[_tensor] softmax prefill_qkv[_tensor]。两条路径都要正确CAUSAL_TRI数据驱动用真实 mask 张量 vs 标量哨兵 mask 两类模型覆盖causal-tri/bound 现由mCausalLayoutinputs[3] 形状自动 gate非手动 env。任何 attention/softmax 改动都要同时覆盖 causal标量 mask与非 causal真实张量 mask两条路径每一个新增 env var默认不设 每个显式值都跑一遍Env 只在 static 初始化时读一次static const int kX getenv(...)不同值 完全不同分支至少 2 个模型 shapehead_dim ∈ {64, 128, 256}×group_size ∈ {1, 2, 4, 8}Qwen3-0.6B (D128, G2)、Qwen3-4B (D128, G4)、Qwen3.5-2B (D256, G4) — 每个都可能踩不同 layout / stride 分支Mask 语义数据驱动非 causal 模型SWA / prefix LM / bidirectional无需再设 envMetal 从 mask 张量形状自动判定真实张量 ⇒ 逐元素 honor、关全部 causal 优化标量/无 maskkvcache ⇒ causal。若非 causal 仍乱码查gen_attention_mask是否为该模型产出真实张量 mask非误走标量判据跟 baseline 前 N (≥ 20) tokens byte-identical或至少输出语义合理无乱码 / 无异常重复 / 无语言跳变。Baseline 选取原则首选CPU 后端 greedy 输出layout 无关最干净的 oracle次选已知正确的 Metal path比如改prefill_qkv_tensor时用 FA on 的输出对拍Step 3. 全模型正确性 sweep模板MAX_TOKENS30 for M in qwen3-0.6b-head-b32 qwen3-4b-head-b32 qwen3.5-0.8b-head-b32 qwen3.5-2b-head-b32; do CFG/Users/jiuqi/models/${M}/config_mtl_greedy.json for FA in 1 0; do echo ${M} FA${FA} MNN_ENABLE_FLASH_ATTN_PREFILL$FA \ DYLD_LIBRARY_PATHbuild:build/express build/llm_demo \ $CFG /tmp/prompt_2048_oneline.txt $MAX_TOKENS 21 \ | awk /^prompt file is/{f1;next} /^#####/{f0} f | head -3 echo done done新增 env var 时把外层循环再加一维for E in 0 1 default; do ...。Step 4. 只在 Step 2/3 全过后才跑性能对比先看正确性正确性 OK 后才有理由测 t/s 数字。跑 3-rep A/BWARMUP SHUFFLE消噪声见下文“性能测试”一节。二、编译标准 / profiling / converter 三种 cmake 配置# 标准 Metal LLM 编译 mkdir -p build cd build cmake .. -DMNN_METALON -DMNN_BUILD_LLMON -DMNN_LOW_MEMORYON -DMNN_SUPPORT_TRANSFORMER_FUSEON make -j8 llm_demo llm_bench MNN # 带 profiling 编译Step 1 cmake .. -DMNN_METALON -DMNN_BUILD_LLMON -DMNN_LOW_MEMORYON -DMNN_SUPPORT_TRANSFORMER_FUSEON -DMNN_METAL_OP_PROFILEON make -j8 llm_demo # 带 converter导出模型需要 cmake .. -DMNN_METALON -DMNN_BUILD_LLMON -DMNN_LOW_MEMORYON -DMNN_SUPPORT_TRANSFORMER_FUSEON -DMNN_BUILD_CONVERTERON make -j8 llm_demo MNNConvert各选项的核心作用MNN_METALON启用 Metal 后端对应source/backend/metal/目录。MNN_BUILD_LLMON构建 LLM 引擎产出llm_demo与llm_bench两个可执行目标见 transformers/llm/engine/CMakeLists.txt其中llm_demo由demo/llm_demo.cpp构建。MNN_LOW_MEMORYON低内存模式对大模型部署KV cache 常驻尤为重要。MNN_SUPPORT_TRANSFORMER_FUSEON启用 transformer 算子融合支持是 LLM 场景下减少 dispatch 数量、降低 CPU 侧开销的前提。MNN_METAL_OP_PROFILEONper-op profiling用于定位具体 kernel 的瓶颈配合技能体系中 skills/metal-optimize/op-bench-and-diagnosis.md 的诊断流程。MNN_BUILD_CONVERTERON构建模型转换工具MNNConvert导出 LLM 模型时必需。注意这些开关调整后同样建议重新跑cmake ..避免缓存旧的源文件清单或宏定义。三、模型导出llmexport.pyMNN LLM 模型的导出入口在 transformers/llm/export/llmexport.pycd transformers/llm/export python llmexport.py --export mnn \ --path /path/to/HuggingFace/model \ --mnnconvert /path/to/build/MNNConvert--export mnn指定导出为 MNN 格式。--pathHuggingFace 格式模型的本地路径。--mnnconvert指向上一节编译出的MNNConvert可执行文件路径。导出产物包含模型文件与config.json。务必记得导出的默认config.json是 CPU 后端 mixed 采样见 Step 1.5落地到 Metal 测试前需先生成config_mtl_greedy.json这类 deterministic config。四、性能测试llm_bench 的 -pg 正确用法用llm_bench而不是llm_demo测性能且必须带-pg。-pg pp,tgprefill pp 个 token 后复用该 KV cache 继续 decode tg 个 token分列报告 prefill / decode 两个速度。参数语义在源码中有直接对应见 transformers/llm/engine/tools/llm_bench.cpp 的参数解析-pg pp,tg追加一组 prefilldecode 组合testParams.nPrompGen可重复传多组累加。-kv--kv-cache已废弃。源码中传入-kv true时直接打印弃用提示-p A -n B -kv true-pg A,B。单独的-p/-nprefill-only / decode-only 口径默认值 512/128 会额外生成独立测试只想跑-pg时须加-p 0 -n 0压掉。-a--backends直接指定后端metal映射到 Metal源码中type metal记 1无需改config.jsoncpu及其他后端同理。-t线程数CPU 后端对比时常用。-rep--n-repeat重复次数用于消噪。-fa--flash-attentionFA 开关的 A/B 控制。cd build # Metal 后端-a metal 直接指定无需改 config.json ./llm_bench -m /path/to/model/config.json -a metal -p 0 -n 0 -pg 512,128 -rep 3 # CPU 后端对比 ./llm_bench -m /path/to/model/config.json -a cpu -t 4 -p 0 -n 0 -pg 512,128 -rep 3 # 不同 prompt 长度一次跑多组 ./llm_bench -m /path/to/model/config.json -a metal -p 0 -n 0 \ -pg 64,64 -pg 512,128 -pg 2048,128 -rep 3 # FA A/B ./llm_bench -m /path/to/model/config.json -a metal -p 0 -n 0 -pg 512,128 -rep 3 -fa 0 ./llm_bench -m /path/to/model/config.json -a metal -p 0 -n 0 -pg 512,128 -rep 3 -fa 1 # 长 prompt 内存受限chunk FA KV int8 # config.json 加 chunk: 512, attention_mode: 10跑 3-rep A/B 时建议 WARMUP SHUFFLE 交替配对消噪声——热态漂移能造出虚假收益这是 Metal 后端基准测试的通用纪律详见 skills/metal-optimize/SKILL.md 的“通用原则速览”第 7 条。五、正确性验证LLM 场景CPU 与 Metal 对拍# CPU 和 Metal 同 prompt temperature0前 N token 应一致 # config 中设 temperature: 0.0 # CPU 基线 ./llm_demo config_cpu.json prompt.txt 30 # Metal 对比 ./llm_demo config_metal.json prompt.txt 30 # FA A/B 对比同一 config MNN_ENABLE_FLASH_ATTN_PREFILL0 ./llm_demo config_metal.json prompt.txt 30 off.log MNN_ENABLE_FLASH_ATTN_PREFILL1 ./llm_demo config_metal.json prompt.txt 30 on.log diff off.log on.log要点对拍的前提是确定性采样greedy temperature 0CPU 后端是 layout 无关的 oracleMetal 内部不同路径之间如 FA on/off也应对拍确保两条实现语义一致。六、Attention causal 假设加载非标准模型前必读Metal 后端两条prefill attention 路径都硬编码了“attention mask 是 causal lower-triangular”的假设运行时不做验证三段路径prefill_qk[_tensor]softmaxprefill_qkv[_tensor]CAUSAL_TRI host 只 dispatch 对角线以下 tileCAUSAL_BOUND softmax 只归约 valid prefix zero-padAV 用av_k_upper早退。违反假设 → 上三角“应有效”位置被静默丢弃。这段逻辑在 source/backend/metal/MetalAttentionShader.hpp 的prefill_qk/prefill_qk_tensor中有明确体现非 causal 的 tile 直接写-FLT_MAX退出省掉整块 QK matmul这也是 causal 优化省 ~50% tile 的原理。FA 路径prefill_flash_attnin_bounds (kv_col_abs q_abs kv_valid_offset)hard-code causal位于 source/backend/metal/MetalAttention.mm。因此以下模型加载 Metal 后端会静默错不崩、不 warning、只是输出乱模型类别举例症状Sliding Window AttentionMistral 7B v0.1, Gemma-2, Ministral短 prompt 可能对超过 window size 后开始漂移Mixed window层交替Gemma-2每层交替 SWA / full层内 window 边界后开始错Prefix LMBaichuan-Base 前缀部分、UL2从第一 token 就错Encoder-decoder cross-attentionT5、UL2、Whisper完全不适用BERT-family bidirectional任何 encoder 模型完全不适用数据驱动的 causal 判定2026-07-31 起自 2026-07-31 起causal 语义不再依赖手动 env而是由 mask 张量形状数据驱动。在 source/backend/metal/MetalAttention.mm 中mHasTensorMask inputs.size() 3 inputs[3]-dimensions() 2真实张量 mask 判定mCausalLayout scalarCausalSentinel mKVCache标量哨兵 mask KV cache ⇒ causal。即标准 causal 模型发标量哨兵 mask非 causal 模型发真实张量 maskMetal 自动分流——真实张量 ⇒ 逐元素 honor、关掉 causal-tri/bound 及 FA标量/无 mask kvcache ⇒ causal优化全开。mCausalLayout是后续mQkCausalTri/mQkCausalBound/ FA 路径选择的总 gate。准入检查导入新模型前跑一次# 数据驱动检测无需 A/B env 对拍。直接与 CPU 后端 greedy 对拍前 20 token DYLD_LIBRARY_PATHbuild:build/express build/llm_demo cfg_metal prompt 20 /tmp/a.log 21 DYLD_LIBRARY_PATHbuild:build/express build/llm_demo cfg_cpu prompt 20 /tmp/b.log 21 diff (awk /^prompt file is/{f1;next}/^#####/{f0}f /tmp/a.log) \ (awk /^prompt file is/{f1;next}/^#####/{f0}f /tmp/b.log) # ✓ 无 diff → Metal 输出与 CPU 一致正确 # ✗ 有 diff → 查 gen_attention_mask 是否为该模型走了正确分支真实张量 vs 标量 # 根因多在 mask 生成/导出侧推荐操作加载Qwen / Llama / Phi / DeepSeek / Yi / Baichuan-Chat等纯 causal LLM直接跑标量哨兵 mask causal 优化全开。加载 SWA / prefix / bidirectional 模型无需再设任何 env数据驱动真实张量 mask 自动触发逐元素 honor、关掉 causal-tri/bound/FA。前提是gen_attention_mask为该模型产出真实张量 maskSWA 走attention_typemix双平面确认导出 config 正确。不确定模型是不是 causal读 HFconfig.json里有没有sliding_window/attention_bias/is_encoder_decoder字段。注意causal-tri/bound 及 FA 路径的 causal 假设现由mCausalLayoutinputs[3] 形状统一 gate。真实张量 mask 会一并关掉 FA不再需要手动关MNN_ENABLE_FLASH_ATTN_PREFILL。七、总结一套可复用的验证闭环把本文内容收敛成每次 Metal 改动后的最小动作序列重建新增文件先cmake ..改动 shader 用touch或make -B强制重编。断言新鲜度ls -l build/libMNN.dylib时间戳 strings | grep新符号非 0。清 cache删mnn_cachefile.bin避免 pipeline JIT 命中旧 binary。造 greedy config新模型先用脚本生成config_mtl_greedy.jsonmetal greedy temperature 0。正确性矩阵短/中/长 prompt × FA on/off × 每个新增 env 的每个值 × ≥2 种模型 shape × causal/非 causal 两条 mask 路径对拍 CPU greedy baseline 前 ≥20 token。全模型 sweep脚本化循环覆盖多个模型与 FA 开关。性能 A/B正确性全过之后用llm_bench -a metal -p 0 -n 0 -pg pp,tg -rep 3分列 prefill/decode 速度配对消噪后再下结论。这套流程与技能体系中的其他分册形成闭环op-bench-and-diagnosis.md负责“瓶颈在哪”kernel-dev-and-optimize.md/graph-fusion.md/runtime-scheduling.md负责“怎么改”本文则负责“改完如何验证与对拍”任何一步缺失都可能把假信号当成真收益。【免费下载链接】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),仅供参考