MNN Metal 优化 Skill(索引)
触发:新增或修改 Metal kernel / shader / dispatcher;LLM decode/prefill 性能优化; 算子融合(导出侧声明 + 后端单 dispatch);per-op profiling 定位瓶颈;跑 Metal LLM 测试或对拍。
边界:不读不改
schema/private/、source/internal/。
使用方式
先按任务类型定位到具体 sub-doc,本文件只做索引和路由,不承载技术内容。
记录规则(默认不记录)
每次实验的结果不需要写进 skill。 skill 是给下一个人用的操作知识,不是实验流水账。绝大多数 A/B 跑完就完了,写在 commit message 和 PR 描述里即可。
只有满足下列之一才值得写进 skill,且只写"可复用的那一句",不搬数据:
| 值得写 | 写到哪 | 写成什么 |
|---|---|---|
| 一条跨形状/跨芯片仍然成立的判据(例:"coop-tensor 路径加宽 K 赚、加宽 N 亏,因为 destination capacity 只跟 N 走") | 对应 sub-doc 的方法论节 | 一句原理 + 适用边界,不带百分比 |
一个会让人重复踩的陷阱(编译期宏四处同步、内存别名、run_test.out 用 precision 1 静默关掉 fp16 kernel、zsh 不分词导致开关没生效) |
kernel-dev-and-optimize.md §1.6 或本文件"通用原则速览" |
症状 + 根因 + 规避写法 |
| 一个新增/删除/改名的 env 开关 | env-registry.md |
只写名称 / 默认值 / 功能 / 生效条件 |
| 一条新的路由或默认值(哪种 shape 走哪条 kernel) | kernel-dev-and-optimize.md 路由速查 |
条件 → 走哪条 kernel |
| 一个已证伪且别人很可能重试的方向 | 只在能提炼成可复用判据时,写进对应手段的「适用边界」 | "这类手段在 Z 条件下不成立,因为 Y"——不留实验档案 |
不要写进任何 skill 文件:
- 单次 A/B 的百分比、ms、GB/s、tok/s、配对明细、扫点表、per-rep 方差;
- 标定过程叙事(先测什么后测什么、哪次翻案、哪次是漂移伪影);
- 只在一台机器一个模型上成立的数字;
- 已回退的实验代码的实现细节。
kernel-dev-and-optimize.md 有额外的硬性约束:它记的是每个手段为什么能赚——赚在哪一类资源上(带宽 / 并行度 / 指令 / 同步)、什么条件下成立、有什么陷阱。不写任何性能数字(百分比、ms、GB/s、TFLOPS、tok/s、扫点表、配对明细),不写日期 / commit hash / 具体机型与模型名(设备与形状只以"类别"出现,如"tensor-coop 设备"、"GQA group 较大的模型"),不写反例存档与标定叙事。详见该文件开头的记录规则。
runtime-scheduling.md 只记录优化方法与原理:保留开销来源、适用条件、安全边界、代码入口与验证方式;不写任何实测数据或实验叙事,包括耗时、吞吐、占比、实测计数、A/B 结果及测试通过数量。详见该文件开头的记录规则。
env-registry.md 有额外的硬性约束:它只是"当前存在哪些开关、默认什么、干什么、什么条件下生效"的查询表。任何环境变量带来的性能收益或损失,一律不许写进那个文件——包括百分比、配对结果、转正/证伪结论、commit hash、日期、设备与模型名。详见该文件开头的约束清单。
Sub-doc 结构
| 文件 | 何时阅读 | 内容 |
|---|---|---|
op-bench-and-diagnosis.md |
比对手慢但不知道慢在哪;或已知某 kernel 慢,想知道"还剩多少空间 / 往哪改 / 何时收工"——任何优化任务的第一站 | 诊断流程:为什么必须「op 单测(信噪比)+ 对手基准(绝对标尺)」两件一起上;怎么造 op 单测(5 条构造要求、品质因子选 GB/s 还是 TFLOPS);怎么造对手镜像(6 项可比性核对、惰性图 donation 陷阱);三种诊断的选用(绝对标尺 / 固定-流式分解 t=a+b·N / 消融阶梯);标定尺度分工表(路径开关认 e2e、kernel 内部旋钮认 op 单测);验证(改归约顺序不能 byte-compare、env 覆盖当零重建基线);收工判据;度量卫生 + 配对脚本模板;现成资产清单;5 个案例总表 |
kernel-dev-and-optimize.md |
写或改任何 Metal kernel 之前都先读第一部分;做 kernel 层性能优化时读第二部分 | 第一部分 开发规范:核心原则、dispatcher 结构、shader 组织与 kernel 命名约定、变量/宏名必须等于真实物理量、编译期宏与 pipeline 缓存 key 的四处同步、Execution 骨架与注册、9 个通用陷阱(A–I)、packed weight 设计、修改流程、正确性验证、tensor API cooperative tensor 布局、Apple GPU 杠杆。第二部分 优化知识库(只写机制与原理,不含数字 / 日期 / 反例存档):优化总纲与 decode 画像、kernel 内部消融阶梯(逐类删工作定位内部瓶颈 + 地板值 vs 对手)、GEMV(deferred dequant / 双 SG / pre-scaling / split-K / GEMV_2OCQUAD_PER_SG / W2W3 / 短序列缺口)、GEMM(fused Q4 / M64 / in-shader 阈值)、Attention(causal-tri / FA / tensor-API tc / 单 pass SDPA / SDPA_QH_PER_TG / QSPLIT / GQA / 路由速查)、其他 kernel(RMSNorm 小 batch / LinearAttention scan 演进 / gated norm)、§2.5 kernel 手段方法论(原理/适用/陷阱/验证,选题与方案评审用) |
graph-fusion.md |
要让多个算子合成一次 dispatch;改 FusedLinear / GatedRMSNorm;排查"融合没命中 / 融合后输出错" |
融合全链路:Python 导出期声明分组 → converter LN 吸收 → geometry 兜底拆分 → Metal setupFusion 装配 leader/follower。含内存别名铁律与 STATIC re-home、链式门控依赖、GatedRMSNorm 独立链路、已删除的后端图匹配历史、排查清单、§9 融合方法论(同构投影合并 / 打包 grid / 正确性门槛) |
runtime-scheduling.md |
怀疑 decode 有 CPU 阻塞 / GPU 空泡;改 resize 时机、commit 节奏、H2D、Encode Replay | per-backend fence、content-cache、队内 H2D 上传、采样去框架化(host ArgMax/topK)、完成等待自旋、commit cadence、Encode Replay(安全模型 / attention 与 LinearAttention 接入 / KV 悬垂指针坑)、调度类改动的验证套路、§9 调度方法论(同步点治理 / 自动阈值 / 开关收敛纪律) |
build-and-test.md |
改完代码要 build / 跑测试 / 对拍 | cmake 编译命令、模型导出命令、性能测试命令、CPU/Metal 对拍 |
env-registry.md |
查 / 新增 / 删除 Metal 相关环境变量开关 | env 集中登记,只有名称 / 默认值 / 功能 / 生效条件四列:attention prefill、attention decode、LayerNorm、decode GEMV、量化 GEMM、融合、调度、profiling。含新增开关规范。⛔ 禁止写入任何性能数字与实验叙事 |
快速任务→sub-doc 索引
| 想做的事 | 优先读 |
|---|---|
| 新加一个 Metal op / kernel(该叫什么名、写在哪、怎么注册) | kernel-dev-and-optimize.md §1.3 / §1.5 |
| 加一个编译期变体宏 | kernel-dev-and-optimize.md §1.4(四处必须同步) |
| 新加 quant bit / 改 dispatcher 路径 | kernel-dev-and-optimize.md §1.2 / §1.6 / §1.7 |
| 想知道 Metal 的坑(宏 alias / weight byte order / getDequantScale coef) | kernel-dev-and-optimize.md §1.6 |
| Apple GPU 优化杠杆选择(sg_matrix / sg_reduce / tensor API) | kernel-dev-and-optimize.md §1.11 / §1.10 |
| 比对手慢,但不知道慢在哪 | op-bench-and-diagnosis.md——优化任务第一站,含完整诊断流程 |
| 要新建一个 op 级 speed 单测 / 对手镜像 | op-bench-and-diagnosis.md §1 / §2(5 条构造要求 + 6 项可比性核对) |
| 差距随规模(ctx/seq)变化,不知道是固定开销还是流式速率 | op-bench-and-diagnosis.md §3.2 固定/流式分解 t = a + b·N |
| 旋钮该在 op 单测还是 e2e 上标定 | op-bench-and-diagnosis.md §4 尺度分工表 |
| 决定要不要投入某个优化方向(先看这条) | kernel-dev-and-optimize.md §2.0——先自测 GPU busy vs wall;若已 GPU-bound 且 occupancy 受限,<5us 级 GPU 节省不兑现为 wall |
| 已知某 kernel 慢,但不知道它内部哪一段慢 | kernel-dev-and-optimize.md §2.0「kernel 内部消融阶梯」——逐类删工作读差值,得到分项成本表 + 地板值 |
| 优化选题 / 方案评审 / 复盘优化方向 | 方法论三节:kernel-dev-and-optimize.md §2.5、graph-fusion.md §9、runtime-scheduling.md §9(只讲原理/适用/陷阱/验证,不含数字) |
| GEMV 优化(decode 主战场) | kernel-dev-and-optimize.md §2.1 |
| GEMM / prefill 优化 | kernel-dev-and-optimize.md §2.2 |
| Attention 优化 / 想知道当前走哪条路径 | kernel-dev-and-optimize.md §2.3(§2.3.8 是路由速查) |
改 prefill_flash_attn_tc(coop-tensor prefill attention) |
机制见 kernel-dev-and-optimize.md §2.3.4;FATC 开关默认值与生效条件见 env-registry.md |
| LinearAttention(Qwen3.5 gated delta rule) | kernel-dev-and-optimize.md §2.4.2 / §2.4.3 |
| 算子融合:为什么没命中 / 融合后输出错 | graph-fusion.md §8 排查清单 |
| 新模型结构要加融合 | graph-fusion.md §1(导出侧声明)+ §4(后端装配) |
| 融合后输出逐次不同 | graph-fusion.md §4.3 内存别名 |
| decode 每 token 的 CPU 阻塞 / 同步开销 | runtime-scheduling.md |
| Encode Replay 相关(新 op 要不要接入 / 为什么被 ban) | runtime-scheduling.md §7 |
| cmake 编译选项 / 模型导出命令 | build-and-test.md |
| 查某个 env 开关的默认值和语义 | env-registry.md |
选题决策路径
瓶颈在哪?
├─ 还不知道 / 只知道"比对手慢" → op-bench-and-diagnosis.md(造 op 单测 + 对手镜像,再做固定/流式分解)
├─ 每 token 固定开销大(dispatch 多 / CPU 段长) → 融合方法论 graph-fusion.md §9 / 调度方法论 runtime-scheduling.md §9
├─ 权重带宽未吃满(decode GEMV) → kernel 方法论 §2.5.3/§2.5.4
├─ attention 随 KV 变慢 → kernel 方法论 §2.5.4/§2.5.5/§2.5.8
├─ GPU→CPU 同步密集 → runtime-scheduling.md §9.2/§9.1
├─ 知道是哪个 kernel,不知道它内部哪段慢 → 消融阶梯 kernel-dev-and-optimize.md §2.0
└─ 不确定 → 先 profile(§2.0 GPU busy vs wall)
通用原则速览(细节见 kernel-dev-and-optimize.md)
- shader 是嵌入的 C++ 字符串(
R"metal(...)metal";在*Shader.hpp里),不是独立.metal文件;公共头靠字符串拼接共享。改完直接 make。 - 变体用
preprocessorMacros,不用 function constants。加宏必须同步改四处:shader#ifdef、pipeline 缓存 key、宏字典、onResize的 grid/threadgroup。 - dispatcher 要先摸清:一个 op 常有多条 kernel,扩之前先决定支持哪几条 + 让其他路径显式 fallback。
- Apple GPU ≠ Android:M3/M4/M5 之间都不能互推,更不能推 Vulkan/OpenCL。设备分档看
architecture.name/isSupportTensorApi()。 - 正确性 oracle 先于性能:fp32(
precision: high)bit-identical 是最强证据;fp16 greedy 对拍是次强。token 级一致不等于 bit 级一致。错误 kernel 一样能"变快",对拍通过之前测出的收益都不可信。 - 融合必查内存别名:把前驱折进后继时,前驱的输入可能已被分配器复用为后继的输出。
- A/B 必须交替配对:热态漂移能造出 3 倍虚假收益;profile build 的绝对数字是伪影。
7b. zsh 不对未加引号的参数展开分词。
run() { env $3 ./bench; }传"A=1 B=2"时,env会把整串当成一个赋值(A="1 B=2"),第二个变量静默丢失——A/B 看起来"没差异",实则探针根本没打开。本次就因此误判过一轮。多变量用${=3}强制分词,或直接写A=1 B=2 ./bench前缀。任何"改动没效果"的结论,先确认开关真的传进去了。 - 先定位瓶颈段再动手:GPU busy vs wall、kernel 计时 vs e2e、带宽兑现率是三种不同口径;优化必须对准真正的那一段,否则 kernel 变快 e2e 不动。
8b. kernel 内部要归因就逐类删工作,别正向猜:临时开关删掉一类工作(输出故意错),一路删到只剩 MMA/FMA,读相邻差值得到分项成本表和地板值。然后拿地板值和对手比,不要拿总时间比——对手 ≈ 你的地板 ⇒ 差距 100% 在重叠效率(该做批量预发射 / 提高计算:访存比);对手 < 你的地板 ⇒ 差距在算法与工作量,改重叠白费力。见
kernel-dev-and-optimize.md§2.0。探针必须防 DCE(删一小段却省掉半个 kernel = 下游被整体消掉,不是发现)。 8c. 标定尺度按开关类型分:路径开关(走不走 kernel X)认 e2e——防"kernel 级快 / e2e 平"的伪收益;已确认在关键路径上的 kernel 内部参数(nsg / tile / batch)反过来——先用该 kernel 占 100% 的 op test 标定,再用 e2e 验证无回退。否则会把 e2e 分辨不出的旋钮标错:MNN_METAL_DECODE_SDPA_NSG的某一档默认值就因为标在 e2e 上而长期是错的——attention 在那个场景只占 e2e 一小片,e2e 根本分辨不出这个旋钮;改用 op test 一测就分出胜负。 8d. 对手基准要先做两件事:固定/流式分解 + 确认惰性图没改语义。(a) 差距随规模变化时,先拿两个干净规模点拟合t = a + b·N分出每次固定开销 a 和流式速率 b——曾据此发现 MNN decode attention 的 b 已胜对手基准,差距全在 a,于是所有内层 ILP 方向都不用试了。(b) 惰性图框架里,把 N 步攒成一张图再 eval 会改变 in-place donation 语义:原地 slice 更新退化成整份拷贝,我据此把对手基准的 KV append 误判成 O(ctx)(比真实贵 ~30×)。量"每步"成本必须每步 eval。 - 布局是全局不变量:数据布局改动牵动所有读写方,必须原子落地,中间状态不可运行。 9b. simdgroup MMA 的两个乘数都必须是 half,只有累加器用 float。把任一乘数提升成 float 会掉到 float MMA 速率——fused attention 的 PV 曾因此慢 1.4×(见 kernel 方法论 §2.3.9)。
- 证伪不建档,只留可复用判据:负向/中性实验绝大多数跑完就完了(写在 commit / PR 里即可)。只有当它能提炼成一句跨形状仍成立的判据时才进 skill,且写成对应手段的「适用边界」——"这类手段在 Z 条件下不成立,因为 Y"。百分比、配对明细、翻案与标定叙事一律不进 skill(见上文"记录规则")。
- 名字必须等于真实物理量:改任何后端算子 / kernel 代码时,先核对沿途变量、成员、宏、env 名是否就是它实际装的那个量(不只是自己新加的名字)。名字骗人不会报错——编译、对拍、单测全都通过,但基于它写的分档、阈值、门控会一起错。最贵的一次是把分档自变量写成"head 数",而 kernel 真正的并行度是 dispatch 的 threadgroup 数(
batch*head_num/qh),默认值换个模型就反向。名字与索引算术不符就改名(宏名 / 缓存 key / 宏字典三处同步)。见kernel-dev-and-optimize.md§1.3「变量 / 字段 / 宏名必须等于真实物理量」。
相关 Skills
skills/general-debug/— 正确性 bug / 回归诊断入口(按症状分册);Metal 常用的两册是general-debug/memory-aliasing.md(§1 内存别名 / 生命周期,含融合引入的别名竞争)与general-debug/gpu-oob.md(§6 shader 越界 / command buffer 故障)skills/opencl-optimize/、skills/vulkan-optimize/、skills/cpu/— 其他后端skills/support-new-llm/— 新增 LLM 模型的完整流程skills/test-ci/— 单测 / 回归测试