ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ik_llama.cpp 慢速 KV Cache 删除剖析:DeepSeek-V3 混合卸载场景下的缓存回收与性能调优

ik_llama.cpp 慢速 KV Cache 删除剖析:DeepSeek-V3 混合卸载场景下的缓存回收与性能调优 ik_llama.cpp 慢速 KV Cache 删除剖析DeepSeek-V3 混合卸载场景下的缓存回收与性能调优【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp本文基于 ik_llama.cpp 仓库中 讨论 #586 Slow KV cache rm operation 展开。该讨论记录了一个真实的高负载 RAG 使用场景在 DeepSeek-V3-0324MoE量化模型、Dense 层卸载到 GPU、专家保留在 CPU 的混合部署中用户在 llama-server 里切换会话时需要等待数分钟才能完成 KV cache 删除日志中的kv cache rm [p0, end)。本文将先还原该问题的现场与命令再从源码层面剖析 KV cache 删除操作的真正开销来源逐一回答讨论中的三个问题最后给出可落地的缓解与调优建议。读完本文你将理解llama_kv_cache_seq_rm的底层机制、锁页内存pinned memory分配失败的影响以及如何针对会话频繁切换 RAG 长文档场景优化配置。问题现场RAG 重负载下的2-5 分钟等待讨论作者运行的硬件为 Intel Xeon QYFS高核心数服务器 CPU、512GB DDR5-4800 内存、NVIDIA RTX PRO 600048GB 级工作站 GPU模型为 ubergarm 制作的DeepSeek-V3-0324-IQ4_K_R4量化。其核心诉求是token 生成速度尚可接受约 11-12 t/s但在 RAG 重负载的真实使用中——喂入长文档、基于文档聊天、再穿插网络搜索并在多个会话之间来回切换——每次切换会话都要等待2-5 分钟的 KV cache 移除完全不可接受。复现命令sweep-bench 版真实使用时把llama-sweep-bench换成llama-server并加上 host/port 即可CUDA_VISIBLE_DEVICES0, \ ./build/bin/llama-sweep-bench \ --model /mnt/x/models/ubergarm/DeepSeek-V3-0324-GGUF/DeepSeek-V3-0324-IQ4_K_R4-00001-of-00010.gguf \ --alias ubergarm/DeepSeek-R1-V3-0324-IQ4_K_R4 \ --ctx-size 98304 \ -ctk q8_0 \ -mla 3 -fa \ -amb 8192 \ -fmoe \ --temp 0.3 \ --min-p 0.05 \ --n-gpu-layers 63 \ -ot blk\.[3-9]\.ffn_.*CUDA0 \ -ot expsCPU \ -ub 8192 -b 8192 \ --parallel 1 \ --threads 57该命令将显存占用推到 90376 / 97887 MiB几乎用满。上下文初始化日志确认了关键配置llama_new_context_with_model: n_ctx 98304 llama_new_context_with_model: n_batch 8192 llama_new_context_with_model: n_ubatch 8192 llama_new_context_with_model: flash_attn 1 llama_new_context_with_model: mla_attn 3 llama_new_context_with_model: attn_max_b 8192 llama_new_context_with_model: fused_moe 1 llama_new_context_with_model: freq_base 10000.0 llama_new_context_with_model: freq_scale 0.025 llama_kv_cache_init: CUDA0 KV buffer size 3499.90 MiB llama_new_context_with_model: KV self size 3499.88 MiB, c^KV (q8_0): 3499.88 MiB, kv^T: not used llama_new_context_with_model: CUDA_Host output buffer size 0.49 MiB ggml_cuda_host_malloc: failed to allocate 3296.09 MiB of pinned memory: invalid argument llama_new_context_with_model: CUDA0 compute buffer size 20496.03 MiB llama_new_context_with_model: CUDA_Host compute buffer size 3296.09 MiB llama_new_context_with_model: graph nodes 4219 llama_new_context_with_model: graph splits 104sweep-bench 的 PP/TG 基准数据raw PP 正常未出现不规则变慢PPTGN_KVT_PP sS_PP t/sT_TG sS_TG t/s81922048065.721124.65173.99511.7781922048819269.385118.07190.41610.76819220481638473.025112.18199.02310.29819220482457676.688106.82204.60710.01819220483276879.945102.47208.3669.83而真实 server 使用中的等待日志如下用户估算 KV 移除耗时约 3 分钟INFO [ update_slots] kv cache rm [p0, end) | tid125357154684928 timestamp1751624758 id_slot0 id_task12104 p08410 INFO [ print_timings] prompt eval time 128443.90 ms / 10172 tokens ( 12.63 ms per token, 79.19 tokens per second) | ... INFO [ print_timings] generation eval time 10688.65 ms / 122 runs ( 87.61 ms per token, 11.41 tokens per second) | ...注意两处关键细节p08410表示从位置 8410 开始截断删除系统提示词之前的内容被保留而随后的 prompt eval 处理了10172 个 token、耗时约 128 秒。这个等待的大部分时长实际发生在新 prompt 的重新处理上而不只是删除操作本身——这一点将在下文源码分析中展开。kv cache rm 在服务端发生在哪里触发点一update_slots 中的公共前缀截断日志kv cache rm [p0, end)出自 examples/server/server-context.cpp 的update_slots流程。当新请求到来时服务端会做公共前缀保留 尾部删除slot.cache_tokens.keep_first(slot.n_past)仅保留与上一轮共同的 token 前缀server-context.cpp计算出删除起点p0等于系统提示词长度加上保留的公共前缀位置见 server-context.cpp随后记录kv cache rm [p0, end)日志表示要将该 slot 从位置p0到末尾的 KV cache 全部作废server-context.cpp。也就是说每次新请求只要前缀不完全重合就会触发一次 KV 删除。在多个会话来回切换的场景里新旧请求的公共前缀通常只有系统提示词于是每次都要把几乎整个会话的 KV 清掉、再从长文档重新做 PP——这正是讨论作者感受到等待几分钟的直接来源。触发点二slot 释放时的整序列删除当 slot 被释放任务结束或会话切换时服务端还会执行llama_kv_cache_seq_rm(ctx, slot-id, -1, -1)做整序列删除server-context.cpp。参数-1, -1表示删除该序列从位置 0 到无穷大的全部内容。触发点三上下文溢出时的部分丢弃长上下文溢出时服务端通过discard_n_kv_and_cache_tokens丢弃一段 KV 并将后续位置前移server-context.cpp其核心同样调用llama_kv_cache_seq_rm删除区间[kv_keep, kv_keep kv_discard)再用llama_kv_cache_seq_add对剩余 token 的位置做负偏移。这条路径主要服务于单会话内的长上下文管理与讨论中的多会话切换问题不同。底层实现删除操作本身有多贵llama_kv_cache_seq_rmO(n_ctx) 的元数据遍历llama_kv_cache_seq_rm的实现位于 src/llama.cpp。它做的事非常轻归一化删除区间p0/p1负数视为直到末尾见 src/llama.cpp遍历缓存中全部cache.size个 cellsrc/llama.cpp凡pos落在[p0, p1)区间内的就调用cache.cells[i].seq_id.erase(seq_id)从该 cell 的序列标记中摘除当前序列若 cell 因此变空则把pos置为 -1、递减used计数并更新可复用起点new_headsrc/llama.cpp。也就是说删除操作本身只修改元数据cell 的 pos、seq_id 集合并不搬运或清零 KV 张量数据。以n_ctx 98304为例一次完整删除就是遍历约 9.8 万个 cell 的 CPU 循环量级在毫秒级。真正昂贵的部分从来不是这个循环。此外代码中有两处值得注意的特殊分支递归/状态型模型如 Mambacache.recurrent true不允许部分删除状态区间不合法时会直接返回falsesrc/llama.cpp。DeepSeek-V3 属于 MLA Transformer 架构不在此列--swa-compress压缩窗口存在时删除区间必须与压缩窗口的单一连续区间约束兼容否则报错返回src/llama.cpp。讨论场景未启用该选项。llama_kv_cache_clear真正的全量清空当需要清空整个缓存时服务端走llama_kv_cache_clearsrc/llama.cpp遍历所有 cell 复位pos、清空seq_id并调用ggml_backend_buffer_clear(buf, 0)把各 KV buffer 置零。注意这里的ggml_backend_buffer_clear会对 3.5 GiB 的量化 KV buffer 做整块清零CUDA 后端通常按页/块执行耗时与 buffer 大小相关但仍远不足以解释分钟级等待。结论等待时间花在了哪从源码结构看llama_kv_cache_seq_rm的元数据遍历与ggml_backend_buffer_clear的显存清零都属于次秒级操作。讨论日志中kv cache rm之后紧跟的128 秒 prompt eval10172 tokens79.19 t/s才是时间大头多会话切换导致缓存命中率极低长文档必须整体重新 PP。可以推断用户感知的KV cache removal 要 2-5 分钟实际是删除 公共前缀判定 长 prompt 全量重算的整体时延而非删除函数本身。三个问题的逐一分析Q1ggml_cuda_host_malloc失败与删除慢有关吗日志ggml_cuda_host_malloc: failed to allocate 3296.09 MiB of pinned memory: invalid argument表示 CUDA锁页内存pinned memory的大块分配失败。锁页内存用于 CPU 与 GPU 之间的高效 DMA 传输一次性分配 3.3 GiB 需要系统具备充足且连续的空闲物理内存分配失败常见于系统内存被大量占用、内存碎片化或超过可锁页上限。从日志看失败后服务端仍打印了CUDA_Host compute buffer size 3296.09 MiB说明它回退到了普通非锁页host 内存继续运行。这种回退对 token 生成的直接影响有限但在本讨论的混合卸载拓扑Dense 在 GPU、MoE 专家在 CPU中attention 与 FFN 的中间结果需要频繁跨 CPU-GPU 传输锁页内存缺失会降低传输效率从而压低 PP 吞吐。与 KV 删除慢的关系两者没有直接因果关系——删除是元数据操作不依赖锁页内存。但ggml_cuda_host_malloc失败本身是系统内存压力的信号值得单独处理关闭占用内存的无关进程、留出足够空闲物理内存本机 512GB 完全足够重点排查同时运行的其它 CUDA 应用/缓存或适当调低-ub/--parallel以减小计算缓冲需求。Q260-120 SPP 对这个配置是否可预期讨论数据中 S_PP 随 N_KV 从 0 增长到 32768从 124.65 t/s 缓降到 102.47 t/sTG 稳定在 9.83-11.77 t/s。对这个具体配置63 层 Dense 卸载到 RTX PRO 6000全部专家留在 CPU 以 57 线程计算而言这个量级是可预期的MoE 的专家计算ffn_*_exps完全落在 CPUexpsCPU的 override 保证了这一点CPU 专家吞吐是 PP 的主要瓶颈之一上下文变长后 attention 与 KV 读取成本上升S_PP 随 N_KV 下降是正常现象作为参照作者在 N_KV0 时 PP 约 124 t/s说明 GPU 侧 dense 部分工作正常。若想提升 PP方向是提高专家侧的并行效率--threads与批大小匹配、启用-fmoe的融合路径已开启或把更多专家层卸载到 GPU受显存 90.4/97.9 GiB 限制空间不大。Q3KV 删除与 PP 是绑定的吗不是同一个东西但时序上串行。从源码看KV 删除llama_kv_cache_seq_rm是一个独立的元数据操作由 server 在update_slots中、于新 prompt 处理之前同步调用server-context.cpp它既不参与 PP 计算图也不依赖 batch 处理删除完成后服务端才把新 prompt 加入 batch 做 PP由于二者在同一调度循环里串行执行删除哪怕只是毫秒级都会阻塞下一个请求的开始而删除之后被迫重算的长 prompt PP本例约 128 秒则成为用户感知的等待主体。换言之删除本身是独立步骤但它在关键路径上且它的后果缓存失效 → 长文档重算才是真正耗时的部分。混合卸载配置解读-ot、-mla、-amb、-ctk、-fmoe讨论命令里几个关键参数的含义与实现位置可对照源码确认参数作用实现/佐证位置-ot patternBUFT张量级 buffer 类型覆盖把匹配张量放到指定设备解析在 common/common.cpp 的parse_buft_overrides格式为tensor_patternbuffer_type支持正则加载时编译为正则并应用见 src/llama-load-tensors.cpp-mla n启用/配置 MLA 注意力日志mla_attn 3参数解析 common/common.cpp-amb nattention 计算的最大 batchattn_max_b低于 128 会被强制抬到 128common/common.cpp-ctk q8_0KV cache 以 q8_0 量化存储日志c^KV (q8_0): 3499.88 MiBKV 初始化日志出自 src/llama.cpp 的上下文创建流程-fmoe启用融合 MoEfused_moe默认开启可用-no-fmoe关闭common/common.cpp、common/common.cpp-ot blk\.[3-9]\.ffn_.*CUDA0的含义是把第 3-9 层的 FFN 相关张量blk.N.ffn_*放到 CUDA0-ot expsCPU则把所有专家张量ffn_*_exps锁定在 CPU。二者组合正是GPU 跑 dense 层、CPU 跑专家的经典混合卸载拓扑用以在有限显存此处 90.4 GiB 已接近上限内跑起千亿级 MoE 模型。缓解与调优建议针对讨论作者会话频繁切换 RAG 长文档的真实负载可操作的方向如下最大化公共前缀复用把不变的长文档、系统指令稳定放在提示词最前部系统提示词/固定前缀服务端会通过keep_first保留公共部分server-context.cpp并用llama_kv_cache_seq_cp拷贝系统提示词server-context.cpp从而避免每次重算系统提示词的 PP。保持前缀稳定是当前版本下提升缓存命中率最有效的手段。明确等待的真实构成通过print_timings的prompt eval time与kv cache rm日志的时间戳对比可以量化删除与重算各自的耗时本例重算约 128 秒、远超删除本身据此决定优化重点是缓存复用还是 PP 速度。处理锁页内存分配失败为cudaHostAlloc留出足够空闲物理内存关闭无关 CUDA 进程、避免内存耗尽必要时调低-ub/-b或--parallel缩小 host 计算缓冲锁页内存恢复后可提升混合卸载下 CPU-GPU 的传输效率。长上下文成本控制--ctx-size 98304 q8_0 KV 本身开销约 3.5 GiB合理但上下文越长、切换会话时被迫重算的 token 越多。若业务上允许可考虑为快速切换场景维护更小的工作上下文。关注上下文复用方向的进展讨论 451 - Context reuse / context shift for long prompts 记录了社区对 token trie、缓存保存/恢复关联 issue 436等跨会话缓存复用方案的探讨可跟踪其进展在此之前稳定的前缀结构是保证缓存命中率的现实手段。用 sweep-bench 做 A/B 基准作者已用 llama-sweep-bench 拿到稳定的 N_KV 递增曲线124.65 → 102.47 t/s后续任何参数调整都建议沿用同一基准对比避免凭感觉优化。小结讨论 #586 的慢 KV cache rm问题本质是多会话切换导致缓存命中率骤降llama_kv_cache_seq_rm本身是毫秒级的元数据操作src/llama.cpp真正的分钟级时延来自删除之后长 prompt 的全量重算ggml_cuda_host_malloc的锁页内存失败虽不直接拖慢删除却是混合卸载拓扑下 CPU-GPU 传输效率的隐患。通过保持提示词前缀稳定、量化删除与重算的时间构成、并恢复锁页内存分配可以有效缩短会话切换的前置等待让 RAG 重负载场景下的多会话体验回到可用水平。【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表