Transformer推理结构简析(Decoder + MHA)

一、Transformer 基本结构

Transformer由encoder和decoder组成,其中:

  • encoder主要负责理解(understanding) The encoder’s role is to generate a rich representation (embedding) of the input sequence, which the decoder can use if needed

  • decoder主要负责生成(generation) The decoder outputs tokens one by one, where the current output depends on the previous tokens. This process is called auto-regressive generation

基本结构如下:

encoder结构和decoder结构基本一致(除了mask),所以主要看decoder即可:

每个核心的Block包含:

  • Layer Norm
  • Multi headed attention
  • A skip connection
  • Second layer Norm
  • Feed Forward network
  • Another skip connection

看下llama decoder部分代码,摘自transformers/models/llama/modeling_llama.py,整个forward过程和上图过程一模一样, 只是layer_norm换成了LlamaRMSNorm:

# 省略了一些不重要的code
class LlamaDecoderLayer(nn.Module):
    def __init__(self, config: LlamaConfig, layer_idx: int):
     ...
    def forward(
        self,
        hidden_states: torch.Tensor,
        attention_mask: Optional[torch.Tensor] = None,
        position_ids: Optional[torch.LongTensor] = None,
        past_key_value: Optional[Tuple[torch.Tensor]] = None,
        output_attentions: Optional[bool] = False,
        use_cache: Optional[bool] = False,
        cache_position: Optional[torch.LongTensor] = None,
        **kwargs,
    ) -> Tuple[torch.FloatTensor, Optional[Tuple[torch.FloatTensor, torch.FloatTensor]]]:

        # hidden_states [bsz, q_len, hidden_size]
        residual = hidden_states
        hidden_states = self.input_layernorm(hidden_states)

        # Self Attention 即MHA
        hidden_states, self_attn_weights, present_key_value = self.self_attn(
            hidden_states=hidden_states,
            attention_mask=attention_mask,
            position_ids=position_ids,
            past_key_value=past_key_value,
            output_attentions=output_attentions,
            use_cache=use_cache,
            cache_position=cache_position,
            **kwargs,
        )
        hidden_states = residual + hidden_states

        # Fully Connecte
        residual = hidden_states
        hidden_states = self.post_attention_layernorm(hidden_states)
        hidden_states = self.mlp(hidden_states)
        hidden_states = residual + hidden_states

        outputs = (hidden_states,)

        return outputs

上述代码展示的是标准的decoder过程,几个关键输入:

  • hidden_states [batch_size, seq_len, embed_dim] seq_len表示输入长度

  • attention mask的size为(batch_size, 1, query_sequence_length, key_sequence_length) 注意力掩码,实际使用的时候,PyTorch 会自动广播这个掩码到注意力权重矩阵的形状 [bsz, num_heads, q_len, kv_seq_len]。

  • position_ids or position_embeddings,位置id或者已经提前计算好的位置embedding

上述最核心的结构是其调用的self.self_attn,即是Multi-headed attention


二、Multihead Attention

Multihead Attention,多头注意力,上述decoder过程中最核心的地方,同时也是算子优化发力的地方。要理解多头先从单头开始。

单个attention

即Scaled Dot-Product Attention,公式如下:

其中QKV的维度一致,比如这里都是(2,3):

那么QKV怎么得到的呢?通过输入embedding和WqWkWv相乘得到qkv,这里WqWkWv是可学习参数:

拆开合起来计算都是等价的,上述的X1和X2是拆开计算,但是组合起来为(2,4)维度,同样可以和WqWkWv进行矩阵乘

实际在decoder的计算中,会带入causal (or look-ahead) mask

Causal mask 是为了确保模型在解码时不会关注未来的 token,这对于生成任务是必不可少的。通过这个掩码,模型只会依赖已经生成的 token,确保解码过程中是自回归的。

多个attention

实际中一般都是多个attention,和单个attention区别不大,下图右侧是多个attention的结构:

自注意力在多个头部之间并行应用,最后将结果连接在一起,我们输入(2,4)维度的X,分别和不同头的WqWkWv进行矩阵乘法得到每个头对应的QKV,然后QKV算出Z,再将所有Z合并和Wo相乘得到维度和X一致的Z:

实际中需要学习的权重为每个头的WqWkWv,同时也需要一个Wo,看一下llama中的实际计算过程:

class LlamaAttention(nn.Module):
    """Multi-headed attention from 'Attention Is All You Need' paper"""

    def __init__(self, config: LlamaConfig, layer_idx: Optional[int] = None):
        super().__init__()
        self.config = config
        self.layer_idx = layer_idx
        self.attention_dropout = config.attention_dropout
        self.hidden_size = config.hidden_size
        self.num_heads = config.num_attention_heads
        self.head_dim = self.hidden_size // self.num_heads
        self.num_key_value_heads = config.num_key_value_heads
        self.num_key_value_groups = self.num_heads // self.num_key_value_heads
        self.max_position_embeddings = config.max_position_embeddings
        self.rope_theta = config.rope_theta
        self.is_causal = True

        # 这行代码是一个检查条件,确保hidden_size能够被num_heads整除。
        # 在多头注意力(Multi-Head Attention, MHA)机制中,输入的hidden_size被分割成多个头,每个头处理输入的一个子集。
        # head_dim是每个头处理的维度大小,它由hidden_size除以num_heads得到。
        if (self.head_dim * self.num_heads) != self.hidden_size:
            raise ValueError(
                f"hidden_size must be divisible by num_heads (got `hidden_size`: {self.hidden_size}"
                f" and `num_heads`: {self.num_heads})."
            )

        # 需要学习更新的四个权重 WqWkWvWo
        self.q_proj = nn.Linear(self.hidden_size, self.num_heads * self.head_dim, bias=config.attention_bias)
        self.k_proj = nn.Linear(self.hidden_size, self.num_key_value_heads * self.head_dim, bias=config.attention_bias)
        self.v_proj = nn.Linear(self.hidden_size, self.num_key_value_heads * self.head_dim, bias=config.attention_bias)
        self.o_proj = nn.Linear(self.num_heads * self.head_dim, self.hidden_size, bias=config.attention_bias)
        self._init_rope()

    def forward(
        self,
        hidden_states: torch.Tensor,
        attention_mask: Optional[torch.Tensor] = None,
        position_ids: Optional[torch.LongTensor] = None,
        past_key_value: Optional[Cache] = None,
        output_attentions: bool = False,
        use_cache: bool = False,
        **kwargs,
    ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]:

        bsz, q_len, _ = hidden_states.size()

        query_states = self.q_proj(hidden_states)
        key_states = self.k_proj(hidden_states)
        value_states = self.v_proj(hidden_states)

        query_states = query_states.view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2)
        key_states = key_states.view(bsz, q_len, self.num_key_value_heads, self.head_dim).transpose(1, 2)
        value_states = value_states.view(bsz, q_len, self.num_key_value_heads, self.head_dim).transpose(1, 2)

        kv_seq_len = key_states.shape[-2]
        if past_key_value is not None:
            if self.layer_idx is None:
                raise ValueError(
                    f"The cache structure has changed since version v4.36. If you are using {self.__class__.__name__} "
                    "for auto-regressive decoding with k/v caching, please make sure to initialize the attention class "
                    "with a layer index."
                )
            kv_seq_len += past_key_value.get_usable_length(kv_seq_len, self.layer_idx)
        cos, sin = self.rotary_emb(value_states, seq_len=kv_seq_len)
        query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin, position_ids)

        if past_key_value is not None:
            cache_kwargs = {"sin": sin, "cos": cos}  # Specific to RoPE models
            key_states, value_states = past_key_value.update(key_states, value_states, self.layer_idx, cache_kwargs)

        key_states = repeat_kv(key_states, self.num_key_value_groups)
        value_states = repeat_kv(value_states, self.num_key_value_groups)

        attn_weights = torch.matmul(query_states, key_states.transpose(2, 3)) / math.sqrt(self.head_dim)

        if attention_mask is not None:
            if attention_mask.size() != (bsz, 1, q_len, kv_seq_len):
                raise ValueError(
                    f"Attention mask should be of size {(bsz, 1, q_len, kv_seq_len)}, but is {attention_mask.size()}"
                )
            attn_weights = attn_weights + attention_mask

        # upcast attention to fp32
        attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query_states.dtype)
        attn_weights = nn.functional.dropout(attn_weights, p=self.attention_dropout, training=self.training)
        # (bsz, self.num_heads, q_len, self.head_dim)
        attn_output = torch.matmul(attn_weights, value_states)
        # (bsz, q_len, self.num_heads, self.head_dim)
        attn_output = attn_output.transpose(1, 2).contiguous()
        attn_output = attn_output.reshape(bsz, q_len, self.hidden_size)

        attn_output = self.o_proj(attn_output)

        return attn_output, attn_weights, past_key_value

上述的计算过程可以总结为以下几个步骤,假设输入张量hidden_states的维度为[batch_size, seq_length, hidden_size]

在这里插入图片描述
在这里插入图片描述

这个过程实现了将输入通过多个注意力"头"并行处理的能力,每个"头"关注输入的不同部分,最终的输出是所有"头"输出的拼接,再经过一个线性变换。这种机制增强了模型的表达能力,使其能够从多个子空间同时捕获信息。

因为不用GQA,q_len 就是 seq_length 就是 kv_seq_len


三、MHA计算

实际的多头计算代码如下,这里是通过torch.matmul实现的:

 attn_weights = torch.matmul(query_states, key_states.transpose(2, 3)) / math.sqrt(self.head_dim)
  if attention_mask is not None:  # no matter the length, we just slice it
      causal_mask = attention_mask[:, :, :, : key_states.shape[-2]]
      attn_weights = attn_weights + causal_mask
  position_ids: Optional[torch.LongTensor] = None,
  # upcast attention to fp32
  attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query_states.dtype)
  attn_weights = nn.functional.dropout(attn_weights, p=self.attention_dropout, training=self.training)
  attn_output = torch.matmul(attn_weights, value_states)

上述代码中query_states和key_states的形状分别为[bsz, num_heads, q_len, head_dim]和[bsz, num_heads, kv_seq_len, head_dim]。matmul操作会自动在最后两个维度上进行矩阵乘法,并在前两个维度上进行广播。

应用注意力掩码attn_weights = attn_weights + causal_mask,causal_mask的形状可能是[1, 1, q_len, kv_seq_len]。PyTorch会自动将其广播到attn_weights的形状[bsz, num_heads, q_len, kv_seq_len]。

应用softmax和dropout,然后最后计算attn_output = torch.matmul(attn_weights, value_states),其中attn_weights的形状为[bsz, num_heads, q_len, kv_seq_len],value_states的形状为[bsz, num_heads, kv_seq_len, head_dim]。matmul操作会在最后两个维度上进行矩阵乘法,并在前两个维度上进行广播。这里attn_output的维度为bsz, num_heads, q_len, self.head_dim


四、torch.matmul

多维矩阵乘法,支持多维和broadcast,比较复杂:

  • 如果两个输入张量都是一维张量,执行的是点积操作,返回一个标量
  • 如果两个输入张量都是二维张量,执行的是矩阵乘法,返回一个新的二维矩阵,这个操作就是常见的
  • 如果第一个张量是一维张量,第二个张量是二维张量,则会在第一张量的维度前面添加一个1(扩展为2维),然后进行矩阵乘法,计算完后会移除添加的维度
  • 如果第一个张量是二维张量,第二个张量是一维张量,则执行的是矩阵-向量乘法,返回一个一维张量
  • 当两个输入张量中有一个是多维的(N > 2),会执行批量矩阵乘法。在这种情况下,非矩阵的维度(批量维度)会被广播(broadcasted)。如果一个张量是一维,会对其进行维度扩展和移除

我们这里的多维数据 matmul() 乘法,可以认为该乘法使用使用两个参数的后两个维度来计算,其他的维度都可以认为是batch维度。

比如,输入张量的形状为 (j×1×n×n) 和 (k×n×n) 时,会输出形状为 (j×k×n×n) 的张量。

具体点,假设两个输入的维度分别是input (1000×500×99×11), other (500×11×99)那么我们可以认为torch.matmul(input, other, out=None) 乘法首先是进行后两位矩阵乘法得到(99×11)×(11×99)⇒(99×99) ,然后分析两个参数的batch size分别是 (1000×500)和 500, 可以广播成为 (1000×500), 因此最终输出的维度是(1000×500×99×99)。

计算QK点积的时候:

attn_weights = torch.matmul(query_states, key_states.transpose(2, 3)) / math.sqrt(self.head_dim)

query_states的形状为[bsz, num_heads, q_len, head_dim],key_states.transpose(2, 3)的形状为[bsz, num_heads, head_dim, kv_seq_len]。matmul操作会在最后两个维度上进行矩阵乘法,得到形状为[bsz, num_heads, q_len, kv_seq_len]的注意力权重。

计算注意力输出:

attn_output = torch.matmul(attn_weights, value_states)   

这里使用torch.matmul将注意力权重与值(value)相乘。attn_weights的形状为[bsz, num_heads, q_len, kv_seq_len],value_states的形状为[bsz, num_heads, kv_seq_len, head_dim]。matmul操作会在最后两个维度上进行矩阵乘法,得到形状为[bsz, num_heads, q_len, head_dim]的注意力输出。

因为我们不用GQA,q_len 就是 kv_seq_len


如何学习大模型

现在社会上大模型越来越普及了,已经有很多人都想往这里面扎,但是却找不到适合的方法去学习。

作为一名资深码农,初入大模型时也吃了很多亏,踩了无数坑。现在我想把我的经验和知识分享给你们,帮助你们学习AI大模型,能够解决你们学习中的困难。

我已将重要的AI大模型资料包括市面上AI大模型各大白皮书、AGI大模型系统学习路线、AI大模型视频教程、实战学习,等录播视频免费分享出来,需要的小伙伴可以扫取。

一、AGI大模型系统学习路线

很多人学习大模型的时候没有方向,东学一点西学一点,像只无头苍蝇乱撞,我下面分享的这个学习路线希望能够帮助到你们学习AI大模型。

在这里插入图片描述

二、AI大模型视频教程

在这里插入图片描述

三、AI大模型各大学习书籍

在这里插入图片描述

四、AI大模型各大场景实战案例

在这里插入图片描述

五、结束语

学习AI大模型是当前科技发展的趋势,它不仅能够为我们提供更多的机会和挑战,还能够让我们更好地理解和应用人工智能技术。通过学习AI大模型,我们可以深入了解深度学习、神经网络等核心概念,并将其应用于自然语言处理、计算机视觉、语音识别等领域。同时,掌握AI大模型还能够为我们的职业发展增添竞争力,成为未来技术领域的领导者。

再者,学习AI大模型也能为我们自己创造更多的价值,提供更多的岗位以及副业创收,让自己的生活更上一层楼。

因此,学习AI大模型是一项有前景且值得投入的时间和精力的重要选择。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/882094.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【深度学习】(5)--搭建卷积神经网络

文章目录 搭建卷积神经网络一、数据预处理1. 下载数据集2. 创建DataLoader(数据加载器) 二、搭建神经网络三、训练数据四、优化模型 总结 搭建卷积神经网络 一、数据预处理 1. 下载数据集 在PyTorch中,有许多封装了很多与图像相关的模型、…

Java/Spring项目的包开头为什么是com?

Java/Spring项目的包开头为什么是com? 下面是一个使用Maven构建的项目初始结构 src/main/java/ --> Java 源代码com.example/ --->为什么这里是com开头resources/ --> 资源文件 (配置、静态文件等)test/java/ --> 测试代码resourc…

Visual Studio-X64汇编编写

纯64位汇编: includelib ucrt.lib includelib legacy_stdio_definitions.lib includelib user32.libextern printf:proc extern MessageBoxA:proc.data szFormat db "%s",0 szHello db "HelloWorld",0 szRk db "123",0.code start p…

无线安全(WiFi)

免责声明:本文仅做分享!!! 目录 WEP简介 WPA简介 安全类型 密钥交换 PMK PTK 4次握手 WPA攻击原理 网卡选购 攻击姿态 1-暴力破解 脚本工具 字典 2-Airgeddon 破解 3-KRACK漏洞 4-Rough AP 攻击 5-wifi钓鱼 6-wifite 其他 WEP简介 WEP是WiredEquivalentPri…

百度智能云API调用

植物识别API import base64 import urllib import requestsAPI_KEY "你的图像识别API_KEY" SECRET_KEY "你的图像识别SECRET_KEY"def main():url "https://aip.baidubce.com/rest/2.0/image-classify/v1/plant?access_token" get_access_t…

24/9/19 算法笔记 kaggle BankChurn数据分类

题目是要预测银行里什么样的客户会流失,流失的概率是多少 我这边先展示一下我写的二分类的算法 import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.model_selection import train_test_split from sklearn.linear_model impo…

60.【C语言】内存函数(memset,memcmp函数)

3.memset函数(常用) *简单使用 memset:memory set cplusplus的介绍 点我跳转 翻译: 函数 memset void * memset ( void * ptr, int value, size_t num ); 填充内存块 将ptr指向的内存块的前num个字节设置为指定值(解释为无符号char)。 (指针ptr类型为…

qt-C++笔记之作用等同的宏和关键字

qt-C笔记之作用等同的宏和关键字 code review! Q_SLOT 和 slots: Q_SLOT是slots的替代宏,用于声明槽函数。 Q_SIGNAL 和 signals: Q_SIGNAL类似于signals,用于声明信号。 Q_EMIT 和 emit: Q_EMIT 是 Qt 中用于发射…

【Linux】Linux的基本指令(1)

A clown is always a clown.💓💓💓 目录 ✨说在前面 🍋知识点一:Linux的背景 •🌰1.Unix发展的历史 •🌰2.Linux发展历史 •🌰3.企业应用现状 •🌰4.发行版本 &…

jmeter得到的文档数据处理

通过前面jmeter得到的输出文档,这里是txt文档,里面包含了很多条数据,每条数据的结构如下: 【request】 uuid:xxxxxxx timestamp:xxxxxxxx No.x question:xxxxxxx 【response】 code&#…

windows cuda12.1 pytorch gpu环境配置

安装cuda12.1 nvcc -V conda创建pythong3.10环境 conda create -n llama3_env python3.10 conda activate llama3_env 安装pytorch conda install pytorch torchvision torchaudio pytorch-cuda11.8 -c pytorch -c nvidia gpu - Pytorch version for cuda 12.2 - Stack Ov…

传输层 IV(TCP协议——流量控制、拥塞控制)【★★★★】

(★★)代表非常重要的知识点,(★)代表重要的知识点。 一、TCP 流量控制(★★) 1. 利用滑动窗口实现流量控制 一般说来,我们总是希望数据传输得更快一些。但如果发送方把数据发送得…

powerbi -L10-文件夹内的文件名

powerbi -L10-文件夹内的文件名 Folder.Contents letSource Folder.Contents("\\your_folder\ your_folder "),#"Removed Other Columns" Table.SelectColumns(Source,{"Name", "Date modified", "Folder Path"}), in#&q…

STM32篇:通用输入输出端口GPIO

一.什么是GPIO? 1.定义 GPIO是通用输入输出端口的简称,简单来说就是STM32可控制的引脚STM32芯片的GPIO引脚与 外部设备连接起来,从而实现与外部通讯、控制以及数据采集的功能。 简单来说我们可以控制GPIO引脚的电平变化,达到我们的各种目的…

MQ(RabbitMQ)笔记

初识MQ 同步调用优缺点 异步调用优缺点 总结: 时效性要求高,需要立刻得到结果进行处理--->同步调用 对调用结果不关心,对性能要求高,响应时间短--->异步调用

花园管理系统

基于springbootvue实现的花园管理系统 (源码L文ppt)4-074 4功能结构 为了更好的去理清本系统整体思路,对该系统以结构图的形式表达出来,设计实现该“花开富贵”花园管理系统的功能结构图如下所示: 图4-1 系统总体结…

植物大战僵尸【源代码分享+核心思路讲解】

植物大战僵尸已经正式完结,今天和大家分享一下,话不多说,直接上链接!!!(如果大家在运行这个游戏遇到了问题或者bug,那么请私我谢谢) 大家写的时候可以参考一下我的代码思…

Nginx反向代理出现502 Bad Gateway问题的解决方案

🎉 前言 前一阵子写了一篇“关于解决调用百度翻译API问题”的博客,近日在调用其他API时又遇到一些棘手的问题,于是写下这篇博客作为记录。 🎉 问题描述 在代理的遇到过很多错误码,其中出现频率最高的就是502&#x…

75、Python之函数式编程:生成器的核心方法及更多使用场景

引言 Python中的函数式编程,依托生成器,可以实现惰性求值的特性。但是,生成器其实还可以有更多的使用场景。本文就聚焦生成器,再次聊聊生成器中的主要方法以及更多的使用场景。 本文的主要内容有: 1、生成器的核心方…

解决DockerDesktop启动redis后采用PowerShell终端操作

如图: 在启动redis容器后,会计入以下界面 : 在进入执行界面后如图: 是否会觉得界面过于单调,于是想到使用PowerShell来操作。 步骤如下: 这样就能使用PowerShell愉快地敲命令了(颜值是第一生…