GiantPandaCV | FasterTransformer Decoding 源码分析(三)-LayerNorm介绍

news2024/9/23 17:16:23

本文来源公众号“GiantPandaCV”,仅用于学术分享,侵权删,干货满满。

原文链接:FasterTransformer Decoding 源码分析(三)-LayerNorm介绍

作者丨进击的Killua

来源丨https://zhuanlan.zhihu.com/p/669440844

编辑丨GiantPandaCV

GiantPandaCV | FasterTransformer Decoding 源码分析(一)-整体框架介绍-CSDN博客

GiantPandaCV | FasterTransformer Decoding 源码分析(二)-Decoder框架介绍-CSDN博客

本文是FasterTransformer Decoding 源码分析的第三篇,主要介绍FasterTransformer中LayerNorm是如何实现及优化的。首先会简单介绍下LayerNorm的背景知识,然后从源码上逐层向下分析具体的实现。

1 背景知识

Layer normalization(层归一化)是一种用于深度神经网络中的归一化技术。它可以对网络中的每个神经元的输出进行归一化,使得网络中每一层的输出都具有相似的分布,目前已被广泛应用于深度学习模型的各个子模块中。LayerNorm的计算很简单,它计算的粒度体现在每一组数据本身上,每组数据之间毫无关系,所以非常适合并行来计算。如下图所示,图中一个batch有3组数据,每组数据分别计算平均值和标准差,再用均值和标准差去处理每组数据中元素即可,公式为如下所示。公式中的gamma和beta为可学习参数,增强数据的可表达性。严格描述和定义可参考文档。

LayerNorm举例

LayerNorm 计算公式

2 源码分析

2.1 方法入口

Decoding实现中最普通的LayerNorm方法调用入口如下所示,出了输入输出的数据描述外就是公式中罗列的gamma、beta和eps参数,这里还是比较好理解的。

        invokeGeneralLayerNorm(decoder_normed_input_,  // layernorm输出
                               decoder_input,          // layernorm输入
                               decoder_layer_weight->at(l).pre_layernorm_weights.gamma,
                               decoder_layer_weight->at(l).pre_layernorm_weights.beta,
                               layernorm_eps_,
                               batch_size,      // 一个批次处理的数据个数
                               hidden_units_,   // 单个数据样本的维度
                               (float*)nullptr,
                               0,
                               stream_);

2.2 调用kernel

入口调用的函数签名如下,opt_version默认是2,int8_mode是量化模式,这里先跳过。

template<typename T>
void invokeGeneralLayerNorm(T*           out,
                            const T*     input,
                            const T*     gamma,
                            const T*     beta,
                            const float  layernorm_eps,
                            const int    m,  // 一个批次处理的数据个数
                            const int    n,  // 单个数据样本的维度
                            float*       scale,
                            float*       dynamic_scale,
                            const int    int8_mode,
                            cudaStream_t stream,
                            int          opt_version)

函数的实现上有一些设计,针对数据维度是偶数且类型是半精度浮点型(half)的数据样本,采用了定制化的kernel实现,这个kernel和后续要讲的联合kernel复用一套底层代码。这里大概说下优化点,就是会对2个half类型的元素处理进行代码展开,减少指令判断加速运行,后续介绍联合算子的时候再详细介绍。

{
    dim3       grid(m);
    const bool dynamic_quant = dynamic_scale != nullptr;
    if (n % 2 == 0 && (std::is_same<T, half>::value)
        && opt_version > 0) {
        int  half_n    = n / 2;
        int  half_n_32 = (half_n + 31) / 32 * 32;
        dim3 block(min(half_n_32, 512));
        int  rolls_per_thread = half_n / block.x;
        int  unroll_factor    = 8;
        while (unroll_factor > rolls_per_thread && unroll_factor > 1) {
            unroll_factor /= 2;
        }
        using T2 = typename TypeConverter<T>::Type;

        /* we launch (and instantiate) the kernel by specializing for unroll_factor -> residual_num -> is_bias ->
         * opt_version */
        dispatch_generalAddBiasResidualLayerNormOpt_unroll_factor((T2*)out,
                                                                  (T2*)out,
                                                                  (const T2*)out,
                                                                  (const T2*)nullptr,
                                                                  (const T2*)input,
                                                                  (const T2*)nullptr,
                                                                  (const T2*)gamma,
                                                                  (const T2*)beta,
                                                                  layernorm_eps,
                                                                  m,
                                                                  half_n,
                                                                  nullptr,
                                                                  nullptr,
                                                                  scale,
                                                                  dynamic_scale,
                                                                  int8_mode,
                                                                  grid,
                                                                  block,
                                                                  stream,
                                                                  opt_version,
                                                                  false,  // is_output
                                                                  1,      // residual_num
                                                                  unroll_factor);
    }

对于其他比较常规的数据类型,会调用generalLayerNorm kernel函数来进行处理。这里gridSize等于一批处理的数据个数,即一个block处理输入的一份数据,对这一份数据进行normalize即可,符合并行处理的思路。blockSize是一份数据的维度和1024的较小值,可以理解,大多数CUDA设备一个block支持的最大线程数就是1024,所以这里要min处理下。这里还有个trick就是维度如果不是32的倍数就也设置为1024,主要是为了最大化利用warp(32个线程)特性来处理数据。动态量化的部分我们先跳过,接下来就是调用函数进入到kernel实现部分。

    else {
        dim3 block(min(n, 1024));

        /* For general cases, n is equal to hidden_units, e.g., 512/1024.
            Since we have warp shuffle inside the code, block.x % 32 should be 0.
        */
        if (n % 32 != 0) {
            block.x = 1024;
        }

        /* should pay attention to the rsqrt precision*/
        if (dynamic_quant) {
            size_t maxbytes = n * sizeof(T);
            if (maxbytes >= (48 << 10)) {
                check_cuda_error(cudaFuncSetAttribute(
                    generalLayerNorm<T, true>, cudaFuncAttributeMaxDynamicSharedMemorySize, maxbytes));
            }
            generalLayerNorm<T, true><<<grid, block, maxbytes, stream>>>(
                input, gamma, beta, out, layernorm_eps, m, n, scale, dynamic_scale, int8_mode);  // For gpt-3
        }
        else {
            generalLayerNorm<T, false><<<grid, block, 0, stream>>>(
                input, gamma, beta, out, layernorm_eps, m, n, scale, dynamic_scale, int8_mode);  // For gpt-3
        }
    }
}

2.3 kernel实现

这里为了代码结构更加清晰先将量化相关的代码先去掉了,整个流程还是比较容易理解,通过两次block级别的归约实现了下面公式的计算,具体在代码中做了详细注释。

一个block处理一个数据(n维度),block中有m个线程,1个线程可能处理1到多个数据中的元素,如下图所示。这里n=8,m=4,所以一个线程需要处理2个数据,反映到代码中就是单个线程对2个元素进行本地求和和差值平方。

block实现逻辑

template<typename T, bool DYNAMIC_SCALING = false>
__global__ void generalLayerNorm(const T* __restrict input,
                                 const T* __restrict gamma,
                                 const T* __restrict beta,
                                 T*          normed_output,
                                 const float layernorm_eps,
                                 int         m,
                                 int         n,
                                 float*      scale,
                                 float*      dynamic_scale,
                                 const int   int8_mode)
{
    const int tid = threadIdx.x;
    // 共享内存,存储block内求得的均值mean、方差
    __shared__ float s_mean;
    __shared__ float s_variance;
    float            mean     = 0.0f;
    float            variance = 0.0f;

    // 该循环将本线程要处理的若干个输入数据元素进行本地求和
    float local_sum = 0.0f;
    for (int i = tid; i < n; i += blockDim.x) {
        // ldg函数用于从全局内存中按照给定的地址加载数据,并且该函数能够利用缓存来提高访问效率
        local_sum += (float)(ldg(&input[blockIdx.x * n + i]));
    }
    // 进行block级别归约,即将本block中所有线程计算的local_sum进行求和,得到这个数据样本所有元素的和
    mean = blockReduceSum(local_sum);

    // 通过0号线程进行取平均
    if (threadIdx.x == 0) {
        s_mean = mean / n;
    }
    // 在block内进行同步,确保所有线程都拿到s_mean
    __syncthreads();

    // 该循环将本线程要处理的元素进行差值平方求和
    float local_var_sum = 0.0f;
    for (int i = tid; i < n; i += blockDim.x) {
        float diff = (float)(ldg(&input[blockIdx.x * n + i])) - s_mean;
        local_var_sum += diff * diff;
    }
   // 进行block级别归约,即将本block中所有线程计算的差值平方进行求和,得到这个数据样本所有元素的方差
    variance = blockReduceSum(local_var_sum);

    // 通过0号线程对方差进行运算
    if (threadIdx.x == 0) {
        s_variance = rsqrtf(variance / n + layernorm_eps);
    }
    // 在block内进行同步,确保所有线程都拿到s_variance 
    __syncthreads();

    Scalar_T abs_max = 1e-6f;

    // 该循环利用均值和方差对本线程要处理的元素进行normalize,并输出到normed_output中
    for (int i = tid; i < n; i += blockDim.x) {
        const int index    = blockIdx.x * n + i;
        float     beta_val = (beta == nullptr) ? 0.0f : (float)ldg(&beta[i]);
        T         val      = (T)((((float)input[index] - s_mean) * s_variance) * (float)(ldg(&gamma[i])) + beta_val);
        normed_output[index] = val;
        
    }
}

下面这个就是block维度归约求和的实现,利用了两次warp维度归约求和来实现,这个实现还是比较经典和常用的,值得参考借鉴。

warp归约求和的实现

  • __shfl_xor_sync(FINAL_MASK, val, mask, 32):这是 warp 内的异或操作,通过每个线程与邻近线程的值进行异或,得到不同的值,本质上就是要获得移位后的元素内容。

  • val = add(val, __shfl_xor_sync(FINAL_MASK, val, mask, 32)):将每个线程的值与邻近线程异或的结果累加到当前线程的值上,最终得到 warp 内的和。

即整个循环通过不断地将 mask 右移,实现了 warp 内的规约操作,下图可清晰表明这个流程,还可以阅读这篇文章了解更详细的线程束洗牌指令的归约使用 jhang:CUDA编程入门之Warp-Level Primitives。

#define FINAL_MASK 0xffffffff
template<typename T>
__inline__ __device__ T warpReduceSum(T val)
{
#pragma unroll
    for (int mask = 16; mask > 0; mask >>= 1)
        val = add(val, __shfl_xor_sync(FINAL_MASK, val, mask, 32));  //__shfl_sync bf16 return float when sm < 80
    return val;
}

block归约求和的实现

有了warp级别的归约之后,block级别的归约先对每个warp都进行求和,通过每个warp中的0号线程把warp内求和的结果存到共享内存中,共享内存的大小是32(一个block最多有1024个线程,而warp大小是32个线程,一个block最多有32个warp,所以这里共享内存大小设置为32可覆盖所有warp),然后再对这个共享内存中存储的32个结果再进行一次warp归约求和,最终得到block级别的最终结果。

block 归约求和数据流

template<typename T>
__inline__ __device__ T blockReduceSum(T val)
{
    // 32个元素即可
    static __shared__ T shared[32];
    // thread在warp中的index
    int                 lane = threadIdx.x & 0x1f;
    //  warp在block的index
    int                 wid  = threadIdx.x >> 5;

    val = warpReduceSum<T>(val);

    if (lane == 0)
        shared[wid] = val;

    __syncthreads();

    // Modify from blockDim.x << 5 to blockDim.x / 32. to prevent
    // blockDim.x is not divided by 32

    // 针对线程数不足的情况,对val进行赋值0,不影响最终结果。
    val = (threadIdx.x < (blockDim.x / 32.f)) ? shared[lane] : (T)(0.0f);
    val = warpReduceSum<T>(val);

    return val;
}

3 总结

本文总结了FasterTransformer中的General LayerNorm实现,主要是CUDA开发中比较基础的共享内存、block归约和warp归约的一些应用,非常基础,没有用到太多华丽的技巧。OneFlow之前也出了一篇关于LayerNorm的优化实现,个人觉得比FasterTransformer中的实现优化力度还要更大一些,可以参考OneFlow:CUDA优化之LayerNorm性能优化实践学习(CUDA优化之LayerNorm性能优化实践 - 知乎 (zhihu.com))

THE END !

文章结束,感谢阅读。您的点赞,收藏,评论是我继续更新的动力。大家有推荐的公众号可以评论区留言,共同学习,一起进步。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ollama + Anythingllm的安装

Ollama官网&#xff1a;https://ollama.com Anythingllm 官网下载&#xff1a;https://useanything.com/download 在Linux下如果直接运行./AnythingLLMDesktop.AppImage 报错的话&#xff0c;可以尝试以下命令&#xff1a; ./AnythingLLMDesktop.AppImage --appimage-extract …

微信公众号排名 SEO的5个策略

随着微信公众号在社交媒体领域的持续发展和普及&#xff0c;如何提升公众号的搜索排名&#xff0c;成为许多运营者关注的焦点。公众号排名SEO&#xff0c;即针对微信公众号进行搜索引擎优化&#xff0c;旨在提高公众号在搜索结果中的曝光率和点击率。下面&#xff0c;我们将深入…

什么是期货?期货的基础知识有哪些?

期货是一种标准化的远期合约&#xff0c;允许买卖双方在未来特定时间以预定价格交易货物或金融资产。也是一种金融衍生品&#xff0c;它为市场参与者提供了一种管理价格波动风险和进行投资的工具。 期货的基础知识有哪些 期货市场是一个复杂的金融环境&#xff0c;对于初学者来…

系统镜像地址

系统镜像 Linux 官网下载地址&#xff1a;Downloadhttps://www.centos.org/download/ 阿里云镜像下载地址&#xff1a;https://mirrors.aliyun.com/centos/https://mirrors.aliyun.com/centos/?spma2c6h.13651104.d-2001.6.6554320cwFqB8E 清华大学镜像下载地址&#xff1…

你对AI的所有疑虑,厚德云替你解答!

遇到难题不要怕&#xff01;厚德提问大佬答&#xff01; 厚德提问大佬答 你是否对AI绘画感兴趣却无从下手&#xff1f;是否有很多疑问却苦于没有大佬解答带你飞&#xff1f;从此刻开始这些问题都将迎刃而解&#xff01;你感兴趣的话题&#xff0c;厚德云替你问&#xff0c;你解…

原型图制作神器!6款软件推荐,助你轻松实现设计构想!

在现代设计领域&#xff0c;原型图的制作是一个至关重要的环节。它们帮助设计师将创意转化为可视化界面&#xff0c;评估用户体验并进行交互测试。本文将介绍六款备受推崇的原型图软件&#xff0c;它们以强大的功能、易用的界面和灵活的工作流程脱颖而出&#xff0c;为设计师创…

每日算法-java

题目来自蓝桥云 // 这是一个Java程序&#xff0c;用于解决最长不下降子序列问题。 // 问题描述&#xff1a;给定一个整数序列&#xff0c;找到最长的子序列&#xff0c;使得这个子序列是不下降的&#xff08;即相邻的元素不严格递减&#xff09;。 // 程序使用了动态规划的方法…

Redis 渐进式遍历 -- scan

前言 keys 可以一次性把 Redis 中的所有 key 都获取到&#xff0c;但这个操作比较危险&#xff0c;一次性获取所有的key 很容易会导致 Redis 阻塞。 而通过渐进式遍历&#xff08;不是一个命令就将所有的 key 值拿到&#xff0c;而是每执行一次命令只获取其中的一小部分&#x…

前后端功能实现——添加品牌

需求 点击新增&#xff0c;跳转到添加品牌的页面&#xff0c;从后一个页面提交品牌数据&#xff1a; 1、BrandMapper接口添加add()方法 /** * 添加品牌 */ void add(Brand brand); 2、BrandMapper.xml中添加sql方法 <insert id"add">insert into brand val…

如何提升通信芯片一次性投片成功率

通信芯片设计是一个非常复杂的系统工程&#xff0c;整体流程设计包括产品定义&#xff0c;算法开发&#xff0c;架构设计&#xff0c;电路设计和验证&#xff0c;后端版图设计&#xff0c;晶圆生产到封装测试等多个环节。在每个环节中&#xff0c;都需要严格遵循设计规则和流程…

2024年 Java 面试八股文——SpringCloud篇

目录 1.Spring Cloud Alibaba 中的 Nacos 是如何进行服务注册和发现的&#xff1f; 2.Spring Cloud Alibaba Sentinel 的流量控制规则有哪些&#xff1f; 3.Spring Cloud Alibaba 中如何实现分布式配置管理&#xff1f; 4.Spring Cloud Alibaba RocketMQ 的主要特点有哪些&…

自编码器网络

1.自编码器网络 自动编码器是一种无监督的数据维度压缩和数据特征表达方法。 无监督 在海量数据的场景下&#xff0c;使用无监督的学习方法比有监督的学习方法更省力。 维度上的压缩 自编码网络可以根据输入的数据&#xff0c;对其进行表征学习。输入数据转换到隐藏层co…

java中如何判断一个数是不是素数(质数)

相关概念 质数就是大于1的自然数字中&#xff0c;只能被1和它自己整除的数。 题目 求101~200之间的质素的个数 代码实现 判断一个数是不是质数 for (int j 2; j < i; j) {if(i % j 0){flag false;break;}}if(flag){System.out.println("当前数字是质数");…

文件删了,回收站清空了怎么恢复?文件恢复软件一览

在日常生活和工作中&#xff0c;我们常常会遇到误删除文件的情况&#xff0c;有时甚至会因为清空了回收站而无法找回这些文件。这些文件可能包含重要的工作数据、个人照片或其他珍贵的回忆。那么&#xff0c;在这种情况下&#xff0c;我们该如何恢复这些被删除且清空回收站的文…

ubuntu配置多版本cuda+cudnn环境,及版本切换方法

ubuntu配置多版本cudacudnn环境&#xff0c;及版本切换方法 环境如下&#xff1a; ubuntu 22.04cuda v11.8cudnn v8.9.7 文章目录 ubuntu配置多版本cudacudnn环境&#xff0c;及版本切换方法1.安装Nvidia显卡驱动1.1卸载默认的驱动nouveau1.2安装nvidia驱动 2.安装cuda3.安装…

《从Paxos到Zookeeper》——第五、六章:经典应用场景

目录 第五章 使用Zookeeper 5.1 服务端部署与运行 5.2 客户端相关 5.2.1 客户端运行 5.2.2 客户端命令 5.3 Java客户端API 5.4 开源客户端 第六章 经典应用场景 6.1 典型应用场景及实现 6.1.1 数据发布/订阅&#xff08;全局配置中心&#xff09; 6.1.2 负载均衡&#xff08;Lo…

谷歌推广和seo留痕具体怎么操作?

留痕跟谷歌推广其实是一回事&#xff0c;你能在谷歌上留痕&#xff0c;其实就是推广了自己的信息&#xff0c;本质上留痕就是在各大网站留下自己的记录&#xff0c;这个记录可以是品牌信息&#xff0c;联系方式&#xff0c;看你想留下什么 如果要问自己怎么操作&#xff0c;正常…

Python 网络编程实践:从基础到进阶

目录 网络编程 一.IP地址简介 1. IP 地址的概念 1.1. IP 地址的表现形式 1.2. IP 地址的作用 2. 查看 IP 地址 3. 检查网络是否正常 4. 小技巧 二.端口和端口号 1. 什么是端口 2. 什么是端口号 3. 端口和端口号的关系 4. 端口号的分类 4.1. 知名端口号 4.2. 动…

网络文件共享

存储类型分三类 直连式存储&#xff1a;DAS存储区域网络&#xff1a;SAN网络附加存储&#xff1a;NAS 三种存储架构的应用场景 DAS虽然比较古老了&#xff0c;但是还是很适用于那些数据量不大&#xff0c;对磁盘访问速度要求较高的中小企业SAN多适用于文件服务器&#xff0c…

毕设:邮件分发系统

文章目录 前言一、登录1.邮箱登录2.账号登录 二、注册三、首页四、写邮件五、收邮件六、草稿箱七、垃圾箱八、已发送九、通讯录十、用户管理十一、邮件管理十二、登录日志总结 前言 分享一下邮件分发系统 一、登录 1.邮箱登录 2.账号登录 二、注册 三、首页 首页有邮件信息&…