- 人工智能
- 大模型
- 推理引擎
- 本地部署
- 模型量化
- 模型优化
【免费下载链接】ik_llama.cpp
llama.cpp fork with additional SOTA quants and improved performance
导读
本文深入剖析 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_nope与q_rope两部分; - 压缩后的潜在向量
kv_compressed(维度为kv_lora_rank,即hparams.n_lora_kv)通过wkv_a_mqa投影得到,并经过 RMS Norm(attn_kv_a_norm)归一化。
预填充阶段的两大性能瓶颈
在未优化的实现中,MLA 的预填充计算存在两个明显的开销点:
- 两次独立的矩阵乘法:K 与 Q 的 no- 与 rope 部分分别计算注意力分数,即
kq_nope = k_nope × q_nope与kq_pe = k_rope × q_rope各自独立执行,随后还需要一次昂贵的加法将它们合并为最终注意力分数; - 独立的 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_nope与kq_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 >= 128、n_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_pp是transpose(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_XS | tg64@pp128 | 33.58 ± 0.06 | 33.05 ± 0.05 |
| deepseek2 16B IQ4_XS | tg64@pp256 | 32.67 ± 0.00 | 31.54 ± 0.07 |
| deepseek2 16B IQ4_XS | tg64@pp512 | 32.38 ± 0.08 | 30.26 ± 0.33 |
| deepseek2 16B IQ4_XS | tg64@pp1024 | 31.50 ± 0.02 | 28.50 ± 0.01 |
| deepseek2 16B IQ4_XS | tg64@pp2048 | 30.01 ± 0.01 | 24.75 ± 0.01 |
| deepseek2 16B IQ4_XS | tg64@pp4096 | 27.08 ± 0.03 | 20.67 ± 0.09 |
| deepseek2 16B IQ4_XS | tg64@pp8192 | 22.82 ± 0.00 | 14.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_XS | pp512 | 478.58 ± 5.14 | 489.40 ± 1.08 | 1.023 |
| deepseek2 16B IQ4_XS | pp1024 | 438.56 ± 0.75 | 458.37 ± 1.51 | 1.045 |
| deepseek2 16B IQ4_XS | pp2048 | 378.95 ± 1.40 | 407.83 ± 2.07 | 1.076 |
| deepseek2 16B IQ4_XS | pp4096 | 294.71 ± 2.86 | 327.88 ± 0.18 | 1.113 |
| deepseek2 16B IQ4_XS | pp8192 | 204.52 ± 0.27 | 234.17 ± 0.37 | 1.145 |
| deepseek2 16B IQ4_XS | pp16384 | 126.31 ± 0.13 | 148.35 ± 0.38 | 1.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_XS | tg64@pp128 | 33.58 ± 0.06 | 33.80 ± 0.00 | 1.007 |
| deepseek2 16B IQ4_XS | tg64@pp256 | 32.67 ± 0.00 | 32.76 ± 0.01 | 1.003 |
| deepseek2 16B IQ4_XS | tg64@pp512 | 32.38 ± 0.08 | 32.68 ± 0.05 | 1.009 |
| deepseek2 16B IQ4_XS | tg64@pp1024 | 31.50 ± 0.02 | 32.02 ± 0.00 | 1.017 |
| deepseek2 16B IQ4_XS | tg64@pp2048 | 30.01 ± 0.01 | 30.31 ± 0.03 | 1.010 |
| deepseek2 16B IQ4_XS | tg64@pp4096 | 27.08 ± 0.03 | 27.54 ± 0.10 | 1.017 |
| deepseek2 16B IQ4_XS | tg64@pp8192 | 22.82 ± 0.00 | 23.12 ± 0.01 | 1.013 |
| deepseek2 16B IQ4_XS | tg64@pp16384 | 17.24 ± 0.00 | 18.74 ± 0.09 | 1.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 优化路径 |
| 1 | MLA + K 与 V^T 缓存 | 非 Flash Attention 路径,V 缓存以转置形式存储 |
| 2 | MLA + 仅 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,64、pp512等测试即由此工具产出,支持指定 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_kv、n_rot等)定义:src/llama-hparams.h、src/llama-hparams.cpp - KV 缓存结构(
k_l、v_l、v_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
相关推荐
ik_llama.cpp 中 DeepSeek MLA 注意力优化的演进:从 PR 180 到可选的 `-mla` 实现
ik_llama.cpp 中 DeepSeek MLA 注意力优化的演进:从 PR 180 到可选的 mla 实现 导读 本文基于 ik_llama.cpp 仓
人工智能大模型推理引擎本地部署模型量化模型优化ik_llama.cpp 中的 MLA 矩阵吸收:基于压缩 KV 缓存的长上下文预填充优化原理与实现
ik_llama.cpp 中的 MLA 矩阵吸收:基于压缩 KV 缓存的长上下文预填充优化原理与实现 本文围绕 ik_llama.cpp 社区讨论《MLA ma
人工智能大模型推理引擎本地部署模型量化模型优化ik_llama.cpp 为 IQ4_KS 引入 CUDA MMQ:量化矩阵乘法实现与 Prompt Processing 性能提升解析
ik_llama.cpp 为 IQ4_KS 引入 CUDA MMQ:量化矩阵乘法实现与 Prompt Processing 性能提升解析 本文聚焦 ik_lla
人工智能大模型推理引擎本地部署模型量化模型优化
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考