DeepSeek MLA 架构详解:多头潜向量注意力如何将 KV Cache 降低 93%
- 作者

- 姓名
- Nino
- 职业
- Senior Tech Editor
自回归大语言模型(LLM)的推理计算模式在结构上划分为两个截然不同的阶段:Prefill(预填阶段) 与 Decode(解码阶段)。在 Prefill 阶段,输入 Prompt 的所有 Token 被并行处理。该阶段属于计算密集型(Compute-Bound),GPU 的 Tensor Core 能够通过高密度的 GEMM(通用矩阵乘法)运算达到极高的算术强度(Arithmetic Intensity)。
然而在 Decode 阶段,模型以自回归方式逐个生成新 Token。每生成一个新 Token,都需要与之前所有 Token 的 Key(键)和 Value(值)向量进行 Attention 计算。此时算术强度断崖式下跌至约 1 FLOP/Byte。这意味着自回归解码过程受到了极强的显存带宽限制(Memory-Bandwidth Bound)。为了避免在每个生成步骤重复计算历史 Token 的 Key 和 Value 向量,推理引擎会将这些向量缓存在高带宽显存(HBM)中,即 KV Cache。在构建高性能 AI 应用或通过 n1n.ai 聚合 API 接入大模型服务时,KV Cache 的显存占用直接决定了系统并发上限与响应延迟。
本文将深入剖析 DeepSeek 所提出的 多头潜向量注意力(Multi-Head Latent Attention, MLA) 架构,从数学原理、旋转位置编码(RoPE)解耦机制到矩阵吸收(Matrix Absorption),揭示其如何在保持多头注意力表达能力的同时实现 93% 的 KV Cache 显存削减。
标准 KV Cache 的显存瓶颈与数学推导
在标准 Transformer 架构中,KV Cache 的显存占用与序列长度 、批次大小 、网络层数 、KV 头数 以及头维度 成线性正比:
其中 代表数值精度所占字节数(FP16/BF16 为 2 字节,FP8 为 1 字节)。
以配备 80GB HBM3 显存、峰值带宽为 3.35 TB/s 的 NVIDIA H100 GPU 为例:运行 FP16 精度的 Llama 3 70B 模型时,仅模型权重就占据约 140 GB 显存(跨张量并行节点分片)。在 的长上下文场景下,单个请求序列的 KV Cache 显存占用高达 40.96 GB。
若在并发批次大小 下运行,每一步解码都需要从显存中读取 的数据。仅显存数据传输所需的理论极速时间为:
这意味着生成速度上限被物理性限制在 10.2 Token/s 左右,同时造成 GPU 超过 90% 的 Tensor Core 计算资源处于空闲等待状态。
架构对比:MHA vs GQA vs MLA
为缓解这一瓶颈,先前研究提出了多查询注意力(Multi-Query Attention, MQA),通过在所有 Query 头间共享单一 KV 头()来降低缓存,但这招致了严重的表达能力下降(在 GSM8k 及长文本检索测试中出现 3.8% 至 6.2% 的准确率下跌)。分组查询注意力(Grouped-Query Attention, GQA)则采取折中方案,将 Query 头划分为 个分组。
DeepSeek 提出的 MLA 架构则彻底颠覆了这种折中思路线路。它并非简单地裁减头数,而是通过低秩矩阵分解将 Key-Value 空间压缩至低维潜向量(Latent Vector)中,同时完整保留 128 个独立的 Query 注意力头。
单 Token 缓存与显存扩展对比表
| 架构类型 | 对应模型基准 | 层数 () | Query 头数 () | KV 头数 () | 单头维度 () | 数据精度 | 单 Token KV 缓存大小 |
|---|---|---|---|---|---|---|---|
| 标准 MHA | DeepSeek 67B Baseline | 60 | 128 | 128 | 128 | FP16 (2 B) | 3,932,160 字节 (3.84 MB) |
| 标准 MHA | Llama 2 70B (假设 MHA) | 80 | 64 | 64 | 128 | FP16 (2 B) | 2,621,440 字节 (2.50 MB) |
| GQA (8:1) | Llama 3 70B / 405B | 80 | 64 | 8 | 128 | FP16 (2 B) | 327,680 字节 (320.0 KB) |
| GQA (4:1) | Mistral Large | 88 | 64 | 8 | 128 | FP16 (2 B) | 360,448 字节 (352.0 KB) |
| DeepSeek MLA | DeepSeek-V2 / DeepSeek-V3 | 60 | 128 | —(潜向量) | 576 标量 | FP16 (2 B) | 138,240 字节 (135.0 KB) |
| DeepSeek MLA | DeepSeek-V2 / DeepSeek-V3 | 60 | 128 | —(潜向量) | 576 标量 | FP8 (1 B) | 69,120 字节 (67.5 KB) |
不同上下文长度下 KV Cache 总容量对比 ()
| 上下文长度 () | DeepSeek 67B (MHA, FP16) | Llama 3 70B (GQA 8:1, FP16) | DeepSeek MLA (FP16) | DeepSeek MLA (FP8) |
|---|---|---|---|---|
| 8,192 (8k) | 30.72 GB | 2.56 GB | 1.08 GB | 0.54 GB |
| 32,768 (32k) | 122.88 GB | 10.24 GB | 4.32 GB | 2.16 GB |
| 65,536 (64k) | 245.76 GB | 20.48 GB | 8.64 GB | 4.32 GB |
| 131,072 (128k) | 503.32 GB | 40.96 GB | 17.28 GB | 8.64 GB |
通过 n1n.ai 接入 DeepSeek-V3 等高效架构模型,开发者能够在长上下文业务中享受更低延迟与更高吞吐量带来的成本优势。
DeepSeek MLA 核心架构与数学原理解析
1. 低秩 Key-Value 潜向量压缩
MLA 并不直接存储所有 128 个注意力的 Key 和 Value 矩阵,而是将输入的隐藏层状态 投影至低维压缩潜向量空间 :
其中 ,,低维潜向量维度 。
在模型训练时,通过升维矩阵 与 解压还原各个注意头。在标准 MHA 架构下(128 头,),单个 Token 需要存储 个标量;而 MLA 仅需存储 512 维潜向量 ,内容空间的原始压缩比达到 ,即实现了 98.44% 的内容缓存削减。
2. RoPE 旋转位置编码不可交换难题
旋转位置编码(RoPE)通过正交旋转矩阵 对 Key 向量进行位置变换。若将 RoPE 直接施加于升维后的 Key 向量:
k_{t,i}^C = \mathcal{R}t (W{(i)}^{UK} c_t^{KV})
此时计算 Query 与历史 Token Key 的注意力内积:
由于旋转矩阵 与升维投影矩阵 不满足乘法交换律(即 ),如果显存中只保存潜向量 ,推理引擎在每个解码步骤都必须对历史所有 Token 重新执行 升维并乘上 。这会导致大量的动态计算开销,彻底抵消显存带宽节省带来的收益。
3. 解耦 RoPE 机制与矩阵吸收(Matrix Absorption)
DeepSeek 巧用解耦设计,将位置信息与语义内容拆分为两个独立的通道:
- 内容流(Content Stream, ):完全由潜向量 线性生成,不包含任何位置编码。
- RoPE 位置流(RoPE Stream, ):由独立投影矩阵 生成并施加旋转矩阵 ,由所有 128 个注意力头共享。
拼接后的单头 Key 向量维度为 。此时注意力内积拆分为两项可加和:
由于内容 Key 属于纯线性变换,我们可以运用矩阵乘法的结合律:
在解码步骤 ,针对当前 Token 算出一组吸收 Query(Absorbed Query) :
该投影变换在当前步骤仅需计算一次。接着,即可直接使用 512 维的吸收 Query 与显存中缓存的 512 维潜向量 进行点积计算!
同样地,对 Value 的加权聚合直接对 512 维缓存向量 进行,升维矩阵 被融合进最终的输出投影矩阵 中:
单 Token 最终缓存标量数
相比 32 头标准 MHA( 标量):
生产级 PyTorch 实现:Single-Token MLA 解码内核
以下代码展示了包含矩阵吸收与解耦 RoPE 的完整单 Token 自回归解码内核实现:
import math
import torch
import torch.nn as nn
from typing import Tuple
class MultiHeadLatentAttentionDecode(nn.Module):
"""
生产级多头潜向量注意力 (MLA) 自回归解码内核
示范矩阵吸收、解耦 RoPE 以及零解压 KV Cache 显存流式计算
"""
def __init__(
self,
d_model: int = 5120,
n_heads: int = 128,
d_head: int = 128,
d_latent: int = 512,
d_rope: int = 64
):
super().__init__()
self.d_model = d_model
self.n_heads = n_heads
self.d_head = d_head
self.d_latent = d_latent # d_c (512 标量)
self.d_rope = d_rope # d_h^R (64 标量)
self.scale = 1.0 / math.sqrt(d_head + d_rope)
# 1. KV 下投影:将隐藏状态投影至共享低维潜向量空间
self.W_DKV = nn.Linear(d_model, d_latent, bias=False)
# 2. KV 上投影矩阵(在 Decode 阶段被矩阵吸收)
self.W_UK = nn.Parameter(torch.empty(n_heads, d_head, d_latent))
self.W_UV = nn.Parameter(torch.empty(n_heads, d_head, d_latent))
# 3. 解耦 RoPE Key 投影(128 个注意力头共享)
self.W_KR = nn.Linear(d_model, d_rope, bias=False)
# 4. Query 压缩与投影
self.W_DQ = nn.Linear(d_model, 1536, bias=False)
self.W_UQ = nn.Linear(1536, n_heads * d_head, bias=False)
self.W_QR = nn.Linear(1536, n_heads * d_rope, bias=False)
# 5. 输出投影矩阵
self.W_O = nn.Linear(n_heads * d_head, d_model, bias=False)
nn.init.normal_(self.W_UK, std=0.02)
nn.init.normal_(self.W_UV, std=0.02)
def apply_rope(self, x: torch.Tensor, pos: int) -> torch.Tensor:
"""对 Key 和 Query 应用 1D 旋转位置编码 (RoPE)"""
half_dim = x.shape[-1] // 2
freqs = torch.exp(-math.log(10000.0) * torch.arange(0, half_dim, device=x.device) / half_dim)
angles = pos * freqs
cos = torch.cos(angles).repeat(2)
sin = torch.sin(angles).repeat(2)
x_rot = torch.cat([-x[..., half_dim:], x[..., :half_dim]], dim=-1)
return (x * cos) + (x_rot * sin)
def forward_decode(
self,
h_t: torch.Tensor,
current_pos: int,
kv_cache_latent: torch.Tensor,
kv_cache_rope: torch.Tensor
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""
单 Token 解码步骤(含全矩阵吸收)
kv_cache_latent: [Batch, SeqLen, d_latent] -> 仅存储 512 标量/Token
kv_cache_rope: [Batch, SeqLen, d_rope] -> 仅存储 64 标量/Token
"""
B = h_t.shape[0]
# 步骤 1:计算当前 Token 的潜向量与共享 RoPE Key(每个 Token 仅保存 576 个标量)
c_t_kv = self.W_DKV(h_t) # [B, 1, 512]
k_t_rope = self.apply_rope(self.W_KR(h_t), current_pos) # [B, 1, 64]
# 追加至 HBM 持久化 KV Cache
kv_cache_latent = torch.cat([kv_cache_latent, c_t_kv], dim=1)
kv_cache_rope = torch.cat([kv_cache_rope, k_t_rope], dim=1)
# 步骤 2:计算瞬态 Query 向量
c_t_q = self.W_DQ(h_t) # [B, 1, 1536]
q_content = self.W_UQ(c_t_q).view(B, self.n_heads, self.d_head) # [B, 128, 128]
q_rope = self.W_QR(c_t_q).view(B, self.n_heads, self.d_rope) # [B, 128, 64]
q_rope = self.apply_rope(q_rope, current_pos)
# 步骤 3:矩阵吸收 (Matrix Absorption)
# 将当前步 Query 投影至潜向量空间:q_absorbed = q_content @ W_UK
# W_UK: [128, 128, 512] -> q_absorbed: [B, 128, 512]
q_absorbed = torch.einsum('bhd,hdm->bhm', q_content, self.W_UK)
# 步骤 4:潜向量空间注意力点积计算
# 内容得分直接在 512 维 Latent 空间计算
score_content = torch.einsum('bhm,bsm->bhs', q_absorbed, kv_cache_latent)
# 位置得分在 64 维 RoPE 空间计算
score_rope = torch.einsum('bhr,bsr->bhs', q_rope, kv_cache_rope)
attention_scores = (score_content + score_rope) * self.scale
attention_weights = torch.softmax(attention_scores, dim=-1) # [B, 128, SeqLen]
# 步骤 5:潜向量空间 Value 聚合
# 注意力权重直接对 512 维潜向量进行线性加权
u_latent = torch.einsum('bhs,bsm->bhm', attention_weights, kv_cache_latent) # [B, 128, 512]
# 通过融合后的 Value-Output 矩阵完成最终投影
v_projected = torch.einsum('bhm,hdm->bhd', u_latent, self.W_UV)
output = self.W_O(v_projected.reshape(B, 1, self.n_heads * self.d_head))
return output, kv_cache_latent, kv_cache_rope
架构工程落地的核心总结
- 显存带宽决定解码吞吐:在长文本场景()下,解码性能瓶颈完全在于 HBM 带宽。降低 KV Cache 体积是提升推理速度的最有效途径。
- 潜向量表征优于注意力头裁剪:MQA 和 GQA 通过物理裁减头数来降低显存,损害了模型的表达丰富度。MLA 采用低秩潜向量空间,在保留 128 个注意力头的同时大幅降低存储占用。
- 矩阵结合律实现零运行时开销:通过在推理阶段将上投影矩阵 融合至当前 Token 的 Query 向量中,推理引擎无需在 HBM 中解压历史 KV 即可直接完成点积。
- 解耦 RoPE 维持位置感知能力:将位置编码分离为独立的 64 维共享通道,既保证了空间位置信息的准确传递,又维持了潜向量的线性吸收能力。
需要高效部署 DeepSeek-V3 或 Claude 3.5 Sonnet 等高吞吐模型的企业开发者,可以通过 n1n.ai 统一 API 接口快速接入低延迟推理服务。
Get a free API key at n1n.ai