LLM大模型: llama源码要点解读(一)

  transformer火了之后,基于transformer架构的llama也火了,可能的原因:
  • 来自meta,一线互联网大厂,质量有保证;自称70b参数的表现比chatGPT3还好(Llama 2:Open Foundation and Fine-Tuned Chat Models)!
  • 可能会成为大模型界的Android:各种基于llama的微调和应用会越来越多(llama的模型的参数量7B、13B、70B,凡是和这个参数量一样的的大模型,很有可能是基于llama二次改造的
  所以学习llama的源码是非常重要的(这不废话么?);整个transformer的源码在这: https://github.com/huggingface/transformers transformer里面收录所有的模型都在这: https://github.com/huggingface/transformers/tree/main/src/transformers/models 那些听说过的、没听说过的大模型在这里都能找到,当然也包括llama:https://github.com/huggingface/transformers/tree/main/src/transformers/models/llama
  llama源码不多,就这些,如下:从名字就能看出来这些文件的核心功能是啥!

       

  核心类不多,就这些:

     

     打开modeling_llama文件:

      1、第一个映入眼帘的就是归一化了:

class LlamaRMSNorm(nn.Module):
    def __init__(self, hidden_size, eps=1e-6):
        """
        LlamaRMSNorm is equivalent to T5LayerNorm
        """
        super().__init__()
        self.weight = nn.Parameter(torch.ones(hidden_size)) #初始化权重为1
        self.variance_epsilon = eps #防止分母为0

    def forward(self, hidden_states):
        input_dtype = hidden_states.dtype
        hidden_states = hidden_states.to(torch.float32)
        variance = hidden_states.pow(2).mean(-1, keepdim=True)
        hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
        return self.weight * hidden_states.to(input_dtype)

  用公式总结就是:

     

      那么问题来了:llama为啥要用RMSNorm,而不用batchNorm或layerNorm?

       3种norm方式综合对比:

  • 计算开销:RMSNorm > LayerNorm > BatchNorm。RMSNorm的计算开销最低,因为它不需要计算均值。
  • 对小批量数据的适用性:RMSNorm和LayerNorm均优于BatchNorm,适用于小批量甚至单个样本的数据。
  • 适用场景:RMSNorm和LayerNorm在序列建模、NLP以及需要处理变长输入的任务中表现更好,而BatchNorm更适合于图像处理和需要大批量数据训练的任务。
  • 训练稳定性:RMSNorm通过均方根归一化,在深层神经网络中提供了较为稳定的训练效果。

      2、(1)第二个重要的类就是MLP,有的地方也要FFN,就是常见的深度神经网络层:

class LlamaMLP(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.config = config
        self.hidden_size = config.hidden_size
        self.intermediate_size = config.intermediate_size # 中间层大小
        self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=config.mlp_bias) #输入升维到中间层
        self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=config.mlp_bias) #输入升维到中间层
        self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=config.mlp_bias) #中间层到输出层
        self.act_fn = ACT2FN[config.hidden_act] #激活函数

    def forward(self, x):
        if self.config.pretraining_tp > 1:
            slice = self.intermediate_size // self.config.pretraining_tp  #切片
            gate_proj_slices = self.gate_proj.weight.split(slice, dim=0) #输入切片
            up_proj_slices = self.up_proj.weight.split(slice, dim=0)
            down_proj_slices = self.down_proj.weight.split(slice, dim=1) #输出切片

            gate_proj = torch.cat(
                [F.linear(x, gate_proj_slices[i]) for i in range(self.config.pretraining_tp)], dim=-1  #每个切片并行执行线性变换后拼接
            )
            up_proj = torch.cat([F.linear(x, up_proj_slices[i]) for i in range(self.config.pretraining_tp)], dim=-1) #每个切片并行做线性变换后拼接

            intermediate_states = (self.act_fn(gate_proj) * up_proj).split(slice, dim=2)
            down_proj = [
                F.linear(intermediate_states[i], down_proj_slices[i]) for i in range(self.config.pretraining_tp)
            ]
            down_proj = sum(down_proj)#结果相加
        else:
            down_proj = self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x))

        return down_proj

    和传统的FFN比,llama的MLP只有1点是一样的:输入通过线性变换升维到intermediate中间层,再从intermediate中间层降维到输出层!其他的差异较大,主要体现在:

  • 输入切片,并行处理
  • gate经过激活函数后继续和up相乘;整个过程图示如下:

      

     那么问题来了:为啥要把input分成gate和up,gate经过激活函数后再乘以up了?

   (2)记得10多年前大数据这个概念刚火热时,很多互联网公司利用用户的基础画像、行为数据等做搜广推。这些数据类从业人员最常用炫技的说辞之一:通过亿级的数据维度快速、精准地做推荐!当时我就纳闷了:用户画像一般有几百到几千维度,用户行为数据大概也这个量级,某些从业人员所谓的亿级别维度是哪来的?后来找了好多资料才发现,这么高维度数据的来源竟然是:特征组合!比如“女性”+“年龄” 两两组合成新维度,计算化妆品、服装等的ctr; “男性” + “运动” 两个原本独立的维度两两组合,计算体育用品、体育类视频的ctr;这只是基础特征维度的两两组合,还有三个、四个、五个、甚至更多特征维度的组合了?这么来看,基础特征组合成上亿个维度完全是有可能的!原本线性不可分的样本,经过维度组合后,产生了非线性特征,更容易区分样本,秒啊!顺着这个思路,从代数角度解释神经网络的一些特性就容易多了:

  • 为什么神经网络要用激活函数?这里的二阶到N阶,展开后不就是特征的2阶到N阶的组合么

      

  •  神经网络为什么要先维,再降维? 升维的核心是特征组合:维度提高了激活函数就多啦,组合的特征就多了,能捕捉到的非线性特征就多了!为什么又要降维了?去掉无用的特征组合

     回到llama这里来:输入为什么要分成gate和up?为什么gate经过激活函数后还要和up相乘了?

  • 激活函数:参考上述,激活函数经过tylor展开后,不就是特征的N阶组合么?
  • 和up相乘,不也是特征之间的两两组合么?

  说到底,干的这些事,最终都是做特征的各种花式组合,让线性不可分的数据产生非线性特征,为最终的分类或回归任务产生强特征

  (3) swiglu函数:先把输入向量均分成2份,1份用silu激活后和另一份相乘,本质也是特征两两组合

         

   silu激活函数和relu比:0点平滑可导,工程上更合适!

       

   3、loss:我个人觉得这个是机器学习最核心的思路了!网络结构确定后,先按照一定的原则随机初始化各个参数,然后正向传播feed forward,计算得到一个结果,和target比得到差距,然后根据这个差距反向传播back propogation,调整网络结构中的各个参数,用来减小loss,最终的目的尽可能贴近target,整个过程和人类学习的思路是一样的!整个transformer架构中,loss计算最核心的两个方法:

  (1)compute_loss: 使用传入的model和input,计算loss值后返回

    def compute_loss(self, model, inputs, return_outputs=False):
        """
        How the loss is computed by Trainer. By default, all models return the loss in the first element.

        Subclass and override for custom behavior.
        """
        if self.label_smoother is not None and "labels" in inputs:
            labels = inputs.pop("labels")
        else:
            labels = None
        outputs = model(**inputs)#使用模型,根据input计算output
        # Save past state if it exists
        # TODO: this needs to be fixed and made cleaner later.
        if self.args.past_index >= 0:
            self._past = outputs[self.args.past_index]

        if labels is not None:
            unwrapped_model = self.accelerator.unwrap_model(model)
            if _is_peft_model(unwrapped_model):
                model_name = unwrapped_model.base_model.model._get_name()
            else:
                model_name = unwrapped_model._get_name()
            if model_name in MODEL_FOR_CAUSAL_LM_MAPPING_NAMES.values():
                loss = self.label_smoother(outputs, labels, shift_labels=True)
            else:
                loss = self.label_smoother(outputs, labels)
        else:
            if isinstance(outputs, dict) and "loss" not in outputs:
                raise ValueError(
                    "The model did not return a loss from the inputs, only the following keys: "
                    f"{','.join(outputs.keys())}. For reference, the inputs it received are {','.join(inputs.keys())}."
                )
            # We don't use .loss here since the model may return tuples instead of ModelOutput.
            loss = outputs["loss"] if isinstance(outputs, dict) else outputs[0] #取出output的loss

        return (loss, outputs) if return_outputs else loss

  (2)根据loss反向传播更改网络参数的值:

def training_step(self, model: nn.Module, inputs: Dict[str, Union[torch.Tensor, Any]]) -> torch.Tensor:
    """
    Perform a training step on a batch of inputs.

    Subclass and override to inject custom behavior.

    Args:
        model (`nn.Module`):
            The model to train.
        inputs (`Dict[str, Union[torch.Tensor, Any]]`):
            The inputs and targets of the model.

            The dictionary will be unpacked before being fed to the model. Most models expect the targets under the
            argument `labels`. Check your model's documentation for all accepted arguments.

    Return:
        `torch.Tensor`: The tensor with training loss on this batch.
    """
    # 设置模型为训练模式
    model.train()
    
    # 准备输入数据
    inputs = self._prepare_inputs(inputs)
    
    # 如果启用了SageMaker模型并行
    if is_sagemaker_mp_enabled():
        # 使用SageMaker模型并行进行前向和后向传播,并计算损失
        loss_mb = smp_forward_backward(model, inputs, self.args.gradient_accumulation_steps)
        # 返回平均损失,并将其移动到指定设备(如GPU)
        return loss_mb.reduce_mean().detach().to(self.args.device)
    
    # 使用上下文管理器计算损失
    with self.compute_loss_context_manager():
        loss = self.compute_loss(model, inputs)
    
    # 删除输入数据,释放内存
    del inputs
    torch.cuda.empty_cache()
    
    # 初始化空字典,存储额外参数
    kwargs = {}
    
    # 如果使用LOMO或ADALOMO优化器,需要显式传递学习率
    if self.args.optim in [OptimizerNames.LOMO, OptimizerNames.ADALOMO]:
        kwargs["learning_rate"] = self._get_learning_rate()
    
    # 如果使用多GPU训练,计算平均损失
    if self.args.n_gpu > 1:
        loss = loss.mean()  # mean() to average on multi-gpu parallel training
    
    # 如果使用apex混合精度训练
    if self.use_apex:
        with amp.scale_loss(loss, self.optimizer) as scaled_loss:
            scaled_loss.backward()  # 进行后向传播,计算梯度
    else:
        # 否则,使用加速器进行后向传播
        self.accelerator.backward(loss, **kwargs)
    
    # 返回损失值,除以梯度累积步骤数
    return loss.detach() / self.args.gradient_accumulation_steps

 

其他要点总结:

  1、做所有的重要操作前(包括但不限于attention、softmax等)前都要乘以一个矩阵做线性变换,所以这些操作都是在新的矩阵空间进行的,目的是和之前的旧空间隔开来,避免互相影响:每个矩阵空间只干一件事,如果要做其他事,继续通过矩阵乘法进入下一个空间操作,使得每个矩阵空间都是专用的,互不干扰(有点像每个厕所坑位只能蹲一个人,坑位之间严格隔开,避免互相影响拉便便)!

  2、特征组合让线性不可分变成线性可分举例:经典的XOR问题;整个思路很简单:现有的维度分不开了怎么办?那就生成新的能分开的维度呗!怎么生成新维度了?下面这个例子用的是旧维度两两相乘。实际生产环境,还可以N个维度互相相乘,或则N个旧维度之间线性组合(神经网络不就是这么干的嘛!)

  3、阅读源码时,建议先看类的两个方法:

  • _init_方法:看看生成对象时会生成并初始化哪些变量,肯定都是重要的变量才会在这里生成
  • forward方法:看看数据的流转细节,体现了模型的架构实现

 

 

参考:

1、https://blog.csdn.net/qq_35812205/article/details/136587013  LLM2模型

2、https://www.bilibili.com/video/BV1qj411y7kF/?spm_id_from=333.788&vd_source=241a5bcb1c13e6828e519dd1f78f35b2    transformers源码阅读——如何看懂模型代码(以llama为例)

posted @ 2024-06-11 09:43  第七子007  阅读(1282)  评论(0编辑  收藏  举报