【多模态大模型面经】现代大模型架构(一): 组注意力机制(GQA)和 RMSNorm

【多模态大模型面经】现代大模型架构(一): 组注意力机制(GQA)和 RMSNorm
在这里插入图片描述
🧔 这里是九年义务漏网鲨鱼,研究生在读,主要研究方向是人脸伪造检测,长期致力于研究多模态大模型技术;国家奖学金获得者,国家级大创项目一项,发明专利一篇,多篇论文在投,蓝桥杯国家级奖项、妈妈杯一等奖。
✍ 博客主要内容为大模型技术的学习以及相关面经,本人已得到B站、百度、唯品会等多段多模态大模型的实习offer,为了能够紧跟前沿知识,决定写一个“从零学习 RL”主题的专栏。这个专栏将记录我个人的主观学习过程,因此会存在错误,若有出错,欢迎大家在评论区帮助我指出。除此之外,博客内容也会分享一些我在本科期间的一些知识以及项目经验。
🌎 Github仓库地址:Baby Awesome Reinforcement Learning for LLMs and Agentic AI
📩 有兴趣合作的研究者可以联系我:[email protected]

文章目录

前言

✍ 在大模型论文学习中,相信很多读者和笔者一样,一开始都会有一种感觉:“现在大模型架构都差不多,主要是数据和算力在堆积。”当笔者慢慢总结LLaMA、Qwen、DeepSeek这些模型架构的时候发现,在 Attention、位置编码、FFN 与归一化 上,其实已经悄悄从经典 Transformer 走到了另一套“默认配置”。相较于最初的 Transformer,现在的主流大模型在架构上,已经逐渐从:

  • MQA → GQA(Grouped Query Attention)
  • 绝对位置编码 → RoPE(Rotary Positional Embedding)
  • ReLU / GELU 前馈网络 → SwiGLU 前馈网络
  • LayerNorm → RMSNorm + Pre-Norm

因此,在本文的学习中,我们主要聚焦于目前的大模型”默认配置“的学习,了解现在的”Transformer“!

一、现如今的”Transformer“

读者肯定很疑惑,为什么我要把第一章名字起为现如今的”Transformer“,实际上在以前,不管是科研还是工作,大家都会把Transformer作为一个baseline去进行优化,就像BERT、GPT等等,一直沿用的是Transformer的架构。但到了现在,研究者发现其中模块的更替可以达到更好的的效果。因此,现如今的大模型,已经不再直接将以前的Transformer架构作为baseline,而是将更换了模块的Transformer架构作为baseline。那现如今的baseline模块长什么样子呢,笔者统计了比较经典的模块所采用的注意力机制、位置编码、MLP激活层以及归一化的方式:

模型家族注意力位置编码MLP 激活归一化
早期 GPT/BERTMHA绝对 PE / learned posGELULayerNorm
LLaMA 1/2/3 系列GQA(大模型)RoPESwiGLURMSNorm
Qwen2 / Qwen2.5GQARoPESwiGLURMSNorm
Mistral 7BGQA + sliding windowRoPESwiGLURMSNorm
DeepSeek-LLMGQA/自研高效注意力RoPESwiGLURMSNorm
Granite / GemmaGQA/MQARoPESwiGLU/GeGLURMSNorm/LN

如表格所示, 对比早期 GPT/BERT 模型我们就可以发现了,现如今大模型的各个模块都有所改变:

  • 注意力机制:MQA → GQA(Grouped Query Attention)
  • 位置编码: 绝对位置编码 → RoPE(Rotary Positional Embedding)
  • MLP 激活层:ReLU / GELU 前馈网络 → SwiGLU 前馈网络
  • 归一化: LayerNorm → RMSNorm + Pre-Norm

所以如果你能把这四件套讲明白,基本就把现代 LLM 架构里 理清,并且可以快速找到文章的贡献点。

二、Attention Serious

在这里插入图片描述

2.1 Multi-Head Attention (MHA)

首先来回顾一下以前的注意力机制:
Attention ( Q , K , V ) = softmax ( Q K ⊤ d k ) V \text{Attention}(Q,K,V) = \text{softmax}\left(\frac{QK^\top}{\sqrt{d_k}}\right)V Attention(Q,K,V)=softmax(dk​​QK⊤​)V
在标准的自注意力中,我们通过 Q K T / d k QK^T / \sqrt{d_k} QKT/dk​​ 来计算不同 token 之间的注意力权重。但作者发现,仅用一个注意力头往往难以同时捕捉多种语义关系(如词法、语义、句法等)。因此,Transformer 提出了多头注意力机制 (Multi-Head Attention, MHA)。

将输入特征通过不同的线性投影矩阵,映射到多个低维子空间中:
head i = Attention ( Q W i Q , K W i K , V W i V ) \text{head}_i = \text{Attention}(QW_i^Q, \, KW_i^K, \, VW_i^V) headi​=Attention(QWiQ​,KWiK​,VWiV​)
然后将所有头拼接(concatenate)再线性变换:
MultiHead ( Q , K , V ) = Concat ( head 1 , … , head h ) W O \text{MultiHead}(Q,K,V) = \text{Concat}(\text{head}_1, \dots, \text{head}_h) W^O MultiHead(Q,K,V)=Concat(head1​,…,headh​)WO

在这里插入图片描述

MHA通过多个小头可以从不同角度捕捉语义信息,增强模型的表达能力和稳定性,比单头更鲁棒。

  • 代码手撕
import torch import torch.nn as nn import torch.nn.functional as F classMultiHeadAttention(nn.Module):def__init__(self, d_model, num_heads, dropout=0.0):super().__init__()assert d_model % num_heads ==0 self.d_model = d_model self.num_heads = num_heads self.head_dim = d_model // num_heads self.w_q = nn.Linear(d_model, d_model) self.w_k = nn.Linear(d_model, d_model) self.w_v = nn.Linear(d_model, d_model) self.w_o = nn.Linear(d_model, d_model) self.dropout = nn.Dropout(dropout)defforward(self, x, attn_mask=None):""" x: [B, L, d_model] """ B, L, _ = x.size()# 1. 线性投影 Q = self.w_q(x)# [B, L, d_model] K = self.w_k(x) V = self.w_v(x)# 2. reshape 为 [B, H, L, Dh]defreshape_heads(t):return t.view(B, L, self.num_heads, self.head_dim).transpose(1,2) Q = reshape_heads(Q) K = reshape_heads(K) V = reshape_heads(V)# Q,K,V: [B, H, L, Dh]# 3. 缩放点积注意力 scores = Q @ K.transpose(-2,-1)/(self.head_dim **0.5)# [B, H, L, L]if attn_mask isnotNone: scores = scores.masked_fill(attn_mask ==0,float('-inf')) attn = F.softmax(scores, dim=-1) attn = self.dropout(attn) out = attn @ V # [B, H, L, Dh]# 4. 合并头 out = out.transpose(1,2).contiguous().view(B, L, self.d_model)return self.w_o(out)

2.2 Multi-Query Attention (MQA)

有了 MHA 之后,大家第一反应是:头越多越好,越能学到多种语义关系。但在大模型、尤其是 Decoder-Only + 长上下文 + 自回归生成 的场景下,MHA 暴露出了一个非常现实的问题:

KV Cache 太贵了。

在自回归生成过程中,每生成一个新 token,都需要用到历史所有位置的 K , V K, V K,V

  • 对于标准 MHA:每个注意力头都维护一份自己的 K h , V h K_h, V_h Kh​,Vh​
  • 如果有 h h h 个头,那么 KV Cache 的内存开销大致是: O ( h ⋅ L ⋅ d head ) \mathcal{O}(h \cdot L \cdot d_{\text{head}}) O(h⋅L⋅dhead​)

当我们把头数堆到 32、64 甚至更多,再把上下文长度拉到 32K、64K 时,这个开销就会变成显存吞噬怪,直接限制推理速度与可部署性。因此,为了在几乎不损失模型效果的前提下,压缩 KV Cache 和带宽成本,就提出了 Multi-Query Attention(MQA)

MHA中的每一个头都是独享一份 K , V K, V K,V,相反的,MQA 提出了所有的头共享同一份 K , V K, V K,V也就是说,只保留一组 WK,WVW^K, W^VWK,WV,而 WiQW_i^QWiQ 仍然为每个头独立:
Q i = X W i Q , K = X W K , V = X W V . Q_i = X W_i^Q,\quad K = X W^K,\quad V = X W^V. Qi​=XWiQ​,K=XWK,V=XWV.
于是每个头的注意力就变成:
head i = Attention ( Q i , K , V ) = softmax ( Q i K ⊤ d k ) V . \text{head}_i = \text{Attention}(Q_i, K, V) = \text{softmax}\left(\frac{Q_i K^\top}{\sqrt{d_k}}\right) V. headi​=Attention(Qi​,K,V)=softmax(dk​​Qi​K⊤​)V.
最后依然是拼接再线性变换:
MQA ( X ) = Concat ( head 1 , … , head h ) W O . \text{MQA}(X) = \text{Concat}(\text{head}_1, \dots, \text{head}_h) W^O. MQA(X)=Concat(head1​,…,headh​)WO.
💡 经验发现“多 KV”并没有带来线性收益, Q 仍然是多头的,多头仍能捕捉多种语义关系。

  • 代码手撕
class MultiQueryAttention(nn.Module): def __init__(self, d_model, num_heads, dropout=0.0): super().__init__() assert d_model % num_heads == 0 self.d_model = d_model self.num_heads = num_heads self.head_dim = d_model // num_heads self.w_q = nn.Linear(d_model, d_model) # 注意:K/V 只有一组,所以输出维度是 head_dim self.w_k = nn.Linear(d_model, self.head_dim) self.w_v = nn.Linear(d_model, self.head_dim) self.w_o = nn.Linear(d_model, d_model) self.dropout = nn.Dropout(dropout) def forward(self, x, attn_mask=None): """ x: [B, L, d_model] """ B, L, _ = x.size() # 1. 多头 Q Q = self.w_q(x) # [B, L, d_model] Q = Q.view(B, L, self.num_heads, self.head_dim).transpose(1, 2) # Q: [B, H, L, Dh] # 2. 单头 K/V K = self.w_k(x) # [B, L, Dh] V = self.w_v(x) # [B, L, Dh] # 3. 为了和 Q 匹配,将 K/V 在头维上 broadcast K = K.unsqueeze(1) # [B, 1, L, Dh] V = V.unsqueeze(1) # [B, 1, L, Dh] K = K.expand(B, self.num_heads, L, self.head_dim) V = V.expand(B, self.num_heads, L, self.head_dim) # 4. 缩放点积注意力(与 MHA 相同) scores = Q @ K.transpose(-2, -1) / (self.head_dim ** 0.5) # [B, H, L, L] if attn_mask is not None: scores = scores.masked_fill(attn_mask == 0, float('-inf')) attn = F.softmax(scores, dim=-1) attn = self.dropout(attn) out = attn @ V # [B, H, L, Dh] out = out.transpose(1, 2).contiguous().view(B, L, self.d_model) return self.w_o(out) 

2.3 Grouped Query Attention (GQA)

根据前面两节的分析,我们可以总结出:

  • MHA:每个头都有独立的 K h , V h K_h, V_h Kh​,Vh​,表达能力强,但 KV Cache 成本最高
  • MQA:所有头共享同一份 K , V K, V K,V,KV Cache 成本最低,但多头之间视角差异弱,表达能力稍打折

于是就自然出现了一个折中思路:能不能在 “省 KV”“头之间有点差异” 之间找个平衡?这就是 Grouped-Query Attention(GQA)。GQA 的核心思想:Q 仍然是很多头,但 K/V 的头数减少为更少的组(num_kv_heads),每组 KV 服务若干个 Q 头。

  • 代码手撕
classGroupedQueryAttention(nn.Module):def__init__(self, d_model, num_q_heads, num_kv_heads, dropout=0.0):super().__init__()assert d_model % num_q_heads ==0assert num_q_heads % num_kv_heads ==0 self.d_model = d_model self.num_q_heads = num_q_heads self.num_kv_heads = num_kv_heads self.head_dim = d_model // num_q_heads self.group_size = num_q_heads // num_kv_heads # 每组多少个 Q 头共享一个 KV self.w_q = nn.Linear(d_model, d_model) self.w_k = nn.Linear(d_model, num_kv_heads * self.head_dim) self.w_v = nn.Linear(d_model, num_kv_heads * self.head_dim) self.w_o = nn.Linear(d_model, d_model) self.dropout = nn.Dropout(dropout)defforward(self, x, attn_mask=None):""" x: [B, L, d_model] """ B, L, _ = x.size()# 1. Q: 多头; K/V: 少量头 Q = self.w_q(x)# [B, L, d_model] K = self.w_k(x)# [B, L, num_kv_heads * head_dim] V = self.w_v(x) Q = Q.view(B, L, self.num_q_heads, self.head_dim).transpose(1,2) K = K.view(B, L, self.num_kv_heads, self.head_dim).transpose(1,2) V = V.view(B, L, self.num_kv_heads, self.head_dim).transpose(1,2)# Q: [B, Hq, L, Dh]# K,V: [B, Hkv, L, Dh]# 2. 将每个 KV 头“扩展”为 group_size 个 Q 头使用# 例如 Hq=8, Hkv=2 -> group_size=4 K = K.repeat_interleave(self.group_size, dim=1)# [B, Hq, L, Dh] V = V.repeat_interleave(self.group_size, dim=1)# 3. 缩放点积注意力 scores = Q @ K.transpose(-2,-1)/(self.head_dim **0.5)# [B, Hq, L, L]if attn_mask isnotNone: scores = scores.masked_fill(attn_mask ==0,float("-inf")) attn = F.softmax(scores, dim=-1) attn = self.dropout(attn) out = attn @ V # [B, Hq, L, Dh]# 4. 合并头 out = out.transpose(1,2).contiguous().view(B, L, self.d_model)return self.w_o(out)

三、归一化:LayerNorm → RMSNorm + Pre-Norm

在 Transformer 里,归一化(Normalization)主要解决两个问题:

  1. 深层网络训练不稳定:梯度可能爆炸或消失;
  2. 不同层输出分布漂移,导致学习变慢。

最早的 Transformer 使用的是 LayerNorm + Post-Norm 残差结构(指在全连接层跟上一个归一化层)

在这里插入图片描述

但到了 LLaMA、DeepSeek 等大模型时,大家开始逐渐转向:RMSNorm + Pre-Norm(指在全连接层跟上一个归一化层)

🔹 Post-Norm(原始 Transformer 用法)

最早的 Transformer 论文(Attention Is All You Need)使用的是 Post-Norm,代码结构类似:

# Post-Norm 结构 out = x + sublayer(x) out = layer_norm(out)
🔹 Pre-Norm(现代 LLM 常用)

大多数现代 LLM(如 LLaMA、DeepSeek 系列)改成了 Pre-Norm:代码结构类似:

# Pre-Norm 结构 h = layer_norm(x) out = x + sublayer(h)

💡 实践上,Pre-Norm 再配合 RMSNorm,只调节尺度不改均值,在 Decoder-only 结构里训练更稳定、实现也更简单。

3.1 LayerNorm

Layer Normalization(LN)是在 Transformer 中使用最广的归一化方式之一。给定一个 token 的隐藏表示 x ∈ R d x \in \mathbb{R}^{d} x∈Rd,LayerNorm 对其 特征维度 进行归一化:
μ = 1 d ∑ i = 1 d x i , σ 2 = 1 d ∑ i = 1 d ( x i − μ ) 2 \mu = \frac{1}{d} \sum_{i=1}^{d} x_i,\quad \sigma^2 = \frac{1}{d} \sum_{i=1}^{d} (x_i - \mu)^2 μ=d1​i=1∑d​xi​,σ2=d1​i=1∑d​(xi​−μ)2

LN ( x ) = x − μ σ 2 + ϵ ⋅ γ + β \text{LN}(x) = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \cdot \gamma + \beta LN(x)=σ2+ϵ​x−μ​⋅γ+β

其中:

  • γ , β ∈ R d \gamma, \beta \in \mathbb{R}^{d} γ,β∈Rd是可学习的缩放和平移参数;
  • 归一化是在单个样本、单个 token 的通道维度上完成的。

🧠 直觉理解:

对每个 token 的特征做一遍“标准化 + 线性变换”,
让每一层看到的分布更平滑,避免某些维度过大/过小导致训练不稳。

在 PyTorch 中,你平时看到的 nn.LayerNorm 就是这个东西:

import torch import torch.nn as nn x = torch.randn(2,4,8)# [B, L, d_model] ln = nn.LayerNorm(8) y = ln(x)# 每个位置的最后一维做 LN
🧠 1.为什么不用 BatchNorm,而用 LayerNorm / RMSNorm?(面经)

这一问是面试官很喜欢的一个考点,尤其是 Transformer / LLM 岗位。核心区别在于:归一化时用哪些维度来统计均值与方差。

  • BatchNorm(BN)
    • 在 CV 里常用,对 batch 维度 + 空间维度 做统计;
    • 对每个通道c,使用整批数据的统计量: m u c = E N , H , W [ x n , c , h , w ] mu_c = \mathbb{E}_{N,H,W}[x_{n,c,h,w}] muc​=EN,H,W​[xn,c,h,w​]
  • LayerNorm(LN)
    • 对单个样本、单个 token 的所有特征求均值和方差,不依赖 batch 大小。

在 Transformer / LLM 场景中,BN 存在几个问题:

  1. 序列长度不固定:BN 在变长序列上不自然,统计维度不好选;
  2. 推理阶段 batch 很小甚至为 1:BN 的 running mean/var 与训练时差异大,容易分布漂移;
  3. 自注意力中不同 token 之间差异大:BN 混合不同 token 的统计量,会引入额外噪声。

因此,大模型里更偏向用 LayerNorm / RMSNorm 这种“不依赖 batch、只看自己”的归一化方式。


3.2 RMSNorm

RMSNorm 是基于“层归一化中主要起作用的是缩放因子,而非平移因子”这个发现而提出的归一化方法。在层归一化中需要减去均值,而模型在训练过程中已经学会通过投影矩阵自动调节均值;而 γ \gamma γ 的作用是调整每一维的相对 scale,是表达力的核心。给定 x ∈ R d x \in \mathbb{R}^d x∈Rd,RMSNorm 的公式为:
RMS ( x ) = 1 d ∑ i = 1 d x i 2 + ϵ \text{RMS}(x) = \sqrt{\frac{1}{d} \sum_{i=1}^{d} x_i^2 + \epsilon} RMS(x)=d1​i=1∑d​xi2​+ϵ​

RMSNorm ( x ) = x RMS ( x ) ⋅ γ \text{RMSNorm}(x) = \frac{x}{\text{RMS}(x)} \cdot \gamma RMSNorm(x)=RMS(x)x​⋅γ

🧠 直觉理解:

RMSNorm 更像是“把这个向量整体缩放到一个合适能量水平”,
不去把它“拉回 0 均值”,只控制它的尺度。

💡 实践上,在 Decoder-only 大模型里:RMSNorm + Pre-Norm 组合在超深层网络(几十层)上表现更稳定,这也是 LLaMA / DeepSeek / Qwen 等系列广泛采用它的原因之一。

  • 代码手撕
classRMSNorm(nn.Module):def__init__(self, d_model, eps=1e-8):super().__init__() self.weight = nn.Parameter(torch.ones(d_model)) self.eps = eps defforward(self, x):""" x: [B, L, d_model] """# 均方根:sqrt(mean(x^2)) rms = x.pow(2).mean(dim=-1, keepdim=True).add(self.eps).sqrt() x_norm = x / rms return self.weight * x_norm 

四、总结

本章我们先把现代大模型里的两块“基础设施”打牢:一块是从 MHA → MQA → GQA 的注意力演化,用更少的 KV 头(甚至共享 KV)在不明显掉点的前提下,大幅降低 KV Cache 与长上下文显存开销;另一块是从 LayerNorm → RMSNorm + Pre-Norm 的归一化升级,用“只归一化能量”的 RMSNorm 配合 Pre-Norm 结构,让超深的 Decoder-only 模型在训练和推理中都更加稳定。后面的章节,我们再把 RoPE / SwiGLU / MoE / MLA 这些“进阶武器”一个个拆开,拼成一整套现代 LLM 的“架构面经图谱”。

Read more

Creative-Commons许可长上下文视频数据集-4个高清MP4视频文件-适用于计算机视觉模型训练-视频内容理解-算法研发-开放数据集-可用于科研与产业应用

Creative-Commons许可长上下文视频数据集-4个高清MP4视频文件-适用于计算机视觉模型训练-视频内容理解-算法研发-开放数据集-可用于科研与产业应用

Creative Commons许可长上下文视频数据集 引言与背景 在当前数字化时代,视频数据作为一种包含丰富信息的多媒体形式,已成为计算机视觉、人工智能和多媒体处理领域的核心研究对象。随着深度学习技术的快速发展,高质量、多样化的视频数据集对于训练高效、鲁棒的算法模型至关重要。本数据集提供了4个基于Creative Commons许可的长上下文视频文件,为科研人员、开发者和产业用户提供了一个开放、可信赖的视频资源。 本数据集包含4个完整的MP4格式视频文件,所有文件均采用Creative Commons许可协议,确保用户可以在合规的前提下自由使用、修改和分发这些资源。数据集的内容构成简洁明了,包含完整的视频原始文件,无需额外的元数据或标注信息即可直接使用。这些视频文件具有不同的文件大小和内容特征,为多样化的研究和应用场景提供了基础支持。 对于科研领域而言,该数据集可用于视频分类、动作识别、场景理解等计算机视觉任务的算法开发和性能评估;对于产业应用来说,这些视频可用于训练产品推荐系统、内容审核模型和视频分析工具。此外,Creative Commons许可的开放性使得这些资源能够

By Ne0inhk
Java前缀和算法题目练习

Java前缀和算法题目练习

前缀和 * 前缀和 * 二维前缀和 * 寻找数组的中心下标 * 除自身以外数组的乘积 * 和为k的子数组 * 和可被K整除的子数组 * 连续数组 * 矩阵区域和 前缀和 题目解析:在一个数组中查询起对应区间的和,会查询多次 算法思想:暴力解法:每次查询都进行一次遍历,时间复杂度O(n*m) 前缀和解法:新定义一个数组,每一个下标存放的值是要查询数组的前下标对应值的和,这样我们在访问起某一个区间的时候,直接利用这个数组就非常快速 importjava.util.Scanner;// 注意类名必须为 Main, 不要有任何 package xxx 信息publicclassMain{publicstaticvoidmain(String[] args){Scanner in =newScanner(System.in);int n = in.nextInt();int m = in.nextInt();int[

By Ne0inhk
数据结构七大排序算法图解——选择排序动图演示

数据结构七大排序算法图解——选择排序动图演示

系列文章目录 四、选择排序 紧接上一篇交换排序 前言: 1、直接选择排序 思想: 例题: 代码部分: 性能分析 2、树形选择排序 思想: 例题一: 例题二: 性能分析 3、堆排序 定义: 方法: 如何“筛选”? 例题: 如何“建初始堆”? 例题: 代码部分 性能分析 4、总结 直接选择排序 树形排序 堆排序 前言: 选择排序的主要思想是每一趟从待排序列中选取一个关键字值最小的记录,也即第 1 趟从 n 个记录中选取关键字值最小的记录,在第 2 趟中,从剩下的 n-1 个记录中选取关键字值最小的记录,直到整个序列中的记录都选完位置。这样,由选取记录的顺序便可得到按关键字值有序的序列。

By Ne0inhk
链表的学习与应用--双向循环链表_增加操作

链表的学习与应用--双向循环链表_增加操作

相信大家都在学习双向链表的过程中痛不欲生,但没关系相信大家看了这篇文章之后会对着抽象的数据结构有一个新的理解    这段时间以来笔者也是成功入职了一家方案公司,也算是实现了最初的那个梦想吧!|    话不多说现在开始双向循环链表插入知识的介绍 本人郑重承诺:         所有文章均不设置任何观看门槛均免费  📜虚拟滚动与长列表优化 🎮游戏开发与多媒体播放列表 🖥️操作系统中的进程调度  ✍️文本编辑器的“撤销”与“重做”  🌐浏览器历史记录 📒双向链表的实际应用 📂list.h 文件 🍋老规矩上源码 #ifndef __LIST_H_ #define __LIST_H_ #include <stdio.h> #include <windows.h> #include <stdbool.h> typedef struct Flight_List { /* 航班号码数据 */ int Flight_Number[10]

By Ne0inhk