一文详解大语言模型多头注意力MHA、分组查询注意力GQA、多查询注意力MQA原理
多头注意力将查询、键、值分成多个头并行计算,但推理时内存带宽需求大。多查询注意力保留多个查询头,仅用单一键值和值头,降低显存占用和提升速度,但精度受损。分组查询注意力将查询头分组,每组共享键值,在质量与速度间取得平衡。
前言
今天我们来聊聊注意力机制家族里三个绕不开的名字:MHA、GQA、MQA。它们既有关联,又有各自的应用场景,尤其在解码器推理效率上,差别不小。下面就从最基本的自注意力开始,一步步拆解它们的设计思路和代码实现。

self-attention

自注意力机制的输入是一个统一的矩阵,它通过三个不同的线性变换,分别生成查询向量(Q)、键向量(K)和值向量(V)。这三个变换对应的权重矩阵W_Q、W_K、W_V都是可学习的,并且维度由模型设计预先定义。之所以要分成三组,是为了让模型能在不同的表示空间里分别优化匹配(Q和K)和信息合成(V)——这样灵活性更高,表达能力也更强。
在注意力计算中,点积结果会除以 √d_k(d_k是键向量的维度),这是为了防止维度较高时数值不稳定。如果不做这个缩放,当d_k很大时,点积的值会变得非常大,softmax的指数运算就会产生极端结果(有的接近1,有的接近0),导致梯度消失,学习效率大打折扣。除以√d_k之后,softmax的输入控制在一个合适的范围内,保证了数值稳定性和梯度流的有效性。这一步看似简单,却是训练能顺利进行下去的关键。
代码实现
import torch
import torch.nn as nn
import torch.nn.functional as F
class SelfAttention(nn.Module):
def __init__(self, seq_length):
super(SelfAttention, self).__init__()
self.input_size = seq_length
self.Wq = nn.Linear(seq_length, seq_length)
self.Wk = nn.Linear(seq_length, seq_length)
self.Wv = nn.Linear(seq_length, seq_length)
def forward(self, input):
q = self.Wq(input)
k = self.Wk(input)
v = self.Wv(input)
attention_scores = torch.matmul(q, k.transpose(-1, -2)) / torch.sqrt(torch.tensor(float(self.input_size)))
attention_weight = F.softmax(attention_scores, dim=-1)
output = torch.matmul(attention_weight, v)
return output
x = torch.randn(2, 3, 4)
Self_Attention = SelfAttention(4)
output = Self_Attention(x)
print(output.shape)
MHA(多头注意力)


MHA的核心思路很简单:把Q、K、V拆成多个头,每个头独立计算自注意力,最后再拼接起来。假设原来Q的形状是(batch_size, seq_len, d_model),拆成h个头后,每个头维度变成d_model/h。每个头各自计算注意力,得到的输出形状还是(batch_size, seq_len, d_model/h),拼接后恢复成(batch_size, seq_len, d_model),再经过一个线性层W_O整合。整个过程可以理解为:把原来的一次大注意力分解成多次小注意力并行执行,让模型从不同子空间学习不同的关系。
,每个头对进行变换后进行注意力机制
代码实现
import torch
import torch.nn as nn
class MultiHeadAttention(nn.Module):
def __init__(self, embed_dim, num_heads):
super(MultiHeadAttention, self).__init__()
self.num_heads = num_heads
self.head_dim = embed_dim // num_heads
self.wq = nn.Linear(embed_dim, embed_dim)
self.wk = nn.Linear(embed_dim, embed_dim)
self.wv = nn.Linear(embed_dim, embed_dim)
self.wo = nn.Linear(embed_dim, embed_dim)
def mh_split(self, hidden):
batch_size = hidden.shape[0]
x = hidden.view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2)
return x
def forward(self, hidden_states, mask=None):
batch_size = hidden_states.size(0)
q, k, v = self.wq(hidden_states), self.wk(hidden_states), self.wv(hidden_states)
q, k, v = self.mh_split(q), self.mh_split(k), self.mh_split(v)
scores = torch.matmul(q, k.transpose(-2, -1)) / torch.sqrt(torch.tensor(self.head_dim, dtype=torch.float32))
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
attention = torch.softmax(scores, dim=-1)
output = torch.matmul(attention, v)
output = output.transpose(1, 2).contiguous().view(batch_size, -1, self.num_heads * self.head_dim)
output = self.wo(output)
return output
x = torch.rand(2, 3, 36)
print(x)
output = MultiHeadAttention(36, 6)
y = output(x)
print(y.shape)
MHA能捕捉输入中不同部分之间的复杂关系,但这种能力是有代价的——对内存带宽的需求非常大,尤其是在解码器推理阶段。自回归模型中,每个解码步骤都需要加载解码器权重以及所有历史键和值,这个过程既消耗算力又消耗带宽。随着模型规模增大,这种开销会变得越来越棘手。于是,多查询注意力(MQA)应运而生,它的理念很直接:保留多个查询头,但只用一个键头和一个值头,从而显著减少内存负载,提升推理速度。
MQA(多查询注意力)

MQA是MHA的一种变体,专为自回归解码设计。图1和图2很直观地展示了MHA和MQA的区别:MQA让所有头共享同一份K和V矩阵(意味着K和V的计算是唯一的),只有Q保留了多头的特性(每个头有不同的变换)。这样一来,K和V的参数量以及KV Cache的显存占用都大幅下降,推理速度自然就上去了。当然,精度上的损失是难免的,但胜在效率提升明显。目前很多LLM(比如ChatGLM2)都用了MQA。

一个很实际的问题:如何把现有的预训练多头注意力模型转换成MQA?通常需要两步走:先是模型结构转换,再是额外的预训练。
- 结构转换:将原始模型多个头的键和值投影矩阵(线性层)做均值池化,合并成单一的键和值投影矩阵。经验表明,这种均值池化方式比直接选其中一个头或随机初始化效果更好。转换后的结构就具备了MQA的特征。
- 预训练阶段:结构转换后,模型需要再训一小段时间(通常是原始训练步数的一个小比例α),让它适应新的简化注意力机制。训练方法和原始保持一致,确保学习动态的连续性。
代码实现
import torch
import torch.nn as nn
class MultiQuerySelfAttention(nn.Module):
def __init__(self, embed_dim, num_heads):
super(MultiQuerySelfAttention, self).__init__()
self.num_heads = num_heads
self.head_dim = embed_dim // num_heads
self.wq = nn.Linear(embed_dim, embed_dim)
# MQA: K和V的输出维度是一个头的维度
self.wk = nn.Linear(embed_dim, self.head_dim)
self.wv = nn.Linear(embed_dim, self.head_dim)
self.wo = nn.Linear(embed_dim, embed_dim)
def q_h_split(self, hidden, head_num=None):
batch_size, seq_len = hidden.size()[:2]
if head_num is None:
x = hidden.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
return x
else:
# MQA: K和V只需要拆成1个头
return hidden.view(batch_size, seq_len, head_num, self.head_dim).transpose(1, 2)
def forward(self, hidden_states, mask=None):
batch_size = hidden_states.size(0)
q, k, v = self.wq(hidden_states), self.wk(hidden_states), self.wv(hidden_states)
q, k, v = self.q_h_split(q), self.q_h_split(k, 1), self.q_h_split(v, 1)
scores = torch.matmul(q, k.transpose(-2, -1)) / torch.sqrt(torch.tensor(self.head_dim, dtype=torch.float32))
print("scores:", scores.shape)
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
attention = torch.softmax(scores, dim=-1)
output = torch.matmul(attention, v)
output = output.transpose(1, 2).contiguous().view(batch_size, -1, self.num_heads * self.head_dim)
output = self.wo(output)
return output
x = torch.rand(3, 12, 512)
atten = MultiQuerySelfAttention(512, 8)
y = atten(x)
print(y.shape)
GQA(分组查询注意力)

MQA虽然参数量大幅减少,推理加速明显,但性能损失和训练不稳定也是实实在在的毛病——复杂度降低往往伴随着质量下降。于是GQA被提了出来:把Query分成若干组,每组内部共享一组Key和Value。这一思路在LLaMA-2和Mistral 7B中已经得到了应用。
GQA的数学原理很简单:将传统多头模型中的查询头分成G个组,每组分配一个键头和值头。这种配置记作GQA-G,G就是组数。两个极端情况:G=1时就是MQA;G=H(头数)时退化为MHA。从MHA转换到GQA同样可以使用均值池化:对每组中原始头的键和值投影矩阵做平均,得到该组单一的键值和值投影。
通过GQA,模型能在MHA的质量和MQA的速度之间找到一个平衡点。键值对少了,内存带宽和数据加载需求自然降低。G的选择代表了一种权衡:更多组(接近MHA)质量更高但速度较慢,更少组(接近MQA)速度更快但可能牺牲质量。而且随着模型规模变大,GQA允许内存带宽和模型容量按比例减少,而MQA中只有一个键值头,对超大模型的限制可能过于严苛。
代码实现
import torch
import torch.nn as nn
class GroupedQueryAttention(nn.Module):
def __init__(self, embed_dim, num_heads):
super(GroupedQueryAttention, self).__init__()
self.num_heads = num_heads
self.head_dim = embed_dim // num_heads
self.wq = nn.Linear(embed_dim, embed_dim)
# GQA: 设置4个组,K和V的输出维度是 group_num * head_dim
self.group_num = 4
self.wk = nn.Linear(embed_dim, self.group_num * self.head_dim)
self.wv = nn.Linear(embed_dim, self.group_num * self.head_dim)
self.wo = nn.Linear(embed_dim, embed_dim)
def split(self, hidden, group_num=None):
batch_size, seq_len = hidden.size()[:2]
if group_num is None:
# Q拆成所有头
x = hidden.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
return x
else:
# K和V拆成 group_num 个头,然后扩展为多头
x = hidden.view(batch_size, seq_len, group_num, self.head_dim).transpose(1, 2)
x = x[:, :, None, :, :].expand(batch_size, group_num, self.num_heads // group_num, seq_len, self.head_dim).reshape(batch_size, self.num_heads, seq_len, self.head_dim)
return x
def forward(self, hidden_states, mask=None):
batch_size = hidden_states.size(0)
q, k, v = self.wq(hidden_states), self.wk(hidden_states), self.wv(hidden_states)
q, k, v = self.split(q), self.split(k, self.group_num), self.split(v, self.group_num)
scores = torch.matmul(q, k.transpose(-2, -1)) / torch.sqrt(torch.tensor(self.head_dim, dtype=torch.float32))
print("scores:", scores.shape)
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
attention = torch.softmax(scores, dim=-1)
output = torch.matmul(attention, v)
output = output.transpose(1, 2).contiguous().view(batch_size, -1, self.num_heads * self.head_dim)
output = self.wo(output)
return output
x = torch.ones(3, 12, 512)
atten = GroupedQueryAttention(512, 8)
y = atten(x)
print(y.shape)
你是一名 AI 行业编辑,请围绕下面这条热点输出一份资讯解读:
热点:一文详解大语言模型多头注意力MHA、分组查询注意力GQA、多查询注意力MQA原理要求:
1. 先用一句话解释这条热点在讲什么
2. 再总结它为什么重要
3. 说明会影响哪些 AI 产品或内容方向
4. 最后给出 3 个适合资讯站使用的标题
游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。
相关热点先来看看Remaker AI这款工具。它的定位非常清晰——专注于解决图像处理中的常见难题:水印、文字、多余元素,以及低分辨率图像的修复与放大。无论是设计师、社交媒体运营人员,还是普通用户,只要遇到需要“清理”图片的场景,它都能轻松应对。下面直接了解它的适用人群和实际能力。 需求人群 Remaker
文心大模型覆盖文化传媒、艺术创作、教育科研、金融保险、医疗健康等需文字与创意的场景,集成文本生成、文生图、智能对话、信息抽取、文本纠错、古诗创作、文案续写等十余种文字处理能力。
今天我们来聊一款非常实用的浏览器工具——Ask AI Browser。如果你经常在Google上搜索问题,又希望随时与AI对话,或者在浏览各类网站时想直接向AI提问,那么这款工具可能会为你的日常浏览体验带来显著提升。 目标用户群体 简单来说,它主要面向以下几类用户:在Google上搜索问题时,希望无
说起来,联通云盘这事儿,其实就是中国联通在云存储这条赛道上的一次重要布局。目标很明确:为个人和家庭用户解决海量数据存储的刚需。具体能干啥呢?个人云、家庭云、微信 通讯录 相册备份、多端文件共享……说白了,就是从存储到共享的一条龙服务。 联通云盘官网网页版登录入口网址:https: pan wo c
- 日榜
- 周榜
- 月榜
热点快看
