ik_llama.cpp 量化 KV 缓存输出乱码问题排查实录:从 CPU FA 的 Q8_0 位布局到 IQ4_NL V-cache 的启用边界
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
导读
本文基于 ik_llama.cpp 仓库中的真实 issue #92(Quantized KV cache produces garbage in situation where llama.cpp does not) 展开。该 issue 记录了 2024 年 10 月至 2025 年 2 月间,用户 saood06 在 Windows 平台以部分 offload 方式运行 Mistral Large 2 时,量化 KV 缓存(-ctk q8_0 -ctv q8_0)输出乱码的完整排查过程:从作者 ikawrakow 定位到"CPU 上 FA 使用 Q8_0 V-cache 不可用"这一实现事实,到社区确认 IQ4_NL 可用于 CUDA 上的 V-cache、再经源码级验证、最终确认问题在后续 CPU FA 重构中消失。读完本文,你将掌握-ctk/-ctv系列参数的正确选型、GGML_CUDA_FA_ALL_QUANTS与GGML_IQK_FA_ALL_QUANTS的编译开关差异、K-cache 与 V-cache 量化精度不对称的原因,以及如何用 perplexity 等指标为不同模型挑选合适的 KV 量化组合。
问题现场:llama.cpp 正常而 ik_llama.cpp 输出乱码
复现环境与命令
用户 saood06 的复现环境:
- 模型:Mistral Large 2(GGUF 为
Mistral-Large-Instruct-2407.i1-IQ4_XS.gguf),约 28k token 的 prompt; - 硬件:AMD 5600X + RTX 3090,采用部分 offload(
-ngl 29); - 系统:Windows;
- 对照版本:llama.cpp 3658(f1485161)输出正常;ik_llama.cpp 3459(baab1d9a)输出乱码。
复现命令:
.\llama-server.exe -m "...\Mistral-Large-Instruct-2407.i1-IQ4_XS.gguf" -t 6 -ngl 29 -c 33333 --host 0.0.0.0 --no-mmap -fa -ctk q8_0 -ctv q8_0其中-fa开启 Flash Attention,-ctk q8_0 -ctv q8_0将 K/V 缓存分别量化为 Q8_0。症状是:输出的 top-10 token 候选基本保持不变(如to, of, for),即模型陷入固定循环式的垃圾输出;进一步实验-ctv q4_0、-ctv q4_0 -nkvo(KV 缓存留在 CPU)后,结果更糟,甚至输出大量[control_36]等特殊控制 token。
缩小问题范围的关键实验
作者 ikawrakow 与社区成员 Nexesenex 通过一组对照实验逐步缩小了问题边界:
| 实验 | 结果 |
|---|---|
| 全量 offload 到 GPU(Nexesenex,Mistral 123b IQ3/IQ4 混合量化) | V iq4_nl+ K(q8_0/q5_1/q5_0) 正常 |
| 部分 offload(saood06,Mistral Large 2) | -ctk q8_0 -ctv q8_0/q4_0乱码 |
| CPU-only + 量化 KV | 同样乱码(全 probs 为 null) |
| CPU-only + FP16 KV(不量化) | 输出正确 |
| 更小模型(Midnight-Miqu-70B、Gemma-2 27B) | 部分 offload 下同样出现全 null probs |
由此得出两个重要推论:
- 问题与模型大小无关,只与"CPU 后端 + 量化 KV 缓存 + Flash Attention"这一组合相关;
- FP16 KV 缓存从未出问题,说明问题出在量化路径而非 FA 本身。
根因剖析:CPU 侧 Q8_0 量化位布局与 V-cache 的兼容性
作者给出的第一层解释:Q8_0 位布局被改变
作者 ikawrakow 在 issue 中明确指出一个关键实现事实:
我在推理过程中量化时改变了
Q8_0的位排列(bit arrangement),结果是当 FA 在 CPU 上运行时,Q8_0不能用于 V 缓存。
也就是说,ik_llama.cpp 在推理阶段(运行时量化 KV)使用的 Q8_0 位布局与标准 Q8_0 并不一致,CPU 上的 FA 内核无法正确解析以该布局存储的 V-cache,从而产生垃圾输出。作者在 CPU FA 之外也反复提醒:从经验上讲,K-cache 的量化精度比 V-cache 更重要,因此更优的实践是 K 用 Q8_0、V 用更高精度的低 bit 类型(见下文 IQ4_NL 一节)。
第二层事实:Q8_0 的位布局差异已被源码印证
在同一时期,Nexesenex 对比了 ik_llama.cpp 与 mainline 的 CUDA FA 代码,指出一处疑似差异:
// ik_llama.cpp(当时): static void ggml_cuda_flash_attn_ext_vec_f16(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { ggml_tensor * Q = dst->src[1]; ggml_tensor * K = dst->src[1]; ggml_tensor * V = dst->src[2];作者确认这是git blame显示源自上游的一处笔误(Q = dst->src[1],应为src[0]),但由于矩阵乘法要求Q->ne[0] == K->ne[0],该笔误不影响功能,并已承诺修正。在当前仓库中,ggml-cuda/fattn.cu 的写法已是const ggml_tensor * Q = dst->src[0]; const ggml_tensor * K = dst->src[1];,与 mainline 一致,说明该笔误早已修复。
CPU 侧真正的原因:IQK 路径对 KV 类型的支持集合
ik_llama.cpp 的 CPU FA 走的是iqk(IQK 量化与 FA 内核)实现。在当前仓库 iqk_flash_attn.cpp 中,CPU 后端支持的 KV 类型由一个显式集合控制:
static inline const std::unordered_set<ggml_type> & supported_kv_types() { #ifdef GGML_IQK_FA_ALL_QUANTS static std::unordered_set<ggml_type> k_supported = { GGML_TYPE_F16, GGML_TYPE_Q8_0, GGML_TYPE_Q8_KV, GGML_TYPE_Q6_0, GGML_TYPE_Q4_0, GGML_TYPE_Q4_1, GGML_TYPE_IQ4_NL }; #else static std::unordered_set<ggml_type> k_supported = { GGML_TYPE_F16, GGML_TYPE_Q8_0, GGML_TYPE_Q8_KV, GGML_TYPE_Q6_0, }; #endif return k_supported; }配套的are_kv_types_supported还规定:BF16 必须 K、V 同为 BF16 且依赖__AVX512BF16__。不支持的组合会直接打印:
==================== K cache q8_0 coupled with V cache q8_0 is not a supported combination on the CPU backend. Warning: ik_llama.cpp does not support Q5_0 or Q5_1 KV cache on the CPU.这就是 issue 中"CPU 上 Q8_0 V-cache 产生垃圾"在当前代码中的最终形态:早期的位布局缺陷已在后续 CPU FA 重构中被吸收,如今不支持的组合会显式报错而非静默乱码(见下文"问题如何被修复")。
编译开关:GGML_CUDA_FA_ALL_QUANTS 与 GGML_IQK_FA_ALL_QUANTS
GPU 侧:fattn-common.cuh 的支持矩阵
issue 中用户初次编译时遇到如下致命错误:
Unsupported KV type combination for head_size 128. Supported combinations: - K == q4_0, V == q4_0, 4.50 BPV - K == q8_0, V == q8_0, 8.50 BPV - K == f16, V == f16, 16.00 BPV Compile with GGML_CUDA_FA_ALL_QUANTS for all combinations of q4_0, q4_1, q5_0, q5_1, q8_0, and f16. Q:\GitHub\ik_llama.cpp.fks\ggml\src\ggml-cuda\fattn-common.cuh:576: fatal error该错误信息来自 CUDA FA 的公共头文件 fattn-common.cuh 中的on_no_fattn_vec_case:按 head_size 分派支持矩阵,Dk==128 && Dv==128时默认编译只包含少量组合(q4_0+q4_0、iq4_nl+iq4_nl、q6_0+q5_0、q8_0+iq4_nl、q8_0+q6_0、q8_0+q8_0、f16+f16),并提示编译GGML_CUDA_FA_ALL_QUANTS以获得 q4_0/q4_1/iq4_nl/q5_0/q5_1/q8_0/f16 的全部组合。
在当前仓库中,错误信息已随之演进:head_size 128 的"Supported combinations"列表明确加入了K == iq4_nl, V == iq4_nl、K == q8_0, V == iq4_nl等组合,说明 IQ4_NL 在 CUDA FA 中已被正式纳入支持(对应 issue 中提到的 #99 及后续GGML_COPY实现)。
CPU 侧:GGML_IQK_FA_ALL_QUANTS
CPU 侧对应开关为GGML_IQK_FA_ALL_QUANTS。在 ggml/src/CMakeLists.txt 中可以看到该宏的注入逻辑:
if (GGML_IQK_FA_ALL_QUANTS) add_compile_definitions(GGML_IQK_FA_ALL_QUANTS) endif()打开该开关后,CPU FA 内核会额外生成 q4_0、q4_1、iq4_nl 等低 bit 类型的内核(见 iqk_fa_templates.h 中大量#if GGML_IQK_FA_ALL_QUANTS的模板分支,以及 iqk_gemm_legacy_quants.cpp 的对应实现)。不开启时,CPU 默认仅支持 F16、Q8_0、Q8_KV、Q6_0(以及支持 AVX512-BF16 时的 BF16)。
注意:issue 中用户遇到的是GPU侧报错
GGML_CUDA_FA_ALL_QUANTS;而在 CPU 侧(本次 issue 的实际故障路径)需要的是GGML_IQK_FA_ALL_QUANTS。两个开关名字不同、作用域不同,容易混淆。
参数解析:-ctk / -ctv 及其家族
-ctk/-ctv在 common/common.cpp 中解析,并被封装为--cache-type-k TYPE与--cache-type-v TYPE。当前仓库还提供了更细粒度的家族参数(参数文档):
| 参数 | 含义 | 默认值 |
|---|---|---|
-ctk, --cache-type-k TYPE | K 缓存数据类型 | f16 |
-ctv, --cache-type-v TYPE | V 缓存数据类型 | f16 |
-ctk-first, --cache-type-k-first TYPE,N | 前 N 层 K 的缓存类型 | 默认-1(不指定) |
-ctk-last, --cache-type-k-last TYPE,N | 后 N 层 K 的缓存类型 | 默认-1 |
-ctv-first, --cache-type-v-first TYPE,N | 前 N 层 V 的缓存类型 | 默认-1 |
-ctv-last, --cache-type-v-last TYPE,N | 后 N 层 V 的缓存类型 | 默认-1 |
-ctkd, --cache-type-k-draft TYPE | 草稿模型(speculative)K 缓存类型 | - |
-ctvd, --cache-type-v-draft TYPE | 草稿模型 V 缓存类型 | - |
-ictk, --indexer-cache-type-k TYPE | Indexer K-cache 类型 | f16 |
-ctk-first/-ctk-last这类参数接受TYPE,N格式(用逗号分隔类型与层数),允许对同一模型的不同层使用不同的缓存精度。
KV 量化组合的实战选型:q8_0 + iq4_nl vs q5_1 + q5_0
IQ4_NL 引入 V-cache:GGML_COPY 是关键
issue 讨论中,Nexesenex 起初认为"IQ quants 在 CUDA 上不可用于 KV 缓存",作者回应:
IQ4_NL 在本仓库中可用于 KV 缓存。关键在于
GGML_COPY已可用——这是我在一段时间前为IQ4_NL实现的;mainline llama.cpp 的 CUDA 代码里也有它,只是不知为何被禁用了。
GGML_COPY决定了量化 KV 在设备间/后端间的拷贝路径是否可用。作者以此为依据,在 CUDA 上用 llama-3.1-instruct-iq4kss 做了两组 perplexity 对比(-fa -ctk q8_0 -ctv iq4_nlvs-fa -ctk q5_1 -ctv q5_0),结论如下:
| 配置 | KV buffer | K | V | 结果 |
|---|---|---|---|---|
-ctk q8_0 -ctv iq4_nl | 104.00 MiB | 68.00 MiB (q8_0) | 36.00 MiB (iq4_nl) | PPL = 7.4896 ± 0.04778 |
-ctk q5_1 -ctv q5_0 | 92.00 MiB | 48.00 MiB (q5_1) | 44.00 MiB (q5_0) | PPL = 7.4978 ± 0.04775 |
作者的解读:
从 PPL 和 KLD 看,
-ctk q8_0 -ctv iq4_nl明显胜过-ctk q5_1 -ctv q5_0。缓存多占约 10% 内存,但推理略快。
即:q8_0 + iq4_nl用约 10% 的额外缓存内存换来了更低的 PPL(更高的量化精度)。作者同时给出 CUDA 上的吞吐对比(pp8192,llama 8B IQ4_KS):
| type_k | type_v | 吞吐 |
|---|---|---|
| q5_1 | q5_0 | 4777.42 ± 3.50 t/s |
| q8_0 | iq4_nl | 4757.62 ± 2.13 t/s |
并补充:CUDA 上两者吞吐几乎相同,而CPU 上-ctk q8_0 -ctv iq4_nl要快不少。
为什么是"K 用更好精度、V 用 IQ4_NL"?
作者解释了背后的设计逻辑:
IQ4_NL用于 K-cache 本就不该工作(我甚至惊讶它没有崩溃或报错)。要让IQ4_NL用于 K-cache,还需要实现点积(dot product),而我认为低于 5 bpw 的 K-cache 没什么用,不值得做。我甚至觉得应该禁用Q4_0 + Q4_0这个 KV 组合,因为它偏差太大。
这与 参数文档 的现行建议一致:
- K-cache 可能需要比 V-cache 更好的量化才能减少质量损失,两者可分别指定:
--cache-type-k q8_0 --cache-type-v q8_0; - 需要对低 bit 类型做实验时,CPU 上用
GGML_IQK_FA_ALL_QUANTS=ON编译(开启后默认启用 Q4_1、IQ4_NL、Q4_0 的 CPU FA 内核,关闭可缩短构建时间); - 提供快速量化类型 Q8_KV:
-ctk q8_KV; - 低于 Q6_0 的量化可配合 Hadamard 变换:
--cache-type-k q6_0 --k-cache-hadamard --cache-type-v q6_0 --v-cache-hadamard(--v-cache-hadamard是 ik_llama.cpp 为 V-cache 额外提供的)。
不同模型对 KV 量化的敏感度差异
saood06 在 issue 中给出了一个重要的反向观察:
并不是所有模型都如此。例如没有 GQA 的 Command-R(35b),在我的经验中降到 Q4/Q4 缓存也没有明显劣化。不同模型对 KV 缓存量化的敏感度差异很大,甚至比不同模型对权重量化的敏感度差异还要大。
这提醒读者:KV 量化选型必须针对具体模型验证,不能一套参数走天下;perplexity(PPL)与 KLD(KL 散度)是 issue 中实际使用的两条客观度量。
排查方法论:从"复现不了"到"确认已修复"
为什么作者最初复现不了
作者 ikawrakow 表示自己无法复现(其测试环境为 CPU/部分 offload,GPU 仅 16GB VRAM,Miqu 与 Gemma-2-27B 在Q8_0+Q8_0下均正常),并指出两个关键差异:
- Nexesenex 是全量 offload,只走 GPU FA 内核(与 mainline 相同),因此不受 CPU FA 缺陷影响;
- saood06 是部分 offload或纯 CPU,走 CPU FA 路径,命中 Q8_0 V-cache 位布局问题。
作者强调"部分 offload 对我来说工作正常,我只是无法测试 Mistral Large"——即问题与模型大小相关而与环境能力相关,给定位带来了困难。
用户侧的二分定位法
saood06 通过两组二分实验锁定了问题域:
- FA 开关二分:
-fa+ 量化 KV → 乱码;FA + FP16 KV → 正常;无 FA + 量化 KV(CPU-only)→ 正常。结论:问题在"CPU FA + 量化 KV"的交叉路径; - 平台二分:先怀疑 Windows 上
long为 4 字节的 ABI 差异,尝试将 iqk_mul_mat.cpp 中的long全部改为long long(并同步修改 iqk_mul_mat.h),仍复现;再用 GCC(无 CUDA)编译并加-mms-bitfields控制 MSVC 结构体布局,仍复现。由此排除了"Windows 特有 ABI/结构体对齐"假设,将问题域进一步收窄到 CPU FA 的量化路径本身。
问题的最终归宿
作者于 2025-01-30 询问问题是否仍然存在;saood06 于 2025-02-11 回复:
问题已无法复现。我没有动力去 git-bisect 定位是哪个提交修复的,但最后一次排查是 12 月 2 日(当时还在用 debug 构建尝试定位)。所以它是在那之后到现在的某个时间点被修复的。
结合当前仓库源码可以确认:CPU FA(iqk 路径)的 KV 类型支持已重构为显式集合校验(iqk_flash_attn.cpp),不支持的组合(如 Q5_0/Q5_1)会直接打印错误并中止,而非静默输出乱码;同时 issue 中作者本人于 2025-01-30 也确认"自 10 月以来 CPU FA 实现已有相当多的修改和修复"。可以推断,该问题在 CPU FA 的后续重构中随 Q8_0 运行时量化路径的修正而被消除,如今用户无需再规避-ctk q8_0 -ctv q8_0这类组合。
实践总结与避坑清单
- KV 量化必须配合 Flash Attention。量化 KV 类型依赖 FA 内核解码,
-fa是使用量化缓存的前提(ik_llama.cpp 中 FA 默认开启);本文涉及的乱码路径全部与 FA 相关。 - 分清两个编译开关:
- GPU:
GGML_CUDA_FA_ALL_QUANTS——启用 CUDA FA 的 q4_0/q4_1/iq4_nl/q5_0/q5_1/q8_0/f16 全部 KV 组合(fattn-common.cuh); - CPU:
GGML_IQK_FA_ALL_QUANTS——启用 CPU IQK FA 的 q4_0/q4_1/iq4_nl 内核(iqk_flash_attn.cpp),默认 CPU 仅 F16/Q8_0/Q8_KV/Q6_0。
- GPU:
- 推荐组合:
-ctk q8_0 -ctv iq4_nl在 PPL 与内存/吞吐的权衡上优于-ctk q5_1 -ctv q5_0,且 CPU 上更快;IQ4_NL 目前只建议用于 V-cache,K-cache 低于 5 bpw 无实用价值。 - 按层定制:使用
-ctk-first/-ctk-last/-ctv-first/-ctv-last TYPE,N为不同层指定不同精度;-ctkd/-ctvd单独控制草稿模型缓存。 - 用数据说话:以
llama-perplexity(-f传入测试语料,-fa -ctk ... -ctv ...)对比 PPL/KLD 来决定组合;不同模型对 KV 量化的敏感度差异很大(如无 GQA 的 Command-R 对 Q4/Q4 不敏感),必须逐模型验证。 - 遇到乱码先二分:依次关闭量化(回退 f16)、关闭 FA、切换全量/部分 offload,即可快速判断问题出在哪个子路径;若遇到"Unsupported KV type combination"类错误,说明内核未编译,请核对对应平台的 ALL_QUANTS 编译开关。
- 查看启动日志验证:加载时日志会输出
llama_new_context_with_model: KV self size = ... K (q8_0): ... V (iq4_nl): ...,可用gguf-py/scripts/gguf_dump.py查看模型的张量构成,帮助判断模型的量化敏感度。
延伸阅读
- KV 量化参数全集与更多实操建议:docs/parameters.md
- CPU FA 的 KV 类型支持实现:ggml/src/iqk/iqk_flash_attn.cpp
- CUDA FA 的支持矩阵与报错逻辑:ggml/src/ggml-cuda/fattn-common.cuh
- CUDA FA 入口(Q 源已修正为 src[0]):ggml/src/ggml-cuda/fattn.cu
- 参数解析实现:common/common.cpp
- 相关 issue 线索:github-data/issues/305(DeepSeek-V3-0324 在混合 CPU/GPU 下的乱码)、github-data/issues/300、github-data/issues/358、github-data/issues/224(IQK_FA_ALL_QUANTS 编译失败)、github-data/pull_requests/101(在 FA 中启用 q6_0)、github-data/pull_requests/197(FA:增加编译全部 FA 内核的选项)
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考