不能直接 new/delete 管理 kv cache,因自回归解码中频繁 malloc/free 会引发 cuda 同步、显存碎片化及分配失败;llama.cpp 中 std::vector 反复 resize 导致单次更新卡顿 2~5ms。

为什么不能直接 new/delete 管理 KV Cache
LLM 自回归解码时,KV Cache 按 token 步进动态增长,每层每头都要存 K 和 V 张量。一次请求从 100 token 扩到 4096 token,KV Cache 显存占用就涨 40 倍——频繁 malloc/free 会触发 CUDA 上下文同步、显存碎片化、甚至 cudaMalloc 失败。llama.cpp 里用 std::vector 或裸指针反复 resize,实测在长上下文场景下,单次 kv_cache_update 调用可能卡住 2~5ms,全是内存管理开销。
用 pinned pool + slab allocator 管理 GPU 显存块
核心思路是:预分配大块连续显存(比如 2GB),按固定大小切片(如 64KB/片),用位图标记空闲状态,避免 runtime 分配。C++20 的 std::pmr::monotonic_buffer_resource 不适合 GPU,得自己写。关键点:
- 用
cudaMallocPitch分配对齐内存(非cudaMalloc),保证每个KVslice 起始地址满足 128-byte 对齐,适配 Tensor Core 加载 - 每个 slice 存一个完整 token 的
K+V(含 layer/num_kv_heads/head_dim/dtype),不跨 slice 存,避免 cache line 跨界 - 用
std::atomic<uint64_t></uint64_t>做位图,64 位掩码管 64 个 slice,bit=1 表示已分配;分配时用__builtin_ctzll找首个 0 位,O(1) 定位 - 释放不立即归还位图,而是延迟到 batch 结束后统一回收(避免高频原子操作拖慢 decode loop)
如何对接 llama.cpp 的 kv_cache 结构
llama.cpp 的 struct llama_kv_cache 是 host 端控制结构,真正数据在 ctx->kv_self.k/v 两个 llama_tensor 里。你要改的是这两个 tensor 的 data 指针和 backend 字段:
- 把
data指向你 pool 里分配的 device ptr,不是cudaMalloc出来的零散地址 - 把
backend设为LLAMA_BACKEND_GPU,并确保ctx->cparams.n_gpu_layers > 0 - 重写
llama_kv_cache_seq_rm:不调cudaFree,只清位图对应 bit,并把该 slice 加入 freelist 链表(用 device ptr 做 next 指针,存在 slice 开头 8 字节)
注意:llama_kv_cache_update 默认假设 k/v 是连续 layout,你的 pool 必须严格按 [layer][kv_head][head_dim] 排列,否则 llama_get_kv_cache 读出来就是乱码。
容易被忽略的边界:context length 变化时的重映射
用户发来新请求,prompt 长度不定(128/2048/8192),但你的 pool 是固定大小 slice。不能让一个 8192-token 请求占满全部 128 个 slice——得支持“逻辑序列”到“物理 slice”的映射。做法是:
- 每个请求维护一个
std::vector<int32_t></int32_t>,存它实际占用的 slice index(不是地址) - decode 第 n 个 token 时,查这个 vector 得到第 n 个 slice 的 offset,再算出 device ptr = pool_base + offset * slice_size
- 当 context 扩展超过当前分配数,才触发新 slice 分配;缩小时不清内存,只 truncate vector
这个映射表本身很小(8192 个 int 是 32KB),放 host memory 即可,别往 GPU 上搬。真正的性能杀手从来不是计算,而是显存地址跳转——少一次 indirect access,就少一次 L2 cache miss。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











