LLM-2 大语言模型结构LLaMA

手撕环节

1.手撕rope

疑问:

为什么现在大多数大语言模型采用仅有解码器组成的网络结构?
出现灾难遗忘怎么处理?
可采用弹性权重固定和记忆增强的方法
弹性权重固定:是根据参数的更新程度来调整学习率,防止大幅度更新参数,导致灾难遗忘。通俗一点就是,防止你学的太多。
记忆增强(rag?):给大语言模型增加一个外部组件,将以往的任务关键词存储起来,如果执行任务中出现与这些关键词匹配的,则可以调用以往的知识,防止灾难遗忘。通俗的理解就是打小抄
1.为什么显卡能加速矩阵运算?
CPU的核心像学识渊博的教授,GPU的核心更像一堆小学生,只会简单的算数运算,可即使教授再神通广大,也不能一秒钟内计算出500次加减法,因此对简单重复的计算来说单单一个教授敌不过数量众多的小学生,在进行简单的算数运算这件事上,500个小学生(并发)可以轻而易举打败教授。
2.为什么flashattention算法中的两层循环,i与j的谁在外边,谁在里面有什么区别?
主要是O迭代公式中需要的是上一个j!所以将kv遍历放在外边。

image
训练过程分为两部分:一部分是自监督预训练和有监督下游任务微调。

自监督预训练

自监督学习是一种无监督学习的形式,其中模型通过从输入数据中生成标签来进行训练。自己生成标签!

词向量输入编码层(数个transformer层),然后给出每个位置上的条件概率,再反向传播。

LLaMA

image
LLaMA采用的transformer结构与上一节介绍的不同的地方有:1.采用前置层归一化(使训练过程更加稳定),2.使用RMSNorm归一化函数,3.激活函数采用SwiGLU,4.采用旋转位置嵌入RoPE。
将第一个层归一化移动到多头注意力之前,将第二个层归一化移动到全连接层之前。同时,将残差连接调整到多头注意力和全连接层之后。其中层归一化中采用了RMSNorm归一化函数。

RMSNorm归一化函数

为什么采用RMSNorm而不是其他的Norm?
根据RMSNorm的公式可知,归一化的过程中仅关注数据变化的幅度,而没有数据的偏移程度。而其他的Norm则要多计算均值。
优点在于:计算量减少
对哪个方向的参数进行RMSNorm?是每一个token,还是每一个特征?
根据代码来看是这样的mean(-1, keepdim=True),但是为什么呢,感觉是为了参数好看一点,但是本来就看不懂,是为了防止训练参数变得很难看,因为机器计算的位数是有限的!既然是对一token中的所有特征一起做归一化,为什么不一开始就将词汇表中进行归一化?这样可以减少一层Norm,不过有可能是考虑不同结构的需求不一样。
$ \text{RMSNorm}(x) = \frac{x}{\sqrt{\frac{1}{d} \sum_{i=1}^{d} x_i^2} + \epsilon} \cdot \gamma + \beta$
注意RMS与RMSNorm区别,RMS是标准差,而RMSNorm则是完整的。其中有两个可学习参数,类似与正则。
*(或者\)是元素乘(除),@和matmul是矩阵乘法需要考虑矩阵形状是否匹配。
注意torch.rsqrt()与torch.sqrt()的区别
.to()变换张量的数据类型

点击查看代码
import torch
import torch.nn as nn

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

        self.dim = dim
        self.eps = eps
        self.weight = nn.Parameter(torch.ones(self.dim))#要训练的参数就这样初始化

    def forward(self, x):
        rms = torch.rsqrt(x.pow(2).mean(-1, keepdim = True) + self.eps)
        print(rms)
        return self.weight * (x * rms)
    
if __name__ == "__main__":
    
    rms_norm = RMSNorm(dim=3)  

    input_tensor = torch.tensor([[1.0, 1.0, 1.0], 
                                  [4.0, 5.0, 6.0]], dtype=torch.float32)

    output_tensor = rms_norm(input_tensor)

    print("输入张量:")
    print(input_tensor)
    print("RMSNorm 归一化后的输出张量:")
    print(output_tensor)

SwiGLU激活函数(混乱)

和GPT生成的不一样,参考哪个?
\(\text{SwiGLU}(x) = \text{Swish}(W_1 x + b_1) \cdot \sigma(W_2 x + b_2)\)
\(\text{Swish}_{\beta}(x) = x \cdot \sigma(x_{\beta}) = \frac{x}{1 + e^{-x_{\beta}}}\)
\(\sigma(x) = \frac{1}{1 + e^{-x}}\)
目前的HF中的transformer库中的Swish函数被SiLU替代。
image

RoPE-待续

为什么需要位置编码?Transformer的模型原因,为什么不考虑输入标记的顺序?
大模型的外推性是什么?:训练时使用有限制的token数量,而实际情况中,询问超过上限的token怎么办。

绝对位置编码的局限性

尽管使用广泛但绝对位置嵌入也并非没有缺点:

1.有限序列长度:如上所述,如果模型学习到某个点的位置向量,它本质上不能表示超出该限制的位置。
2.位置嵌入的独立性:每个位置嵌入都是独立于其他位置嵌入的。这意味着在模型看来,位置 1 和 2 之间的差异与位置 2 和 500 之间的差异相同。但是其实位置 1 和 2 应该比位置 500 相关性更密切,位置 500 距离明显更远。这种相对定位的缺乏可能会阻碍模型理解语言结构的细微差别的能力。

提前打表的思想?加入相对位置信息,并且不破坏原有的向量信息。
https://www.zhihu.com/tardis/zm/art/647109286?source_id=1003
向量表达到复数表达,感觉很不自然。维度不同,可以随意添加性质。
image
那么高维如何处理呢?二维你可以对第二位置的向量加虚部,那么其他维度呢?
高纬度是两两分组?

点击查看代码
def precompute_freqs_cis(dim: int, seq_len: int, theta: float = 10000.0):
    # 计算词向量元素两两分组之后,每组元素对应的旋转角度
    freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim))#为什么到dim/2?因为是复数,所以要除以二,后面还要拓展开来。
    # 生成 token 序列索引 t = [0, 1,..., seq_len-1]
    t = torch.arange(seq_len, device=freqs.device)
    # freqs.shape = [seq_len, dim // 2] 
    freqs = torch.outer(t, freqs).float()
    # torch.polar 的文档
    # https://pytorch.org/docs/stable/generated/torch.polar.html
    # 计算结果是个复数向量
    # 假设 freqs = [x, y]
    # 则 freqs_cis = [cos(x) + sin(x)i, cos(y) + sin(y)i]
    freqs_cis = torch.polar(torch.ones_like(freqs), freqs)
    return freqs_cis

def apply_rotary_emb(
    xq: torch.Tensor,
    xk: torch.Tensor,
    freqs_cis: torch.Tensor,
) -> Tuple[torch.Tensor, torch.Tensor]:
    # xq.shape = [batch_size, seq_len, dim]
    # xq_.shape = [batch_size, seq_len, dim // 2, 2]
    xq_ = xq.float().reshape(*xq.shape[:-1], -1, 2)
    xk_ = xk.float().reshape(*xk.shape[:-1], -1, 2)
    
    # 转为复数域
    xq_ = torch.view_as_complex(xq_)
    xk_ = torch.view_as_complex(xk_)
    
    # 应用旋转操作,然后将结果转回实数域
    # xq_out.shape = [batch_size, seq_len, dim]
    xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(2)
    xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(2)
    return xq_out.type_as(xq), xk_out.type_as(xk)

class Attention(nn.Module):
    def __init__(self, args: ModelArgs):
        super().__init__()

        self.wq = Linear(...)
        self.wk = Linear(...)
        self.wv = Linear(...)
        
        self.freqs_cis = precompute_freqs_cis(dim, max_seq_len * 2)

    def forward(self, x: torch.Tensor):
        bsz, seqlen, _ = x.shape
        xq, xk, xv = self.wq(x), self.wk(x), self.wv(x)

        xq = xq.view(batch_size, seq_len, dim)
        xk = xk.view(batch_size, seq_len, dim)
        xv = xv.view(batch_size, seq_len, dim)

        # attention 操作之前,应用旋转位置编码
        xq, xk = apply_rotary_emb(xq, xk, freqs_cis=freqs_cis)
        
        # scores.shape = (bs, seqlen, seqlen)
        scores = torch.matmul(xq, xk.transpose(1, 2)) / math.sqrt(dim)
        scores = F.softmax(scores.float(), dim=-1)
        output = torch.matmul(scores, xv)  # (batch_size, seq_len, dim)
  # ......
缺点:速度慢,计算效率低下? 看了源代码之后,发现只是在计算attention之前加入旋转位置编码。

https://github.com/HarleysZhang/llm_note/blob/main/1-transformer_model/RoPE位置编码算法详解.md#一-torch-背景知识
其中关于几个torch函数讲的挺好。

image

整体框架

注意力机制优化

Flashattention
https://www.zhihu.com/question/611236756/answer/3310819022
https://zhuanlan.zhihu.com/p/676655352

kv-cache

https://zhuanlan.zhihu.com/p/630832593

MLA

其中U是维度升上去,D是维度降下去
啥位置敏感,啥耦合?啥啥啥?
到底存储了什么东西?
https://zhuanlan.zhihu.com/p/23062701108
https://zhuanlan.zhihu.com/p/16730036197

posted @ 2025-03-24 16:39  keepsoft123  阅读(44)  评论(0)    收藏  举报