Transformer推理揭秘:Prefill阶段的QKV并行计算与KV Cache的真相
1、背景
在大语言模型(LLM)的推理世界里,有两个核心阶段:Prefill(预填充)和Decode(解码)。
很多开发者,包括我自己,在初学时常被一个看似矛盾的问题所困扰:
【疑问1】 “Transformer的自注意力机制(Self-Attention)不是要计算当前Token与所有历史Token的关系吗?这听起来是一个串行的、一步步来的过程。那为什么在Prefill阶段,我们却说QKV的计算是‘高度并行’的?这个‘并行’到底是怎么做到的?”
【疑问2】“既然每个Token的K和V计算出来后就被缓存,而模型参数又是固定的。那我是不是可以‘一劳永逸’?比如,把‘苹果’这个词的KV值算一次,然后所有用到‘苹果’的请求都共用这份缓存,岂不是能省下海量计算?”
2、Prefill vs. Decode:两种截然不同的模式
Decode(解码)阶段:这是一个自回归(Autoregressive)的串行过程。模型生成第一个Token后,必须将这个Token作为输入,才能生成第二个Token,以此类推。每一步都严格依赖上一步的结果,无法并行。
Prefill(预填充)阶段:这是一个离线批处理的并行过程。在这个阶段,模型接收的是用户输入的完整Prompt。所有输入Token都是已知的。正是因为拥有了这个“上帝视角”,模型才能对整个序列进行一次性、大规模的并行计算。
3、并行计算的三大步骤:从矩阵乘法说起
在Prefill阶段,模型并非逐个处理Token,而是将整个Prompt序列组织成矩阵,利用GPU强大的并行计算能力,通过三次关键的矩阵运算完成所有工作。
假设我们的Prompt是 [A, B, C, D],长度为 N=4。
步骤1:并行计算Q、K、V向量
模型首先将整个Prompt的嵌入向量(Embedding)矩阵 X(形状为 (N, d_model))一次性输入。然后通过三个固定的线性变换矩阵 W_Q, W_K, W_V,计算出所有Token的Q、K、V。
Q = X * W_Q
K = X * W_K
V = X * W_V
并行性分析:这是一个标准的矩阵-矩阵乘法(GEMM)。GPU可以瞬间并行计算出结果矩阵中的每一个元素。Token A的Q向量计算完全不依赖Token B,它们是彼此独立的。
结论:这一步是完全并行的。
步骤2:并行计算注意力分数
这是解开我们第一个疑惑的关键。逻辑上,我们需要计算每个Q与所有K的点积。但这在实现上,依然是一次巨大的矩阵乘法。
Attention_Scores = Q * K^T
这个单一的运算,会一次性生成一个 N x N 的分数矩阵,其中包含了所有Token对之间的关联度。
| (Q对K) | K_A | K_B | K_C | K_D |
|---|---|---|---|---|
| Q_A | Score(A,A) | Score(A,B) | Score(A,C) | Score(A,D) |
| Q_B | Score(B,A) | Score(B,B) | Score(B,C) | Score(B,D) |
| Q_C | Score(C,A) | Score(C,B) | Score(C,C) | Score(C,D) |
| Q_D | Score(D,A) | Score(D,B) | Score(D,C) | Score(D,D) |
- 并行性分析:GPU的数千个核心会同时开工,计算出这个矩阵里的所有16个分数。虽然计算
Score(C,A)需要Q_C和K_A,但这并不妨碍硬件同时计算Score(A,B)。 - 结论:这一步也是高度并行的
步骤3:并行聚合信息与FFN处理
得到分数后,经过Softmax归一化,再与V矩阵相乘,得到每个Token融合了上下文信息的新表示。
Output = Softmax(Attention_Scores) * V
- 并行性分析:这同样是一个矩阵-矩阵乘法,可以并行完成。随后,
Output会进入前馈神经网络(FFN)。FFN对每个Token位置的计算是独立的,因此也是完全并行的。
4、KV Cache的真相:为什么不能“算一次,大家用”
现在我们来解决第二个,也是更深刻的那个疑问。既然QKV计算是并行的,而且模型参数(W_Q, W_K, W_V)是固定的,为什么不能把“苹果”的KV缓存下来,供所有请求复用呢?
问题的核心在于:KV Cache存储的不是“字典定义”,而是“语境化的理解”。
【你的疑惑:固定的矩阵,变化的输出?】
你可能会想:K = W_K * xW_K是固定的,那只要输入x一样,K不就应该一样吗?
这里的关键是,输入x真的不一样!
【顿悟:Token的向量表示是“层层进化”的】
Transformer不是一层,而是有几十层(例如LLaMA有32层或更多)。KV Cache存储的是最后一层计算出的K和V。
-
第0层(输入层):
- 输入
x_0= “苹果”的词嵌入 + 位置编码。 - 此时,你的想法是对的,如果位置也一样,
x_0就一样。
- 输入
-
第1层(信息融合开始):
- Self-Attention机制开始工作。它让“苹果”这个Token“看到”了后面的词,比如“很好吃”。
- “很好吃”的语义信息通过Attention的加权求和,被融合进了“苹果”的向量表示中。
- 经过FFN处理后,输出的新向量
x_1已经不再是当初那个纯粹的“苹果”了,它变成了一个“正在被吃的苹果”的表示。
-
第2层到第N层(持续进化):
- 这个过程在每一层重复。
x_1作为输入进入第2层,再次与上下文交互,被进一步“染色”和“重塑”,变成x_2。 - 这就像一个滚雪球的过程,每一层都在向量里加入更多关于整个句子的信息。
- 这个过程在每一层重复。
-
最后一层(生成KV Cache):
- 当信息传递到最后一层时,得到的向量
x_final已经是一个“集大成者”。它不仅仅代表“苹果”这个词,还编码了它在“苹果很好吃”这个特定句子中的全部语境。 - 最后,模型用这个独一无二的
x_final去乘以固定的W_K,得到的K_final自然也是独一无二的。
- 当信息传递到最后一层时,得到的向量
【生动的比喻】
- 模型参数(W_K):就像一个榨汁机。
- 输入向量(x):就像要榨的水果。
- KV Cache:就是榨出来的果汁。
在场景A中,你放入的是“苹果+橙子”(“苹果很好吃”),榨出来的是苹果橙汁。
在场景B中,你放入的是“苹果+胡萝卜”(“苹果发布了”),榨出来的是苹果胡萝卜汁。
虽然榨汁机(W_K)是同一个,但因为投入的“水果组合”(上下文)不同,最终产出的“果汁”(KV Cache)也完全不同。你显然不能用“苹果橙汁”去替代“苹果胡萝卜汁”。
5、总结
- Prefill的并行性:源于对整个已知Prompt序列的矩阵化批量处理。通过三次大规模的矩阵乘法(GEMM),GPU可以并行完成所有Token的QKV生成、注意力计算和信息聚合。这是一个计算密集型(Compute-Bound)的过程。
- KV Cache的不可复用性:KV Cache存储的是一个Token在特定上下文中,经过几十层网络“层层加工”后的最终状态。由于每个Prompt的上下文都不同,Token的向量表示在每一层都会被重塑,导致最终的KV值也是独一无二的。因此,每个新请求都必须重新进行Prefill计算。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)