ik_llama.cpp 的 Faster MLA Prompt Processing 优化:原理、实现与实测
2026/9/19 19:35:52 网站建设 项目流程
  • 人工智能
  • 大模型
  • 推理引擎
  • 本地部署
  • 模型量化
  • 模型优化

【免费下载链接】ik_llama.cpp

llama.cpp fork with additional SOTA quants and improved performance

项目地址:https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
点击查看免费下载

导读

本文深入剖析 ik_llama.cpp(llama.cpp 的衍生分支)中一项针对 MLA(Multi-head Latent Attention,多头潜在注意力)架构的 Prompt Processing(PP,提示词预填充)加速优化方案。该方案通过将 K/Q 的 no- 与 rotational 位置编码部分拼接、消除独立的 k_r 缓存,并把原先两次独立的 kq_nope、kq_pe 矩阵乘法合并为一次,从而显著提升长上下文下的预填充吞吐,同时保持 MLA 的核心优势——生成(TG)阶段的 KV 缓存压缩。读完本文,你将掌握该优化的核心设计思想、源码实现位置、实测性能数据,以及如何在当前仓库中通过-mla参数与MLA_USE_TRANSPOSED_CACHE编译开关配置、验证这项优化。

背景:MLA 的机制与性能困境

MLA 的核心思想:压缩 KV 缓存

MLA(Multi-head Latent Attention)是一种注意力机制变体,由 DeepSeek-V2 系列模型引入并得到广泛应用。它的核心思想是:在 KV 进入注意力计算之前,先用低秩投影将其压缩到一个低维的"潜在"空间,从而大幅缩减 KV 缓存的大小,这是 MLA 相比标准 MHA(Multi-Head Attention)的关键优势。在 ik_llama.cpp 中,这一设计主要体现在 DeepSeek2、GLM、Qwen4 等模型的图构建代码中,例如 src/graphs/build_deepseek2.cpp 就是 MLA 的核心实现文件。

具体来说,在 MLA 中:

  • K 被拆成两部分
    • k_nope(no position encoding,无位置编码部分):维度为n_embd_head_qk_nope,只包含语义信息;
    • k_rope(rotational position encoding,旋转位置编码部分):维度为n_embd_head_qk_rope(即hparams.n_rot),包含位置信息,需要经过 RoPE 旋转位置编码处理。
  • Q 同样被拆成q_nopeq_rope两部分
  • 压缩后的潜在向量kv_compressed(维度为kv_lora_rank,即hparams.n_lora_kv)通过wkv_a_mqa投影得到,并经过 RMS Norm(attn_kv_a_norm)归一化。

预填充阶段的两大性能瓶颈

在未优化的实现中,MLA 的预填充计算存在两个明显的开销点:

  1. 两次独立的矩阵乘法:K 与 Q 的 no- 与 rope 部分分别计算注意力分数,即kq_nope = k_nope × q_nopekq_pe = k_rope × q_rope各自独立执行,随后还需要一次昂贵的加法将它们合并为最终注意力分数;
  2. 独立的 k_r 缓存:旋转编码部分的 K 需要单独维护一份缓存,增加了缓存管理与访存开销。

正是这两点,使得 MLA 在预填充阶段比无 MLA 的标准注意力更慢——这也是 PR #205 需要解决的问题。

核心优化:拼接 K/Q、合并矩阵乘法、消除 k_r 缓存

优化思路一:拼接 no- 与 rope 部分

PR #205 的核心改动非常直接:将 no- 与 rotational 位置编码部分直接拼接(concatenate)起来。这样 K 的完整维度就变为kv_lora_rank + n_embd_head_qk_rope,Q 的完整维度变为n_embd_head_qk_nope + n_embd_head_qk_rope,二者共享同一个矩阵乘法运算。

在源码中,这一拼接操作在 src/graphs/build_deepseek2.cpp 中体现为:

// 写入缓存前的拼接:rope 在前、压缩潜在在后 ggml_tensor * kvr = ggml_concat(ctx0, ggml_permute(ctx0, k_rope, 0, 2, 1, 3), kv_compressed, 0);

即缓存中的每一行存储的完整 K 为[k_rope | kv_compressed]的拼接形式。相应地,在 PP 优化路径中,K 与 Q 也按一致顺序拼接(见 src/graphs/build_deepseek2.cpp 中的k = ggml_concat(ctx0, k_rope_rep, k_nope, 0)q = ggml_concat(ctx0, q_rope, q_nope, 0))。

优化思路二:两次乘法合并为一次

拼接的直接收益是:原先的kq_nopekq_pe两次矩阵乘法可以合并为一次,并且省去了二者相加的ggml_add操作。在合并后的计算图中,一次ggml_mul_mat(或 Flash Attention 的ggml_flash_attn_ext)即可同时完成 nope 与 rope 两部分的注意力分数计算。这既减少了算子调度开销,又提升了计算密度。

优化思路三:消除独立的 k_r 缓存

由于 K 的 rope 部分与 no- 部分拼接存储,原先为旋转编码 K 单独维护的k_r缓存被完全消除。缓存中不再需要区分"K 的哪一段属于 rope 部分、哪一段属于 no- 部分"——它们天然地在同一行内连续排布,通过偏移量即可切分访问(例如源码中的ggml_row_size(cache_local->type, n_embd_head_qk_rope)用于定位 no- 部分的起始位置)。

源码级验证:拼接与吸收路径的完整实现

PP 优化路径(pp_opt)

在 src/graphs/build_deepseek2.cpp 中,当满足pp_opt条件(MLA 模式大于 1、n_tokens >= 128n_kv >= k_pp_opt_min_kv(1024))时,走的是"物化路径":从潜在缓存中恢复出完整的 K/V,然后使用标准 Flash Attention(ggml_flash_attn_ext)计算注意力。这一路径的关键片段为:

// V 与 k_nope 从压缩缓存中恢复 ggml_tensor * v_2d = ggml_mul_mat(ctx0, wv_b_2d, kv_cache_nope); ggml_tensor * k_nope_2d = ggml_mul_mat(ctx0, wk_b_T_2d, kv_cache_nope); // ... ggml_tensor * k = ggml_concat(ctx0, k_rope_rep, k_nope, 0); // 拼接后的完整 K ggml_tensor * q = ggml_concat(ctx0, q_rope, q_nope, 0); // 拼接后的完整 Q ggml_tensor * kqv = ggml_flash_attn_ext(ctx0, q, k, v, KQ_mask, kq_scale, hparams.f_max_alibi_bias, 0.f);

注意其中的wk_b_pptranspose(wk_b)的预物化张量(在llm_prepare_mla中生成),其形状为[kv_lora_rank, n_embd_head_qk_nope, n_head_local],用于将压缩潜在向量直接恢复为各头的 K。这与 PR #205 描述的"合并 kq_nope 与 kq_pe"在计算层面完全对应。

吸收路径(FlashMLA-3)

对于 token 数较少、不满足pp_opt条件的情况,走的是"吸收路径":将wk_b吸收进 Q 的投影,直接在压缩潜在空间计算注意力。此时 Q 被拼接为q_combined = [q_rope | q_nope2],K 直接使用缓存kv_cache(完整潜在 + rope),V 使用kv_cache_lora(仅潜在部分),同样是一次ggml_flash_attn_ext完成全部计算:

ggml_tensor * q_combined = ggml_concat(ctx0, ggml_permute(ctx0, q_rope, 0, 2, 1, 3), q_nope2, 0); ggml_tensor * kqv_compressed = ggml_flash_attn_ext(ctx0, q_combined, kv_cache, kv_cache_lora, KQ_mask, kq_scale, hparams.f_max_alibi_bias, 0.f);

非 Flash Attention 路径

在不使用 Flash Attention 的 MLA 模式下(mla_attn == 1),注意力通过两次ggml_mul_mat完成:先用kv_cache × q得到注意力分数并 softmax,再用kv_cache_trans × softmax 结果加权聚合 V。此时 V 缓存以转置形式存储(ggml_transpose(kv_compressed)写入v_l),这正是 PR #205 附带引入的"转置 KV 缓存"机制的由来:

// note: storing transposed c^KV in the transposed KV cache ggml_build_forward_expand(gf, ggml_cpy(ctx0, ggml_transpose(ctx0, kv_compressed), kv_cache_trans_view));

转置 KV 缓存与 MLA_USE_TRANSPOSED_CACHE 开关

为什么需要转置缓存

在非 Flash Attention 的 MLA 实现中,为了支持kv_cache_trans(供ggml_mul_mat(kv_cache_trans, kq)使用的转置 K/V 视图),需要在 V 缓存中额外保存一份转置后的潜在向量。PR #205 同时新增了一个编译期开关

llama.cpp中查找MLA_USE_TRANSPOSED_CACHE并将其设为 0,即可禁用 MLA 的转置 KV 缓存。

从关联文档中的说明可知:

  • 开启转置缓存(默认):KV 缓存大小几乎翻倍,但长上下文下的 TG(生成)性能更优;
  • 关闭转置缓存(设为 0):KV 缓存大小几乎减半,PP 性能基本不变,但长上下文 TG 性能有所下降。

这一点在仓库的 github-data/pull_requests/206 - MLA_ allow Q8_0 K-cache for MLA.md 中有直接印证:

kvl_t,即kv_l的转置版本,无法被量化。可以通过在llama.cpp中将MLA_USE_TRANSPOSED_CACHE设为 0 来消除它(但那样kv_l也无法量化,因为目前从量化张量构造连续转置张量以用于推理是不可行的)。

也就是说,转置缓存与"量化 KV 缓存"是两个互斥的选项:想要量化 K 缓存,就必须放弃转置缓存。这是选择该开关时的重要权衡。

实测对比:开启与关闭转置缓存

PR #205 附带的对比数据(DeepSeek-Lite 的IQ4_XS量化版,K 缓存为 Q8_0,Ryzen 7950X,TG 测试为llama-bench -gp -Np,64):

模型测试t/s(带 c^T)t/s(不带 c^T)
deepseek2 16B IQ4_XStg64@pp12833.58 ± 0.0633.05 ± 0.05
deepseek2 16B IQ4_XStg64@pp25632.67 ± 0.0031.54 ± 0.07
deepseek2 16B IQ4_XStg64@pp51232.38 ± 0.0830.26 ± 0.33
deepseek2 16B IQ4_XStg64@pp102431.50 ± 0.0228.50 ± 0.01
deepseek2 16B IQ4_XStg64@pp204830.01 ± 0.0124.75 ± 0.01
deepseek2 16B IQ4_XStg64@pp409627.08 ± 0.0320.67 ± 0.09
deepseek2 16B IQ4_XStg64@pp819222.82 ± 0.0014.89 ± 0.01

可以看到,上下文越长,转置缓存的 TG 优势越明显(8192 token 上下文时差距约 53%)。这印证了"以 KV 缓存空间换 TG 速度"的设计取向。

实测性能:PP 加速效果与 TG 无损验证

PP 加速效果(本 PR 的核心目标)

PR 作者在 Ryzen-7950X 上,使用 DeepSeek-Lite(IQ4_XS量化、K 缓存 Q8_0)对比了 main 分支与本 PR 的 PP 吞吐:

模型测试t/s(main)t/s(PR)加速比
deepseek2 16B IQ4_XSpp512478.58 ± 5.14489.40 ± 1.081.023
deepseek2 16B IQ4_XSpp1024438.56 ± 0.75458.37 ± 1.511.045
deepseek2 16B IQ4_XSpp2048378.95 ± 1.40407.83 ± 2.071.076
deepseek2 16B IQ4_XSpp4096294.71 ± 2.86327.88 ± 0.181.113
deepseek2 16B IQ4_XSpp8192204.52 ± 0.27234.17 ± 0.371.145
deepseek2 16B IQ4_XSpp16384126.31 ± 0.13148.35 ± 0.381.174

关键发现

  • 加速比随上下文长度增长而提升:从 512 token 的约 2.3%,到 16384 token 的约 17.4%。这是因为长上下文中注意力计算(矩阵乘法)占比更大,合并乘法的收益也随之放大;
  • 作者本人也谨慎指出:16k token 时约 9% 的提升"可能是真实的,也可能只是由于预填充阶段更早结束、热降频(thermal throttling)减轻所致"。这说明长上下文下的加速数据需要结合硬件功耗状态谨慎解读。

TG 性能不受影响

PR 同时用llama-bench -gp -Np,64验证了 TG(生成)性能没有因为 PP 优化而退化:

模型测试t/s(main)t/s(PR)加速比
deepseek2 16B IQ4_XStg64@pp12833.58 ± 0.0633.80 ± 0.001.007
deepseek2 16B IQ4_XStg64@pp25632.67 ± 0.0032.76 ± 0.011.003
deepseek2 16B IQ4_XStg64@pp51232.38 ± 0.0832.68 ± 0.051.009
deepseek2 16B IQ4_XStg64@pp102431.50 ± 0.0232.02 ± 0.001.017
deepseek2 16B IQ4_XStg64@pp204830.01 ± 0.0130.31 ± 0.031.010
deepseek2 16B IQ4_XStg64@pp409627.08 ± 0.0327.54 ± 0.101.017
deepseek2 16B IQ4_XStg64@pp819222.82 ± 0.0023.12 ± 0.011.013
deepseek2 16B IQ4_XStg64@pp1638417.24 ± 0.0018.74 ± 0.091.087

结论:TG 吞吐在各上下文长度下基本持平(多数在 1.0~1.7% 之间),即"TG(MLA 的立身之本)未被牺牲"。

数据可复现性说明

需要强调的是,上述数据是 PR 作者在单一硬件平台(Ryzen 7950X CPU)上、针对单一模型的测量结果,并非普适性保证。作者在 PR 描述中也明确表达了希望社区在其他系统上验证的意愿:

它仍然比无 MLA 慢,所以我先将其作为草稿,再尝试一些改进。不过如果其他人能帮忙测试,确认 a) 我没有引入 bug,b) 它确实在自己的系统上更快,那就太好了。

因此在实际项目中,建议用当前仓库的llama-bench工具在自己目标硬件上复测。关于如何使用llama-bench,可参考 examples/llama-bench/README.md。

如何在当前仓库中启用与验证

通过 -mla 参数启用 MLA 注意

MLA 注意力模式在 ik_llama.cpp 中通过命令行参数-mla--mla-use)启用,定义于 common/common.cpp 的参数解析中:

if (arg == "-mla" || arg == "--mla-use") { CHECK_ARG params.mla_attn = std::stoi(argv[i]); return true; }

其取值定义于 common/common.h:

int mla_attn = 3; // MLA 0: standard, 1: MLA with K and V^T cache, // 2: MLA with just K cache, 3: the best of both worlds

各取值含义(结合 src/llama.cpp、src/graphs/build_deepseek2.cpp 的实现):

取值含义说明
0标准注意力不使用 MLA 优化路径
1MLA + K 与 V^T 缓存非 Flash Attention 路径,V 缓存以转置形式存储
2MLA + 仅 K 缓存仅维护 K 缓存
3两者最佳结合(默认)结合模式,兼顾 PP 与 TG 性能

在实际图构建中,mla_attn的取值还会与 Flash Attention 组合出更细的分支:例如mla_attn > 1 && flash_attn && pp_opt走物化路径,mla_attn > 1时还涉及kv_cache_trans = ggml_cont(ggml_transpose(kv_cache_lora))的转置构造。值得注意的是,PR #205 的 PP 优化(拼接 K/Q、合并乘法)需要mla_attn > 1才能生效,因为物化路径只在mla_attn > 1的分支中执行。

启用 MLA 的示例命令(以llama-server为例,-c为上下文长度):

./build/bin/llama-server -m /path/to/deepseek2-16b-iq4_xs.gguf -mla 3 -c 8192

编译期开关 MLA_USE_TRANSPOSED_CACHE

如前所述,MLA_USE_TRANSPOSED_CACHE是一个编译期宏,用于控制是否生成转置 KV 缓存。默认开启;若需要压缩 KV 缓存占用(例如内存受限场景),可将其设为 0。注意该开关与量化 K 缓存互斥——这是选择时的关键权衡。

复测脚本与工具

仓库提供了多个适合复测的工具与脚本:

  • examples/llama-bench/:llama-bench基准工具,PR 中的-gp -Np,64pp512等测试即由此工具产出,支持指定 prompt 长度(-p/-Np相关参数)与生成长度;
  • scripts/run-all-perf.sh 与 scripts/compare-commits.sh:可用于批量对比不同提交的性能;
  • scripts/compare-llama-bench.py:解析llama-bench输出并对比。

对于 MLA 模型,仓库还提供了专门的深挖工具,例如 examples/spec-bench/ 与 MLA 相关讨论可参考 github-data/discussions/306 - Confused by the -mla flag. What's supported_.md。

进一步阅读

  • MLA 在 ik_llama.cpp 中的完整图构建实现:src/graphs/build_deepseek2.cpp、src/graphs/build_glm5next.cpp、src/graphs/build_qwen4exp.cpp
  • MLA 相关超参数(n_lora_kvn_rot等)定义:src/llama-hparams.h、src/llama-hparams.cpp
  • KV 缓存结构(k_lv_lv_trans)定义:src/llama-context.h
  • 同一时期的后续工作(Q8_0 K 缓存支持 MLA):github-data/pull_requests/206 - MLA_ allow Q8_0 K-cache for MLA.md
  • -mla参数的社区答疑:github-data/discussions/306 - Confused by the -mla flag. What's supported_.md
  • 仓库的 PR 讨论归档目录:github-data/pull_requests/
  • 人工智能
  • 大模型
  • 推理引擎
  • 本地部署
  • 模型量化
  • 模型优化

【免费下载链接】ik_llama.cpp

llama.cpp fork with additional SOTA quants and improved performance

项目地址:https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
点击查看免费下载

相关推荐

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

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

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

立即咨询