KV cache 缓存与量化:加速大型语言模型推理的关键技术

引言

在大型语言模型(LLM)的推理过程中,KV 缓存(Key-Value Cache) 是一项至关重要的优化技术。自回归生成(如逐 token 生成文本)的特性决定了模型需要反复利用历史token的注意力计算结果,而 KV 缓存通过存储这些中间值(即键值对 K/V),避免了重复计算,大幅提升了推理效率。然而,随着上下文长度的增加,KV 缓存占用的内存也迅速膨胀(例如 7B 模型处理 10k token 输入时需约 5GB 内存),成为制约长文本生成的瓶颈。

为了解决这一问题,KV 缓存量化技术应运而生。通过将缓存的数值从高精度(如FP16)压缩为低精度(如 INT4或 INT2),在几乎不影响生成质量的前提下,内存需求可降低 2.5 倍以上。本文将深入解析 KV 缓存的工作原理、量化技术的实现细节。

KV caching 详解

参考1参考2

  • KV cache 流程展示
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  • LLM 推理的过程是一个自回归的过程,每次生成一个 token 的时候需要结合前面所有的 token 做 attention 操作。也就是说前 i 次的token会作为第 i+1 次的预测数据送入模型,才能得到第 i+1 次的推理 token

  • 由于解码器是因果的(即,一个 token 的注意力仅取决于其前面的 token),因此在每个生成步骤中,我们都在重新计算相同的先前 token 的注意力,而实际上我们只是想计算新 token 的注意力。

  • KV Cache 核心节约的时间有三大块:1)前面 n-1 次的 Q 的计算,当然这块对于一次一个 token 的输出本来也没有用;2)同理还有 Attention 计算时对角矩阵变为最后一行,和 1)是同理的,这样 mask 矩阵也就没有什么用了;3)前面 n-1 次的 K 和 V 的计算,也就是上图紫色部分,这部分是实打实被 Cache 过不需要再重新计算的部分。

  • 使用 Transformer 🤗 来比较有和没有 KV 缓存的 GPT-2 生成速度

import numpy as np
import time
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

device = "cuda" if torch.cuda.is_available() else "cpu"
tokenizer = AutoTokenizer.from_pretrained("gpt2")
model = AutoModelForCausalLM.from_pretrained("gpt2").to(device)

for use_cache in (True, False):
  times = []
  for _ in range(10):  # measuring 10 generations
    start = time.time()
    model.generate(**tokenizer("What is KV caching?", return_tensors="pt").to(device), use_cache=use_cache, max_new_tokens=1000)
    times.append(time.time() - start)
  print(f"{'with' if use_cache else 'without'} KV caching: {round(np.mean(times), 3)} +- {round(np.std(times), 3)} seconds")

在 Google Colab 笔记本上,使用 Tesla T4 GPU,生成 1000 个新 token 的报告平均时间和标准差如下:
使用 KV 缓存:11.885 ± 0.272 秒
没有 KV 缓存:56.197 ± 1.855 秒

KV cache 量化

参考1, 参考2, 参考3

  • 机器学习中常用的数据类型( float32、float16、bfloat16、int8)以及基本的量化原理介绍:link

  • 模型量化简介:

    • 假设你要用 absmax 对向量 [1.2, -0.5, -4.3, 1.2, -3.1, 0.8, 2.4, 5.4] 进行量化。首先需要计算该向量元素的最大绝对值
    • Int8 的范围为 [-127, 127],因此我们将 127 除以 5.4,得到缩放因子 23.5。
    • 最后,将原始向量乘以缩放因子得到最终的量化向量 [28, -12, -101, 28, -73, 19, 56, 127]。
    • 要恢复原向量,可以将 int8 量化值除以缩放因子,但由于上面的过程是“四舍五入”的,我们将丢失一些精度。
      在这里插入图片描述
  • 为什么需要 kv cache 量化?

    • 估算一下,当用 7B Llama-2 模型处理 10000 个词元的输入时,我们需要多少内存来存储 KV 缓存。存储一个词元的 KV 缓存所需的内存大致为 2 * 2 * 层数 * 键值抽头数 * 每抽头的维度 ,其中第一个 2 表示键和值,第二个 2 是我们需要的字节数 (假设模型加载精度为 float16 )。因此,如果上下文长度为 10000 词元,仅键值缓存所需的内存我们就要:
      2 * 2 * 32 * 32 * 128 * 10000 ≈ 5GB
      该内存需求几乎是半精度模型参数所需内存的三分之一。
    • 因此,通过将 KV 缓存压缩为更紧凑的形式,我们可以节省大量内存并在消费级 GPU 上运行更长上下文的文本生成
  • KV cache 量化方式

    • 给定形状为 batch size, num of head, num of tokens, head dim 的键或值,我们将其分组为 num of groups, group size 并按组进行仿射量化,如下所示:
      X_Q = round(X / S) - Z
      这里:
      X_Q 是量化后张量
      S 是比例,计算公式为 (maxX - minX) / (max_val_for_precision - min_val_for_precision)
      Z 是零点,计算公式为 round(-minX / S)
  • 实验效果:两个后端的 int4 缓存的生成质量与原始 fp16 几乎相同,而使用 int2 时出现了质量下降
    在这里插入图片描述

  • transformers 中使用量化 kv cache 的方式

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-chat-hf")
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-chat-hf", torch_dtype=torch.float16, device_map="cuda:0")
inputs = tokenizer("I like rock music because", return_tensors="pt").to(model.device)

out = model.generate(**inputs, do_sample=False, max_new_tokens=20, cache_implementation="quantized", cache_config={"backend": "quanto", "nbits": 4})
print(tokenizer.batch_decode(out, skip_special_tokens=True)[0])
# I like rock music because it's loud and energetic. It's a great way to express myself and rel

out = model.generate(**inputs, do_sample=False, max_new_tokens=20)
print(tokenizer.batch_decode(out, skip_special_tokens=True)[0])
# I like rock music because it's loud and energetic. I like to listen to it when I'm feeling
<think>嗯,用户问的是大模型中KV Cache的实现方式,需要详细解释。首先,我得回忆一下自己对KV Cache的理解。KV Cache主要是在Transformer的自注意力机制中用来缓存键(Key)和值(Value)的,这样可以避免在生成每个token时重新计算之前的KV,节省计算资源,提高推理速度。不过具体实现细节可能比较复杂,需要分步骤说明。 首先,用户可能需要知道KV Cache的基本原理,比如预填充阶段和解码阶段的区别。预填充阶段处理整个prompt序列,生成对应的KV缓存,而解码阶段逐步生成新token,并更新缓存。这部分需要结合引用里的内容,比如引用[1]提到的两个阶段,还有引用[3]提到的更新KV Cache的过程。 然后,实现细节可能包括数据结构设计,比如如何存储KV缓存。通常是用张量来保存,每个层都有自己的KV缓存。引用[3]提到形状的变化,比如batch_size、num_heads、seq_len和维度,这部分需要详细说明。 另外,内存管理也很重要,用户可能关心内存优化方法,比如引用[2]提到的量化技术,或者动态缓存和分块存储。还有计算流程,比如在预填充阶段并行计算,解码阶段增量更新,引用[4]里的预填充阶段解释可能需要提到。 还要注意实现中的关键技术点,比如注意力掩码调整,防止越界访问,以及内存预分配策略。可能用户还会关心不同框架的具体实现差异,比如Hugging Face和vLLM的不同处理方式,这可以引用相关例子说明。 最后,优化措施也是重点,比如量化、分块、压缩,这些在引用里都有提到,需要整合到回答中。同时,生成相关问题的时候,要围绕实现方式、优化、应用场景等展开,确保问题有针对性。 总结一下,回答的结构应该是先介绍KV Cache的作用和原理,再分阶段详细说明预填充和解码,接着讲具体实现中的数据结构、内存管理、计算流程,关键技术点,不同框架的实现,优化措施,最后给出相关问题。需要确保引用正确的文献,比如引用[1][3][4]来支持各个部分的内容,同时保持语言的中文和格式要求。</think>### 大模型 KV Cache 实现方法详解 KV Cache大型语言模型(LLM)推理优化的核心技术,通过缓存历史 token 的键(Key)和值(Value)张量,避免重复计算,提升生成效率。以下是其实现方式的详细拆解: --- #### 一、KV Cache 的核心原理 1. **自注意力机制中的关键角色** Transformer 的自注意力计算公式为: $$ \text{Attention}(Q,K,V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V $$ 其中 $Q,K,V$ 分别表示查询(Query)、键(Key)、值(Value)矩阵。KV Cache 的作用是缓存历史 token 的 $K$ 和 $V$ 矩阵,避免每次生成新 token 时重新计算整个序列的注意力权重[^1][^4]。 2. **空间换时间的本质** 通过牺牲存储空间(缓存历史 KV 值)换取计算时间(减少重复计算),尤其对长序列生成任务(如文本续写)效果显著[^1]。 --- #### 二、KV Cache 的实现步骤 ##### 1. **预填充阶段(Prefill Phase)** - **输入处理**:将完整的输入序列(如 prompt)一次性输入模型。 - **并行计算**:为每个 Transformer 层生成初始的 KV 缓存: ```python # 示例:PyTorch 实现逻辑 k_cache = torch.zeros(batch_size, num_heads, max_seq_len, head_dim) v_cache = torch.zeros_like(k_cache) for i in range(input_seq_len): k_cache[:, :, i, :] = current_layer.key(input_tokens[i]) v_cache[:, :, i, :] = current_layer.value(input_tokens[i]) ``` - **缓存形状**:形状为 $(batch\_size, num\_heads, seq\_len, head\_dim)$,其中 $head\_dim$ 是注意力头的维度[^3][^4]。 ##### 2. **解码阶段(Decode Phase)** - **增量更新**:每生成一个新 token,仅计算当前 token 的 $K_t$ 和 $V_t$,并追加到缓存末尾: ```python # 当前 token 的 KV 计算 k_t = current_layer.key(new_token) v_t = current_layer.value(new_token) # 更新缓存 k_cache = torch.cat([k_cache, k_t], dim=2) v_cache = torch.cat([v_cache, v_t], dim=2) ``` - **注意力计算优化**:仅需计算新 token 的 $Q$ 历史 $K$、$V$ 的注意力权重,避免全序列重复计算[^3][^4]。 --- #### 三、关键技术实现细节 1. **内存管理策略** - **动态缓存**:根据序列长度动态扩展缓存空间(如使用链表或可扩展张量)。 - **分块存储**:将长序列拆分为固定大小的块(如 512 token/块),减少内存碎片[^2]。 2. **计算优化** - **掩码调整**:在自注意力中应用因果掩码(Causal Mask),确保新 token 仅关注历史信息。 - **内存预分配**:预先分配最大序列长度的缓存空间(如 4096 tokens),避免频繁内存操作[^3]。 3. **框架级实现差异** | 框架/库 | 实现特点 | |---------------|--------------------------------------------------------------------------| | Hugging Face | 使用 `past_key_values` 参数管理缓存,支持动态序列扩展 | | vLLM | 采用 PagedAttention 技术,将 KV Cache 分页存储以支持超长序列生成[^2] | --- #### 四、优化措施挑战 1. **内存压缩技术** - **量化(Quantization)**:将 KV Cache 从 FP16 压缩至 INT8,内存占用减少 50%[^2]。 - **稀疏化**:通过剪枝移除冗余的 KV 条目。 2. **性能瓶颈** - **显存带宽限制**:KV Cache 频繁读写可能导致显存带宽成为瓶颈。 - **长序列稳定性**:缓存累积可能引发数值溢出或注意力权重衰减[^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值