GQA
共享 KV 头,KV cache 缩小 N/K 倍
CS336 · 第 4 课
长上下文会放大标准 attention 的计算与数据移动成本。这一课分开研究两类稀疏性:减少 token 之间的 attention edges,以及让每个 token 只激活一部分专家参数;两者解决的不是同一个瓶颈。
Spring 2026 官方来源:Lecture 4 PDF · 课程日程
本页是中文学习导读;讲次主题、教师与材料版本以 Stanford 官方页面和讲义为准。
本讲问题:标准 attention 在上下文、KV cache 和计算量上形成瓶颈,因此需要先明确想减少的是 KV heads、序列交互还是每 token 激活的参数量,再选择替代机制。
标准 attention 成本 → GQA / local 等减少 attention 开销 → MoE 让每 token 只激活部分参数 → routing / load balance / communication 成为新约束。
标准 Transformer 的两种扩展压力不同:attention 随 context 增长,dense MLP 则让每个 token 使用全部 FFN 参数。先复习 \(QK^\top\) 的序列轴与 MLP 的逐 token 独立性,再比较“共享”“稀疏激活”究竟改的是哪个对象。
GQA: query heads 保留,KV heads 共享 → KV cache / KV projection 变小
local attention: 每个 query 可见的 key positions 变少 → attention edges 变少
MoE: total expert parameters 很大,每 token 只路由到 top-k → active FLOPs 变小
total parameters ≠ active parameters per token
router probability ≠ attention probability
attention sparsity ≠ MoE parameter sparsity
capacity ≠ guaranteed throughput4 个 experts、top-1 routing、8 个 tokens。若 7 个都选 expert 0,理论上每 token 仍只算一个 expert,但 expert 0 排队而其余空闲;若 capacity factor 只容纳 3 个 token,还会 drop 或 reroute。这个反例说明 MoE 的 FLOPs 优势不自动变成 wall-clock 优势,load balancing 与 all-to-all 是机制的一部分。
“GQA 把整个模型缩小 \(h_q/h_{kv}\) 倍”错误,它主要减少 K/V 相关对象;“local attention 让所有长程依赖消失”也不必然成立,多层传播或间歇 global layers 仍可传递信息;“MoE 参数更多所以每 token FLOPs 必更多”混淆了 total 与 active parameters。
标准 self-attention 对长度为 n 的序列:
scores = Q @ K^T # (n, d) @ (d, n) → (n, n) ← O(n²d) FLOPs
attn = softmax(scores)
output = attn @ V # (n, n) @ (n, d) → (n, d) ← O(n²d) FLOPs
朴素训练实现的中间量: 可能物化 (n, n) 的 attention matrix
当 n = 128K(长上下文),n² 超过 16G 元素,单个 bf16 矩阵约 32 GiB。FlashAttention 可以在不改变 full attention 数学结果的前提下避免物化该矩阵;若要降低 O(n²d) 计算量,则需要稀疏、局部、低秩或其他序列模型。
| 路径 | 思路 | 代表 |
|---|---|---|
| 更高效的 attention 实现 | 减少 HBM IO,不改变 full attention | FlashAttention |
| 改变 attention/序列模型 | 近似、稀疏化或递归状态 | Linear Attention, Sliding Window, SSM |
| 稀疏激活 | 每次只激活部分参数 | Mixture of Experts |
核心思想:把 softmax 去掉,用核函数近似:
# 标准 attention: O(n²)
attn = softmax(Q @ K^T / √d) @ V
# Linear attention: O(n)
# 利用 (Q @ K^T) @ V = Q @ (K^T @ V) 的结合律
# 先算 K^T @ V(d × d),再算 Q @ (K^T @ V)
output = φ(Q) @ (φ(K)^T @ V) # φ 是核函数(如 ReLU, elu+1)
# 复杂度: O(nd²) —— 当 n >> d 时远优于 O(n²d)
关键洞察:矩阵乘法满足结合律。标准 attention 先算 (Q @ K^T) 得到 n×n 矩阵,再乘 V。Linear attention 先算 (K^T @ V) 得到 d×d 矩阵,再被 Q 乘。当 n >> d 时,后者快得多。
代价:核化 linear attention 改变了 softmax attention 的相似度与归一化形式,表达能力和数值行为可能不同;token 仍由 Q/K 内容动态加权,并不是“所有 token 被平等对待”。
Mistral 7B 和 LongFormer 使用:
# 每个 token 只看前后 w 个 token
for i in range(n):
scores[i] = Q[i] @ K[max(0,i-w):i+w]^T # 只看窗口内的 key
attn[i] = softmax(scores[i])
output[i] = attn[i] @ V[max(0,i-w):i+w]
# 复杂度: O(nwd) —— 线性于 n
直觉:大多数语言建模任务中,附近的 token 比远处的更重要。
大多数层用 sliding window
少数层用 full attention("全局层")
例如: 每 6 层有 1 层 global attention
若每层只有 causal local window,理论 receptive field 会随层数扩大,数量级可到 (Lw);但这只是“存在一条传播路径”,不等于远端信息能被可靠检索。加入 global layer 则会改变这个传播边界。
固定窗口预先规定每个 query 能看哪些位置;DeepSeek Sparse Attention 一类方法则增加轻量 indexer,为当前 query 从历史位置中挑选少量相关 key/value。计算图仍是“query 连接一小部分 token”,不是把 FFN experts 做 top-k routing。索引器的额外成本、召回错误与稀疏 kernel 效率共同决定是否真正比 full attention 快。
完全抛弃 attention,用经典控制论的状态空间模型:
# SSM: 递推形式
h(t) = A × h(t-1) + B × x(t)
y(t) = C × h(t)
# 训练时: 用并行 scan / 专用 kernel 高效计算
# 推理时: 递推;状态内存相对序列长度为 O(1)
# Mamba 的改进: A, B, C 是输入相关的(选择性 SSM)
# 让模型学会"忘记"不相关的信息
SSM 的边界:固定大小状态会形成信息瓶颈,部分 associative-recall 与精确检索任务可能弱于 attention;混合模型常用少量 attention 层弥补这一点。具体能力取决于 state size、选择机制和训练数据,不能概括为 SSM 一定需要“极大 state”才可工作。
| 方法 | 训练复杂度 | Attention intermediate memory(不含 KV cache) | 长程依赖 |
|---|---|---|---|
| Full Attention | O(n²d) | 朴素实现物化 score/probability 为 O(n²);FlashAttention 避免保存完整矩阵 | 任意位置直接交互 |
| Linear Attention | 常见形式 O(nd²) | 依实现而定 | 受核函数与状态容量限制 |
| Sliding Window | O(nwd) | O(nw) | 局部好 |
| Hybrid (Sparse) | O(nwd + n²/L) | O(nw + n²/L) | 好 |
| SSM/Mamba | 对序列长度近似线性 | 推理状态不随历史长度增长 | 受固定状态瓶颈限制 |
不要与 decode KV cache 混淆:上表这一列讨论单次 attention 计算的中间张量。标准 autoregressive decode 的历史 K/V storage 则随已缓存 context length 线性增长;二者是不同对象、不同时间尺度。
Multi-Head Attention (MHA)
N 个 Q 头, N 个 K 头, N 个 V 头
KV cache: N × d_head × 2 (K+V) per token per layer
Multi-Query Attention (MQA)
N 个 Q 头, 1 个 K 头, 1 个 V 头
KV cache: 1 × d_head × 2 per token per layer
权衡: KV cache 最小,但共享 K/V 可能带来质量损失
Grouped-Query Attention (GQA)
N 个 Q 头, K 个 KV 头 (1 < K < N)
每 N/K 个 Q 头共享 1 个 KV 头
KV cache: K × d_head × 2 per token per layer
KV cache 是推理瓶颈:生成一个 token 时,需要读取所有层的 KV cache。对于长序列和大 batch,这占了推理时间的大部分。
GQA 把 KV cache 缩小 N/K 倍(例如 8 倍),通常能降低 decode 阶段的内存带宽压力;实际延迟还取决于 batch、上下文长度、kernel 和并行策略。
Llama 2 70B 配置:
n_heads = 64, n_kv_heads = 8
GQA ratio = 8:1
KV cache 缩小 8 倍!
Ainslie et al. (2023) 的 uptraining 实验中:
GQA 可以接近 MHA 质量并获得接近 MQA 的 KV-cache 优势;
这不是对所有模型、数据与训练方式的“无损”保证。
DeepSeek-V2 的进一步创新:
不存储完整的 K, V 向量
存储一个低秩压缩的 "latent" 向量 c (C 维)
需要时通过上投影恢复 K, V
KV cache 存储: C 维 (而不是 N × d_head = 16384 维)
DeepSeek-V2: C = 512 (压缩 32 倍!)
直接把全部 K/V 投影吸收到 latent 计算时会与位置相关的 RoPE 冲突
DeepSeek 用 decoupled RoPE:保留额外的 RoPE key/query 维度
总: 512 + 64 = 576 维 per layer per token
| 方法 | KV cache / token / layer | 相对大小 |
|---|---|---|
| MHA (Llama 70B) | 64 × 128 × 2 = 16,384 | 1× |
| GQA (Llama 70B) | 8 × 128 × 2 = 2,048 | 0.125× |
| MQA | 1 × 128 × 2 = 256 | 0.016× |
| MLA (DeepSeek V2) | 576 | 0.035× |
MoE 的直觉:把一个大 FFN 替换为多个小 FFN("专家"),每次只激活其中几个。
模型总参数 = 所有专家参数之和
每次计算量 = 被激活的专家参数之和
效果:每个 token 只路由到少数专家时,总参数量可以远大于每 token 的激活参数量;具体倍数取决于专家数、top-k、共享专家和路由开销,不能固定写成 8×。
class MoELayer(nn.Module):
def __init__(self, d_model, d_ff, num_experts, top_k):
self.gate = nn.Linear(d_model, num_experts) # 路由器
self.experts = nn.ModuleList([
FFN(d_model, d_ff) for _ in range(num_experts)
])
self.top_k = top_k # 通常 2-4
def forward(self, x):
# 1. 路由: 每个 token 选择 top_k 个专家
router_probs = self.gate(x).softmax(dim=-1) # (batch*seq, num_experts)
top_k_scores, top_k_indices = router_probs.topk(self.top_k, dim=-1)
top_k_scores = top_k_scores / top_k_scores.sum(dim=-1, keepdim=True)
# 2. 分发: 把 token 送到对应的专家
output = torch.zeros_like(x)
for k in range(self.top_k):
expert_idx = top_k_indices[:, k]
expert_mask = (
expert_idx.unsqueeze(1)
== torch.arange(len(self.experts), device=x.device)
)
# 把属于该专家的 token 送入计算
for i, expert in enumerate(self.experts):
token_mask = expert_mask[:, i]
if token_mask.any():
weight = top_k_scores[token_mask, k].unsqueeze(-1)
output[token_mask] += expert(x[token_mask]) * weight
return output
| 策略 | 描述 | 使用 |
|---|---|---|
| Top-K | 选分数最高的 K 个专家 | 标准做法 |
| Top-K with noise | 加噪声后选 top-K | 训练时负载均衡 |
| Expert choice | 专家选择固定容量的 token | Expert Choice Routing 等研究 |
| Soft routing | 所有专家加权混合 | Soft MoE |
DeepSeek-V3 使用的是 token-choice top-k 路由,并通过可学习的 expert bias 做近似 auxiliary-loss-free 的负载均衡;它不是 Expert Choice Routing。
MoE 最大的挑战:负载不均衡。热门专家过载,冷门专家饥饿。
负载不均衡的后果:
# Switch Transformer / DeepSeek 类辅助损失的简化示意
# 同时考虑离散 token 分配和连续路由概率
def load_balancing_loss(router_logits, top_k, num_experts):
# router_logits: (num_tokens, num_experts)
router_probs = router_logits.softmax(dim=-1)
selected = router_probs.topk(top_k, dim=-1).indices
# 实际离散路由后,每个专家接收的 token-assignment 比例
assignments = F.one_hot(selected, num_classes=num_experts).float()
token_fraction = assignments.mean(dim=(0, 1))
# 路由器分配给每个专家的平均概率
prob_fraction = router_probs.mean(dim=0)
# Switch/DeepSeek 类辅助项的简化写法
load_loss = num_experts * (token_fraction * prob_fraction).sum()
return load_loss
# 总损失 = 主损失 + α × 辅助损失
# α 是需要按模型、路由方案与训练规模调节的超参数
在多 GPU 上,不同专家分布在不同 GPU 上:
Token 需要"旅行"到专家所在的 GPU
这需要 all-to-all 通信
一次 MoE 前向通常包含两阶段通信:
1. dispatch all-to-all:把 token hidden states 发到专家所在设备
2. combine all-to-all:把专家输出发回原设备
忽略本地命中、padding 与元数据时,理想化数据量与
2 × top_k × num_tokens × d_model × bytes_per_element 同阶;
实际网络流量取决于 expert-parallel group、专家放置、容量限制和负载均衡。
| 策略 | 描述 | 通信 |
|---|---|---|
| Expert Parallelism | 专家分布在不同 GPU | all-to-all |
| Expert + Data Parallel | DP 组内 EP | all-to-all within group |
| Token Dropping | 超载 token 直接跳过 | 无(但损失信息) |
| 模型 | 总参数 | 激活参数 | Experts | Top-K |
|---|---|---|---|---|
| Mixtral 8×7B | 47B | 13B | 8 | 2 |
| DeepSeek-V2 | 236B | 21B | 160 | 6 |
| DeepSeek-V3 | 671B | 37B | 256 | 8 |
| Qwen 3 235B | 235B | 22B | 128 | 8 |
MoE 的 scaling 优势:在专家大小和 top-k 不变时,增加专家数可以提高总参数容量,而每个 token 的专家计算量近似不变。不过这并非“免费扩展”:参数内存、路由与负载均衡难度、all-to-all 通信以及推理部署成本都会随系统规模变化。
MoE 训练容易发散,特别是当某些专家被过度使用时。
解决方案:
all-to-all 通信在跨节点时成为瓶颈。
解决方案:
专家粒度、共享专家是否有效以及如何从已有 dense 模型初始化,都需要通过模型与数据上的实验验证。
常见设计:
虽然激活参数少,但推理时所有专家都要在内存中。
解决方案:
最新的模型开始混合不同架构:
Transformer + Mamba 混合
比例: 1 层 Transformer : 7 层 Mamba
52B MoE (16 experts, 2 active)
兼顾 Transformer 的质量和 Mamba 的效率
Linear Attention + Full Attention 混合
456B MoE
大多数层用 linear attention(高效)
少数层用 full attention(保证质量)
Multi-head Latent Attention (MLA) + MoE
671B 总参数, 37B 激活参数
256 个路由专家 + 1 个共享专家
每个 token 激活 8 个路由专家 + 共享专家
问题 1:GQA 的主要优势是什么?
问题 2:一个 MoE 子层有 64 个等大小 routed experts、top_k=2;忽略共享专家和路由器时,每个 token 激活该专家子层参数的多少?
问题 3:MoE 训练中最大的挑战是什么?
问题 4:Linear attention 为什么比标准 attention 快?
问题 5:MoE 在多 GPU 上的主要通信瓶颈是什么?
4 experts、top-1、8 tokens 全选 expert 0。每 token 是否仍只激活一个 expert?系统是否仍理想并行?
答案:前者是,后者否;active FLOPs 稀疏,但负载集中且可能超过 capacity。
若认为“只激活一个所以一定快”,你把算法 FLOPs 上限当成了均衡后的硬件吞吐。
现在已经知道算法想减少哪些 FLOPs 与 bytes;下一课把这些逻辑对象放进真实 GPU memory hierarchy,解释为什么同样的数学量会有完全不同的 wall time。
共享 KV 头,KV cache 缩小 N/K 倍
结合律 + 核近似,O(nd²) 替代 O(n²d)
稀疏激活,大容量低计算
MoE 的核心挑战,需要辅助损失