ik_llama.cpp Vulkan 后端新增 GGML_OP_FUSED_MUL_UNARY 融合算子:实现原理与性能评估
2026/9/20 23:03:37 网站建设 项目流程

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 投影)。如果计算图把mulunary拆成两个独立节点,后端在执行时就需要两次内存读写;而将它们合并为一个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)张量;输出张量resultop被置为GGML_OP_FUSED_MUL_UNARYsrc[0] = asrc[1] = b,一元算子类型通过op_params[0]记录;
  • 约束二(形状相同路径):当ab形状相同时,允许的激活算子为GGML_UNARY_OP_GELUGGML_UNARY_OP_RELUGGML_UNARY_OP_SILUGGML_UNARY_OP_SIGMOID
  • 约束三(形状不同路径):当ab形状不同(要求a->ne[0] == 1,即a为按行广播的权重),仅允许GGML_UNARY_OP_SILUGGML_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)只接受F32F16,且三者类型必须一致,否则返回nullptr表示不支持;
  • dst->op_params[0]读取一元算子类型,按SILUGELURELU分别返回对应的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),与MULADDUNARY等共享同一套 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)

PPTGN_KVT_PP sS_PP t/sT_TG sS_TG t/s
102425600.2444200.632.74293.37
102425610240.2564002.552.73093.78
102425620480.2823634.362.82790.57
102425630720.2973452.542.89788.36
102425640960.3193212.572.95686.62
102425651200.3353056.043.04584.08
102425661440.3492937.193.12681.90
102425671680.3652807.693.18480.40
102425681920.3782710.133.28477.97
102425692160.3962589.003.36476.10
1024256102400.4072514.063.45374.13
1024256112640.4242415.063.51872.77
1024256122880.4412322.303.62170.70
1024256133120.4552249.223.70469.12
1024256143360.4682190.303.78667.62
1024256153600.4852111.543.85266.45

ik_llama.cpp(含本 PR)

PPTGN_KVT_PP sS_PP t/sT_TG sS_TG t/s
102425600.2424234.032.67395.77
102425610240.2534052.192.64696.74
102425620480.2653866.732.71894.20
102425630720.2833618.482.77592.25
102425640960.2863584.282.83090.45
102425651200.2983441.802.90588.13
102425661440.3213194.992.98385.81
102425671680.3353059.903.03484.37
102425681920.3403007.633.08982.87
102425692160.3532897.763.12881.83
1024256102400.3642814.863.19280.21
1024256112640.3722753.813.24178.99
1024256122880.3802692.313.29177.78
1024256133120.3972580.873.37075.97
1024256143360.4122486.453.44474.33
1024256153600.4232420.493.49873.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 的价值可以从三个层面理解:

  1. 算子层面:Vulkan 后端完整支持GGML_OP_FUSED_MUL_UNARY,覆盖 F32/F16 下的 SiLU、GELU、RELU 三种融合激活,与 CPU/CUDA/Metal 后端对齐;
  2. 工程层面:消除了计算图构建阶段对 Vulkan 的特殊分支处理,使各后端共享统一的融合图构建路径,降低后续维护与扩展成本;
  3. 数据层面:为 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询