ik_llama.cpp Vulkan 后端新增 GGML_OP_FUSED_MUL_UNARY 融合算子:实现原理与性能评估
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
本篇文章以 ik_llama.cpp 仓库 PR #580(Vulkan: add GGML_OP_FUSED_MUL_UNARY,作者 ikawrakow,2025-07-03)为骨架,深入解析该融合算子在 GGML 计算图中的语义、Vulkan 后端的管道实现与调度路径,并完整呈现作者在同一硬件环境下对比 mainline llama.cpp 与 ik_llama.cpp 的 sweep-bench 实测数据。读完本文,你将理解"mul + 一元激活"为何值得融合、Vulkan 后端如何为 F32/F16 各挑选专用 shader 管道,以及该 PR 对计算图构建去特化的真正价值。
PR 背景与动机:让 Vulkan 不再被特殊对待
在 LLM 的 Transformer 前馈网络中,SiLU(SwiGLU)、GELU等激活函数几乎总是紧跟在一次逐元素乘法之后(例如 MoE 的 up/gate 分支、MLP 的 gate 投影)。如果计算图把mul与unary拆成两个独立节点,后端在执行时就需要两次内存读写;而将它们合并为一个GGML_OP_FUSED_MUL_UNARY节点,可以在一次 kernel 内核循环里同时完成乘法和激活,减少一次中间张量的落盘与回读。
作者在 PR 描述中直言:该改动带来的性能提升极其微小("The tiniest of performance increases, barely measurable"),但真正的收益在于工程层面——构建计算图时不再需要对 Vulkan 做特殊处理("we no longer need to special-case Vulkan when building the graph")。也就是说,此前图构建阶段需要为 Vulkan 后端单独绕开或改写mul + unary模式,而有了该算子后,CPU、CUDA、Metal、Vulkan 各后端可以共享同一条融合路径,图构建逻辑更统一、更干净。
算子语义:GGML 层的融合定义
GGML_OP_FUSED_MUL_UNARY是 GGML 算子枚举的一员,定义于 ggml/include/ggml.h,紧随GGML_OP_FUSED_RMS_NORM之后,属于"融合类"算子家族;其文本名称"FUSED_MUL_UNARY"记录在 ggml/src/ggml.c 的算子名表中。
它的语义实现位于 ggml/src/ggml.c 的ggml_fused_mul_unary_impl,核心逻辑如下:
- 约束一:
a必须是连续(contiguous)张量;输出张量result的op被置为GGML_OP_FUSED_MUL_UNARY,src[0] = a、src[1] = b,一元算子类型通过op_params[0]记录; - 约束二(形状相同路径):当
a与b形状相同时,允许的激活算子为GGML_UNARY_OP_GELU、GGML_UNARY_OP_RELU、GGML_UNARY_OP_SILU、GGML_UNARY_OP_SIGMOID; - 约束三(形状不同路径):当
a与b形状不同(要求a->ne[0] == 1,即a为按行广播的权重),仅允许GGML_UNARY_OP_SILU或GGML_UNARY_OP_SIGMOID——这正是 MoEup_gate融合(fused_up_gate)等场景的典型形态,a是共享的 gate 权重、b是逐 token 的激活值。
在 CPU 端,算子分发位于 ggml/src/ggml.c,由ggml_compute_forward_fused_mul_unary完成逐元素计算;CUDA 与 Metal 后端同样在各自算子列表中处理该 op(见 ggml/src/ggml-cuda.cu 与 ggml/src/ggml-metal.m)。这说明该算子并非 Vulkan 独有,而是 GGML 跨后端的统一融合抽象,PR #580 正是把这一能力补齐到了 Vulkan。
Vulkan 后端实现:管道选择与调度路径
Vulkan 后端的实现集中在 ggml/src/ggml-vulkan.cpp,可以拆成三个层次:
1. 专用 shader 管道的注册
设备初始化阶段创建了 6 条融合管道,每个激活函数各配 F32 与 F16 两个变体(ggml/src/ggml-vulkan.cpp):
ggml_vk_create_pipeline(device, device->pipeline_fused_mul_silu[0], "fused_mul_silu_f32", fused_mul_silu_f32_len, fused_mul_silu_f32_data, ...); ggml_vk_create_pipeline(device, device->pipeline_fused_mul_silu[1], "fused_mul_silu_f16", fused_mul_silu_f16_len, fused_mul_silu_f16_data, ...); ggml_vk_create_pipeline(device, device->pipeline_fused_mul_gelu[0], "fused_mul_gelu_f32", ...); ggml_vk_create_pipeline(device, device->pipeline_fused_mul_gelu[1], "fused_mul_gelu_f16", ...); ggml_vk_create_pipeline(device, device->pipeline_fused_mul_relu[0], "fused_mul_relu_f32", ...); ggml_vk_create_pipeline(device, device->pipeline_fused_mul_relu[1], "fused_mul_relu_f16", ...);对应管道句柄数组声明于 ggml/src/ggml-vulkan.cpp。
2. 算子支持判定与管道匹配
在ggml_vk_op_supports_op的 switch 分支(ggml/src/ggml-vulkan.cpp)中,对GGML_OP_FUSED_MUL_UNARY做了严格的类型与激活匹配:
- 三个张量(src0、src1、dst)只接受
F32或F16,且三者类型必须一致,否则返回nullptr表示不支持; - 从
dst->op_params[0]读取一元算子类型,按SILU、GELU、RELU分别返回对应的fused_mul_*管道,并以dst->type == GGML_TYPE_F16作为数组下标选择 F16 变体; - 其它一元算子(如 SIGMOID 之外的组合)直接返回不支持,走回退路径。
3. 调度与执行
执行入口ggml_vk_fused_mul_unary(ggml/src/ggml-vulkan.cpp)做了三项断言(src0 连续、src0 与 src1 同形、src0 与 dst 同形),然后调用泛型调度ggml_vk_op_f32<vk_op_push_constants>,以GGML_OP_FUSED_MUL_UNARY为 op、以ggml_nelements(src0)为元素总数下发 GPU 任务。该 op 被列入逐元素计算组的 grid 拆分逻辑(ggml/src/ggml-vulkan.cpp),与MUL、ADD、UNARY等共享同一套 workgroup 划分策略:元素数超过 262144 时按 512×512×Z 三维拆分,否则退化为 512×Z 或一维,保证大张量下的调度效率。
最终在ggml_vk_compute_forward的分发处(ggml/src/ggml-vulkan.cpp)直接落入ggml_vk_fused_mul_unary。整个链路"支持判定 → 管道匹配 → 元素级调度 → 单 kernel 完成 mul+激活"由此闭合。
性能评估:同环境下的 sweep-bench 实测
作者在 PR 中附带了完整的 sweep-bench 数据。测试环境为:RTX-4080 GPU + Ryzen-5975WX 主机,Nvidia 驱动 575(启用 coopmat2 特性),模型为 LLaMA-3.1-8B-Instruct,prefill(PP)1024、生成(TG)256,N_KV 从 0 递增到 15360。
mainline llama.cpp(build 5821)
| PP | TG | N_KV | T_PP s | S_PP t/s | T_TG s | S_TG t/s |
|---|---|---|---|---|---|---|
| 1024 | 256 | 0 | 0.244 | 4200.63 | 2.742 | 93.37 |
| 1024 | 256 | 1024 | 0.256 | 4002.55 | 2.730 | 93.78 |
| 1024 | 256 | 2048 | 0.282 | 3634.36 | 2.827 | 90.57 |
| 1024 | 256 | 3072 | 0.297 | 3452.54 | 2.897 | 88.36 |
| 1024 | 256 | 4096 | 0.319 | 3212.57 | 2.956 | 86.62 |
| 1024 | 256 | 5120 | 0.335 | 3056.04 | 3.045 | 84.08 |
| 1024 | 256 | 6144 | 0.349 | 2937.19 | 3.126 | 81.90 |
| 1024 | 256 | 7168 | 0.365 | 2807.69 | 3.184 | 80.40 |
| 1024 | 256 | 8192 | 0.378 | 2710.13 | 3.284 | 77.97 |
| 1024 | 256 | 9216 | 0.396 | 2589.00 | 3.364 | 76.10 |
| 1024 | 256 | 10240 | 0.407 | 2514.06 | 3.453 | 74.13 |
| 1024 | 256 | 11264 | 0.424 | 2415.06 | 3.518 | 72.77 |
| 1024 | 256 | 12288 | 0.441 | 2322.30 | 3.621 | 70.70 |
| 1024 | 256 | 13312 | 0.455 | 2249.22 | 3.704 | 69.12 |
| 1024 | 256 | 14336 | 0.468 | 2190.30 | 3.786 | 67.62 |
| 1024 | 256 | 15360 | 0.485 | 2111.54 | 3.852 | 66.45 |
ik_llama.cpp(含本 PR)
| PP | TG | N_KV | T_PP s | S_PP t/s | T_TG s | S_TG t/s |
|---|---|---|---|---|---|---|
| 1024 | 256 | 0 | 0.242 | 4234.03 | 2.673 | 95.77 |
| 1024 | 256 | 1024 | 0.253 | 4052.19 | 2.646 | 96.74 |
| 1024 | 256 | 2048 | 0.265 | 3866.73 | 2.718 | 94.20 |
| 1024 | 256 | 3072 | 0.283 | 3618.48 | 2.775 | 92.25 |
| 1024 | 256 | 4096 | 0.286 | 3584.28 | 2.830 | 90.45 |
| 1024 | 256 | 5120 | 0.298 | 3441.80 | 2.905 | 88.13 |
| 1024 | 256 | 6144 | 0.321 | 3194.99 | 2.983 | 85.81 |
| 1024 | 256 | 7168 | 0.335 | 3059.90 | 3.034 | 84.37 |
| 1024 | 256 | 8192 | 0.340 | 3007.63 | 3.089 | 82.87 |
| 1024 | 256 | 9216 | 0.353 | 2897.76 | 3.128 | 81.83 |
| 1024 | 256 | 10240 | 0.364 | 2814.86 | 3.192 | 80.21 |
| 1024 | 256 | 11264 | 0.372 | 2753.81 | 3.241 | 78.99 |
| 1024 | 256 | 12288 | 0.380 | 2692.31 | 3.291 | 77.78 |
| 1024 | 256 | 13312 | 0.397 | 2580.87 | 3.370 | 75.97 |
| 1024 | 256 | 14336 | 0.412 | 2486.45 | 3.444 | 74.33 |
| 1024 | 256 | 15360 | 0.423 | 2420.49 | 3.498 | 73.19 |
数据解读
- 融合本身收益微小:对比两表同配置行,prefill 与生成吞吐的差距主要来自 PR 之外的既有差异,
FUSED_MUL_UNARY带来的直接增益正如作者所说"barely measurable"——这也是该 PR 定位为工程整洁性改进而非性能突破的原因; - ik_llama.cpp 在 16k 上下文下整体快 10–15%:在 N_KV=15360 时,mainline 的 prefill 为 2111.54 t/s、生成 66.45 t/s,而 ik_llama.cpp 为 2420.49 t/s(+14.6%)与 73.19 t/s(+10.1%);在 N_KV=0 时差距约 0.8%(生成)到 2.6%(prefill)。需要强调:这一对比发生在 ik_llama.cpp尚无显著 Vulkan 专属优化(作者原话 "there aren't any noticeable Vulkan optimizations in ik_llama.cpp yet")的前提下,且测试环境开启了 coopmat2 特性;
- 关于差异来源的推断:作者对 mainline 在长上下文下的相对劣势提出一个待验证的猜测——是否 mainline 正在为"大统一的 KV cache 重构"付出性能代价。这属于作者在 PR 中的个人观察与推测,并非定论,读者可结合自身硬件与构建版本复测。
结语:一次"为未来铺路"的算子补齐
PR #580 的价值可以从三个层面理解:
- 算子层面:Vulkan 后端完整支持
GGML_OP_FUSED_MUL_UNARY,覆盖 F32/F16 下的 SiLU、GELU、RELU 三种融合激活,与 CPU/CUDA/Metal 后端对齐; - 工程层面:消除了计算图构建阶段对 Vulkan 的特殊分支处理,使各后端共享统一的融合图构建路径,降低后续维护与扩展成本;
- 数据层面:为 ik_llama.cpp 在 Vulkan 路径上的后续专项优化(如 KV cache 相关改动、coopmat2 特性的进一步利用)提供了基准起点——融合算子已就位,后续针对 Vulkan 的深度优化可以在这个统一的图结构上展开,而不必再被特判逻辑牵制。
对于希望复现的读者,可在当前仓库构建 ik_llama.cpp 后使用sweep-bench示例(examples/sweep-bench/sweep-bench.cpp)与llama-bench(examples/llama-bench/llama-bench.cpp)复测同类场景;Vulkan 后端相关代码与管道定义均可在 ggml/src/ggml-vulkan.cpp 中查阅。
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考