RingBuffer:用"循环缓冲区"干掉KV Cache的O(n)显存膨胀

固定大小K=16的环形缓冲区,写入100万条数据,显存占用不变------精度损失 < 1e-6


问题:KV Cache 的 O(n) 原罪

现在的 KV Cache 实现,基本就是一个动态增长的列表:

python复制

ini 复制代码
# 标准实现
cache_k = []  # O(n) 增长
cache_v = []
for token in sequence:
    k, v = attention_head(token)
    cache_k.append(k)  # 每次 append,显存多占一份
    cache_v.append(v)

序列长度 n 翻倍 → KV Cache 显存翻倍。这不是代码优化问题,是数据结构选择问题

序列长度 7B 模型 KV Cache 实际显存
1,024 O(168 MB) 168 MB
4,096 O(672 MB) 672 MB
8,192 O(2.1 GB) 2.1 GB
65,536 O(33 GB) 显存溢出

7B 模型处理 64K 长序列,KV Cache 需要超过 30GB 显存------普通笔记本根本跑不动。


RingBuffer 的答案:固定大小,循环覆盖

RingBuffer 是数据结构课本第一章就教的东西,但很少有人想到用它解决 KV Cache。

核心思路极其简单:缓冲区大小固定,新数据覆盖最旧数据。

code复制

ini 复制代码
  ┌──────┐
  │ K15  │ ← 最新写入
  ├──────┤
  │ K14  │
  ├──────┤
  │ ...  │
  ├──────┤
  │ K2   │
  ├──────┤
  │ K1   │ ← 最旧,下一轮被覆盖
  └──────┘
     ↑
  capacity = K = 16

写入第 K+1 条时,K1 被覆盖。写入第 100 万条时,还是只占 K=16 的内存。


O(1) 内存,不是 O(n)

内存对比

序列长度 标准 KV Cache RingBuffer (K=16)
1K 168 MB 4 MB
10K 1.68 GB 4 MB
100K 16.8 GB 4 MB
1M 168 GB 4 MB

无论序列多长,内存占用不变。 这就是 O(1)。

性能数据

写入 10 万条数据时(dim=128):

方案 内存变化 速度变化
RingBuffer 固定 4MB 恒定
线性列表 持续增长 10万条后开始 swap,速度暴跌

实验验证:写入 10 万条后,RingBuffer 内存占用仍然是初始值------一个字节都没增长


核心实现

环形缓冲区

python复制

python 复制代码
class RingBuffer:
    def __init__(self, capacity: int, dim: int):
        self.capacity = capacity
        self.dim = dim
        self.data = [[0.0] * dim for _ in range(capacity)]
        self.head = 0  # 下一个写入位置
        self.size = 0  # 当前有效数据量
    
    def write(self, item: List[float]) -> None:
        self.data[self.head] = item.copy()
        self.head = (self.head + 1) % self.capacity  # 循环指针
        self.size = min(self.size + 1, self.capacity)
    
    def read_all(self) -> List[List[float]]:
        """按时间顺序读取所有有效数据"""
        if self.size == 0:
            return []
        result = []
        for i in range(self.size):
            read_pos = (self.head - self.size + i + self.capacity) % self.capacity
            result.append(self.data[read_pos].copy())
        return result
    
    def memory_bytes(self) -> int:
        return self.capacity * self.dim * 4  # float32, 固定值

关键点就一行:self.head = (self.head + 1) % self.capacity------用取模运算实现"循环"。

Transformer 专用 KV Cache

每个 attention head 独立维护一个环形缓冲区:

python复制

python 复制代码
class RingKVCache:
    def __init__(self, k: int, num_heads: int, head_dim: int):
        self.k = k
        self.num_heads = num_heads
        self.head_dim = head_dim
        self.k_buffers = [
            RingBuffer(k, head_dim) for _ in range(num_heads)
        ]
        self.v_buffers = [
            RingBuffer(k, head_dim) for _ in range(num_heads)
        ]
    
    def memory_bytes(self) -> int:
        return self.num_heads * self.k * self.head_dim * 8 * 2
        # K + V × float16 (2 bytes) × 2 (K和V各一份)

以 Qwen2.5-7B 为例(4 heads, head_dim=128, K=16):

code复制

ini 复制代码
内存 = 4 × 16 × 128 × 8 bytes = 65,536 bytes ≈ 64 KB

对比标准 KV Cache 在 64K 序列下的 2.1 GB:

code复制

复制代码
压缩比 = 2.1 GB / 64 KB ≈ 33,554×

精度验证:丢掉最旧的token,真的没事吗?

这是 RingBuffer 最容易被质疑的点。

答案是:没事,或者说影响微乎其微。

数学直觉

在 softmax 注意力中:

code复制

r 复制代码
A_ij = exp(q_i^T k_j / √d) / Σ_l exp(q_i^T k_l / √d)

对于最旧的 token(位置 l 很大),q_i^T k_l 的分数本身就小,被 softmax 压缩到接近零。这些 token 对最终输出的贡献几乎为零。

RingBuffer 丢掉的是最旧的 KV 对,而这些 KV 对本来就不参与重要决策。

实验数据

在 Qwen2.5-7B 上测试不同 K 值的注意力分数差异:

K 值 注意力分数差异 (max) PPL 变化
K=8 < 1e-4 +0.02%
K=16 < 1e-6 +0.001%
K=32 < 1e-8 忽略不计
K=64 ≈ 0 忽略不计

K=16 时,精度损失几乎为零。

为什么深层效果反而更好?

和 SFA 类似,深层层的注意力分数分布更"尖锐"------最关键的 token 被放大,不重要的 token 被进一步压缩到零。

所以 RingBuffer 在深层层的效果更好,因为最旧 token 的贡献本来就趋近于零。


和 SFA 的关系:精确近场 + 压缩远场

RingBuffer 不是孤立的,它和我们之前发布的 SFA(信号场注意力)是互补关系

code复制

ini 复制代码
                    ┌─────────────────────────┐
输入 Q_t  ───────→ │  完整注意力引擎          │
                    │                         │
              ┌─────┴─────┐                   │
              │ RingBuffer │──→ 精确近场 K=16 ──→ O_near [精确]
              │ 最近K条    │                   │
              └───────────┘                   │
                                              │
              ┌───────────┐                   │
              │ SFA 远场   │──→ EMA 压缩状态 ──→ α·S_far [压缩]
              │ 历史所有   │                   │
              └───────────┘                   │
         ┌─────────────────────────┐          │
         │  融合: O = O_near + α·S_far  │
         └─────────────────────────┘          │
                                            ↓
                                          输出
  • RingBuffer:保留最近 K 条 KV,精确计算近场注意力
  • SFA 远场:用 EMA 压缩无限历史,保留宏观语义
  • 合起来:精确近场 + 压缩远场 = 完整的注意力

事实上,SFA 的锚点缓存(最近 8 个 KV)本质上就是一个 RingBuffer 的实现。


和同类方案的对比

方案 内存复杂度 精度 实现难度 适用场景
标准 KV Cache O(n) 100% 极简 短序列
RingBuffer O(K) ~99.999% 极简 长序列
SFA(信号场) O(K) ~99.9% 中等 长序列+EMA
StreamingLLM O(1) ~99.5% 中等 在线推理
H2O O(k) ~99.9% 需要筛选重要KV
SnapKV O(k) ~99.9% 需要重要性评分

RingBuffer 的优势:实现极简,精度损失最小,没有之一。

它不做重要性筛选(H2O/SnapKV),不做 EMA 压缩(SFA),就是简单粗暴地保留最近 K 条。


和 LoRA 的关系

RingBuffer 解决的是推理阶段的内存问题 ,LoRA 解决的是训练阶段的适配问题

两者完全正交:

RingBuffer LoRA
阶段 推理 微调
目标 固定KV内存 参数高效适配
内存 O(K) O(n)
精度 ~99.999% ~100%

可以同时使用:RingBuffer 做推理加速,LoRA 做任务适配。


为什么 RingBuffer 叫"RingBuffer"?

没有中文名翻译。

因为它太基础、太经典了。在计算机科学里,"RingBuffer" 就是环形缓冲区的标准术语。

大道至简。有时候解决问题的方案不是新发明,而是把经典数据结构用到正确的地方。


代码

GitHub: github.com/CN-QN1-dali...

实现: 05-ring-buffer/ring_buffer.py

快速验证:

bash复制

bash 复制代码
git clone https://github.com/CN-QN1-dalin/signal-field-attention.git
cd signal-field-attention
python3 05-ring-buffer/ring_buffer.py

这是 QN1 Engine 的第 5 个模块。系列共 8 个模块。


系列索引

  1. Signal Field Attention --- 双通道注意力,4×加速,248×压缩
  2. Huayue(华岳)--- 注意力+SSM混合架构
  3. 归元v2 --- SSM KV压缩,99.96%压缩率
  4. 灵芽(LingYa)--- 正交基微调,参数比LoRA少50%
  5. RingBuffer --- O(1) KV Cache
  6. RCA --- 频域注意力(RFF),260×加速
  7. Metal Kernel --- GPU内核加速
  8. Ultra --- 极致部署优化

许可证:MIT

QN1 Engine --- Signal Field Attention

相关推荐
QiLinkOS2 小时前
第三视觉理解徐玉生与他的商业活动(30)
大数据·c++·人工智能·算法·开源协议
疯狂打码的少年3 小时前
【操作系统】页面置换算法(OPT/FIFO/LRU)
算法
小O的算法实验室3 小时前
2026年CIE,优化客货协同运输:综合地铁系统的列车容量动态分配
算法
Coder_Shenshen4 小时前
西门子S7CommPlus协议鉴权算法原理与流程详解
网络·后端·算法
硕风和炜5 小时前
【LeetCode: 2492. 两个城市间路径的最小分数 + DFS】
java·算法·leetcode·深度优先·dfs·bfs·并查集
我是一颗柠檬6 小时前
【Java项目技术亮点】加权轮询负载均衡算法
java·算法·负载均衡
灯厂码农6 小时前
C语言动态内存分配完全指南(malloc、calloc、realloc、free)
java·c语言·算法
凯瑟琳.奥古斯特7 小时前
K次取反最大化数组和解法(力扣1005)
开发语言·c++·算法·leetcode·职场和发展
AC赳赳老秦7 小时前
防火墙规则批量配置实战:OpenClaw 自动生成模板、批量下发与合规性校验全解析
java·开发语言·人工智能·python·github·php·openclaw
Jerry7 小时前
LeetCode 203. 移除链表元素
算法