想象你走进一家叫“GPU 中央厨房”的豪华饭店。这家饭店的规矩很特别:你不需要亲自洗菜、切菜、颠勺,只需要坐在餐桌前看菜单点菜。菜单上的每道菜,都是米其林主厨团队反复打磨了几十年的招牌菜——有的菜叫“三通道卷积煲”,有的叫“矩阵乘法拼盘”。这些招牌菜,就是 cuDNN 和 cuBLAS 算子库。cuDNN 是专精“煎炒烹炸”(卷积、池化等深度学习算子)的档口,cuBLAS 是专攻“刀工切配”(矩阵乘、向量运算等线性代数)的档口。 你作为 AI 工程师,角色是“点菜的食客”。你不必从零开始写一个卷积的 CUDA 内核(那等于自己从种小麦开始做一碗面),你要做的是学会“点菜”:拿起菜单(API),写清楚口味偏好(配置描述符),指定喜欢的做法(选择算法),然后等着后厨把菜端上来。这篇教程,就是带着你把这套“点菜流程”从头到尾走一遍:从认识两位主厨,到看懂每道菜背后的备料逻辑。
对 AI 从业者而言,掌握算子库调用的调用模式,意味着不必重复发明轮子,能把精力从底层的 CUDA 细节中解放出来,专注算法与业务创新——这是工程效率与性能优化的分水岭。
可视化演示
展示GPU算子库调用链路、BLAS三级运算分类及算法自动调优三种核心模式,直观呈现cuDNN/cuBLAS从CPU下发到GPU执行的完整流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
GPU算子库调用链路 Host参数经Handle绑定Stream下发到GPU Kernel 四阶段调用链路从CPU参数准备到GPU内核执行 Host → Handle → Stream → Kernel cuDNN/cuBLAS调用本质是CPU下发参数GPU执行kernel
算子库总览
中央厨房里,两位主厨各守一摊。cuDNN 主厨专精“煎炒烹炸”的招牌菜——卷积、池化、归一化、激活函数,这些深度学习里反复出现的算子,被它打磨成了一个个黑盒爆款。cuBLAS 主厨则专攻“刀工切配”——矩阵乘法、向量点积、矩阵分解,凡是线性代数里的基础操作,它都能切得又快又匀。你点的每一道菜,后厨都能找到对应的拿手菜。
为什么非要“点菜”而不是自己下厨?想象你第一次做红烧肉:要买肉、焯水、炒糖色、炖两小时,每一步都可能翻车。而中央厨房的红烧肉,是主厨们花了十年时间,试过上千种火候和调料配比后定型的——同样的道理,直接手写 CUDA 内核做卷积,不仅要处理线程块划分、共享内存、寄存器优化,性能还往往只有库函数的几分之一。算子库的内核经过汇编级调优、针对不同 GPU 架构做了专门适配,还自动利用了张量核心(Tensor Core)等硬件加速单元。
用加速比这个指标能直观感受差距。如果我们把“自己写的朴素内核”的耗时记为 T\_naive,把“调用算子库”的耗时记为 T\_cudnn,那么加速比就是两者相除:
回到厨房的比喻:你把“自己做菜”的时间除以“点菜上桌”的时间,得到的倍数,就是中央厨房存在的意义。一份招牌菜卖 50 块,你自己做要折腾 5 小时,这 5 小时足够你接三单私房菜的外包了——AI 工程师的时间,应该花在更有创意的地方。
- 分子是朴素实现耗时分子 T\_naive 代表你从零手写卷积核所需的执行时间,没有做任何针对性优化。
- 分母是算子库耗时分母 T\_cudnn 是调用 cuDNN 内部调优内核的耗时,可能用了 Winograd、FFT 或隐式 GEMM 等高级算法。
- 相除得加速倍数S 是无量纲的倍数,衡量库优化带来的收益。S 越大,越说明“点菜”比“自己动手”划算。
cuDNN 卷积
点菜时你不能只说“来个辣的”,得说清楚辣度、分量、要不要香菜。cuDNN 的卷积调用也一样——它不是让你直接丢一个张量进去就完事,而是要你填写一张“菜品定制单”,这张单子在 cuDNN 里叫描述符(descriptor)。
这张定制单分两张表。第一张叫张量描述符(tensor descriptor),描述食材本身:数据的形状(N、C、H、W 四个维度)、数据类型(float、half 等)、内存布局。第二张叫卷积描述符(convolution descriptor),描述做法:卷积核大小、填充(padding)、步长(stride)、是否带偏置。两张表都填清楚了,后厨才知道你要的是什么。
填表之前,最好先算一下这盘菜出锅后有多大——也就是卷积输出的尺寸。给定输入高度 H\_in、卷积核尺寸 k、填充 p、步长 s,输出高度 H\_out 的计算方式是:
这张“定制单”填完后,调用流程就清晰了:先创建张量描述符和卷积描述符 → 查询并分配工作空间 → 选定算法 → 执行卷积。每一步都像后厨的流水线:收单、备料、选锅、开炒。你不需要知道炒锅怎么颠,但你把单子填清楚,后厨就不会上错菜。
- 分子是“实际参与滑动的范围”输入高度 H\_in 两边各补 p 圈填充,相当于食材“加大了一圈”,所以要加 2p;再减去卷积核 k 覆盖的部分。
- 除以步长并加 1每次滑动 s 个像素,能滑多少次就用分子除以 s,注意滑动的次数加 1 才是能放下的位置总数。
- 向下取整保证整数如果除不尽,说明最后一步滑出边界了,丢弃多余部分,用向下取整得到最终的输出尺寸。
cuBLAS 矩阵乘
矩阵乘法是深度学习里最硬核的一道菜——全连接层、注意力机制、卷积的 im2col 实现,最后几乎都归结为矩阵乘。cuBLAS 主厨的拿手好戏,就是把这道菜做到极致。但点这道菜有个特别的规矩:cuBLAS 默认按列主序(column-major)理解矩阵,和很多编程语言习惯的行主序(row-major)不一样。就像这位主厨习惯竖着切菜,你递过去的肉是横纹的,不交代一声,炒出来口感就不对。
cuBLAS 的矩阵乘接口叫 GEMM(General Matrix Multiply),它的完整语义不是简单一个 C = A×B,而是带两个“佐料系数”的版本:
这道“佐料系数”的设计,就像点菜时你说“多放辣、少放盐”——α 和 β 就是辣度和咸度的旋钮。调用 cuBLAS 前,你还需要通过 cublasCreate 创建一个句柄(handle),它是你和主厨之间的“专属服务员”,所有后续调用都通过这个服务员传达。句柄创建好后,把布局参数、转置标志、α、β 一次性交代清楚,GEMM 就能开炒了。
- 核心乘法 A×B这是矩阵乘本身,A 是 M×K 维,B 是 K×N 维,相乘得到 M×N 维的中间结果。
- α 缩放中间结果α 是一个标量,可以放大或缩小乘法结果。比如想算 0.5 倍的 A×B,把 α 设为 0.5 即可,不需要先算完再乘。
- β 控制原矩阵的保留程度β×C 表示对输出矩阵 C 的原始值做缩放。如果 β=0,直接覆盖 C;如果 β=1,则在 C 的基础上累加——这就是“累加模式”,在残差连接里非常有用。
工作空间管理
你留意过饭店后厨的备料台吗?大厨炒菜前,会把葱姜蒜、切好的肉片、调好的酱汁先摆在手边。cuDNN 和 cuBLAS 执行算子时,也有类似的“临时备料区”,这就是工作空间(workspace)。有些算法(比如 Winograd 卷积)需要先把输入变换到特殊域,这些中间结果就放在工作空间里。
工作空间的“面积”可不是越大越好。你见过把备料台占满整个厨房、连炒锅都放不下的场面吗?盲目分配超大工作空间,会挤占其他算子需要的显存,甚至触发显存不足(OOM)导致程序崩溃。正确做法是:先问后厨“这盘菜要多大备料台”,再按需分配。cuDNN 提供了查询函数,让你针对每个算法返回所需工作空间大小。因为不同算法需求不同,合理的工作空间大小是所有候选算法需求的最大值:
工作空间属于典型的“高频申请、低频释放”资源。正确的管理方式是在初始化阶段一次性分配好,然后在多个算子调用之间复用这块缓冲区——就像一个多功能备料台,炒完这盘菜擦一擦,下一盘继续用。频繁分配和释放不仅慢,还会造成显存碎片化,就像后厨满地都是用了一半的塑料袋。
- 算法集合第一个符号 \mathcal{A} 表示所有候选算法的集合,比如启发式选出的几种卷积算法。
- 单算法需求w(a_j) 是第 j 个算法需要的工作空间字节数,每个算法各不相同。
- 取最大值max 运算是“要最大那个”,保证无论最终选中哪个算法,空间都够用。
算法选择机制
同样的菜,大厨有好几种做法:可以爆炒、可以红烧、可以清蒸。cuDNN 对同一个卷积也有多种算法,比如 DIRECT、FFT、Winograd、隐式 GEMM。不同的输入形状和 GPU 架构下,各有各的优势。那么问题来了:后厨怎么知道今天该用哪种做法?
答案是两种模式。第一种叫启发式模式(heuristic),就像大厨凭经验扫一眼食材就能判断“这肉适合爆炒”——cuDNN 通过启发式函数,根据输入形状、硬件型号、数据精度等信息快速给出一个较好的算法,速度极快但不保证全局最优。第二种叫自动调优模式(autotuning),像后厨搞品鉴会:把每种做法都试做一遍,用计时器掐表,选出最快的那个。这个模式选出的算法是最优的,但代价是每次都要跑一遍 benchmark,耗时可能达几秒甚至几分钟。
自动调优的本质,是在所有候选算法里找一个执行时间最短的算法:
在实践中有个重要的策略:训练场景中张量形状通常不变,可以开启 cudnn.benchmark 让 cuDNN 自动调优一次并缓存结果;推理场景中形状频繁变化或要求低延迟时,就改用启发式模式,避免每次重新跑 benchmark 的开销。这就好比:如果今天固定只做十桌同样的宴席,都值得花时间研究最佳菜谱;但如果是散客点单,大厨就得凭经验快速出菜。
- 候选集合变量 a 是候选算法的索引,\mathcal{A} 是所有候选算法的集合,例如 DIRECT、FFT、WINOGRAD。
- 耗时函数t(a) 表示算法 a 的实际执行时间,由基准测试测得。
- argmin 选出最优\underset{a}{\arg\min} 的意思是“找到让 t(a) 最小的那个 a”,即把最优算法的编号挑出来。
数据格式约定
同样的食材,竖着码和横着码,对厨师的手感影响很大。张量在显存里的排列方式,就是“摆盘”。cuDNN 里最常见的两种摆盘是 NCHW 和 NHWC。NCHW 表示把同一张图片的所有通道放在一起,像把红、绿、蓝三层的食材叠成一摞;NHWC 表示把每个像素的通道挨在一起,像把彩椒切丁后混匀。
这两种布局不仅影响算子的执行效率,还直接决定了数据在内存中的偏移计算。以 NCHW 为例,一张批大小为 N、通道数 C、高 H、宽 W 的图像,第 n 张图、第 c 个通道、第 h 行、第 w 列的元素,在连续内存中的偏移量是:
布局选择影响性能的原因在于显存的局部性:如果算子需要频繁访问同一位置的多个通道(如逐像素的归一化),NHWC 更友好;如果算子按通道做聚合(如卷积),NCHW 往往更高效。很多框架默认用 NCHW,TensorFlow 早期常用 NHWC。关键是:一旦选定布局,全程保持一致。临时转换布局的成本很高,就像把所有食材重新摆一遍盘,费时费力还可能碰坏摆好的造型。
- 跳过整张图n×C×H×W 是指第 n 张图之前,已有 n 张“整图”大小的数据块需要跳过。
- 跳过完整通道c×H×W 是当前图像中,第 c 个通道前有 c 个完整的 H×W 平面。
- 定位行与列h×W + w 是在选定的通道平面内,先跳过 h 行每行 W 个元素,再偏移到第 w 列。
统一封装层
点菜点多了你会发现,每次都要跟服务员解释“不要香菜、微辣、多放葱”,太累了。于是你干脆写了一张“常点菜品偏好卡”:只要是川菜,就默认微辣多葱不要香菜。AI 工程里,这张偏好卡就是统一封装层——把 cuDNN/cuBLAS 的调用模式抽象成一套简洁的内部 API。
封装层的价值在于“一次配置、多次调用”。原生的 cuDNN 调用流程冗长:每执行一次卷积,要创建描述符、查询工作空间、选算法、分配显存、执行、销毁描述符——像个每盘菜都重新问一遍口味的笨拙服务员。封装层把“口味偏好”缓存起来,同类算子只配置一次描述符,后续调用直接复用。假设一次完整配置的耗时为 C\_setup,单次执行耗时为 C\_exec,调用 n 次算子的平均成本是:
封装层还承担两个隐性职责:错误处理和资源管理。cuDNN/cuBLAS 每次调用都会返回一个状态码(如 CUDNN\_STATUS\_SUCCESS),封装层负责统一检查状态码、把错误转换成业务异常,并在程序退出时统一销毁句柄和描述符。这就像后厨有个“质检员”,每道菜出锅前先看一眼色香味,不合格立刻打回重做,还负责检查燃气灶有没有关火。一个设计良好的封装层,能让整个推理引擎的算子调用像“拿着菜单一键下单”一样干净利落。
- 单次执行成本C\_exec 是算子真正在 GPU 上计算的时间,不受封装影响。
- 配置成本被摊薄C\_setup 除以调用次数 n,表示每次调用分担的配置开销。n 越大,这部分越小。
- 平均总成本两者相加得到单次调用的平均成本,封装层让 C\_setup 只付出一次。
AI 应用场景
视觉模型训练加速平台
阶段:模型训练与优化
解决的问题:训练 ResNet、YOLO 等视觉模型时,若手动实现卷积或矩阵乘,速度慢且易出错。传统做法要么直接调底层 API 导致代码冗长,要么盲目依赖框架黑盒无法针对性优化。通过封装 cuDNN 算子调用,可在训练框架中自动配置描述符与算法选择,让卷积计算接近硬件峰值性能。
具体案例:某自动驾驶公司的感知模型训练平台,在 PyTorch 后端开启 torch.backends.cudnn.benchmark = True,让 cuDNN 针对固定输入尺寸自动调优。上线后训练吞吐提升约 40%,且通过封装层的日志追踪定位到了某个频繁重建描述符的瓶颈,修复后显存占用下降 30%。
推荐系统大规模 Embedding 与矩阵分解
阶段:特征工程与模型推理
解决的问题:推荐系统中,用户与物品的 Embedding 相似度计算、矩阵分解更新,本质是大规模矩阵乘。传统实现用 Python 双层循环计算,耗时以小时计。改用 cuBLAS GEMM 调用后,同样的计算在分钟级完成。
具体案例:某电商推荐团队在召回阶段计算 1 亿用户 × 100 万物品的 Embedding 内积矩阵,原始实现使用 NumPy 分块计算,但显存拷贝开销大。他们基于 cuBLAS 封装了一个批式 GEMM 工具,自动处理列主序与转置参数,将内积计算整体下放到 GPU,单次全量召回从 40 分钟缩短到 3 分钟。
自研推理引擎的算子统一调度
阶段:推理引擎开发与优化
解决的问题:自研推理引擎往往需要同时支持卷积、全连接、归一化等数十种算子,若每种算子独立调用 cuDNN/cuBLAS 的原始 API,代码重复且难维护。统一封装层将算子注册、描述符管理、工作空间分配集中处理,使新算子接入只需实现一个“前向函数”。
具体案例:某芯片公司的推理引擎团队,实现了一个算子调度器:内部维护算子描述符缓存池和工作空间缓冲区,所有算子通过统一接口调用。接入一个新的 cuDNN 算子只需约 50 行代码,且因为复用了工作空间,整个模型的内存峰值降低了 18%。
Python 演示
import timeimport torchimport torch.nn as nn torch.manual_seed(42)torch.backends.cudnn.benchmark = True class CudnnNet(nn.Module): def __init__(self): super().__init__() self.conv1 = nn.Conv2d(3, 64, kernel_size=3, padding=1) self.conv2 = nn.Conv2d(64, 128, kernel_size=3, padding=1) self.fc = nn.Linear(128 * 8 * 8, 10) def forward(self, x): x = torch.relu(self.conv1(x)) x = torch.relu(self.conv2(x)) x = torch.mean(x, dim=[2, 3]) x = torch.flatten(x, 1) return self.fc(x) def time_model(device, loops=30): model = CudnnNet().to(device) x = torch.randn(16, 3, 32, 32, device=device) for _ in range(5): model(x) torch.cuda.synchronize(device) t0 = time.perf_counter() for _ in range(loops): model(x) torch.cuda.synchronize(device) return (time.perf_counter() - t0) / loops if __name__ == "__main__": if torch.cuda.is_available(): dev = torch.device("cuda") lat = time_model(dev) print("GPU 平均耗时(毫秒): %.3f" % (lat * 1000)) print("cuDNN 是否可用:", torch.backends.cudnn.is_available()) print("benchmark 是否开启:", torch.backends.cudnn.benchmark) else: dev = torch.device("cpu") lat = time_model(dev) print("CPU 平均耗时(毫秒): %.3f" % (lat * 1000)) print("未检测到 GPU,当前走 CPU 回退路径")
torch.backends.cudnn.benchmark 开关控制算法自动选择。torch.backends.cudnn.benchmark = True开启 cuDNN 自动调优模式,固定输入形状下会缓存最优算法。nn.Conv2d(3, 64, kernel_size=3, padding=1)定义一个卷积层,实际计算由 cuDNN 的卷积算子完成。nn.Linear(128 * 8 * 8, 10)全连接层,底层计算由 cuBLAS 的 GEMM 完成。torch.cuda.synchronize(device)同步 GPU 与 CPU,确保计时覆盖完整的算子执行时间。torch.backends.cudnn.is_available()检查当前环境是否具备 cuDNN 运行时,便于判断加速路径是否生效。
常见误区
误区很多初学者以为调 cuDNN 就像调用普通函数,把张量指针和形状传进去就能出结果。实际上 cuDNN 要求先创建并配置张量描述符和卷积描述符,否则 API 直接报错或行为未定义。
正解描述符是 cuDNN 的“菜品定制单”,它封装了数据布局、数据类型、卷积参数等全部语义。正确的做法是:创建描述符 → 用 Set 函数填充参数 → 传给算子执行函数 → 用完销毁。跳过这一步,就像跑到大厨面前只扔一袋土豆,不说是做醋溜土豆丝还是薯条,大厨根本没法开工。
为何常见:因为 PyTorch 等高级框架把描述符配置完全隐藏了,初学者直接用框架 API 习惯了“传数据就出结果”的心智模型,换成底层库调用时不适应。
误区不少人认为工作空间是“缓冲区”,那干脆把显存剩余空间全部分配给它,免得不够用。结果程序跑起来显存爆炸,OOM 频发。
正解工作空间的正确大小应该通过查询函数获取。不同算法需要的工作空间差异巨大,取所有候选算法的最大值即可。盲目分配超大空间,就像把整个厨房都铺成备料台,炒锅、砧板、调料架全被挤到走廊上,反而让整个后厨瘫痪。
为何常见:工作空间的概念对新手来说很抽象,容易把它理解成“越大越安全”的普通缓冲区。忽略了显存是整张 GPU 卡所有算子共享的稀缺资源。
误区有工程师把 NumPy 的行主序矩阵直接传给 cuBLAS,发现结果不对,第一反应是“库坏了”。其实 cuBLAS 默认列主序,需要配合转置标志(op 参数)使用。
正解cuBLAS 的 GEMM 接口提供了 CUBLAS\_OP\_N / CUBLAS\_OP\_T 等开关,让你声明对 A 和 B 是否做转置。正确做法是理解内存布局后再调用,而不是盲目传指针。这就像大厨默认竖纹切肉,你递来横纹的肉不交代切法,出锅的肉片口感自然不对。
为何常见:计算机视觉从业者常用行主序的 PyTorch/Numpy 张量,很少有人专门去了解 Fortran 风格的列主序传统——cuBLAS 源自 LAPACK 体系的 C 接口,保留了列主序约定。
误区调用 cudnnConvolutionForward 后只判断 “等于 CUDNN\_STATUS\_SUCCESS 就继续”,一旦失败就打印"cuDNN error"然后不知道怎么办。
正解不同的错误码(如 CUDNN\_STATUS\_BAD\_PARAM、CUDNN\_STATUS\_NOT\_SUPPORTED、CUDNN\_STATUS\_ALLOC\_FAILED)对应完全不同的原因:参数错、算法不支持、显存分配失败。正确做法是用状态码映射到具体错误信息并记录上下文。这就像服务员说“做不了”时,得知道是没食材、锅坏了还是厨师下班了,才能对症下药。
为何常见:初学者已习惯于高级语言的异常机制(try/except),而 C 接口用枚举返回值表示错误,不够直观,容易被忽略细节。
误区认为 cuDNN/cuBLAS 是万能的,任何算子在任意布局、任意硬件上都自动最优,于是完全放弃了解 GPU 内存和并行原理。
正解算子库封装了内核实现,但没有封装“决策职责”。布局选择、工作空间策略、算法模式仍需工程师根据场景配置。就像点菜不用会颠勺,但得知道这个季节哪些食材新鲜、哪些食材适合清蒸——这些知识直接决定菜品上限。
为何常见:高级框架把性能优化做得很好,容易给人“调用库就能躺赢”的错觉。但一旦遇到非标准算子或极端形状,不会排查布局和算法配置,性能就会断崖式下跌。
面试题
Q1. cuDNN 算子库主要负责哪类计算的加速?
- A.卷积、池化、归一化等深度算子← 正确答案
- B.文本分词、词嵌入等 NLP 算子
- C.网络通信、数据分发等系统算子
- D.文件读写、磁盘缓存等 IO 算子
答案:A。cuDNN 的“前菜”是深度学习核心算子,卷积、池化、归一化、激活都在它的菜单上。文本分词是 NLP 工具库的活,网络通信归 NCCL 管,文件 IO 是操作系统的事。记住:cuDNN = Deep Neural Network 的专属档口。
Q2. cuBLAS 的核心能力是以下哪一项?
- A.线性代数中的矩阵乘法运算← 正确答案
- B.图像数据的数据增强流程
- C.模型参数的梯度裁剪逻辑
- D.训练数据的分布式采样
答案:A。cuBLAS 的招牌菜是 BLAS(基础线性代数子程序),矩阵乘 GEMM 是它的看家本领。数据增强、梯度裁剪、分布式采样属于训练框架的职责,并不在线性代数算子库的服务范围内。
Q3. cudnnStatus 枚举类型的主要作用是什么?
- A.表示算子执行的成功或失败状态← 正确答案
- B.表示 GPU 显卡的硬件版本号
- C.表示卷积核的尺寸大小数值
- D.表示当前学习率的配置参数
答案:A。cuDNN 的每个 API 都返回 cudnnStatus_t 枚举,用来报告执行状态,比如 CUDNN_STATUS_SUCCESS 表示成功,CUDNN_STATUS_BAD_PARAM 表示参数错误。它就像后厨质检员的验收章,而不是菜谱上的数值信息。
Q4. 调用 cuDNN 卷积前,为什么要先创建描述符?
- A.描述符封装了卷积的全部参数语义← 正确答案
- B.描述符用于存储训练数据样本
- C.描述符负责编译整个工程代码
- D.描述符用来渲染可视化界面
答案:A。描述符是 cuDNN 的“菜品定制单”,把形状、布局、填充、步长等参数打包成一个不透明对象,让 API 调用不必每次传几十个参数。它不是数据存储器,也不参与编译和渲染,只服务于算子语义的传递。
Q5. 工作空间的正确分配时机是什么?
- A.查询算法需求之后按需分配← 正确答案
- B.在编译阶段静态写死最大显存
- C.每次执行卷积前随机分配大小
- D.调用成功后才发现空间不足
答案:A。正确流程是先选算法、再查询该算法的工作空间大小、最后按需分配,并在多个算子间复用。编译期写死不灵活,随机分配和事后补救更是灾难。这就像先问大厨要多大备料台,再腾地方。
Q6. 启发式算法选择与暴力自动调优相比,主要特点是什么?
- A.速度快但可能非全局最优← 正确答案
- B.速度慢但结果总是更差
- C.与暴力调优结果完全相同
- D.不需要任何工作空间参与
答案:A。启发式模式靠经验规则快速给出一个“不错”的算法,耗时在毫秒级,但不保证全局最优。自动调优逐个实测所有算法,结果最优但耗时以秒计。二者各有适用场景,结果也不可能完全相同,更不是“更差”的关系。
Q7. NCHW 数据布局中,同一张图片的所有通道数据是如何排列的?
- A.每个通道完整排列后接下一通道← 正确答案
- B.每个像素的所有通道交错排列
- C.所有图片按宽度优先混在一起
- D.所有通道随机打乱无固定顺序
答案:A。NCHW 是“通道优先”排列:先存放第 0 个通道的整个 H×W 平面,再放第 1 个通道,以此类推。像素通道交错是 NHWC 的特征。固定顺序是内存寻址的基础,不可能随机打乱。
Q8. 以下哪些参数需要配置到 cuDNN 卷积描述符中?
- A.卷积核的宽与高尺寸← 正确答案
- B.填充与步长的数值← 正确答案
- C.输入输出的通道数量← 正确答案
- D.训练数据的保存路径
答案:ABC。卷积描述符记录的是“做法”层面的参数:核尺寸、padding、stride、通道数、dilation 等,这些直接决定卷积的滑动方式。数据保存路径属于工程配置,与算子语义无关,不需要也不应该进入描述符。
Q9. 以下哪些是工作空间管理的合理策略?
- A.查询后分配所需的最小容量← 正确答案
- B.在多个算子调用间复用缓冲区← 正确答案
- C.直接分配全部剩余显存空间
- D.根据最大需求预留足够缓冲← 正确答案
答案:ABD。合理策略是先查询再按需分配,分配后复用,或按最大值预留缓冲。直接把全部显存当 workspace 是典型反模式,会挤占其他算子甚至导致 OOM。这就像备料台再大也不能堵死厨房通道。
Q10. 在什么情况下,手写自定义 CUDA 内核优于直接调用 cuDNN 算子库?
- A.需要融合多个算子减少访存时← 正确答案
- B.标准卷积常见尺寸的推理场景
- C.标准矩阵乘与全连接计算场景
- D.池化与批归一化的常规实现
答案:A。当你有独特的融合需求(如把卷积+激活+残差融合成一个内核),算子库无法提供这种“定制菜”,手写内核反而能减少多次访存开销。标准卷积、GEMM、池化、BN 都是算子库的强项,手写通常难以超越其优化水平。