QWEN技术报告重点干货

概述

qwen2.5 模型重要组件:

1. decoder only transformer 用于高效token生成任务

2. ROPE 用于编码位置信息

3. grouped query attention 用于高效地利用 KV 缓存

4. swishGLU 增强非线性激活

5. QKV bias 用于提升注意力机制的表现

6. RMSNorm 在预归一化后使用,以保证训练过程稳定

##MOE

1 fine-gained expert segmantation

2 shared experts routing

##tokenizer

1 BBPE encoding

2 vocab nums 151643 regular tokens, 22 control token

1 decoder only transformer

decoder only - encoder decoder 对比

Decoder-Only 模型没有显式的编码器模块,与 Encoder-Decoder 架构不同,Decoder-Only 模型不显式区分“理解”和“生成”阶段:①Encoder-Decoder 模型:输入通过 Encoder 被编码为上下文向量,Decoder 利用这些向量生成输出。这种结构中,输入的理解与输出生成是分离的。②Decoder-Only 模型:用户输入直接作为 Decoder 的输入,模型在自注意力机制中隐式完成理解和建模,同时为生成任务提供基础。Decoder-Only 模型虽然也能分析或理解用户输入,但是因为结构差异,

Decoder-Only 模型局限性

1、输入长度受限于生成长度

Decoder-Only 模型处理用户输入时,将输入视为生成序列的开头部分,与未来生成的内容共享同一序列窗口。

如果输入非常长,可能会占用较多的序列窗口空间,导致对上下文信息的捕捉不完整。

例如,GPT 模型可能在处理特别长的输入时无法很好地捕捉全局上下文。

2、隐式建模输入语义

用户输入的理解与生成目标共享同一个模块(自注意力机制)。这种共享机制可能在生成复杂输出时分散注意力,导致对输入的理解不够精确。

3、缺乏明确的输入表示优化

Decoder-Only 模型没有独立优化输入表示的过程,这可能导致它对长文本、复杂结构输入的处理效果逊色于 Encoder-Decoder 模型。但通过以下操作可以缓解:①增加模型规模:更大的模型(如 GPT-4、GPT-4 Turbo)通过参数量提升,能够更好地捕捉输入信息。②优化训练数据:通过多样化、精细化的训练数据,让模型在隐式理解方面有更好的表现③使用注意力机制增强:例如基于注意力稀疏化的改进,提升模型对长序列的捕捉能力

Decoder-only优点及存在必要性

1、模型架构的任务适配性

Encoder-Decoder 模式 是为“输入-输出”强耦合的任务设计的,例如机器翻译、问答和摘要生成。

​ -输入(源语言文本)需要被 Encoder 充分理解。

​ -输出(目标语言文本)需要由 Decoder 基于 Encoder 的表示生成。

对于“纯生成”任务(如对话、续写),没有明确的“输入”和“输出”分界,Encoder 的引入会显得多余。

2、Decoder-Only 模式的高效性

Decoder-Only 模型省略了 Encoder:输入序列与输出序列在同一个模块中处理,避免了模型结构的复杂化。在推理过程中,只需一次向前传播(Forward Pass),而不是 Encoder 和 Decoder 分别处理,推理效率更高。适合那些不需要复杂输入分析的任务,如补全、对话生成。

3、 更适合生成任务

很多实际应用(如 GPT 系列的应用场景)更关注生成的连贯性和语义丰富性,而不是对输入的复杂理解。Decoder-Only 模型通过大规模预训练,在语义生成上表现出卓越的能力,能够很好地满足这些需求。

4、训练数据效率高

自监督学习的完美适配:Decoder-Only 模型的训练目标是预测下一个 Token(Next Token Prediction),这是大规模预训练任务的核心目标。这种目标与网络架构直接对齐,能高效利用海量的非结构化文本数据。Encoder-Decoder 模型需要额外设计输入输出配对的数据(例如源语言到目标语言的翻译对),数据准备成本更高。

总结:Decoder-Only模型通过一个统一的解码器模块实现了用户输入的隐式理解和内容生成,依赖于:

①自注意力机制捕捉输入语义。

②因果掩码确保生成顺序性。

③大规模训练中的上下文建模能力。

擅长于:

①生成任务:例如文本补全、代码生成、对话生成。

②由于其架构专注于生成,适合从上下文中逐步预测后续内容。

这种统一架构虽然在输入理解上可能不如Encoder-Decoder模型精确,但在生成任务中表现出极大的灵活性、高效性和适应性,是其被广泛应用的主要原因。

2位置编码-绝对位置进化到相对位置

绝对位置编码

这里我们介绍最经典的sinusoidal position embedding,来自于《attention is all you need》

PE_{(POS, 2i)} = sin(\frac{pos}{10000^{\frac{2i}{d}}})

PE_{(POS, 2i+1)} = cos(\frac{pos}{10000^{\frac{2i}{d}}})

在公式中,符号定义如下:

  • pos:位置索引(token 的位置)

  • i:维度索引(偶数维度用 sin,奇数维度用 cos)

  • d:编码总维度

  • 分母:10000^{\frac{2i}{d}} 控制该维度的“波长”

  • 角度变化速率: \omega_i = \frac{1}{10000^{\frac{2i}{d}}}它决定了每个位置增加 1 时,正弦函数的相位变化多少。

一个完整的正弦波周期对应 相位变化 2π 所需的 pos 增量:

T_i = 2\pi*10000^{\frac{2i}{d}}

对于同一个pos,在不同dimension上采用不同频率的周期函数来进行位置编码,在低维度的dimension上采用高频(短周期),高维度采用低频(长周期)来进行不重复的位置编码(注意,这里的不重复是指在一定token长度内)

所以:

  • 最小周期(最高频率):当 i = 0时, 有 T_{min} = 2\pi * 10000^0 = 2\pi = 6.28 → 每约 6.28 个 token 编码就会重复一次(在最高频维度上)。

  • 最大周期(最低频率):当 i = \frac{d}{2} - 1 时,有 T_{max} = 2\pi * 10000^0 = 20000\pi = 62831 → 大约 62831 个 token 后最低频率的维度会重复一次。

同时,pos+k可以被pos线性表示,只需要和差化积公式即可

PE_{(pos+k, 2i)} = sin(\omega_i *(pos+k)) = sin(\omega_ipos)cos(\omega_ik) + cos(\omega_ipos)sin(\omega_ik) = cos(\omega_ik)PE_{(pos,2i)} + sin(\omega_ik)PE_{(pos, 2i+1)}

PE_{(pos+k, 2i+1)} = cos(\omega_i *(pos+k)) = cos(\omega_ipos)cos(\omega_ik)-sin(\omega_ipos)sin(\omega_ik) = cos(\omega_ik)PE_{(pos, 2i+1)} - sin(\omega_ik)PE_{(pos, 2i)}

即:

其中 

\mu = cos(\omega_ik) \\ \nu = sin(\omega_ik)

为常数。可以看出,对于 pos+k 位置的位置向量某一维2i或2i+1 而言,可以表示为pos位置向量的 2i 与 2i+1维的线性组合,这样的线性组合意味着位置向量中蕴含了相对位置信息。所以 PE_{(pos+k)}可以被PE_{pos} 线性表示。

计算 PE_{(pos+k)} 与PE_{pos} 的内积如下:

PE_{pos} \cdot PE_{pos+k} = \sum_{i=0}^{\frac{d}{2}-1}{sin(\omega_ipos)\cdot sin(\omega_i(pos+k) + cos(\omega_ipos)\cdot cos(\omega_i(pos+k)))} = \sum_{i=0}^{\frac{d}{2}-1}{cos(\omega_i(pos-(pos+k)))} = \sum_{i=0}^{\frac{d}{2}-1}{cos(\omega_ik)}

PE_{(pos+k)}PE_{pos} 的内积会随着相对位置的递增而减小,从而表征位置的相对距离,由于距离的对称性,Sinusoidal Position Encoding方法虽然能够反映相对位置的距离关系,但是无法区分方向, 即

PE_{pos} \cdot PE_{pos+k} = PE_{pos+k} \cdot PE_{pos}

正弦波位置编码无法表达过长token情况,虽然周期可以不断扩大以减小带来的位置重复问题,但是整体位置向量是否“碰撞”(高度相似)还取决于所有频率的组合。随着周期的扩大,也就是 ωi 的缩小,会导致。高频分量仍会很快重复,因此在很长序列上仍可能出现相似度升高的问题

对vanilla self attention做因式展开如下:

q_{i} = \omega_q\cdot(E_{x_i} + pos_i) \\ k_{j} = \omega_k\cdot(E_{x_j} + pos_j)\\ \\ A_{i,j}^{abs} = q_{i}\cdot k_j = E_{x_i}^T\omega_q^T\omega_kE_{x_j} + E_{x_i}^T\omega_q^T\omega_kpos_j + pos_i^T\omega_q^T\omega_kE_{x_j} +pos_i^T\omega_q^T\omega_kpos_j

其中 A_{i,j}^{abs}代表的是q,k相乘后计算出来的attention score,可以看到展开后的第一项是与位置无关的,第二三项分别是有绝对位置显示参与的,最后一项是同时包含两个绝对位置的(最有可能包含相对位置信息,正如上边以及提到过的两个绝对位置编码的内积是可以表述相对距离的),但是最后一项的中间包含了两个transform 参数 \omega_q^T\omega_k 导致最后一项的相对位置信息被破坏,从而导致vanilla self-attention基本无法表示相对位置信息

随后谷歌便提出Self-Attention with Relative Position Representations,做法也很简单,既然相对位置在self attention中丢失,直接在着这个时候补回来即可,具体做法就是计算attention score和weighted value时各加入一个可训练的表示相对位置编码的参数,同时在multihead之间可以共享

e_{ij} = \frac{x_i\omega_q(x_j\omega_k + \alpha_{ij}^k)^T}{\sqrt{d_z}}\\ z_i = \sum_{j}{e_{ij}(x_j\omega_\upsilon + \alpha_{ij}^{\upsilon})}

其中\alpha_{ij}^k, \alpha_{ij}^v代表的是相对位置信息,他们只和i,j的插值k相关。

为什么引入相对位置编码?绝对位置表示,模型更难泛化到比训练时更长的序列。不能天然表达“相对位置信息”,而注意力机制本质上更依赖相对位置信息。

ROPE旋转位置编码

RoPE 的关键是:

  • 把 embedding 的偶数/奇数维度两两分组,映射到复平面或二维平面;

  • 每个分组用一个不同频率的旋转角度来做旋转。

严格来说:

  • RoPE 本身没有任何可训练参数,它是纯数学变换。

  • 所有 cos/sin 频率都由公式固定: \alpha_{i} = 10000^\frac{-2i}{d}

  • 也就是说,RoPE 是 完全无参的。

虽然 RoPE 没有参数,但模型设计里有几个可控因素:

  • base (默认 10000)控制频率分布,间接影响“旋转速度”。

  • 较小的 base → 高频更多 → 更关注短程关系

  • 较大的 base → 低频更多 → 更关注长程关系;

  • 更高维度意味着更多频率覆盖,能同时表示短程和长程依赖

  • 通常只对 Q、K 向量应用 RoPE(不对 V)。

ROPE优点

  • 当计算注意力的内积时,结果只与 位置差 (posq−posk)(pos_q - pos_k)有关!

  • 在绝对位置编码的基础上,变成了 相对位置感知。

  • 模型能自然地推广到训练长度之外(extrapolation)。

  • 只需在 Query、Key 的 embedding 上应用旋转,几乎没有额外开销。

  • ROPE不会改变模长,只会改变向量方向

3 GQA(grouped query attention)

实现GQA+ROPE

import torch
import torch.nn as nn
import torch.nn.functional as F


def build_rope_cache(max_seq_len, dim, base=10000):
    half_dim = dim // 2
    freq_seq = torch.arange(half_dim, dtype=torch.float32)
    inv_freq = 1.0 / (base ** (freq_seq / half_dim))
    t = torch.arange(max_seq_len, dtype=torch.float32)
    freqs = torch.outer(t, inv_freq)  # [max_seq_len, half_dim]
    cos = freqs.cos()  # [max_seq_len, half_dim]
    sin = freqs.sin()
    return cos, sin


def apply_rope(x, cos, sin, offset: int = 0):
    """
    x: [batch, seq_len, n_heads, head_dim]
    cos/sin: [max_seq_len, head_dim/2]
    offset: 已缓存的长度(位置偏移量)
    """
    bsz, seq_len, n_heads, head_dim = x.shape
    half_dim = head_dim // 2

    x1 = x[..., :half_dim]
    x2 = x[..., half_dim:]

    cos = cos[offset:offset+seq_len].unsqueeze(0).unsqueeze(2)  # [1, seq_len, 1, half_dim]
    sin = sin[offset:offset+seq_len].unsqueeze(0).unsqueeze(2)

    x1_new = x1 * cos - x2 * sin
    x2_new = x1 * sin + x2 * cos

    return torch.cat([x1_new, x2_new], dim=-1)


class RoPEAttentionWithKVCache(nn.Module):
    def __init__(self, d_model, n_heads, max_seq_len=2048):
        super().__init__()
        self.d_model = d_model
        self.n_heads = n_heads
        self.head_dim = d_model // n_heads

        self.q_proj = nn.Linear(d_model, d_model)
        self.k_proj = nn.Linear(d_model, d_model)
        self.v_proj = nn.Linear(d_model, d_model)
        self.o_proj = nn.Linear(d_model, d_model)

        # RoPE 缓存(提前生成最大长度)
        self.register_buffer("cos", None, persistent=False)
        self.register_buffer("sin", None, persistent=False)
        cos, sin = build_rope_cache(max_seq_len, self.head_dim)
        self.cos, self.sin = cos, sin

        # KV Cache
        self.k_cache = None
        self.v_cache = None

    def reset_cache(self):
        """清空 KV cache"""
        self.k_cache = None
        self.v_cache = None

    def forward(self, x, use_cache=True):
        """
        x: [batch, seq_len, d_model] (可以是增量token)
        """
        bsz, seq_len, _ = x.shape

        # 1. 投影到 Q,K,V
        q = self.q_proj(x).view(bsz, seq_len, self.n_heads, self.head_dim)
        k = self.k_proj(x).view(bsz, seq_len, self.n_heads, self.head_dim)
        v = self.v_proj(x).view(bsz, seq_len, self.n_heads, self.head_dim)

        # 2. 位置偏移量 = 已缓存的长度
        past_len = 0 if self.k_cache is None else self.k_cache.size(1)

        # 3. 对 Q,K 应用 RoPE
        q = apply_rope(q, self.cos, self.sin, offset=past_len)
        k = apply_rope(k, self.cos, self.sin, offset=past_len)

        # 4. 更新 KV Cache
        if use_cache:
            if self.k_cache is None:
                self.k_cache = k
                self.v_cache = v
            else:
                self.k_cache = torch.cat([self.k_cache, k], dim=1)
                self.v_cache = torch.cat([self.v_cache, v], dim=1)

            k_all, v_all = self.k_cache, self.v_cache
        else:
            k_all, v_all = k, v

        # 5. 注意力计算
        attn_scores = torch.einsum("bthd,bshd->bhts", q, k_all) / (self.head_dim ** 0.5)
        attn_probs = F.softmax(attn_scores, dim=-1)
        out = torch.einsum("bhts,bshd->bthd", attn_probs, v_all)

        # 6. 合并 heads
        out = out.reshape(bsz, seq_len, self.d_model)
        return self.o_proj(out)

if __name__ == '__main__':
    d_model = 64
    n_heads = 4
    seq_len = 5
    bsz = 1

    attn = RoPEAttentionWithKVCache(d_model, n_heads, max_seq_len=20)

    # 第一次输入
    x1 = torch.randn(bsz, seq_len, d_model)
    out1 = attn(x1)
    print("out1:", out1.shape)  # [1, 5, 64]

    # 第二次输入新token(增量计算)
    x2 = torch.randn(bsz, 2, d_model)
    out2 = attn(x2)
    print("out2:", out2.shape)  # [1, 2, 64]

    # 注意:第二次 forward 时,KV Cache 已经包含了前 5 个位置的 k,v

MHA:

q,k,v head数量完全一样,缓存kv

GQA:

q保持原来的head不变, k,v head数量减少,同时保证q head num可以整除k\v head num,实际运算是采用的是repeat 方式,将uniqu k\v head 组合成与 q head num完全一致的tensor中进行计算,所以本质上GQA减少的并不是运算时间,而是减小缓存,进而在有限的贷款中计算缓存以及缓存切换

MQA:

同样MQA是一种更加极端的方式,直接将K,V的head num强制为1,在和q做self attention的时候,采用的是broadcast的方式进行向量乘法(所以并没有减少计算量),同样是kv cache的内存量大幅减少

4 swishGLU

  • GLU (Gated Linear Unit, 2017):在 MLP 里引入门控结构,把输入分成两部分,其中一部分作为“信号”,另一部分作为“门”。

  • Swish (2017, Google 提出):是一种平滑激活函数,形式为

    Swish(x)=x⋅σ(βx)\text{Swish}(x) = x \cdot \sigma(\beta x)Swish(x)=x⋅σ(βx)

    (其中 β\betaβ 通常取 1,退化为 x⋅σ(x)x \cdot \sigma(x)x⋅σ(x))。
    它比 ReLU 更平滑、梯度更稳定,在很多模型里表现优于 ReLU。

 SwishGLU 就是 GLU 的门函数由 sigmoid 改成 Swish,结合了 GLU 的 gating 和 Swish 的平滑非线性。

标准 GLU

        

SwishGLU

即把门函数的 \sigma换成了 Swish。

展开:

  • GLU 的优点:通过门控机制控制信息流,类似 LSTM 的 gating。

  • Swish 的优点

    • 平滑(避免 ReLU 的梯度截断问题)

    • 在负区间也有非零梯度(改善学习)

    • 在大模型里表现比 GeLU、ReLU 更优

  • SwishGLU 的结合点

    • 保留 GLU 的 gating 结构

    • 用 Swish 替代 sigmoid,使门控部分更“柔和”、梯度更好

    • 对 Transformer / LLM 的 FFN 层有更好效果(尤其在大规模预训练时)

5. QKV bias 

定义

这里的 bias 就是每个投影线性层是否带偏置项 b_{q}, b_k, b_v

为什么要加 bias?

  1. 提升表达能力

    • 偏置项能让线性变换不依赖于输入为零时的输出,增加自由度。

    • 对注意力来说,可能帮助模型在无输入信号时仍能学习到偏移特征。

  2. 位置编码/零输入时的影响

    • 当输入 X=0X=0X=0 时,如果没有 bias,则 Q=K=V=0Q=K=V=0Q=K=V=0,注意力退化;

    • 有 bias 时,仍然能产生非零信号,增加模型鲁棒性。

  3. 实证效果

    • 有些论文和实践表明,给 QKV 加 bias 可以略微提升模型收敛速度和表现。

    • 但在大模型里,这种提升不一定显著。

6. RMSNorm 

经典Layer Norm

在 Transformer 等模型中,常见的归一化方法是 LayerNorm (LN)
LN 的公式是对每个 token 的 hidden states 做均值和方差归一化:

\text{LN}(x) = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \cdot \gamma + \beta

RMSNorm (Root Mean Square Normalization) 来自论文:"Root Mean Square Layer Normalization" (Zhang & Sennrich, 2019)

它是 LN 的一个简化变种,不再减均值,只利用 均方根 (RMS) 来归一化。公式如下所示

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

\text{RMS}(x) = \sqrt{\frac{1}{d} \sum_{i=1}^{d} x_i^2 + \epsilon}

特点与优势

  1. 更轻量

    • 省去了减均值和方差计算,只需平方、均值、开方。

    • 计算和内存开销更低。

  2. 保留均值信息

    • LN 会强制零均值,可能丢失语义特征。

    • RMSNorm 保留了均值,模型更灵活。

  3. 效果

    在大模型(如 GPT、T5、LLaMA)中,RMSNorm 往往比 LN 稍快,收敛和性能接近甚至更好。

对比

特性LayerNormRMSNorm
是否减均值✅ 是❌ 否
是否计算方差✅ 是❌ 否
参数γ,β\gamma, \betaγ,β仅 γ\gammaγ
保留均值❌ 否✅ 是

实现

import torch
import torch.nn as nn

class RMSNorm(nn.Module):
    def __init__(self, dim, eps=1e-8):
        super().__init__()
        self.eps = eps
        self.weight = nn.Parameter(torch.ones(dim))

    def forward(self, x):
        # x: (batch, seq, dim)
        norm = x.norm(2, dim=-1, keepdim=True)  # L2 norm
        rms = norm / (x.size(-1) ** 0.5)
        return x / (rms + self.eps) * self.weight

本文为原创文章,未经作者允许禁止转载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值