共享 KV 缓存实战:llama.cpp 多会话推理如何少算、省内存、快响应
【免费下载链接】llama.cppLLM inference in C/C++项目地址: https://gitcode.com/GitHub_Trending/ll/llama.cpp
写过多客户端调用的 LLM 服务的人都遇到过这种场景:十来个用户连着同一个服务,每次请求都带一段相同的系统提示词,首字延迟却高得离谱。原因很简单——这段重复前缀的注意力结果(KV 缓存,可以理解为注意力计算中的"中间结果存档",存下来就不用再算一遍)每来一个新会话就得重算一遍。llama.cpp 的共享 KV 缓存机制正是为此设计:同一份前缀只计算一次,多个会话复用同一块缓存内存。本文按"本地试用 → 会话克隆 → 高并发压测"三步,把这套机制真正用起来。
从一次重复的提示词算起:KV 缓存如何被多会话共享
生成式模型每吐出一个新 token,都要回头看之前所有 token 的 Key/Value 向量。这些向量存进 KV 缓存后,后续计算直接查表,省掉大量重复矩阵乘法——这是推理提速的基本盘。
共享发生在更上层。llama.cpp 把缓存组织成环形单元池,核心实现见 src/llama-kv-cache.h,其中find_slot负责为一批 token 找到可放置的空闲槽位;每个会话用一个seq_id(序列编号)标记自己的 token,互不干扰。于是"共享"就有了三种形态:
- 同进程多会话:多个
seq_id挂进同一个缓存池,公共前缀只占一份空间 - 会话状态克隆:
llama_memory_seq_cp把一个序列的 KV 状态整体拷给另一个序列 - 缓存量化:K、V 各自可选更低的存储精度(
--cache-type-k/--cache-type-v),用少量精度换几倍空间
这三个函数签在公共 API 头文件 include/llama.h 里:
llama_memory_seq_cp (llama_memory_t mem, llama_seq_id src, llama_seq_id dst, llama_pos p0, llama_pos p1); llama_memory_clear (llama_memory_t mem, bool data); llama_memory_seq_rm (llama_memory_t mem, llama_seq_id seq_id, llama_pos p0, llama_pos p1);前两个参数之外的p0/p1指定作用区间,传负数表示"从起点/到终点",覆盖整个序列。
单机多会话:启动一个共享前缀的推理服务
目标:一台机器跑llama-server,多个客户端并发,系统提示词只算一遍。
服务端启动脚本可直接参考 examples/server-llama2-13B.sh:
./llama-server -m model.gguf --ctx-size 4096 --batch-size 1024几个关键参数(参数定义见 common/arg.cpp):
--parallel N:槽位数量,即最多多少路会话并行解码;不填时自动按 4 路处理(见 tools/server/server.cpp)--kv-unified:开启统一 KV 模式,允许槽位间复用前缀、空闲槽位自动保存/清理,是共享收益的主要开关--cache-type-k q8_0 --cache-type-v q8_0:缓存降精度存储,空间占用约为 f16 的 1/2,精度损失通常很小,但建议对自己的模型实测确认
预期效果:多路会话共用同一段提示词时,提示词处理(首 token)只需做一次,缓存内存不再随并发数线性增长。
进程内克隆会话状态:一个接口完成 A/B 测试与会话迁移
目标:把一段已生成到一半的对话状态,原样拷给另一个seq_id继续跑——对比两个采样策略、把会话从一个上下文迁移到另一个,都靠它。
调用就是开头那个签名,区间传负值即全量克隆:
llama_memory_seq_cp(mem, /*src*/ 0, /*dst*/ 1, -1, -1);之后seq_id = 1拥有和seq_id = 0完全一致的 KV 状态,两条线从此独立演化、互不影响。配套的llama_memory_seq_rm用于在会话结束后回收它占用的槽位,避免缓存池被死数据占满。接口定义与注释见 include/llama.h。
高并发压测:用 batched-bench 对比共享前后差距
目标:用数据回答"共享提示词到底省了多少"。仓库内置的 tools/batched-bench 提供两种模式:不加-pps时每个批次各带独立提示词;加上-pps后全部批次共用同一段提示词,正好对应is_pp_shared开关(见 tools/batched-bench/batched-bench.cpp)。
./llama-batched-bench -m model.gguf -c 16384 -b 2048 -ub 512 -npp 128,256,512 -ntg 128,256 -npl 1,2,4,8,16 -pps跑两次(一次加-pps,一次不加),对比表格里的N_KV(所需缓存总量)与S_PP(提示词处理速度),共享收益一目了然;加--output-format jsonl可导出原始数据留档。
共享 KV 缓存排障:前缀不命中、内存超限与精度下降
| 现象 | 原因 | 处理 |
|---|---|---|
| 并发多起来后首字延迟不降反升 | 各会话前缀实际并不相同,或没开--kv-unified,前缀命中不了 | 对齐提示词模板;确认--kv-unified已启用;必要时用--parallel收窄并发 |
| 缓存量化后回答质量下滑 | K/V 精度过低,长对话误差累积 | 先只量化 K 侧,再逐步收紧;对精度敏感的业务保留 f16 |
| 大上下文直接加载失败或爆内存 | -c给得过大,超出显存/内存预算 | 下调--ctx-size,或配合--cache-ram把空闲槽位缓存压进指定内存上限 |
| 跑久了缓存越占越满 | 结束的会话没清理,槽位残留 | 会话退出时调用llama_memory_seq_rm,空闲时用llama_memory_clear回收 |
边界与局限:哪些场景别急着上共享
说实话,这套机制的作用域是单机、单进程内的缓存池:
- 跨机器不生效。多节点部署需要额外的后端同步(仓库里 RPC 后端的实现见 ggml/src/ggml-rpc),KV 状态本身不会自动在节点间流动
- 前缀完全不同的请求共享收益趋近于零,反而因为槽位管理更复杂而可能变慢
- 超长上下文 + 低精度量化属于风险组合,误差会随长度放大
- 涉及滑动窗口注意力、Flash Attention 等特性时,表现与全量缓存不同,建议以官方 benchmark 和自己模型的实测为准
延伸学习
- 运维与接口:docs/ops.md、docs/install.md
- 动手示例:examples/simple-chat 演示了如何用
llama_memory_seq_pos_max查看已用上下文 - 状态持久化的完整测试:tests/test-save-load-state.cpp
- 源码入口:src/llama-kv-cache.h、include/llama.h
下期预告:滑动窗口注意力与混合 KV 缓存(iSWA)是怎么把长上下文的内存再砍一刀的。
【免费下载链接】llama.cppLLM inference in C/C++项目地址: https://gitcode.com/GitHub_Trending/ll/llama.cpp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考