作者:Lawliet

翻译:仿佛若有光

前言:

几个月前,我根据 Simoncelli 2016 年的论文编写了自己的自动编码器,用于研究目的。一开始,我想使用一些流行的深度学习框架(例如 Tensor Flow、Caffe2 或 MXNet)来做我的实验。然而,在对所有这些框架进行了几周的调查之后,我发现了一个非常令人头疼的问题——可扩展性。我不是说这些框架设计得不好,而是不允许用户开发第三方算子,就像写一个插件一样,你给我一个没有任何参数的函数。那么改变函数行为的唯一方法就是修改源代码,由于文档组织不善,这无疑是一个巨大的工程。(这似乎是开源软件的通病。)因此,由于不常见的算子 GDN 并未包含在所有这些框架中,因此设计一个新框架似乎是唯一的解决方案。

点个关注,专注于计算机视觉的技术总结和分享

GDN

这个算子是这个理论中的核心非线性函数,表达式如下(公式不重要,如果你不喜欢这些该死的符号,你可以直接跳过这一节。):

上标(k)和(k+1)表示层数,w和u是多通道图像的输入和输出,下标i是通道数。β 和 γ 是我要训练的参数。假设我们有 N 个通道,那么 γ 是一个 N × N 矩阵,β 是一个 N × 1 向量。乍一看,这个功能与 cudnn 和所有深度学习框架都很好地支持的批量归一化 (BN) 或局部响应归一化 (LRN) 非常相似。但相信我,不要让你的眼睛欺骗你。这是非常不同的。(注意大除法是元素除法。)

前向不会消耗太多计算能力,而后向会消耗我 GPU 的大部分能量。现在让我们看看后面。我需要计算 3 个梯度,∇β、∇γ 和 ∇u。

我知道人们第一次看到这个的感觉,因为我第一次看到这个怪物时也想自杀。 但如果我能为所有这些狗屎画一幅画,你会感觉更舒服。

首先,我们可以很容易地注意到输入可以看作是一个长度为 m x n 的向量。其次,(blabla...)^(-3/2) 出现在所有这些梯度中。这意味着我们可以只计算该术语 1 次,并将它们缓存以备后用。我们称其为“(blabla...)^(-1/2)”矩阵 D 。最后,δ 是传播到前一层的误差。

Fig 1. Computation of γ

经过一些简化,它更清楚了,对吧? 我知道仍然需要一些解释。 对于等式的右侧,每个矩形都是由我们上面提到的矩阵堆叠而成的向量。 D 是 GDN 公式中的分母项,还记得我们刚刚提到的“(blabla...)^(-1/2)”吗?

与一些高级算法不同,这种计算对大多数人来说非常直观,我们可以轻松编写 CPU 程序来处理它。只要稍微了解一下 CUDA,每个人都可以将他们的 CPU 代码移植到 GPU。但是,如果您可以选择不同的组织来启动内核,则速度会有很大的不同。

1. 不仅仅是天真的算法。

我称这种方法“不只是天真”是因为这是我用过的第一种方法。即使使用小尺寸图像作为输入,它也几乎耗尽了我所有的 GPU 内存,并实现了最慢的性能。没有利用任何内存重用,我只是垂直和水平复制所有这些小矩形以获得更大的矩阵,如下图所示,并启动许多一维组织的内核。然后将它们相加。

Fig 2. Less than naive Algo.

该算法唯一的优点是不需要在每个CUDA线程中计算索引,因为线程id只是唯一对应的内存索引。所以你需要做的就是一些乘法,然后使用 cublas 将每个小彩色矩形与 1 向量(一个充满所有 1 的向量)的点积相加。但是正如你所看到的,矩形的大小并不像我这里画的那么小,大小和图像一样。对于这张图片中的每个向量,大小将为 N x N x imageSize x batchSize。很明显,我们浪费了 (N-1) x N x imageSize x batchSize x 4 个字节,更不用说浪费在访问所有这些冗余全局内存上的时间了。

2. 朴素算法。

对于第一种算法,我每次迭代只能在我的网络中训练不到 4 张大小为 128 x 128 的图像,时间几乎为 2 秒。(我的 GPU 是 GTX 1080。)这个现实迫使我改进我的算法,否则,我必须等待近 2 个月才能得到我的结果。

因为我需要启动的内核数量肯定比我GPU中的CUDA内核多很多,所以不管我用什么方法,cuda驱动都会把这些任务序列化。然后我决定不复制所有这些记忆。相反,我将启动 N x 一维组织的 N x imageSize 内核 N 次(N 是通道总数)。

Fig 3. Without memory replication

可以看出,改进是显而易见的。因为,我们不再需要大量复制数据。 GPU 中的全局内存访问非常昂贵。内存访问模式也很简单,因为当您获得线程 id 时,只需使用一个 mod 操作就可以获得内存索引(内存索引 = 线程 id % imageSize)。但是,在这种方法中,由于内核仍然是一维组织的,并且我们使用for循环来启动所有这些内核,那么我们可能无法从GPU更智能的调度算法中受益,尽管我已经尝到了血的滋味.现在,通过这个小小的改变,2 个月的训练时间可以缩短到将近 2 周。

3. 更智能的组织算法。

到目前为止,我还没有考虑过共享内存的威力,因为对我来说,通常设计一个好的内核模式是枯燥和头痛的。显然,一维内核模式是最容易编写的代码。然而,更好的性能值得更仔细的设计。令我惊讶的是,本节中的算法实现了第二个算法的 3 倍速度。

回到图 1,可以看到前 3 个右侧矩阵的第一行 δ0、w0 和 D0 是相同的。因此,我们可以在一个块中计算一行 γ,对于每个块我们可以启动 imageSize 个线程,并且对于每个线程我们可以使用 for 循环计算所有通道。

Fig 5. Computation in one block

所以从图 5 来看,将 δ0、w0 和 D0 放在共享内存中是非常直观的,而对于线程 i,它从 0 到 N-1 读取 N 个通道中的一个像素与 δ0、w0 和 D0 相乘 分享回忆。伪代码如下:

blockId = blockIdx.x;
threadId = threadIdx.x;shareDelta <- delta[blockId];
shareW <- W[blockId];
shareD <- D[blockId];
_synchronize();for(i = 0; i < N-1; i++)
{
result[threadIdx i*imgSize] = shareDelta[threadId] *
shareW[threadId] *
shareD[threadId] *
W[threadId + i*imgSize];
}

Algo 2 选择行主计算而不是列主计算是因为在一个网格中计算一行,我们可以共享 3 个向量 δ0、w0 和 D0。但是如果我们像在 Algo 中那样计算一列,我们只能共享 1 个向量 w0。(再次参见图 1。)。

在这段代码片段中,没有 if ... else ... 块。这在并行计算中非常重要。因为所有线程都是并行运行的,理想的情况是所有这些线程同时完成它们的工作。但是如果有 if ... else ... 阻塞,分支会让这些线程做不同的任务,以便它们在不同的时间完成。然后计算时间将由最慢的线程决定。

无索引计算也是一个优势。通过设计一维模式,我们必须使用线程id来计算内存索引,但这里不需要将blockId和threadId转换为一维内存索引来访问数据。

最后,因为我的数据存储在列major中,这意味着,像向量δ0一样,这个向量中的所有元素都是连续存储的。所以它受益于全局内存合并机制。全局内存也是cuda中的一个重要概念。

在硬件方面,16个cuda内核被组织在一个warp中。当其中一个线程访问数据时,例如上图中的 a1,数据总线不仅会传输 a1,还会将 a1~a32 传输到缓存中,以加速其他 15 个内核的数据访问。因此,当我读取全局数据以共享内存时,每 32 个字节我只读取一次,所有其他字节都从缓存中读取,速度快了数百。多亏了时空局域性理论。

4. 多一点改进

今天突然发现其实我不需要共享内存,但是可以使用const内存。因为对于向量δ0、w0和D0,一个block中的每个线程只需要访问一次。所以在for循环之前,我们实际上可以将元素缓存在const内存中。另一个糖是因为每个线程只访问一个元素,不需要线程同步。

代码如下:

blockId = blockIdx.x;
threadId = threadIdx.x;const float constDelta = delta[blockId * imgSize + threadId];
const float constW = W[blockId * imgSize + threadId];
const float constD = D[blockId * imgSize + threadId];for(i = 0; i < N-1; i++)
{
result[threadIdx + i*imgSize] = constDelta * constW *
constD *
W[threadId + i*imgSize];
}

从上面的代码可以看出,constDelta、constW、constD可以从本地内存中重复使用N次,本地内存总是存储在本地寄存器中。因此,带宽大于共享内存。

Reduce Operation

我讲的所有算法都没有完成,因为我从上述算法中得到的实际上都是原始γ,如下所示:

我需要在左侧累积每个向量以获得一个元素。第一个选择是 cublas API,cublasSsbmv。此函数将进行矩阵向量乘法。所以我们可以把左边的向量看成一个矩阵,将它与一个全1向量相乘,得到γ的一行梯度。并重复N次以获得最终结果。但我注意到还有其他 API cublasSgemmBatched。此函数可以进行批量矩阵向量乘法。然后我做了一个实验来测试哪个更快:

N 个矩阵向量乘法 VS 批处理矩阵向量乘法的 for 循环。

结果表明for循环要快得多。但是我不知道原因,也许是因为我这里的 N 太小(N = 256)。

我不会展示如何计算 ∇β 和 ∇u,因为它们类似于 ∇γ。我知道必须有比我更进一步的优化或更好的设计。CUDA 优化对于不深入了解 GPU 组织的人来说通常是困难的。熟悉 CPU 的程序员总是受益于现代操作系统和强大的编译器。然而,GPU 在编写足够的代码方面与 CPU 有很大不同和复杂性,尽管它比以前使用图形着色器进行计算要方便得多。生态环境的完善还需要几年时间。

原文链接:

https://medium.com/@Lawliet0320/ramble-in-cuda-optimization-8fbbcf81e7c5

本文来源于公众号 CV技术指南 的论文分享系列。

欢迎关注公众号 CV技术指南 ,专注于计算机视觉的技术总结、最新技术跟踪、经典论文解读。

在公众号中回复关键字 “技术总结” 可获取以下文章的汇总pdf。

其它文章

计算机视觉中的自注意力

经典论文系列--胶囊网络:新的深度学习网络

综述专栏 | 姿态估计综述

漫谈CUDA优化

为什么GEMM是深度学习的核心

使用深度神经网络为什么8位足够?

经典论文系列 | 目标检测--CornerNet & 又名 anchor boxes的缺陷

如何看待人工智能的泡沫

使用Dice loss实现清晰的边界检测

PVT--无卷积密集预测的多功能backbone

CVPR2021 | 开放世界的目标检测

Siamese network总结

视觉目标检测和识别之过去,现在及可能

在做算法工程师的道路上,你掌握了什么概念或技术使你感觉自我提升突飞猛进?

计算机视觉专业术语总结(一)构建计算机视觉的知识体系

欠拟合与过拟合技术总结

归一化方法总结

论文创新的常见思路总结

CV方向的高效阅读英文文献方法总结

计算机视觉中的小样本学习综述

知识蒸馏的简要概述

优化OpenCV视频的读取速度

NMS总结

损失函数技术总结

注意力机制技术总结

特征金字塔技术总结

池化技术总结

数据增强方法总结

CNN结构演变总结(一)经典模型

CNN结构演变总结(二)轻量化模型

CNN结构演变总结(三)设计原则

如何看待计算机视觉未来的走向

CNN可视化技术总结(一)特征图可视化

CNN可视化技术总结(二)卷积核可视化

CNN可视化技术总结(三)类可视化

CNN可视化技术总结(四)可视化工具与项目

漫谈CUDA优化的更多相关文章

  1. CUDA优化

    cuda程序优化 一:程序优化概述 1:精度 在关键步骤使用双精度,其他步骤使用单精度,以获得指令吞吐量和精度的平衡. 2:延迟 先缓冲一定量数据,在交给GPU计算.可以获得较高的数据吞吐量. 3:计 ...

  2. AAAI 2021 最佳论文公布

    ​ 作者:Synced 翻译:仿佛若有光 第三十五届 AAAI 人工智能会议 (AAAI-21) 以虚拟会议的形式拉开帷幕.组委会在开幕式上公布了最佳论文奖和亚军.三篇论文获得了最佳论文奖,三篇被评为 ...

  3. CVPR2021 | Transformer用于End-to-End视频实例分割

    ​ 论文:End-to-End Video Instance Segmentation with Transformers 获取:在CV技术指南后台回复关键字"0005"获取该论文 ...

  4. ICCV2021 | 重新思考视觉transformers的空间维度

    ​ 论文:Rethinking Spatial Dimensions of Vision Transformers 代码:https://github.com/naver-ai/pit 获取:在CV技 ...

  5. ICCV2021 |重新思考人群中的计数和定位:一个纯粹基于点的框架

    ​ 论文:Rethinking Counting and Localization in Crowds:A Purely Point-Based Framework 代码:https://github ...

  6. CVPR2021 | 重新思考BatchNorm中的Batch

    ​ 前言 公众号在前面发过三篇分别对BatchNorm解读.分析和总结的文章(文章链接在文末),阅读过这三篇文章的读者对BatchNorm和归一化方法应该已经有了较深的认识和理解.在本文将介绍一篇关于 ...

  7. ICCV2021 | MicroNet:以极低的 FLOPs 改进图像识别

    ​前言:这篇论文旨在以极低的计算成本解决性能大幅下降的问题.提出了微分解卷积,将卷积矩阵分解为低秩矩阵,将稀疏连接整合到卷积中.提出了一个新的动态激活函数-- Dynamic Shift Max,通过 ...

  8. 轻量化模型系列--GhostNet:廉价操作生成更多特征

    ​  前言  由于内存和计算资源有限,在嵌入式设备上部署卷积神经网络 (CNN) 很困难.特征图中的冗余是那些成功的 CNN 的一个重要特征,但在神经架构设计中很少被研究. 论文提出了一种新颖的 Gh ...

  9. Batch Size对神经网络训练的影响

    ​ 前言 这篇文章非常全面细致地介绍了Batch Size的相关问题.结合一些理论知识,通过大量实验,文章探讨了Batch Size的大小对模型性能的影响.如何影响以及如何缩小影响等有关内容. 本文来 ...

随机推荐

  1. 【二分 贪心】覆盖问题 BZOJ1052 HAOI2007

    覆盖问题 bzoj1052 题目来源:HAOI 2007 题目描述 某人在山上种了N棵小树苗.冬天来了,温度急速下降,小树苗脆弱得不堪一击,于是树主人想用一些塑料薄膜把这些小树遮盖起来,经过一番长久的 ...

  2. Error in invoking target 'mkldflags ntcontab.o nnfgt.o' of mkdefile '/u01/app/oracle/product/11.2.0

    rpm -ivh cpp-4.4.7-3.el6.x86 64.rpm  --nodeps  --force  (强制安装rpm包)

  3. 从五大结构体,带你掌握鸿蒙轻内核动态内存Dynamic Memory

    摘要:本文带领大家一起剖析了鸿蒙轻内核的动态内存模块的源代码,包含动态内存的结构体.动态内存池初始化.动态内存申请.释放等. 本文分享自华为云社区<鸿蒙轻内核M核源码分析系列九 动态内存Dyna ...

  4. Jenkins 流水线远程部署 .NET Core/Framework 到 IIS

    目录 Windows 安装 Git WebDeploy Windows 从节点 .NET Core 处理 IIS 处理项目 Jenkinsfile .NET Framework 安装环境 .NET F ...

  5. Arduino参考手册-函数和变量及电路图

    标题: Arduino参考手册-函数和变量及电路图 作者: 梦幻之心星 sky-seeker@qq.com 标签: [#Arduino,#参考手册,#函数,#变量] 目录: [Arduino] 日期: ...

  6. NAT介绍与配置

    一,NAT定义 二.NAT的分类 三,NAT配置实验 一,NAT定义 NAT(Network Address Translation),网络地址转换技术,随着Internet的发展,IPv4地址枯竭已 ...

  7. CAS你知道吗?底层如何实现?ABA问题又是什么?关于这些你知道答案吗

    CAS你知道吗?如何实现? 1. compareAndSet 在volatile当中我们提到,volatile不能保证原子语义,所以当用到变量自增时,如果用到synchronized会太"重 ...

  8. 1、mysql基础入门(1)

    1.mysql基础入门: 1.1.数据库介绍:

  9. hdu 6025 前缀 后缀 gcd

    大致题意: 去掉一个元素能使这个数列的GCD最大为多少 分析: 我们求一个数列的GCD,是先求前两个元素的GCD,然后将这个GCD值在与下一个元素进行GCD运算.由此可知进行GCD运算的顺序对最终的结 ...

  10. AcWing 1289. 序列的第k个数

    BSNY 在学等差数列和等比数列,当已知前三项时,就可以知道是等差数列还是等比数列. 现在给你 整数 序列的前三项,这个序列要么是等差序列,要么是等比序列,你能求出第k项的值吗. 如果第k项的值太大, ...