前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >如何在CUDA中为Transformer编写一个PyTorch自定义层

如何在CUDA中为Transformer编写一个PyTorch自定义层

作者头像
机器之心
发布于 2019-04-29 07:28:46
发布于 2019-04-29 07:28:46
2K00
代码可运行
举报
文章被收录于专栏:机器之心机器之心
运行总次数:0
代码可运行

选自tunz

作者:Choongwoo Han

机器之心编译

参与:Geek AI、张倩

如今,深度学习模型处于持续的演进中,它们正变得庞大而复杂。研究者们通常通过组合现有的 TensorFlow 或 PyTorch 操作符来发现新的架构。然而,有时候,我们可能需要通过自定义的操作符来实现更多的优化。随着深度学习模型规模不断增长,为实际生产和可扩展训练设计专门优化的操作符将会变得更加重要。因此,本文作者学习了如何在 CUDA 中为 Transformer 编写一个 PyTorch 自定义层。

性能分析

首先,我们需要对一种深度学习模型很熟悉,这样我们就可以找到其性能瓶颈,并查看在我们进行了优化之后有多大的提升。我们可以使用内置的 PyTorch 分析器,也可以使用通用的 python 分析器。我们将同时考察这两种方法。

torch.autograd.profiler

PyTorch 提供了一个名为「torch.autograd.profiler」的 API。我们可以通过如下方式使用该 API:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
with torch.autograd.profiler.profile(use_cuda=True) as prof:
    # Execute ops here
print(prof)

接着,PyTorch 会自动找到每个操作符并衡量他们的性能。性能分析结果如下:

分析器显示出了每个操作符在 CPU 和 GPU 上花费的时间。分析结果很直观,并且看起来似乎很精确,但是我们很难分辨出每个操作符并将它们与我的源代码匹配起来。例如,上面的输出结果显示出了三个不同的「unsqueeze」操作符,但是我们并不知道它们是在哪里被调用的。因此,我转而使用其它的分析器来寻找性能的瓶颈点

逐行分析器

因为 PyTorch 是基于 python 编写的,所以我们也可以使用通用的 python 分析器。我找来了一个逐行分析器(https://github.com/rkern/line_profiler),它可以逐行分析一个 python 应用程序。在要分析的函数的顶部添加「@profiler」装饰器之后,我们可以在命令行中用「kernprof」替换「python」来运行分析器。此外,在 CUDA 的环境下,我们必须设置一个环境变量「CUDA_LAUNCH_BLOCKING」来同步对 CUDA 调用。

运行一个 epoch 的后分析多头注意力机制前馈函数的结果如上图所示。结果显示了测量每一行所花费的时间,因此我们可以很容易地找到需要优化的目标代码。我们将重点关注第 85、87 和 88 行中的掩码操作。它组合了多个操作符来模拟「掩码处理后的 softmax」操作:为 softmax 的掩码输入填充负无穷数,从而使 softmax 忽略它们。在本文中,我将尝试优化这些操作。请注意,它当前花费了函数执行时间的 19.1%(7.2 + 5.9 + 6.0),而第 86 行花费了 15.2% 的执行时间。让我们使用这个值作为对比基线。

还有另一个适合优化的地方:第 86 行和第 90 行中的矩阵乘法,因为它们的输入或输出都填满了许多 0。本文不会对此进行深入探讨。

掩码处理后的 Softmax

首先,我认为我们可以通过将运算过程封装进一个操作符中来优化掩码处理后的 softmax,因为执行多个操作符本身就会产生开销。每次调用每个独立的操作符时,对 CUDA 核函数的调用会产生开销,而主机和 GPU 之间的数据传输也需要时间。

我们将使用一个名为「MaskedSoftmax」的自定义 CUDA 操作符。我们将其直接简略地定义如下:

x 是一个softmax 函数数的输入张量,m 代表一个掩膜张量,s 是一个用于归一化的标量值。该方程与 softmax 类似,只是掩码处理后值被规定为零,并乘以归一化系数。下图显示了掩码处理后的 Softmax 的一个示例。掩码处理后的位置变为零,并且使用 softmax 计算出其余位置上的值。

第一版

我首先写了一个简单版的 Masked Softmax。它由三个与 softmax 具有相同计算流程的遍历组成:(1)找到一个输入的最大值,(2)计算指数运算的值的和,以及(3)将每个值作为输入计算出指数运算的值,用它们分别除以指数运算的值的和。与 softmax 的不同之处在于,它还会加载掩码值,如果掩码值为 1,则将每个对应位置上的输入值转换为零。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
template <typename scalar_t>
__global__ void __launch_bounds__(32) masked_softmax_cuda_forward_kernel(
    const scalar_t* __restrict__ input,
    const scalar_t* __restrict__ mask,
    scalar_t* __restrict__ output,
    unsigned int hidden_size,
    unsigned int m0, // size of mask dimension 0
    unsigned int m1, // size of mask dimension 1
    unsigned int m2, // size of mask dimension 2
    scalar_t scale) {

  // This threadIdx.x is a number between 0 and 31 because we only launched 32 threads.
  const int tid = threadIdx.x;
  // blockIdx.x, y, z are offsets of 0th, 1st, 2nd dimensions of input tensor.
  const unsigned int ibase = blockIdx.x * gridDim.y * gridDim.z * hidden_size +
                             blockIdx.y * gridDim.z * hidden_size +
                             blockIdx.z * hidden_size;
  const unsigned int mbase = blockIdx.x * (m0 > 1 ? m1 * m2 * hidden_size : 0) +
                             blockIdx.y * (m1 > 1 ? m2 * hidden_size : 0) +
                             blockIdx.z * (m2 > 1 ? hidden_size : 0);
  unsigned shfl_mask = __ballot_sync(0xffffffff, threadIdx.x < hidden_size);

  // Find a maximum input.
  scalar_t max_x = -FLT_MAX;
  for (unsigned int i = tid; i < hidden_size; i+=blockDim.x) {
    scalar_t m = mask[mbase + i];
    max_x = fmaxf(max_x, m == 0 ? input[ibase + i] * scale : -FLT_MAX);
  }
  // Reduce values in threads to find a global maximum number.
  for (unsigned int i = 16; i > 0; i >>= 1) {
    max_x = max(max_x, __shfl_xor_sync(shfl_mask, max_x, i));
  }

  // Find a sum of exponential inputs.
  scalar_t exp_sum = 0;
  for (unsigned int i = tid; i < hidden_size; i+=blockDim.x) {
    scalar_t m = mask[mbase + i];
    exp_sum += m == 0 ? std::exp(input[ibase + i] * scale - max_x) : 0;
  }
  // Reduce values in threads to find a global summation of exponential inputs.
  for (unsigned int i = 16; i > 0; i >>= 1) {
    exp_sum += __shfl_xor_sync(shfl_mask, exp_sum, i);
  }

  // Calculate outputs and save to global memory.
  for (unsigned int i = tid; i < hidden_size; i+=blockDim.x) {
    scalar_t m = mask[mbase + i];
    output[ibase + i] = m == 0 ? std::exp(input[ibase + i] * scale - max_x) / exp_sum : 0;
  }
}

CUDA 中有「warp」和「block」的概念。Warp 是一组 32 个线程,而一个 block 则包含多个 warp。每个 block 有一个共享的内存,任何线程都可以访问一个全局内存。每个线程使用不同的线程和 block 的 id 执行相同的核函数代码,因此每个核函数使用全局内存中的 id 查找和读取相关输入,并将每个输出保存到全局内存中。由于计算是分布式的,如果有需要,我们可能需要减少不同 block 或线程中的值。

在这个 softmax 的实现中,我们需要一个约简来获得值的和或最大值。由于访问全局/共享内存是 CUDA 核函数中常见的瓶颈,所以我试图绕开它。为此,我为每个 block 创建了一个 warp,并使用了「shuffle」函数。它使用寄存器进行 warp 内的通信,因此线程可以在不访问共享内存的情况下交换值。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
for (unsigned int i = 16; i > 0; i >>= 1) {
  max_x = max(max_x, __shfl_xor_sync(shfl_mask, max_x, i));
}

通过这个自定义的操作符,掩码处理后的 softmax 占用执行时间的比例降至了 15%。这并不是一个巨大的提升,但无论如何也比之前要快一些了。

现在,内置的 PyTorch 分析器也显示出了这个自定义操作符的性能提升。因此,由于逐行的分析器需要用太长的时间进行性能分析,我将这个第一版的掩码处理后的 softmax 用作进行进一步优化的对比基线。

进一步的优化

正如我所提到的,对于全局内存的访问是一个主要的瓶颈。在一些假设条件下,我们可以最小化内存访问的次数。前面的第一版现在可以从全局内存中读取两种类型的值(掩码和输入)。用于归一化后的点乘注意力机制的掩码通常有如下所示的形式。

从最左或最右开始,它们是连续的,而基本的 transformer 只有从最左开始的三种形式。因此,我们不需要为每个输入加载掩码值。在读取每一行之前,加载一个表示掩码长度的值就足够了。

我们可以使用下面的代码直接将掩码转化为一种新的形式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
mask = mask.size(2) - mask.sum(dim=2, dtype=torch.int32)

接着,我们只需要首先加载掩码长度,将每个循环迭代与掩码长度相同的次数,并将其余输出设置为零。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// Load a mask length.
  const unsigned int mask_offset = blockIdx.x * (m0 > 1 ? m1 : 0) +
                                   blockIdx.z * (m1 > 1 ? 1 : 0);
  unsigned int mask_size = min(static_cast<unsigned int>(mask[mask_offset]),
                               hidden_size);

  unsigned shfl_mask = __ballot_sync(0xffffffff, threadIdx.x < mask_size);

  scalar_t max_x = -FLT_MAX;
  // Iterate loop as much as the mask length.
  for (unsigned int i = tid; i < mask_size; i+=blockDim.x) {
    max_x = fmaxf(max_x, input[ibase + i] * scale);
  }
  for (unsigned int i = 16; i > 0; i >>= 1) {
    max_x = max(max_x, __shfl_xor_sync(shfl_mask, max_x, i));
  }

  scalar_t exp_sum = 0;
  for (unsigned int i = tid; i < mask_size; i+=blockDim.x) {
    exp_sum += std::exp(input[ibase + i] * scale - max_x);
  }
  for (unsigned int i = 16; i > 0; i >>= 1) {
    exp_sum += __shfl_xor_sync(shfl_mask, exp_sum, i);
  }

  // We initialized "output" to zero, so remaining outputs will be zero.
  for (unsigned int i = tid; i < mask_size; i+=blockDim.x) {
    output[ibase + i] = std::exp(input[ibase + i] * scale - max_x) / exp_sum;
  }

这样一来,这个操作就变得快多了。它现在只占用了执行时间的 9%。

掩码处理后的 Softmax(MaskedSoftmax)的执行时间现在比第一版快 2.5 倍。

我还检查了这种优化在多大程度上提高了整个训练的速度。我在 lm1b 数据集上训练了语言模型,并且测量了运行每个(碎片)epoch 的平均时间。第一个 CUDA 的版本比单纯组合 PyTorch 操作符的方法快了约 0.8%,第二个版本比原始版本快了约 1.8%。

结语

我在 CUDA 中编写了一个自定义的操作符并使 Transformer 的训练快了约 2%。我首先希望仅仅在 CUDA 中重写一个操作符来得到巨大的性能提升,但事与愿违。影响性能的因素有很多,但是我不可能找到每一个因素。此外,由于我对 CUDA 并不熟悉,我也遇到了很多 bug。代码越多,bug 越多。这使得我写了很多意想不到的测试代码。这是在提升模型性能和用于写代码的时间之间的一种折中。

编写一个自定义的操作符并没有我想象的那么简单,但是我可以从中学到许多关于 CUDA 如何工作的知识,以及诸如 block、线程、核函数、内存、同步、缓存这样的概念。我希望本文能够对那些想要入门 CUDA 性能优化的人有所帮助。

  • 完整代码:https://github.com/tunz/tcop-pytorch
  • 使用场景:https://github.com/tunz/transformer-pytorch.

原文链接:https://tunz.kr/post/5

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-03-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 机器之心 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
LightSeq: Transformer高性能加速库
Transformer,Bert模型在NLP领域取得巨大成功,得到广泛应用。而Transformer系列模型大小通常很大,在应用层提供相应服务是一个巨大的挑战。
BBuf
2021/08/19
1.2K0
Pytorch拓展进阶(二):Pytorch结合C++以及Cuda拓展
之前的文章中:Pytorch拓展进阶(一):Pytorch结合C以及Cuda语言。我们简单说明了如何简单利用C语言去拓展Pytorch并且利用编写底层的.cu语言。这篇文章我们说明如何利用C++和Cuda去拓展Pytorch,同样实现我们的自定义功能。
老潘
2023/10/19
1.4K0
Pytorch拓展进阶(二):Pytorch结合C++以及Cuda拓展
《GPU高性能编程 CUDA实战》(CUDA By Example)读书笔记
写在最前 这本书是2011年出版的,按照计算机的发展速度来说已经算是上古书籍了,不过由于其简单易懂,仍旧被推荐为入门神书。先上封面: 由于书比较老,而且由于学习的目的不同,这里只介绍了基础
用户1148523
2018/01/09
2.9K0
《GPU高性能编程 CUDA实战》(CUDA By Example)读书笔记
Python王牌加速库:奇异期权定价的利器
在金融领域,计算效率有时可以直接转化为交易利润。量化分析师面临着在研究效率和计算效率之间进行权衡的挑战。使用Python可以生成简洁的研究代码,从而提高了研究效率。但是,一般的Python代码速度很慢,不适合用于生产环境。在这篇文章中,我们将探索如何使用Python的GPU库来高性能实现奇异期权定价领域遇到的问题。
量化投资与机器学习微信公众号
2020/04/24
2.6K0
Python王牌加速库:奇异期权定价的利器
【cuda 编程】gpu_burn 源码解析
gpu_burn 是一款专为多 GPU 设计的、通过 CUDA 实现高强度压力测试的工具。它旨在帮助系统管理员、研究人员和硬件发烧友深入了解GPU的潜能。
Librant
2024/11/30
4720
【cuda 编程】gpu_burn 源码解析
【BBuf的CUDA笔记】十四,OpenAI Triton入门笔记二
接着【BBuf的CUDA笔记】十三,OpenAI Triton 入门笔记一 继续探索和学习OpenAI Triton。这篇文章来探索使用Triton写LayerNorm/RMSNorm kernel的细节。
BBuf
2024/02/22
9990
【BBuf的CUDA笔记】十四,OpenAI Triton入门笔记二
CUDA-MODE 课程笔记 第一课: 如何在 PyTorch 中 profile CUDA kernels
一直想系统看一下某个课程系统和科学的学习下 CUDA ,感觉 CUDA-MODE 这个课程能满足我的需求。这个课程是几个 PyTorch 的 Core Dev 搞的,比较系统和专业。不过由于这个课程是 Youtube 上的英语课程,所以要学习和理解这个课程还是需要花不少时间的,我这里记录一下学习这个课程的每一课的笔记,希望可以通过这个笔记帮助对这个课程以及 CUDA 感兴趣的读者更快吸收这个课程的知识。这个课程相比于以前的纯教程更加关注的是我们可以利用 CUDA 做什么事情,而不是让读者陷入到 CUDA 专业术语的细节中,那会非常痛苦。伟大无需多言,感兴趣请阅读本文件夹下的各个课程的学习笔记。
BBuf
2024/07/02
7590
CUDA-MODE 课程笔记 第一课: 如何在 PyTorch 中 profile CUDA kernels
AI部署篇 | CUDA学习笔记2:矩阵乘法与GPU优化(附CUDA代码)
获得 C 矩阵的计算方法都是相同的,只不过使用的是矩阵 A、B 不同的元素来进行计算,即不同数据的大量相同计算操作,这种计算是特别适合使用GPU来计算,因为GPU拥有大量简单重复的计算单元,通过并行就能极大的提高计算效率。
集智书童公众号
2022/02/10
6K0
AI部署篇 | CUDA学习笔记2:矩阵乘法与GPU优化(附CUDA代码)
cuda教程[新手入门学编程]
大家好,我是架构君,一个会写代码吟诗的架构师。今天说一说cuda教程[新手入门学编程],希望能够帮助大家进步!!!
Java架构师必看
2022/03/22
3.1K0
cuda教程[新手入门学编程]
cuda runtime/driver API解析
tankaro
2025/03/04
1460
Transformers 4.37 中文文档(五十八)
它建议对传统 Transformer 注意力进行微调,使其线性化。这样,模型可以用作循环网络:同时传递时间戳 0 和时间戳 1 的输入与在时间戳 0 传递输入,然后在时间戳 1 传递输入以及时间戳 0 的状态是相同的(见下面的示例)。
ApacheCN_飞龙
2024/06/26
1620
【BBuf的CUDA笔记】十二,LayerNorm/RMSNorm的重计算实现
我也是偶然在知乎的一个问题下看到这个问题,大概就是说在使用apex的LayerNorm/RMSNorm的时候可以打开这个api的memory_efficient开关,这个开关可以在速度和精度无损的情况下节省网络训练的显存占用。感觉比较有趣,我就研究了一下,因此也就有了这篇文章。
BBuf
2024/01/17
9590
【BBuf的CUDA笔记】十二,LayerNorm/RMSNorm的重计算实现
一文理解 PyTorch 中的 SyncBatchNorm
我们知道在分布式数据并行多卡训练的时候,BatchNorm 的计算过程(统计均值和方差)在进程之间是独立的,也就是每个进程只能看到本地 GlobalBatchSize / NumGpu 大小的数据。
BBuf
2022/09/28
3.2K0
一文理解 PyTorch 中的 SyncBatchNorm
CUDA WarpReduce 学习笔记
之前看我司的 如何实现一个高效的Softmax CUDA kernel?多少还是有些细节没有理解,恰好最近要做一个类似的 Reduce+Scale Kernel,原理机制还是比较相似的,所以翻出来重新理解一下。
BBuf
2022/05/25
9630
CUDA WarpReduce 学习笔记
CUDA-入门(转)
CUDA,Compute Unified Device Architecture的简称,是由NVIDIA公司创立的基于他们公司生产的图形处理器GPUs(Graphics Processing Units,可以通俗的理解为显卡)的一个并行计算平台和编程模型。
祝你万事顺利
2019/06/03
1.7K0
CUDA C最佳实践-CUDA Best Practices(二)
9. 内存优化 看页数也知道,内存优化是性能提升最重要的途径。目标在于通过最大化带宽获得对硬件的最大使用率。最好使用快速内存而减少慢速内存的访问。这章就是各种讨论内存优化。 9.1. 主机和设备之间的
用户1148523
2018/01/09
2.2K0
CUDA C最佳实践-CUDA Best Practices(二)
零拷贝内存 or 页锁定内存
这是一个小实验,在于验证GPU上使用零拷贝内存和页锁定内存的性能差别。使用的是点积计算,数据量在100M左右。实验步骤很简单,分别在主机上开辟普通内存,页锁定内存以及进行零拷贝内存的操作,看三者哪个完
用户1148523
2018/01/09
2.1K0
零拷贝内存 or 页锁定内存
PyTorch 2.2 中文官方教程(十二)
PyTorch 提供了大量与神经网络、任意张量代数、数据处理和其他目的相关的操作。然而,您可能仍然需要更定制化的操作。例如,您可能想使用在论文中找到的新型激活函数,或者实现您作为研究的一部分开发的操作。
ApacheCN_飞龙
2024/02/05
1.1K0
快来操纵你的GPU| CUDA编程入门极简教程
2006年,NVIDIA公司发布了CUDA(http://docs.nvidia.com/cuda/),CUDA是建立在NVIDIA的CPUs上的一个通用并行计算平台和编程模型,基于CUDA编程可以利用GPUs的并行计算引擎来更加高效地解决比较复杂的计算难题。近年来,GPU最成功的一个应用就是深度学习领域,基于GPU的并行计算已经成为训练深度学习模型的标配。目前,最新的CUDA版本为CUDA 9。
机器学习算法工程师
2018/07/27
5.1K0
快来操纵你的GPU| CUDA编程入门极简教程
PyTorch自定义CUDA算子教程与运行时间分析
最近因为工作需要,学习了一波CUDA。这里简单记录一下PyTorch自定义CUDA算子的方法,写了一个非常简单的example,再介绍一下正确的PyTorch中CUDA运行时间分析方法。
godweiyang
2021/04/08
2.8K0
PyTorch自定义CUDA算子教程与运行时间分析
相关推荐
LightSeq: Transformer高性能加速库
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验