夜幕降临,“蜀香园”美食广场的中央后厨灯火通明。厨房总管老张正为一件事发愁:今天要出八百份同样的“酱香小排”,每一份都要在油锅里反复翻面刷酱。最要命的是,这“翻面刷酱”的动作要在后厨的巨型操作台上重复几十万次。 这时,NVIDIA 公司送来了一位“总厨助理”——cuDNN。他不是来顶替厨师们的,而是专门负责把“刷酱”这类最重复、最耗时的工序做到极致。在深度学习的世界里,这个“刷酱”就是卷积运算:一张特征图被一个卷积核滑过每一个位置,逐格相乘再累加,生成新的特征图。一个CNN模型里,这样的“刷酱”动作动辄要执行几百万次。 cuDNN(CUDA Deep Neural Network library)自2014年发布以来,已成为几乎所有深度学习框架默认调用的底层加速库。它专门针对NVIDIA GPU的并行结构设计了多种卷积实现,并自动从这些实现中挑出最快的一种。对于常见的3×3卷积,cuDNN可以比最朴素的写法快上数倍。这篇文章,我们就走进这间“后厨”,看看cuDNN到底是怎么把“刷酱”刷出花来的。

岗位关联

理解cuDNN的加速原理和调用方式,是深度学习框架开发、算子优化和推理部署工程师的必备技能,直接决定你的模型训练与推理速度能压榨到什么地步。

目录
  1. 可视化演示
  2. cuDNN是什么
  3. 卷积计算基础
  4. 核心加速算法
  5. 启发式调优
  6. 内存与工作区
  7. API编程接口
  8. 最佳实践
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

cuDNN是什么

夜幕降临,“蜀香园”美食广场的中央后厨灯火通明。厨房总管老张正为一件事发愁:今天要出八百份同样的“酱香小排”,每一份都要在油锅里反复翻面刷酱。最要命的是,这“翻面刷酱”的动作要在后厨的巨型操作台上重复几十万次。这时,NVIDIA公司送来了一位“总厨助理”——cuDNN。他不是来顶替厨师们的,而是专门负责把“刷酱”这类最重复、最耗时的工序做到极致。

在深度学习的世界里,这个“刷酱”就是卷积运算:一张特征图(食材)被一个卷积核(刷子)滑过每一个位置,逐格相乘再累加,生成新的特征图。一个CNN模型里,这样的刷酱动作动辄要执行几百万次。如果每次都老老实实滑动窗口、逐个相乘累加,GPU再快也会被拖垮。

cuDNN(CUDA Deep Neural Network library)是NVIDIA软件栈中的底层加速库,2014年首次发布,现已成为TensorFlow、PyTorch、PaddlePaddle等所有主流深度学习框架默认调用的“总厨助理”。它不是单一算法,而是集成了直接卷积、implicit GEMM、Winograd、FFT等多种卷积实现,并根据输入张量形状自动选择最快的一种。cuDNN还可以加速池化、批量归一化、激活、RNN等张量算子,是名副其实的深度学习底层加速引擎。

对于最常见的3×3卷积,cuDNN 可以比手写的朴素实现快上数倍。就像老张后来发现,cuDNN这个助理不但自己有一套“省力刷法”,还会在他换菜单时主动测试哪种刷法最快。从此,出餐效率翻了不止一番。

卷积计算基础

为什么“刷酱”这么费时?后厨老张把动作拆开看了看:所谓刷酱,无非是在每一片小排的固定位置,把9种酱料按不同分量涂上去再算出总味道——也就是逐项相乘再累加。一片一片来,自然是慢。数学上,这就是二维卷积:输入一张特征图 x,用一个 R×S 的卷积核 w 滑过每个位置,取局部窗口逐元素相乘再求和,得到输出特征图 y。

这个朴素的“滑窗乘加”在 GPU 上并行度其实不差,但它绕不开一个问题:每个窗口的数据在显存里离得很远,反复读取很费带宽。于是聪明的后厨想了个办法:把每一片要刷的窗口都预先切好、摆到指定的托盘上(im2col),再把所有托盘排成一张大桌子,让流水线一口气刷完(GEMM)。im2col 把输入转换成一个大矩阵,卷积核转换成另一个矩阵,一次矩阵乘法就完成了所有窗口的计算。

不过显式 im2col 的中间矩阵会膨胀 R×S 倍。cuDNN 更常用的路线是隐式 GEMM(implicit GEMM):一边算一边生成“托盘”,不真的把大矩阵写进显存,用计算换内存。老张恍然大悟:原来“刷酱快”的秘密,不在手速,而在于怎么摆盘、怎么走流水线。

y[m,n]=∑i=0R−1∑j=0S−1x[m+i,n+j]⋅w[i,j]\displaystyle y[m,n] = \sum_{i=0}^{R-1} \sum_{j=0}^{S-1} x[m+i, n+j] \cdot w[i,j]
  1. 滑动窗口定位x[m+i, n+j] 表示从输出坐标 (m,n) 对应的输入位置开始,取出一个与卷积核等大的局部窗口。
    x[m+i,n+j]x[m+i, n+j]
  2. 核权重相乘卷积核的每个权重 w[i,j] 与窗口中对应位置的输入值相乘。
    w[i,j]⋅x[m+i,n+j]w[i,j] \cdot x[m+i, n+j]
  3. 双重累加求和对 i、j 两个方向求和,即把窗口内所有乘积累加成一个数。
    ∑i=0R−1∑j=0S−1(⋅)\sum_{i=0}^{R-1} \sum_{j=0}^{S-1} (\cdot)
  4. 得到输出像素累加结果 y[m,n] 就是输出特征图在该位置的值。
    y[m,n]y[m,n]

核心加速算法

摆盘解决了“够不够整齐”的问题,但后厨里还有几位高手,追求的是“少刷几下”。三种流派被 cuDNN 收进了自己的“菜谱”。

第一派是直接卷积:窗口怎么滑,就怎么算,图个实在。第二派是 Winograd:它发现,如果一次刷一个 2×2 的小方阵,只要换一套特制刷子(线性变换),同一个 3×3 的卷积核原本每 4 个输出点要做 36 次乘法,现在只要 16 次,省下超过一半的乘法。

第三派是 FFT:它换了口锅——把整块食材和刷子先各自打成“味觉频谱”,在频域里只做逐元素相乘,再变回原来的“空间域”。根据卷积定理,时域的卷积等价于频域的元素级相乘:

FFT 在小核(3×3)上并不占便宜——三次变换的开销不小;只有卷积核很大、特征图很大时,它才真正划算。cuDNN 把这三种流派全部内置,到底用谁,交给“启发式调优”来决定。

multdirect=m2r2,multwinograd=(m+r−1)2\displaystyle \text{mult}_{\text{direct}} = m^2 r^2, \qquad \text{mult}_{\text{winograd}} = (m+r-1)^2
  1. 输入转频域对输入特征图做快速傅里叶变换 FFT,得到频域表示。
    F(x)\mathcal{F}(x)
  2. 卷积核转频域把卷积核补零到与输入相同尺寸后,同样做 FFT。
    F(w)\mathcal{F}(w)
  3. 频域逐元素相乘两组频谱对应位置相乘,等价于时域的卷积。
    F(x)⊙F(w)\mathcal{F}(x) \odot \mathcal{F}(w)
  4. 逆变换回时域对乘积做逆傅里叶变换,得到真正的卷积结果。
    F−1(⋅)\mathcal{F}^{-1}(\cdot)

启发式调优

后厨总管老张最后拍板:新菜式不定做法,直接把几位厨师都叫来各做一遍,掐秒表选最快的。cuDNN 内部其实就是这么“赛马”的。

当输入尺寸、通道数确定后,不同的算法在 GPU 上的表现差异很大:有时 Winograd 最快,有时 implicit GEMM 更优,有时 FFT 反而领先。cuDNN 提供了一套启发式函数(如 cudnnGetConvolutionForwardAlgorithm_v7),它会基于内部规则快速给出一组候选算法,并按预估性能排序。如果想更精准,就开启 benchmark 模式,让 cuDNN 真实试跑每个候选算法,选出耗时最短的那一个。整个过程可以形式化地写成:

benchmark 的结论会被缓存下来。下次遇到同样形状,cuDNN 直接复用最优算法,不再重新赛马。这也就是 PyTorch 里 torch.backends.cudnn.benchmark = True 背后的机制。老张很满意:有了这套“赛马机制”,后厨遇上什么规模的单子,都能在几秒钟内找到最快的刷法。

a∗=arg⁡min⁡a∈A  Time(a∣shape)\displaystyle a^* = \arg\min_{a \in \mathcal{A}} \; \text{Time}(a \mid \text{shape})
  1. 候选算法集合A 表示 cuDNN 支持的所有卷积算法,如直接卷积、implicit GEMM、Winograd、FFT。
    A={adirect,agemm,awinograd,afft}\mathcal{A} = \{ a_{\text{direct}}, a_{\text{gemm}}, a_{\text{winograd}}, a_{\text{fft}} \}
  2. 运行条件 shape给定输入张量形状(N、C、H、W)、卷积核大小、stride、padding 等。
    shape=(N,C,H,W,K,R,S)\text{shape} = (N, C, H, W, K, R, S)
  3. 实测耗时Time(a) 返回算法 a 在给定 shape 上的真实执行时间。
    Time(a∣shape)\text{Time}(a \mid \text{shape})
  4. 取最小argmin 表示在全部候选中挑出耗时最小者,即最优算法 a*。
    a∗a^*

内存与工作区

不过,聪明的摆盘和刷法也有代价——需要一块额外的备料台。im2col 要临时存放展开后的巨型矩阵;Winograd 要存储变换后的中间结果。这块备料台,就是 cuDNN 的 workspace(工作区)。

工作区是一块在显存中临时分配的缓冲,不属于模型权重。如果一个算法需要工作区,cuDNN 会提供接口查询所需字节数。以显式 im2col 为例,中间矩阵的元素个数大致是:

可见 im2col 的内存开销大约是输入张量的 R×S 倍。若显存紧张,workspace 可能申请失败,此时 cuDNN 的算法选择器会退回不依赖大工作区的算法(如 implicit GEMM)。此外,特征图在显存中的排列方式(NCHW 与 NHWC,即“通道在前”还是“宽高在前”)也会影响访存效率。cuDNN 对两种布局都支持,但不同布局下最快算法可能不同。老张的比喻:食材是叠成一摞按“种类”排,还是摊开在桌上按“位置”排,直接影响厨师拿取的速度——这决定了“备料台”怎么搭、流水线怎么走。

wsim2col=N×Hout×Wout×C×R×S×bytes\displaystyle \text{ws}_{\text{im2col}} = N \times H_{\text{out}} \times W_{\text{out}} \times C \times R \times S \times \text{bytes}
  1. 批次 N一次前向输入多少个样本,batch 越大展开矩阵行数越多。
    NN
  2. 输出空间尺寸H_out 与 W_out 是输出特征图的高和宽,也是滑窗总数。
    Hout×WoutH_{\text{out}} \times W_{\text{out}}
  3. 通道数 C输入通道越多,展开矩阵每列越宽。
    CC
  4. 卷积核尺寸R×S 是卷积核的高×宽,im2col 每列要复制 C×R×S 个输入值。
    R×SR \times S
  5. 数据类型字节数FP32 每个数占 4 字节,FP16 占 2 字节,直接决定显存占用。
    bytes\text{bytes}

API编程接口

下单的时候,老张不会只喊一句“来一份刷酱”,而是要写清楚:用哪锅食材、刷子多大、步长多少、边缘要不要补料。cuDNN 的 API 就是这张正式的点单流程。

使用 cuDNN 跑一次卷积前向,经典流程是:创建句柄 → 配置输入/输出张量描述符 → 配置卷积描述符 → 查询算法 → 分配工作区 → 执行。其中配置卷积描述符时,必须根据输入尺寸算好输出尺寸:

对应的接口调用套路大致是:先 cudnnCreate 得到 handle(句柄),再用 cudnnCreateTensorDescriptor 和 cudnnCreateFilterDescriptor 描述输入输出数据,用 cudnnSetConvolution2dDescriptor 写入 padding、stride、dilation,然后 cudnnGetConvolutionForwardAlgorithm_v7 拿到排序好的算法列表,最后 cudnnConvolutionForward 真正执行。每个函数都返回 cudnnStatus_t 状态码,要像大厨验菜一样逐个确认无误。老张感叹:虽然这张单子写起来麻烦,但写清楚一次,之后就能照着单子无限复做出餐——这也是 cuDNN 句柄和描述符可以复用的道理。

Hout=⌊Hin+2p−ks⌋+1\displaystyle H_{\text{out}} = \left\lfloor \frac{H_{\text{in}} + 2p - k}{s} \right\rfloor + 1
  1. 补边后的尺寸H_in + 2p 表示 padding 之后输入的高(宽同理)。
    Hin+2pH_{\text{in}} + 2p
  2. 减去卷积核减去核尺寸 k,得到卷积核能“滑过”的有效范围。
    Hin+2p−kH_{\text{in}} + 2p - k
  3. 除以步长取整除以 stride s 并向下取整,得到移动步数。
    ⌊Hin+2p−ks⌋\left\lfloor \frac{H_{\text{in}} + 2p - k}{s} \right\rfloor
  4. 加一得输出最后加 1,得到输出特征图的尺寸。
    HoutH_{\text{out}}

最佳实践

有了总厨助理,还得会用。老张在后厨摸爬滚打,总结出几条铁律。

第一,固定菜单才赛马。训练时输入尺寸固定,就打开 benchmark 模式(PyTorch 里 torch.backends.cudnn.benchmark = True),让 cuDNN 试跑挑选最优算法并缓存;如果输入尺寸总在变,每次重新赛马反而更慢。对于自动驾驶推理这种对延迟抖动敏感的场景,更建议提前固定算法,不做反复调优。

第二,显存有限要量力而行。查询算法所需 workspace 大小,结合 GPU 剩余显存决策;如果空间不够,宁愿选择慢一些但无需大工作区的 implicit GEMM,也不要让训练 OOM 中断。

第三,精度与速度要权衡。Winograd、TF32、FP16 等加速手段或多或少会改变浮点计算顺序,带来微小误差。训练时通常无碍,做医疗影像、科学计算等对精度敏感的应用时,需要实测验证。

把这三个原则合并成一句话,cuDNN 内部做的事其实就是:在候选算法中找一个比朴素实现快最多的方案。加速倍数可以写成:

老张最终把后厨流程固定下来:固定菜单、提前赛马、量力分配备料台、警惕精度损失。cuDNN 这个总厨助理,从此成为“蜀香园”出餐效率的核心引擎。

Speedup=TbaselineTcuDNN\displaystyle \text{Speedup} = \frac{T_{\text{baseline}}}{T_{\text{cuDNN}}}
  1. 基线耗时T_baseline 是未使用 cuDNN 或未开启调优的朴素实现耗时。
    TbaselineT_{\text{baseline}}
  2. cuDNN 耗时T_cuDNN 是使用 cuDNN 并选中最优算法后的耗时。
    TcuDNNT_{\text{cuDNN}}
  3. 加速倍数比值大于 1 说明有加速,数值越大越好;卷积网络整体提速常可达 2~10 倍。
    Speedup>1\text{Speedup} > 1

AI 应用场景

场景1:训练大模型时开启 cuDNN 自动调优

阶段:训练阶段

解决的问题:不开启 benchmark 时,PyTorch 对每个卷积层使用默认启发式算法,可能不是当前输入形状下最快的方案。传统做法是手动逐个层试探,非常低效。cuDNN 提供的 benchmark 模式就像让后厨“赛马”,自动试跑所有候选算法(直接卷积、implicit GEMM、Winograd、FFT),选出最优并缓存。

具体案例:某团队训练 ResNet-50,输入固定为 224×224、batch=64。设置 torch.backends.cudnn.benchmark = True 后,3×3 卷积层自动采用 Winograd 路线,训练吞吐从 850 images/s 提升到 1150 images/s,提速约 35%。

场景2:推理部署时锁定算法,消除延迟抖动

阶段:推理阶段

解决的问题:在线推理服务如果每次调用都重新做 autotune,会造成延迟抖动,甚至因 benchmark 试跑占用显存而影响并发。传统方式是硬编码使用某种算法,但不一定最优。cuDNN 的算法查询接口可以在部署初始化阶段一次性选出最快算法,之后固定复用,相当于老张提前把菜谱定好。

具体案例:自动驾驶感知系统部署 YOLOv5,输入分辨率固定 1280×768。初始化时用 cudnnGetConvolutionForwardAlgorithm_v7 锁定首选算法并分配好 workspace,推理 p99 延迟从 28ms 降到 24ms,且不再出现偶发 40ms 尖峰。

场景3:自定义 CUDA 算子以 cuDNN 为性能基准

阶段:算子开发

解决的问题:自研融合算子(如 Conv+BN+ReLU)时,需要与 cuDNN 的分离实现做对比,证明融合确实带来收益。cuDNN 提供稳定可复现的性能基线,相当于给后厨树立一个官方标杆。

具体案例:某算子库团队开发 3×3 卷积融合算子,用 cuDNN v8 的 conv+BN 组合作为对照,验证自己的融合 kernel 在 A100 上端到端提速 18%,从而决定是否合入主干。

Python 演示

python62 行
import timeimport numpy as npimport torchimport torch.nn as nn  def im2col_conv(x, weight, stride=1, pad=0):    """用im2col加矩阵乘法实现卷积, 对应正文中的展开思路"""    N, C, H, W = x.shape    K, _, R, S = weight.shape    Hp = H + 2 * pad    Wp = W + 2 * pad    xp = np.pad(x, ((0, 0), (0, 0), (pad, pad), (pad, pad)),                mode='constant')    Ho = (H + 2 * pad - R) // stride + 1    Wo = (W + 2 * pad - S) // stride + 1    cols = np.zeros((N * Ho * Wo, C * R * S), dtype=np.float32)    for i in range(Ho):        for j in range(Wo):            patch = xp[:, :, i * stride:i * stride + R,                       j * stride:j * stride + S]            cols[i * Wo + j::Ho * Wo, :] = patch.reshape(N, C * R * S)    w_mat = weight.reshape(K, C * R * S)    out = np.matmul(cols, w_mat.T)    out = out.reshape(N, Ho, Wo, K).transpose(0, 3, 1, 2)    return out  if __name__ == "__main__":    np.random.seed(42)    x = np.random.randn(2, 3, 8, 8).astype(np.float32)    w = np.random.randn(4, 3, 3, 3).astype(np.float32)    out = im2col_conv(x, w)    print("im2col+GEMM 输出形状:", out.shape)     if not torch.cuda.is_available():        print("当前环境无CUDA, 跳过cuDNN性能对比")    else:        torch.backends.cudnn.benchmark = False        conv = nn.Conv2d(3, 4, 3).cuda()        xt = torch.from_numpy(x).cuda()        for _ in range(10):            conv(xt)        torch.cuda.synchronize()        t0 = time.time()        for _ in range(50):            conv(xt)        torch.cuda.synchronize()        t_off = (time.time() - t0) / 50         torch.backends.cudnn.benchmark = True        for _ in range(10):            conv(xt)        torch.cuda.synchronize()        t0 = time.time()        for _ in range(50):            conv(xt)        torch.cuda.synchronize()        t_on = (time.time() - t0) / 50        print("cuDNN benchmark off: %.4f ms" % (t_off * 1000))        print("cuDNN benchmark on : %.4f ms" % (t_on * 1000))        print("加速比: %.2f" % (t_off / t_on))
目的:先用 NumPy 手写 im2col+GEMM 验证卷积等价性,再对比 PyTorch 开启 cuDNN benchmark 前后的耗时差异。
  1. cols = np.zeros((N * Ho * Wo, C * R * S), dtype=np.float32)预分配 im2col 展开矩阵,相当于后厨的“备料台”
  2. patch = xp[:, :, i * stride:i * stride + R, j * stride:j * stride + S]取出一个滑窗块,对应卷积核覆盖的窗口
  3. cols[i * Wo + j::Ho * Wo, :] = patch.reshape(N, C * R * S)把各样本同一位置的窗口写入矩阵的对应行
  4. out = np.matmul(cols, w_mat.T)一次矩阵乘法完成所有窗口的卷积,即 GEMM 核心
  5. torch.backends.cudnn.benchmark = False关闭 cuDNN 自动调优,作为基线
  6. torch.backends.cudnn.benchmark = True开启 cuDNN 自动调优,让 cuDNN “赛马”选最快算法

常见误区

误区只要把 torch.backends.cudnn.benchmark 设为 True,训练就会一直快下去,输入尺寸变来变去也没关系。

正解benchmark 只在输入形状固定时划算。一旦形状变化,cuDNN 需要重新试跑候选算法,反而浪费 GPU 时间。正确做法是:固定尺寸的训练开 benchmark,动态尺寸或推理场景显式固定算法。

为何常见:benchmark 这个单词听起来像“全局优化开关”,大家默认它是百利无害的。

误区3×3 卷积用 Winograd 一定最快,直接让 cuDNN 执行 Winograd 就行了。

正解Winograd 在通道多、batch 大时优势明显;但 batch 小、特征图小、stride>1 时,implicit GEMM 或直接卷积可能更快。cuDNN 的自动选择器会综合形状在多个候选中挑选,不必手动硬编码。

为何常见:论文和博客常强调 Winograd 把乘法从 36 降到 16,让人误以为它绝对胜出。

误区cuDNN 名字里带“卷积”,所以它只加速卷积层。

正解cuDNN 全称是 CUDA Deep Neural Network library,除了卷积,还提供池化、LRN、BatchNorm、激活、RNN 等深度网络算子的 GPU 实现。只是卷积的加速效果最受关注。

为何常见:"convolution" 在名字里太显眼,而且绝大多数介绍 cuDNN 的文章都在讲卷积。

误区FFT 把卷积复杂度从 O(n²) 降到 O(n log n),所以 CNN 都应该用 FFT。

正解FFT 在中大尺寸卷积核上才占优。CNN 主流是 3×3 小核,FFT 要完成输入和核两次正向变换加一次逆变换,固定开销远超直接滑动几行代码。cuDNN 只有在算法选择器判断划算时才会选用 FFT。

为何常见:信号处理课程里 FFT 的数学美感太强,容易让人忽略小核场景下的变换固定开销。

误区为了拿到最快的卷积算法,我把 workspace 配置到最大,越多越快。

正解workspace 是算法所需的固定缓冲,给多了不会加速。反而显存有限时超大 workspace 可能让分配失败、OOM。最佳实践是先通过 API 查询算法所需大小,再结合剩余显存选择。

为何常见:GPU 显存一般有几十 GB,初学者容易觉得“多给点没什么”,忽略了显存被其他层占用的事实。

面试题

Q1. cuDNN 是由哪家公司开发的深度学习加速库?

单选题 · easy

  • A.AMD 公司推出的 GPU 加速库
  • B.NVIDIA 公司推出的 GPU 加速库← 正确答案
  • C.Intel 公司推出的 CPU 加速库
  • D.Google 公司推出的 TPU 加速库

答案:B。cuDNN 全称 CUDA Deep Neural Network library,由 NVIDIA 开发,基于 CUDA 平台。就像“蜀香园”后厨里那位总厨助理,是 NVIDIA 专门派来给自家 GPU 所用的。

Q2. cuDNN 主要针对以下哪类运算进行加速?

单选题 · easy

  • A.纯字符串匹配与正则运算
  • B.深度神经网络的卷积运算← 正确答案
  • C.数据库事务的排序运算
  • D.网页渲染时的布局计算

答案:B。cuDNN 面向深度神经网络中的张量运算,核心是 CNN 的卷积。虽然名字里有“卷积”字样,但它也支持池化、归一化、激活等算子,不止于卷积。

Q3. 将卷积转化为矩阵乘法,通常被称为什么操作?

单选题 · single

  • A.rowwise 归一化变换
  • B.im2col 矩阵展开← 正确答案
  • C.onehot 独热编码
  • D.softmax 概率采样

答案:B。im2col 把每个滑窗位置的数据复制成矩阵行,卷积核展成列,再用 GEMM 一次性完成所有窗口计算。就像把每片要刷的食材提前摆到一个统一规格的托盘上。

Q4. Winograd 算法 F(2,3) 与直接卷积的乘法次数对比,正确的是?

单选题 · single

  • A.直接36次,Winograd 16次← 正确答案
  • B.直接16次,Winograd 36次
  • C.直接24次,Winograd 12次
  • D.直接9次,Winograd 4次

答案:A。F(2,3) 输出块 2×2、核 3×3。直接乘法为 2²×3²=36 次,Winograd 利用变换域只需 (2+3-1)²=16 次,节省约 55.6% 的乘法。

Q5. cuDNN 中可选的卷积实现算法包括哪些?

单选题 · multiple

  • A.直接滑动卷积← 正确答案
  • B.隐式 GEMM 矩阵乘← 正确答案
  • C.Winograd 变换← 正确答案
  • D.FFT 频域相乘← 正确答案

答案:ABCD。cuDNN 的算法选择器会在直接卷积、implicit(或 explicit)GEMM、Winograd、FFT 之间做选择。四种路线都内置在库中,具体用谁由启发式或 benchmark 决定。

Q6. torch.backends.cudnn.benchmark = True 的主要作用是?

单选题 · single

  • A.让模型拟合得更快
  • B.自动试跑选择最快算法← 正确答案
  • C.彻底关闭设备GPU
  • D.提升多线程加载速度

答案:B。开启后 PyTorch 会让 cuDNN 对候选算法实测耗时,选最优并缓存,就像让几位厨师各做一遍再掐秒表。前提是输入形状固定,否则频繁试跑反而拖慢速度。

Q7. 开启 cuDNN benchmark 时,哪些情况可能反而降低性能?

单选题 · multiple

  • A.每个batch的输入尺寸动态变化← 正确答案
  • B.输入尺寸固定且长期不变
  • C.频繁新建不同尺寸的张量← 正确答案
  • D.模型的训练权重逐轮更新

答案:AC。benchmark 是在首遇新形状时做试跑。形状频繁变化(A、C)时每次都要重新“赛马”,额外开销变大;B 是 benchmark 的理想场景;D 只改权重不改形状,不影响算法选择。

Q8. cuDNN 的 workspace(工作区)主要用来做什么?

单选题 · single

  • A.存放训练日志信息
  • B.存放中间转换的缓冲区← 正确答案
  • C.存储模型权重的副本
  • D.管理多卡通信队列

答案:B。workspace 是 cuDNN 算法执行时需要的临时显存缓冲,比如 im2col 展开后的矩阵或 Winograd 变换的中间结果。它不存日志、不存权重备份,需要单独分配和释放。

Q9. 关于公式 H_out = (H_in + 2p - k) / s + 1,以下说法正确的是?

单选题 · multiple

  • A.p 表示 padding 大小← 正确答案
  • B.k 表示卷积核边长← 正确答案
  • C.s 表示 stride 步长← 正确答案
  • D.H_out 总是等于 H_in

答案:ABC。padding 扩大输入、kernel 决定滑动范围、stride 决定步长,三者共同决定输出尺寸。D 只有 p=0、k=1、s=1 时才成立,并非总是相等。

Q10. 关于 FFT 卷积,下列说法准确的是?

单选题 · single

  • A.FFT 在任意尺寸下都比直接卷积快
  • B.FFT 适合大卷积核小核时开销反而大← 正确答案
  • C.FFT 完全不需要分配额外显存
  • D.FFT 会从根本上改变卷积结果结构

答案:B。FFT 优势在大卷积核、大特征图;CNN 常见 3×3 小核下要额外做两正一反三次变换,开销更大。FFT 需要显存存频谱,结果是卷积的等价实现,不改变结构。