cuDNN 的初始设计

news2024/12/23 9:20:41

cuDNN V1.0在2014年的发布,并集成到 Caffe、Paddle 等深度学习框架中。论文 cuDNN: Efficient Primitives for Deep Learning 介绍了 NVIDIA 对于该库的设计和实现。近十年间,NVIDIA 迭代推出了8代架构,cuDNN 也更新到 8.9。硬件上引入了 Tensor Core,软件方面 cuDNN V8 中的 Graph API 相比之前变化较大。然而,作为深度学习领域影响广泛的软件基础设施,一窥 cuDNN 的初始设计仍是有意义的。

cuDNN 类似于 cuBLAS,提供了深度学习原语的高效实现。其卷积实现可以在各种输入大小范围内提供可靠的性能,并利用高度优化的矩阵乘法程序来提供高性能,而无需任何辅助内存。

Library

cuDNN 的主要目标之一是使神经网络框架社区能够平等地从其 API 中受益。相应地,cuDNN 的用户不需要采用任何特定的软件框架,甚至不需要进行数据布局:

  • cuDNN 不提供层抽象,而是提供较低级别的计算原语,以简化与现有深度学习框架的集成,每个框架都有自己的抽象。
  • 大部分 API 专用于对存储在用户控制的缓冲区中的数据执行原语操作的函数。通过维持低级别的 API,该库可以简单地集成到其他框架中。
  • cuDNN 支持其所有程序在单精度和双精度浮点运算中的前向和反向传播变体。这些包括卷积、池化和激活函数。该库允许可变数据布局和步幅,以及对输入图像的子部分进行索引。它还包括一组辅助张量变换程序,可以轻松操作4d 张量。

Overview and Handles

cuDNN 公开了一个主机可调用的 C 语言 API,但要求输入和输出数据驻留在 GPU 上,类似于 cuBLAS。

cuDNN 库是线程安全的,其程序可以从不同的主机线程调用。库中提供了一个基于上下文的 API,可以轻松实现多线程和(可选)与 CUDA 流的互操作性。

前向和后向传递的卷积程序使用一个通用的描述符来封装层的属性。Tensor 和 Filter 用不透明的描述符表示,可以灵活地使用张量的每个维度上的任意步长来指定张量布局。 cuDNN句柄、描述符和函数调用的自包含设计以及框架的模块化使集成变得简单。

Spatial Convolutions

卷积神经网络中最重要的计算原语是批处理卷积的一种特殊形式。下表列出了控制此卷积的参数。本节描述了这种卷积的前向形式——反向传播所需的其他形式密切相关。

ParameterMeaning
NNumber of images in mini-batch
CNumber of input feature maps
HHeight of input image
WWidth of input image
KNumber of output feature maps
RHeight of filter kernel
SWidth of filter kernel
uVertical stride
vHorizontal stride
pad_hHeight of zero-padding
pad_wWidth of zero-padding

卷积有两个输入:

  • 输入数据 D ∈ R N C H W D \in \mathbb{R}^{NCHW} DRNCHW
  • 卷积滤波器 F ∈ R K C R S F \in \mathbb{R}^{KCRS} FRKCRS

输入数据范围为小批量 N N N 幅图像, C C C 个输入特征图,每幅图像 H H H 行和 W W W 列。滤波器的范围包括 K K K 个输出特征图、 C C C 个输入特征图、每个滤波器的 R R R 行和 S S S 列。

输出也是一个四维张量 O ∈ R N K P Q O \in \mathbb{R}^{NKPQ} ORNKPQ,其中 N N N K K K 如前所定义, P = f ( H , R , u , p a d _ h ) P=f(H, R, u, pad \_h) P=f(H,R,u,pad_h), Q = f ( W , S , v , p a d _ w ) Q=f(W, S, v, pad\_w) Q=f(W,S,v,pad_w),这意味着输出图像的高度和宽度取决于图像和滤波器的高度和宽度,以及填充和步幅选择。

步幅参数 u u u v v v 允许用户通过仅计算输出像素的子集来减少计算量。填充参数允许用户指定每张图片附加多少行或列的 0 0 0 条目。更具体地说,

f ( H , R , u , p a d _ h ) = ⌈ H − R + 1 + 2 p a d _ h u ⌉ f(H, R, u, pad\_h) = \left\lceil\frac{H - R + 1 + 2pad\_h}{u}\right\rceil f(H,R,u,pad_h)=uHR+1+2pad_h

定义一个访问函数来考虑卷积的步幅、填充和反转:
g ( p , u , R , r , p a d _ h ) = p ⋅ u + R − r − 1 − p a d _ h g(p, u, R, r, pad\_h) = p\cdot u + R - r - 1 - pad\_h g(p,u,R,r,pad_h)=pu+Rr1pad_h

然后,前向卷积计算 O [ n , k , p , q ]    ∀ n ∈ [ 0 , N ) , ∀ k ∈ [ 0 , K ) , ∀ p ∈ [ 0 , P ) , ∀ q ∈ [ 0 , Q ) O[n, k, p, q] \; \forall n \in [0, N), \forall k \in [0, K), \forall p \in [0, P), \forall q \in[0, Q) O[n,k,p,q]n[0,N),k[0,K),p[0,P),q[0,Q)。为方便起见,将 D 0 D_0 D0 定义为 D D D 的零扩展版本。

O [ n , k , p , q ] = ∑ c = 0 C − 1 ∑ r = 0 R − 1 ∑ s = 0 S − 1 F [ k , c , r , s ] ⋅ D 0 [ n , c , g ( p , u , R , r , p a d _ h ) , g ( q , v , S , s , p a d _ w ) ] O[n, k, p, q] = \sum_{c=0}^{C-1} \sum_{r=0}^{R-1} \sum_{s=0}^{S-1} F[k, c, r, s] \cdot D_0[n, c, g(p, u, R, r, pad\_h), g(q, v, S, s, pad\_w)] O[n,k,p,q]=c=0C1r=0R1s=0S1F[k,c,r,s]D0[n,c,g(p,u,R,r,pad_h),g(q,v,S,s,pad_w)]

从上式可以看出,计算卷积涉及一个七层嵌套循环,具有四个独立循环和三个累加循环。有多种实现此计算的方法,我们将在下一节中讨论其中的一些方法。

cuDNN 的卷积程序包含了这些函数的卷积以及互相关变体的实现。这些函数支持用户在输入和输出张量的每个维度上自定义步幅。这很重要,因为不同的框架使用不同的内存布局存储张量。例如,一些框架将特征图交错放置,而另一些则将它们分开。cuDNN 允许用户指定内存布局,这使得集成到现有框架中变得更加简单。对于具有共享参数或有向无环图结构的模型,cuDNN 的程序还有一种模式可以返回原始梯度或将它们累积在缓冲区中。

Implementation

cuDNN 提供的大多数函数都有直接的实现。卷积的实现并不那么明显,因此我们将概述我们设计选择背后的动机和推论。有几种方法可以有效地实现卷积:

  • im2col convolution,即 im2col + GEMM;
  • FFT convolution;
  • direct convolution。

我们的目标是在不使用辅助内存的情况下,提供尽可能接近矩阵乘法的性能。GPU 内存带宽高,但容量低,因此是一种稀缺资源。在训练深度网络时,理想情况下,GPU 内存中应该装满数据、参数和神经元响应,而不是卷积算法所需的辅助数据结构。几种计算卷积的方法需要大型辅助数据结构,因此我们不考虑将这些方法用于 cuDNN。

Explicit GEMM

一种方法是按照 High Performance Convolutional Neural Networks for Document Processing 将卷积降级为矩阵乘法。这可以通过以下操作来完成:

  • 将滤波器张量 F F F 重塑为维度为 K × C R S K \times CRS K×CRS 的矩阵 F m F_m Fm
  • 通过复制原始输入数据收集维度为 C R S × N P Q CRS \times NPQ CRS×NPQ 的数据矩阵矩阵 D m D_m Dm

然后可以使用单个矩阵乘法执行计算,以形成维度为 K × N P Q K \times NPQ K×NPQ 的输出矩阵 O m O_m Om。下图说明了如何将一个简单的卷积降级为矩阵乘法。
在这里插入图片描述

此图中的颜色代表输入特征图, D D D F F F 的元素在图中被唯一标记,以显示每个元素如何参与形成 D m D_m Dm F m F_m Fm

  • 滤波器矩阵 F m F_m Fm 的维度为 K × C R S = 2 × 12 K \times CRS = 2 \times 12 K×CRS=2×12
  • 而数据矩阵 D m D_m Dm 的维度为 C R S × N P Q = 12 × 4 CRS \times NPQ = 12 \times 4 CRS×NPQ=12×4。请注意, D D D 的每个元素在 D m D_m Dm 中最多重复 R S = 4 RS=4 RS=4 次。
  • 输出矩阵 O m O_m Om 的维度为 K × N P Q = 2 × 4 K \times NPQ = 2 \times 4 K×NPQ=2×4

将卷积降级到矩阵乘法是有效的,因为矩阵乘法是高度优化的。矩阵乘法速度很快,因为它对每个传输数据字节的浮点运算比率很高。这个比率随着矩阵变大而增加,这意味着矩阵乘法在小矩阵上的效率较低。因此,这种卷积方法在创建大矩阵进行乘法运算时最为有效。如前所述, D m D_m Dm F m F_m Fm 的大小取决于卷积参数的乘积,而不是参数本身。这意味着使用这种方法的性能可以非常一致,因为算法不关心其中一个参数是否很小,只要其乘积足够大即可。例如,通常在卷积网络的前几层中, C C C 很小,但 R R R S S S 很大,而在网络的末尾, C C C 很大,但 R R R S S S 很小。然而,对于所有层来说,乘积 C R S CRS CRS 通常都相当大,因此性能可以一直很好。

这种方法的缺点是组成 D m D_m Dm 需要将输入数据复制多达 R S RS RS 次,这可能需要非常大的临时分配。为了解决这个问题,实现有时会逐个具体化 D m D_m Dm,例如,为小批量的每个元素迭代调用矩阵乘法。然而,这限制了实现中的并行性,并可能导致矩阵乘法太小而无法有效利用 GPU 的情况。这种方法还降低了卷积的计算强度,因为除了读取 D D D 本身之外,还必须写入和读取 D m D_m Dm,作为一种更直接的方法,需要明显更多的内存流量。

因此,我们选择不直接使用此实现,尽管正如我们将解释的那样,我们的实现是相关的。

FFT Convolution

另一种方法是使用快速傅立叶变换来计算卷积。FFT 可以显著降低卷积的工作复杂度,巧妙的工程化可以有效地用于深度神经网络 [Fast Training of Convolutional Networks through FFTs]。

然而,基于 FFT 的方法使用了大量的临时内存,因为必须将滤波器填充为与输入相同的大小。这在滤波器相对于图像较小的情况下尤其昂贵,通常发生在卷积网络的前几层。

此外,当步幅参数 u u u v v v 大于 1 1 1 时,基于 FFT 的方法无法高效执行。这在许多最先进的网络中很常见,例如 [OverFeat] 和 InceptionV1 中的初期层。步幅通过仅计算输出的稀疏子集,将卷积的计算量降低到原来的 1 / u v 1/uv 1/uv。然而,FFT 算法的性质使得计算修整后的 FFT 是一项不寻常的任务,并且通常比计算密集 FFT 后跟一个额外的下采样步骤慢。

由于这些缺点,我们选择放弃 FFT 方法,尽管我们同意它在某些情况下很有用。

Direct Convolution

另一种常用的方法是直接计算卷积。这可能非常有效,但需要大量专门的实现来处理隐含在卷积的 11 维参数空间中的许多极端情况。采用这种方法的实现往往对参数空间中某些部分的卷积进行了很好的优化,但对其他部分则表现不佳。例如,cuda-convnet2 在批量较大时表现良好,但一旦批量降至64或以下时表现不佳。优化和维护所有这些特化是一项艰巨的任务。

由于我们设想该库将被维护一段时间,并被移植到尚未构思的未来架构中,因此我们寻找更简单的东西,使其在参数空间中表现得更稳健,更容易移植到新的架构中。

Implicit GEMM

NVIDIA 提供了一个矩阵乘法程序,可在 GPU 上实现了相当高比重的浮点吞吐量。该程序的算法类似于 Fast implementation of DGEMM on Fermi GPU 中描述的算法。

cudnn 使用 double buffering 技术。依次读取输入矩阵 A A A B B B 的固定大小子矩阵到片上存储器,然后用于计算输出矩阵 C C C 的子矩阵。我们在计算 A A A B B B 分块的同时,将 A A A B B B 的下一个分块从片外内存中提取到片上缓存和其他内存中。这种技术隐藏了与数据传输相关的内存延迟,使得矩阵乘法计算仅受执行算术所需时间的限制。

正如我们前面所讨论的,卷积可以降级到矩阵乘法上。这种方法提供了实现的简单性以及在整个参数空间中性能的一致性,尽管在内存中实物化降级的矩阵可能代价高昂。

我们的解决方案遵循这种方法,但我们通过仅在片上内存中延迟地实物化 D m D_m Dm,而不是在调用矩阵乘法程序之前在片外内存中实现它,来避免在内存中实现降级矩阵的问题。

由于矩阵乘法程序所需的分块与卷积的任何参数无关,因此 D m D_m Dm 的分块边界与卷积问题之间的映射是非平凡的。因此,我们的方法需要计算此映射并使用它来将 A A A B B B 的正确元素加载到片上内存中。随着计算的进行,这种情况会动态发生,这使得我们的卷积算法能够利用优化的基础设施进行矩阵乘法。

与矩阵乘法相比,我们需要额外的索引算法,但充分利用矩阵乘法的计算引擎来执行工作。计算完成后,我们执行所需的张量转置,将结果存储在用户期望的数据布局中。计算额外的索引需要通过启动时间常数除数来重复计算整数除法和模运算。我们利用 Hacker’s delight 中提出的整数除法和模数算法,将这些代价高昂的运算转换为整数乘法和移位,从而减少我们的方法所需的索引开销。

参考资料:

  • cuDNN: Efficient Primitives for Deep Learning 论文阅读
  • Optimizing Convolutional Layers
  • 2.5 机器码生成
  • Go 编译器介绍
  • PyTorch 2.0 发布:除了编译,还是编译!
  • 编译原理相关
  • MLIR:摩尔定律终结的编译器基础结构 论文解读
  • 指令集架构、机器码与 Go 语言
  • 编译原理:中间代码IR
  • 深入浅出GPU优化系列:GEMM优化(一)
  • CUDNN LIBRARY User Guide
  • 从AI系统角度回顾GPU架构变迁–从Fermi到Ampere(V1.2)
  • Accelerate Machine Learning with the cuDNN Deep Neural Network Library

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

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

相关文章

超级详细的 VirtualBox 虚拟机安装 及入门教程

一、前言 虚拟机(Virtual Machine)指通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。在实体计算机中能够完成的工作在虚拟机中都能够实现。 虚拟机是在一些开发测试工作中常常需要用到的功能,常见的虚拟机…

数学建模算法汇总(全网最全!含matlab案例代码)

数学建模常用的算法分类 全国大学生数学建模竞赛中,常见的算法模型有以下30种: 最小二乘法数值分析方法图论算法线性规划整数规划动态规划贪心算法分支定界法蒙特卡洛方法随机游走算法遗传算法粒子群算法神经网络算法人工智能算法模糊数学时间序列分析马…

【对比度增强】Learning Tone Curves for Local Image Enhancement(LTMNet)

文章目录 0. 前言1. 理解1.1 整体框架1.2 网络结构1.3 细节 2. 亮点3. 总结 0. 前言 LTMNet这篇文章借鉴了CLAHE算法,所有步骤与CLAHE一致,不同之处在于LTMNet中局部映射曲线是通过CNN预测得到,而CLAHE中是通过直方图均衡化而得。关于CLAHE&…

MySQL_第10章_创建和管理表

第10章_创建和管理表 讲师:尚硅谷 - 宋红康(江湖人称:康师傅) 官网: http://www.atguigu.com 1. 基础知识 1.1 一条数据存储的过程 存储数据是处理数据的第一步 。只有正确地把数据存储起来,我们才能…

【C++11】智能指针

目录 一、异常层层嵌套执行流乱跳容易导致内存泄漏 二、使用智能指针解决上述问题 1、RAII 2、像指针一样 3、智能指针RAII运算符重载 三、C98的auto_ptr 四、C11的unique_ptr和shared_ptr 1、unique_ptr唯一指针 2、shared_ptr共享指针 2.1shared_ptr是否线程安全 …

MySQL_第09章_子查询

第09章_子查询 讲师:尚硅谷 - 宋红康(江湖人称:康师傅) 官网: http://www.atguigu.com 子查询指一个查询语句嵌套在另一个查询语句内部的查询,这个特性从 MySQL 4.1 开始引入。 SQL 中子查询的使用大大…

计算机组成原理 作业5

作业5 题量: 21 满分: 100 作答时间:03-23 09:45至03-29 23:59 91.7分 一. 单选题(共11题,35分) 1. (单选题, 3分)计算机的存储器采用分级存储体系的主要目的是________。 A. 便于读写数据B. 便于系统…

NetSuite Sublist解释

今朝汇编一下Sublist主题的知识点以备忘。 2个数据源类型 Related Record - 以Saved Search建立的关联记录;Child Record - 父子表; 1. Related Record Saved Search关键点 这种形式的Sublist是利用Saved Search作为Sublist的数据源,将某…

【群智能算法】一种改进的白鲸优化算法IBWO【Matlab代码#17】

文章目录 1. 原始BWO算法1.1 勘探阶段1.2 开发阶段1.3 鲸落阶段 2. 改进白鲸优化算法2.1 Tent映射种群初始化2.2 反向学习策略 3. 部分代码展示4. 仿真结果展示5. 资源获取 1. 原始BWO算法 BWO算法的种群初始化和大多数智能算法相同,即随机产生搜索空间中的若干候选…

python协程实战

协程简介 协程(Coroutine)又称微线程、纤程,协程不是进程或线程,其执行过程类似于 Python 函数调用,Python 的 asyncio 模块实现的异步IO编程框架中,协程是对使用 async 关键字定义的异步函数的调用; 一个进程包含多个线程,类似…

MySQL学习笔记第三天

第04章 运算符 1.算术运算符 算术运算符主要用于数学运算,其可以连接运算符前后的两个数值或表达式,对数值或表达式进行加()、减(-)、乘(*)、除(/)和取模&a…

13、go并发编程

目录 一、并发模型二、MPG并发模型三、Goroutine的使用1 - 协程使用2 - panic与defer 四、channel的同步与异步‘’1 - 同步与异步channel2 - 关闭channel 五、并发安全性1 - 资源竞争2 - 原子操作3 - 读写锁4 - 容器的并发安全 六、多路复用1 - 阻塞I/O2 - 非阻塞I/O3 - 多路复…

差分(一维+二维)

类似于数学中的求导和积分,差分可以看成前缀和的逆运算。 前缀和我们是求原数组的前缀和,这里是把原数组当成前缀和,构造一个差分数组来运算 以一维为例,如原数组为a[1],a[2],a[3]...a[n] 前缀和的思想是构造st[1]a[1],st[2]a[…

【C++】vector的使用

文章目录 1. 主要结构2. 构造函数与复制重载3. 迭代器4. 容量相关1.容量读取2.容量修改 5. 数据访问6. 数据修改1. 尾插尾删2.任意位置的插入删除 7.其他接口 在之前我们学习了string的使用与模拟实现,在参考文档中可以发现,vector和string以及其他的容器…

I/O 设备

CPU有两种方法访问IO设备 都是基于PMIO的,Port Mapped I/O 给IO总线上的寄存器编号,CPU向IO总线请求写入或读取数据 (x86)给特定的内存地址对应上目标IO设备,当CPU读取这段内存的时候,就会把访问转发给IO…

微服务 - Consul服务注册中心

概述 上篇说到构建良好的架构,依托于基础设施建设(自动化测试、自动化部署、服务监控,服务发现、配置中心等等),决定成败的往往是基础设施建设,所以从搭建一个注册中心和配置中心开始我们新一阶段的启程。 注册中心 注册中心选型…

Cordic算法原理详解

目录 坐标旋转分析 Cordic算法原理 应用举例1:求sin值与cos值 应用举例2:求反正切值 cosθ的还原补偿 坐标旋转数字计算机CORDIC(COordinate Rotation DIgital Computer)算法,通过移位和加减运算,能递归计算常用函数值&#…

《Netty》从零开始学netty源码(四十一)之PoolChunk.runsAvail

runsAvail runsAvail用于记录long型的指针值,是一个LongPriorityQueue数组,LongPriorityQueue的结构如下: array数组用于存储handle的值,其中下标对应SizeClasses中pageIdx,size为array数组的大小,size的大…

1.13|1.14|1.15|1.6、GDB调试

1.13|1.14|1.15|1.6、GDB调试 1.13、GDB调试(1),GDB调试(2)1. 什么是GDB2. 准备工作3. GDB命令—启动、推出、查看代码实际操作①用list查看代码 1.15、GDB调试(3)1. GDB命令—断点操作实际操作…

Redis 快速上手 Java 增删改查(包含 RedisTemplateConfig 的编写)

一:Redis 数据类型 先了解 redis 的五种基本数据类型。 String 字符串类型:name: "value1"List 列表:names: ["value1", "value2", "value2"]Set 集合:names: ["value1", &qu…