ik_llama.cpp GQA 模型 CPU 解码提速:PR 332 的 TG 性能优化与 FA 收益分析
2026/9/19 5:29:12 网站建设 项目流程

ik_llama.cpp GQA 模型 CPU 解码提速:PR #332 的 TG 性能优化与 FA 收益分析

【免费下载链接】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 #332 的改进展开,介绍该项目如何在纯 CPU 环境下针对 GQA(Grouped-Query Attention)架构模型(LLaMA-2+、Gemma 系列等)提升 TG(Token Generation,逐 token 解码)性能。文章完整复现了 PR 中使用的llama-sweep-bench基准方法与关键数据,并结合仓库源码剖析K*QV*softmax(K*Q)矩阵乘法在 Flash Attention(FA)开关下的线程分配差异,帮助读者理解 GQA 模型在 CPU 上的计算瓶颈与优化思路,同时掌握在 ik_llama.cpp 中复现这套基准实验的完整操作。

背景:GQA 模型在 CPU 解码阶段的性能瓶颈

在多轮对话与长上下文的实际使用中,模型的生成阶段(TG)是主要的耗时环节。对于 LLaMA-2 及之后的 LLaMA 系列、Gemma 系列等 GQA 架构模型,每次生成一个 token 都需要执行两次关键的矩阵运算:

  • K*Q:查询向量与键缓存(KV cache)的注意力分数计算;
  • V*softmax(K*Q):注意力分数加权后的值缓存聚合。

这两个矩阵乘法的形状由「KV 头的数量」与「KV cache 中已缓存的 token 数(N_KV)」共同决定。当只生成 1 个 token 时,Q 矩阵的行数等于 KV 头数(而非全部注意力头数),这使得传统上按 GEMV(矩阵-向量乘)方式调度的计算,在头数较少、行数很少时无法充分利用多核 CPU 的并行能力。

在 PR #332 之前,ik_llama.cpp 的 CPU 路径对这两个乘法在开启与关闭 FA 时的收益并不均衡;PR #332 的核心思路是:改变K*QV*softmax(K*Q)两个矩阵乘法之间的线程(threads)分配方式,将计算更合理地分摊到各线程上,从而在 GQA 模型上获得 TG 性能提升。从仓库当前的注意力构建代码可以看到,这两个乘法位于 build_llama.cpp 的llm_build_kvbuild_std_attention调用链中(KQ_maskkq_scalekv_headn_kv等参数均在此处汇聚),PR 正是围绕这一调用链的 CPU 后端实现进行的调度调整。

优化效果概述:FA 在 TG 阶段由劣势转为优势

根据 PR 描述,改进带来的收益分两种情况:

  • 未启用 FA(no-FA):性能提升相对较小,主要来源于上述线程分配的重新平衡;
  • 启用 FA(Flash Attention):性能提升非常显著,且FA 在 TG 阶段首次全面超越 no-FA

这一结论很重要:此前在部分场景中 FA 主要被视作提示词处理(PP)阶段的加速手段,而 TG 阶段往往因为单 token 的形状限制而收益有限。PR #332 之后,GQA 模型在 CPU 上开启 FA 进行解码同样能获得可观收益。

从实现层面看,ik_llama.cpp 的 FA 路径由ggml_flash_attn_ext提供(例如 build_deepseek2.cpp 中ggml_flash_attn_ext_set_prec(kqv, GGML_PREC_F32)的用法),其开关由cparams.flash_attn控制,最终通过公共参数-fa, --flash-attn (auto|on|off|0|1)暴露给用户,参数解析位于 common/common.cpp 的选项表(第 3079-3080 行),同时支持环境变量LLAMA_ARG_FLASH_ATTN-no-fa, --no-flash-attn反向关闭。FA 关闭时若 V-cache 为量化类型,框架还会强制将 V-cache 视作 f16 处理(common/common.cpp 中if (!cparams.flash_attn && ggml_is_quantized(cparams.type_v))的检查),这与 PR 中「V-cache 在未启用 FA 时为 f16」的测试前提完全对应。

基准方法:使用 llama-sweep-bench 复现实验

PR 作者使用仓库自带的llama-sweep-bench工具完成全部测量,其源码位于 examples/sweep-bench/sweep-bench.cpp,说明文档见 examples/sweep-bench/README.md。

工具原理

sweep-bench与普通 benchmark 的区别在于:它不是对整个上下文长度做一次平均,而是按 ubatch 大小的窗口对整个上下文做扫描,在每个窗口内分别测量 PP 与 TG 性能,从而绘制「性能随上下文长度(N_KV)变化」的曲线。其基准流程(摘自 README)为:

  1. 为上下文中每个 ubatch 大小的窗口:生成ubatch/4个 token(节省时间,不生成整个窗口);
  2. 测量生成(TG)性能;
  3. 将生成的 token 从 KV cache 中移除;
  4. 准备一批ubatch大小的随机 token;
  5. 处理该批次(PP);
  6. 测量提示词处理(PP)性能。

复现命令

PR 中给出的完整命令为:

./bin/llama-sweep-bench -m $model -c 10240 -ctk q8_0 -ctv q8_0 -t 32 -fa

各参数含义如下:

参数含义PR 测试取值
-m, --model模型文件路径(GGUF)LLaMA-3.1-8B-Instruct / Gemma3-12B-Instruct,均Q4_0量化
-c, --ctx-size上下文总长度10240(后续复跑扩展到 16k)
-ctk, --cache-type-kK-cache 量化类型q8_0
-ctv, --cache-type-vV-cache 量化类型q8_0(no-FA 时实际按 f16 生效)
-t, --threadsCPU 线程数32
-fa, --flash-attn是否启用 Flash Attention启用

sweep-bench每次运行会以 JSON 行格式输出每个窗口的测量结果,字段包括n_kv_max(最大上下文)、n_batchn_ubatchflash_attnn_threadspp(每窗口 PP token 数)、tg(每窗口 TG token 数)、n_kv(当前 KV cache 大小)、t_pp/speed_pp/t_tg/speed_tg等(对应输出代码位于 sweep-bench.cpp 第 416-425 行)。仓库还提供了绘图脚本 examples/sweep-bench/sweep-bench-plot.py,可将多次运行的 JSON 结果按label分组,绘制 PP/TG 吞吐量随n_kv变化的误差条曲线——PR 讨论中用户正是用该脚本生成了对比曲线并定位了两实现的「交叉点」。

测试环境

  • CPU:AMD Ryzen 5975WX(vanillaAVX2,即未启用 AVX-512 的常规 x86-64 路径);
  • 模型:LLaMA-3.1-8B-Instruct 与 Gemma3-12B-Instruct,均以Q4_0量化;
  • KV cache:Q8_0(K 与 V);FA 关闭时 V-cache 实际为f16
  • 对照组:主线 llama.cpp(build 5139)开启 FA 的结果。

需要说明的是,作者在讨论中指出:该测试机对运行间缓存(page cache)的丢弃与否较为敏感,且结果对 KV cache 分配量也有一定依赖,因此不同批次运行之间存在小幅波动;本文所列数据均为 PR 讨论中作者亲测的原始输出,具体数字会随硬件、量化组合与上下文分配而变,不应视为绝对性能结论

结果分析:LLaMA-3.1-8B-Instruct(head size = 128)

在 16k 上下文的复跑中,ik_llama.cpp 与主线 llama.cpp 的对比数据如下(PP=512, TG=128S_TG为生成速度 t/s):

主线 llama.cpp(build 5139,FA 开启)

PPTGN_KVT_PP sS_PP t/sT_TG sS_TG t/s
51212802.737187.047.54816.96
5121285123.185160.767.95316.09
51212810243.721137.608.40915.22
51212815364.219121.358.82614.50
51212820484.711108.689.19913.91
51212825605.20698.349.59213.34
51212830725.70489.769.98012.83
51212835846.25281.8910.37012.34
51212840966.86774.5510.76511.89
51212846087.50768.2011.15711.47
51212851208.23162.2111.55211.08
51212856329.21455.5711.94110.72
512128614410.46748.9112.33010.38
512128665611.64643.9612.71310.07
512128716813.10439.0713.1099.76
512128768014.81334.5613.5009.48
512128819216.57030.9013.8859.22
512128870418.24628.0614.2778.97
512128921620.14225.4214.6758.72
512128972821.72923.5615.0728.49
5121281024023.61521.6815.4548.28
5121281075225.40620.1515.8408.08
5121281126427.29918.7616.2367.88
5121281177629.12217.5816.6257.70
5121281228831.07916.4717.0127.52
5121281280033.05215.4917.4077.35
5121281331234.95814.6517.7967.19
5121281382437.17013.7718.1887.04
5121281433639.42512.9918.5706.89
5121281484841.66112.2918.9596.75
5121281536043.76611.7019.3506.62
5121281587246.12911.1019.7306.49

ik_llama.cpp(FA 开启)

PPTGN_KVT_PP sS_PP t/sT_TG sS_TG t/s
51212801.638312.567.73916.54
5121285121.661308.287.85216.30
51212810241.705300.357.96116.08
51212815361.766289.908.07515.85
51212820481.806283.528.17015.67
51212825601.860275.348.26115.50
51212830721.914267.518.36315.31
51212835841.981258.458.46815.11
51212840962.022253.228.59214.90
51212846082.076246.618.70614.70
51212851202.132240.128.80014.55
51212856322.189233.928.90214.38
51212861442.240228.588.99814.23
51212866562.298222.819.09314.08
51212871682.352217.669.19113.93
51212876802.407212.699.29713.77
51212881922.462207.929.40913.60
51212887042.519203.229.51413.45
51212892162.573199.029.61913.31
51212897282.630194.719.70213.19
512128102402.683190.829.79613.07
512128107522.739186.919.90412.92
512128112642.795183.1910.01812.78
512128117762.851179.6210.12412.64
512128122882.905176.2410.22812.51
512128128002.963172.7810.32112.40
512128133123.018169.6410.41312.29
512128138243.078166.3410.53812.15
512128143363.133163.4310.63212.04
512128148483.192160.4010.73811.92
512128153603.249157.6110.83811.81
512128158723.305154.9110.94211.70

从数据可以读出两个关键点:

  1. PP 差距巨大:ik_llama.cpp 的 PP 从零上下文时的 312.56 t/s 衰减到 16k 时的 154.91 t/s,而主线从 187.04 t/s 暴跌至 11.10 t/s。在 16k 时主线 PP 仅为 ik_llama.cpp 的约 7.2%(约 14 倍差距)。作者据此解释了其在别处对 DeepSeek-V3/R1 观察到的「PP 性能 6 倍下滑」的惊讶——相比之下 ik_llama.cpp 在 16k 时 PP 仅下降约 2 倍(LLaMA-3.1)至 2.3 倍(Gemma3)。
  2. TG 差异:在零上下文时两者 TG 几乎相同(16.96 vs 16.54 t/s),但随着N_KV增长,主线的生成速度衰减更快;到 16k 时主线 S_TG(6.49 t/s)仅为 ik_llama.cpp(11.70 t/s)的约 55.5%。不过两条曲线呈现逐渐收敛的走势,说明差距主要来自对长 KV cache 的调度效率,而非短上下文的峰值能力。

作者还指出,主线为优化 head size = 256 场景所做的改动,对 head size = 128(最常见的 head size)产生了严重的负面效果,这正是两个实现在不同架构模型上表现迥异的重要原因。

结果分析:Gemma3-12B-Instruct(head size = 256,8 个 KV 头)

作者在另一组运行中给出了 Gemma3-12B-Instruct 的完整数据(同样Q4_0模型、Q8_0KV cache、FA 开启):

主线 llama.cpp(build 5139,FA 开启)

PPTGN_KVT_PP sS_PP t/sT_TG sS_TG t/s
51212804.669109.6712.16410.52
5121285124.811106.4213.0619.80
51212810245.049101.4013.8189.26
51212815365.16499.1513.9609.17
51212820485.28096.9714.1079.07
51212825605.42394.4014.2488.98
51212830725.61991.1114.3958.89
51212835845.82387.9214.5358.81
51212840966.07084.3514.6778.72
51212846086.30681.1914.8258.63
51212851206.54778.2014.9698.55
51212856326.89074.3115.1318.46
51212861447.22770.8515.2818.38
51212866567.51368.1515.3948.32
51212871687.91864.6715.5378.24
51212876808.33461.4315.6808.16
51212881928.80058.1815.8308.09
51212887049.20055.6515.9718.01
51212892169.52353.7616.1017.95
512128972810.04850.9516.2427.88
5121281024010.49548.7816.3717.82
5121281075210.95546.7316.5077.75
5121281126411.37545.0116.6627.68
5121281177611.83743.2616.7987.62
5121281228812.32041.5616.9497.55
5121281280012.61340.5917.0857.49
5121281331212.81539.9517.2087.44
5121281382413.10039.0817.3647.37
5121281433613.46638.0217.5187.31
5121281484813.66937.4617.6557.25
5121281536013.78937.1317.7977.19
5121281587213.87436.9017.9377.14

ik_llama.cpp(FA 开启)

PPTGN_KVT_PP sS_PP t/sT_TG sS_TG t/s
51212802.593197.4612.30110.41
5121285122.662192.3412.50110.24
51212810242.756185.7712.70310.08
51212815362.854179.4212.9469.89
51212820482.946173.7813.1439.74
51212825603.040168.4213.3319.60
51212830723.136163.2613.5079.48
51212835843.235158.2513.7119.34
51212840963.336153.4813.9079.20
51212846083.432149.2014.0889.09
51212851203.530145.0514.2908.96
51212856323.632140.9914.4838.84
51212861443.729137.3114.6738.72
51212866563.834133.5314.8798.60
51212871683.934130.1415.0748.49
51212876804.046126.5515.2668.38
51212881924.140123.6715.4438.29
51212887044.243120.6615.6168.20
51212892164.342117.9115.8388.08
51212897284.450115.0616.0088.00
512128102404.552112.4816.1977.90
512128107524.721108.4616.4297.79
512128112644.762107.5116.6227.70
512128117764.869105.1616.8237.61
512128122884.973102.9616.9827.54
512128128005.077100.8417.2087.44
512128133125.17598.9317.4197.35
512128138245.27897.0217.6037.27
512128143365.46193.7517.7987.19
512128148485.56092.0819.1267.12
512128153605.71789.5519.3837.06
512128158725.89186.9119.6407.00

Gemma3 数据呈现出与 LLaMA-3.1 不同的对比格局:

  1. TG 差距明显缩小:在 16k 上下文,主线 TG(7.14 t/s)甚至略优于 ik_llama.cpp(7.00 t/s);两条 TG 曲线在中长上下文处存在交叉点(这正是讨论中用 sweep-bench-plot.py 绘制的图所呈现的现象)。作者在首轮测试中多次复跑主线并切换缓存策略,确认主线「前 1024 token 出现突发性能下降」的现象稳定存在。
  2. PP 依旧全面领先:ik_llama.cpp 的 PP 从零上下文的 197.46 t/s 衰减到 16k 的 86.91 t/s(约 55.5% → 42.4% 的下降幅度),而主线从 109.67 t/s 跌至 36.90 t/s,两者始终存在明显差距。

为什么 Gemma3 的 TG 增益小于 LLaMA-3?

作者给出了清晰的解释,这也是理解 PR #332 收益边界的核心:

  • Gemma3 总共 16 个注意力头、8 个 KV 头。在 TG 阶段生成单个 token 时,K*QV*softmax(K*Q)两个 GEMM 的矩阵只有2 行
  • 而 LLaMA-3 系列在该阶段对应矩阵为4 行
  • PR #332 的本质收益来自「把原本按 GEMV 处理的运算升级为多行 GEMM 并按行分配线程」。行数越多,GEMM 相对 GEMV 的并行优势越明显。当只有 2 行时,该收益自然被摊薄。

此外,Gemma3 的 head size 为 256(LLaMA-3 为 128),而主线 llama.cpp 的 CPU 代码在 PR 讨论时已经过多轮改动,其中针对 head size = 256 场景的优化在 Gemma3 上表现出相对优势,但对 head size = 128 的模型(如 LLaMA-3 系列)却造成了明显的性能拖累。作者明确表示「主线的 CPU 代码在我离开项目后有大量变化,我无法确定其具体实现」,因此对主线的内部机制不做断言。

从 ik_llama.cpp 的架构构建代码可以佐证 head size 与注意力形状如何影响 GEMM 维度:在 build_llama.cpp 中,n_embd_head = hparams.n_embd_head_v(0)n_embd_head_k(0)被断言相等,注意力分数缩放系数kq_scale1/sqrtf(n_embd_head),KV 头维度kv_headn_kv直接决定llm_build_kv中两个乘法张量的形状。这些参数在不同架构间差异很大,直接决定线程分配优化的收益上限。

实践建议与结论

综合 PR #332 的描述与数据,可以得出以下可用于实际部署的结论:

  1. GQA 模型在 CPU 上应优先开启 FA:PR 之后 FA 在 TG 阶段已全面超越 no-FA,使用-fa(或环境变量LLAMA_ARG_FLASH_ATTN)即可启用;
  2. KV cache 使用q8_0量化-ctk q8_0 -ctv q8_0在保持精度的同时显著减少显存/内存占用,且与 FA 路径兼容;注意 FA 关闭时 V-cache 会被框架按 f16 处理(common/common.cpp 第 4304、4393 行的类型检查);
  3. 收益与模型形状强相关:KV 头越少、TG 阶段 GEMM 行数越少,本优化的绝对收益越小(Gemma3 的 2 行 vs LLaMA-3 的 4 行);head size = 128 的主流模型(LLaMA-2/3 系)收益最大;
  4. 复现实验请使用 sweep-bench:它按窗口扫描上下文,能清晰呈现性能随N_KV的衰减曲线,配合 sweep-bench-plot.py 可做多实现对比;测试时注意缓存状态与 KV cache 分配量对结果的扰动;
  5. 结论有边界:本文数据来自单一 CPU(Ryzen 5975WX,AVX2)与特定量化组合,不同硬件(AVX-512、AMX、ARM)和量化(如Q8_0IQ4_KS)下的相对差距会变化,建议在目标机器上自行复测。

PR #332 的价值不仅在于一组漂亮的数字,更在于它揭示了 CPU 解码优化的一个关键维度:在 GQA 架构下,K*QV*softmax(K*Q)的线程分配方式直接决定多核利用率。对于希望深入理解 llama.cpp 系 CPU 后端调度逻辑的读者,可以从 src/graphs 目录下各架构的build_*文件入手,对照 build_llama.cpp 的注意力构建流程,并结合 examples/sweep-bench 的测量工具验证自己的改动。

【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询