想象你接管了一家 GPU 大饭店的后厨,手下有两支神秘团队:cuBLAS 是刀工出神入化的切配组,cuDNN 是擅长大火快炒的掌勺组。你接了一单大生意——要在 NVIDIA 的灶台上搭建实时推荐系统。菜单上几十道“算子菜”:矩阵乘法、卷积、池化……但哪种做法最快?换一批客人(batch size)、换菜量(矩阵维度)、换食材等级(精度),速度排名又全变了。你需要一位带着秒表和记录本的品鉴师,把每道菜在不同条件下的出菜时间系统地测一遍,写成一册“点菜攻略”。这套用数据说话的方法,就是性能基准测试(Benchmark)——它不回答“这家店好不好吃”,只回答“这道菜在什么情况下上得最快、最划算”。
对 AI 从业者而言,掌握性能基准测试意味着你的模型优化不再靠感觉拍脑袋,而是有了一张可复用的量化底牌。无论调参、选型还是定位 GPU 瓶颈,它都是最硬核的决策依据。
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
算子库与性能基准
品鉴师上任第一天要摸清家底。他把两位大厨叫到跟前:cuBLAS 刀工组,负责切切剁剁,一切矩阵乘法的活计都归它;cuDNN 掌勺组,负责煎炒烹炸,神经网络里最耗时的卷积操作是它的招牌菜。这两个算子库是 NVIDIA 官方精心调校过的“后厨班底”,普通人写矩阵乘法像用菜刀削土豆,它们却用上了特制的高速刀组——GPU 上的 SIMT 并行、寄存器和共享内存的层层调度,全被封装成了一个个开着现成接口的老火灶。
但“官方出品”不等于“永远最快”。同样一道矩阵乘法,换一批客人(batch size)、换一种食材分量(矩阵行列数),两位大厨的发挥天差地别。于是性能基准测试有了第一重意义:把抽象的“快”翻译成可比较的数字。它像品鉴师拿着秒表,记录每道菜从下单到上桌的真实时间,为后厨排班(算子选择)、食材采购(精度设置)提供不容辩驳的定量依据。
算法工程师做模型优化时,常常遇到两难:这个卷积该用 FP16 还是 FP32?batch size 开到 64 还是 128?没有基准数据,你就只能轮询试错、靠玄学调参。而一套规范化的 benchmark 能产出一张“性能地图”,让你在动手优化前就看清每条路的大致走向。
回看这几位后厨大厨:cuBLAS 和 cuDNN 再厉害,也需要品鉴师用吞吐量这把尺子量一量。尺子拿稳了,后面每道“菜”的评测才站得住脚。
- 吞吐量符号$T_{\text{throughput}}$ 表示吞吐率,单位常写作 samples/s(每秒样本数),是把“快慢”变成数字的核心指标。
- 样本总数$N_{\text{samples}}$ 是所有测试样本的个数,相当于“一桌一共点了多少道菜”,样本越多,统计越稳。
- 总耗时$t_{\text{total}}$ 是从开始计算到结束的墙钟时间,好比“最后一道菜上齐用了多久”,包含实际计算与调度开销。
核心算子原理
品鉴师要写评测方案,先得看懂两道招牌菜是怎么做的。第一道是 GEMM——通用矩阵乘法(General Matrix Multiply)。想象你要把一批样本从输入层变换到隐藏层,每个样本的向量都要和权值矩阵做一次乘法累加。对 batch size 为 $b$ 的输入 $A$ 和权值 $B$,输出的第 $i$ 行第 $j$ 列,是 $A$ 的第 $i$ 行与 $B$ 的第 $j$ 列逐元素相乘后求和。这个过程的浮点运算量是固定的 $2mnk$:三个维度分别是输出行数、输出列数和累加长度。
第二道是卷积。它像一个滑动窗口在输入特征图上扫描,每滑到一个位置,就把窗口内的像素与卷积核逐元素相乘再求和,得到输出特征图的一个点。图像分类、目标检测这些任务几乎全靠它在底层撑腰。cuDNN 为卷积准备了多套算法:直接计算、隐式 GEMM、Winograd 变换……不同 shape 下各有胜负。
这套原理直接决定了 benchmark 设计的方向:既然耗时主要由 $m,n,k$ 和 batch size 决定,那么测试用例就要围绕这些维度展开;既然不同算法各有优劣,那么就要在统一接口下替算子库做“算法探试”。
理解了这两道菜,品鉴师才明白:原来出菜快慢,很大程度上已经被菜量($m,n,k$)和做菜方式(算法)写死在剧本里了。接下来要做的,就是设计一桌能覆盖各种口味的“评测套餐”。
- 矩阵规模$m$ 是输出矩阵行数,$n$ 是输出矩阵列数,$k$ 是两个输入矩阵共享的累加维度。就像买菜:$m$ 是几道主菜,$n$ 是几样配菜,$k$ 是每种菜用几种调料。
- 乘法部分每个输出元素需要 $k$ 次乘法,总共 $m \times n \times k$ 次乘法,等价于“每道菜都翻炒 $k$ 勺调料”。
- 累加部分每次乘法后还有一次加法,所以总量翻倍,系数 2 就是这层含义。
测试用例设计
设计套餐,最忌讳的是漫无目的地乱点菜。品鉴师遵循的第一原则叫控制变量:一次只动一个因素,其他全部锁死。他要摆弄的调料主要有三味。
第一味是 batch size——一次同时处理多少样本。GPU 是个流水线厨房,批量越大,每道菜的固定准备成本(内核启动、显存搬运)就被摊得越薄,吞吐量通常先涨后趋于饱和。第二味是矩阵维度 M/N/K——菜的分量。分量越大,计算密度越高,越能发挥 GPU 的算力。第三味是精度模式:FP32 像用精细电子秤称量每一位小数,FP16 像用托盘天平,速度快一半但可能称不准。此外还有 TF32、BF16 等变体。
把这三维组合起来,就形成了一个测试矩阵。用枚举法生成:batch size 取 {1, 4, 16, 64, 256},维度取 {128, 256, 512},精度取 {FP16, FP32},排列组合出几十到上百个用例。组合数量随维度增长呈乘积式爆炸,所以必须用手写脚本批量生成,而不是一个个手动敲。
三味调料齐了,套餐单子也拉好了。但光有点菜的单子还不够——后厨什么时候开火、每次烧几遍、记录本怎么填,这些流程细节,直接决定了评测数据可不可信。
- 档位数量$N_b$ 表示 batch size 取了几个档位(比如 5 个),$N_{\text{dim}}$ 是矩阵维度档位数,$N_{\text{prec}}$ 是精度档位数。
- 组合计数把三个维度的档位数相乘得到总用例数。如同一家餐厅有 3 种汤、4 种主食、2 种甜点,套餐组合就是 $3 \times 4 \times 2$。
- 结果解读如果 batch 5 档、维度 3 档、精度 2 档,共 $5 \times 3 \times 2 = 30$ 个用例,每个用例都要跑多次取稳定值。
基准测试流程
品鉴师把评测流程拆成四步走,每一步都像标准化厨房的 SOP(标准作业程序)。
第一步是制定测试计划。明确测试对象(是 cuBLAS 的 GEMM 还是 cuDNN 的卷积)、硬件环境(GPU 型号、驱动版本、算子库版本)和测量口径(以毫秒记还是微秒记)。环境不锁死,数据就是废纸。第二步是批量生成用例。用脚本把设计好的参数组合循环展开,每一条用例包含完整的输入 shape、精度和预热次数,自动生成可执行的测试代码。
第三步是采集性能数据。这里有个关键细节:正式计时前必须预热(warmup)。GPU 像刚启动的发动机,频率还没拉起来,缓存也是空的。先空跑几次让“水温上来了”,再开始计时。同样一个用例要重复多次,取中位数或最小值,而不是第一次的数值——因为环境抖动、临近任务的干扰都会让单次测量失真。第四步是结果记录与存储。每次实验的结果按统一格式写入 CSV 或数据库,附上环境信息,方便日后回溯比较。没有一个结构化的记录,测一百次也只是一堆散沙。
这套四步流程跑完后,品鉴师手上就有了一大摞记录。但“耗时 2.3 毫秒”这种孤零零的数字没有意义——它需要放在算力的天秤上称一称,才知道这道菜到底有没有发挥出厨房的全部潜力。
- 重复次数$R$ 是同一用例重复执行的次数,工程上常取 3~10 次。相当于一道菜让不同客人分别试吃,防止某位客人嘴馋吃快了。
- 单次耗时$t_i$ 是第 $i$ 次测量得到的算子耗时。每次测量的环境温度、频率状态都可能不同。
- 求平均把所有单次耗时求和后除以 $R$,得到平均耗时的估计量 $\bar{t}$,作为该用例的代表值。
吞吐量分析
拿到一个个耗时的原始记录,品鉴师要做的第二步是换算成吞吐量和利用率。先把 FLOPs 除以耗时得到实测 TFLOPS:计算量是固定的 $2mnk$,时间是测出来的,它们一比,就得到每秒完成了多少次浮点运算。接着把实测 TFLOPS 除以 GPU 的理论峰值 TFLOPS,得到算力利用率。利用率 80% 说明后厨火力全开;利用率只有 20%,就要警惕了:是不是菜量太小(小矩阵),大部分时间浪费在“端菜”(数据传输和内核启动)上?
这就是著名的瓶颈定位逻辑。当矩阵很小时,GPU 计算核心大部分时间在等待数据从显存搬进来,此时是带宽瓶颈;当矩阵足够大时,计算核心满负荷运转,此时才逼近计算瓶颈。品鉴师要把每个用例的利用率标注在图表上,一眼看出哪道菜是“配菜拖累了主厨”。
分析做完,品鉴师手里的零散记录终于拼出了一张有洞察的性能地图。但单份地图只对一次评测有效——他把所有地图装订成册,就建起了后厨最宝贵的资产:性能基线数据库。
- 实测算力$\text{TFLOPS}_{\text{measured}}$ 是用 $\text{FLOPs} / t_{\text{total}}$ 换算出的每秒浮点运算量,单位是“每秒万亿次浮点运算”。
- 理论峰值$\text{TFLOPS}_{\text{peak}}$ 是 GPU 厂商标称的最大算力,如 A100 的 FP16 峰值 312 TFLOPS。这是“满功率运转”的上限。
- 比值与百分比实测除以峰值,再乘 100% 得到利用率。利用率接近 100% 说明瓶颈在计算,利用率低说明瓶颈在搬运(带宽)。
性能基线数据库
最后一步,是把所有评测结果沉淀成一本可以反复查阅的“点菜攻略”。这本攻略的第一项功能是横向对比:同样一个 GEMM,batch 32 和 batch 128 谁快?FP16 比 FP32 快多少?一目了然。第二项功能是版本回归:NVIDIA 更新了 cuDNN 版本,你担心升级后某些 shape 反而变慢。怎么办?把新旧版本在相同用例集上各跑一遍,拿新结果去和基线库里的旧记录比,慢于基线 5% 就报警。第三项功能是最优配置推荐:模型训练时该不该把 torch.backends.cudnn.benchmark 打开?打开后算子库会在第一次调用时遍历所有算法,选最快的那个,后续复用。而这个选型决策的依据,正是基线数据库中积累的 shape→最优算法映射。
更好的做法是把这套基准做成自动化回归:每次代码提交或环境变更,CI 流水线自动跑一遍核心用例集,把关键指标画成趋势图。某天曲线突然下跌,说明最新的改动引入了性能回退,开发者能第一时间定位。这就把一次性的评测,升级为持续保障性能的引擎。对模型优化来说,基线数据库是“参考书”——面对新的网络结构,先查库,再决定精度、batch size 与算子算法,避免从零试错。
从最初的秒表计时,到如今的自动化回归与智能选型,性能基准就这样完成了从“一次评测”到“持续资产”的跃迁。品鉴师的这本攻略,让后厨的每一次决策都有了据可查、有数可依。
- 基线耗时$t_{\text{before}}$ 是优化前(或旧版本)的算子耗时,存储在基线库中作为参照物,相当于“上一次的品鉴记录”。
- 当前耗时$t_{\text{after}}$ 是优化后(或新版本)的算子耗时,通过新一轮 benchmark 测得。
- 比值判断两数相除得到加速比。Speedup = 1.2 表示快了 20%,Speedup = 0.9 表示慢了约 11%,是版本回归的直接证据。
AI 应用场景
训练框架的算子选型
阶段:模型训练与部署优化
解决的问题:深度学习框架调用 cuDNN 时,默认不进行算法探试,可能选了次优算法。而 torch.backends.cudnn.benchmark = True 会在首次调用时通过基准测试选出最快的卷积算法。传统做法是凭经验开关,不知道打开后是否真的更快;用性能基线数据可以判断当前网络 shape 下探试收益有多大。
具体案例:某推荐系统团队训练一个 3D 卷积模型,发现训练速度忽快忽慢。工程师设计了一组覆盖输入 shape 的 GEMM/卷积 benchmark,对比开启 benchmark 模式前后的耗时,发现 3D 卷积在开启探试后提速 32%,同时确认了小 batch 场景下探试开销反而大于收益,于是按 batch 大小动态开关。
推理服务的动态批处理配置
阶段:模型推理与 serving
解决的问题:在线推理系统面对波动的请求量,需要决定 batch size 设多大才能最大化吞吐。传统做法是固定 batch=32,造成闲时浪费、忙时排队。基于性能基线数据库,可以画出一条“吞吐量 vs batch size”曲线,找到拐点,让服务在拐点附近弹性伸缩。
具体案例:一家 OCR 服务商对 cuBLAS GEMM 做了 batch = 1/4/16/64/128 的基准扫描,发现 batch=64 时 TFLOPS 已趋饱和,再增大收益极小且显存压力变大。于是把动态批处理的上限设为 64,并配合预热池,最终在同等 GPU 数量下将 QPS 提升了 2.1 倍。
算子库升级的 CI 回归门禁
阶段:平台工程与持续集成
解决的问题:NVIDIA 发新版 cuDNN/cuBLAS 后,直接升级有性能回退风险。传统做法是升级后人工抽查,粗糙且不可追溯。将固定用例集的 benchmark 接入 CI,任何算子库/驱动版本变更都自动对比历史基线。
具体案例:某自动驾驶公司维护一个 500 行用例的算子基准集,覆盖训练与推理的关键 shape。某次 cuDNN 升级后 CI 报警:特定 5×5 卷积在 FP16 下慢 14%。团队定位到是新版回退到非 Winograd 算法,于是锁定版本并在基线库中标记该 shape 的可用版本范围。
Python 演示
import timeimport csvimport numpy as np def run_gemm_like(a_shape, b_shape, dtype=np.float32): """模拟一次 cuBLAS GEMM 调用, 返回耗时秒数与输出形状。""" a = np.random.rand(*a_shape).astype(dtype) b = np.random.rand(*b_shape).astype(dtype) t0 = time.perf_counter() np.dot(a, b) elapsed = time.perf_counter() - t0 return elapsed def measure_once(batch, m, k, dtype, repeat=3): """测试一个用例, 重复多次取最小耗时, 换算成 TFLOPS。""" n = k times = [] for _ in range(repeat): a_shape = (batch, m, k) b_shape = (k, n) times.append(run_gemm_like(a_shape, b_shape, dtype)) best = min(times) flops = 2.0 * batch * m * n * k return best, flops / best / 1e12 def main(): dtype_map = {"fp32": np.float32, "fp16": np.float16} configs = [ (1, 128, 128, "fp32"), (4, 128, 128, "fp32"), (16, 128, 128, "fp32"), (64, 128, 128, "fp32"), (64, 256, 256, "fp32"), (64, 512, 512, "fp16"), ] rows = [] for batch, m, k, name in configs: sec, tflops = measure_once(batch, m, k, dtype_map[name]) rows.append([batch, m, k, name, round(sec, 5), round(tflops, 2)]) print("%4d x %4dx%-4d %-5s 耗时 %7.5fs 吞吐 %7.2f TFLOPS" % (batch, m, k, name, sec, tflops)) with open("gemm_bench.csv", "w", newline="") as f: writer = csv.writer(f) writer.writerow(["batch", "m", "k", "dtype", "time_s", "tflops"]) writer.writerows(rows) print("结果已写入 gemm_bench.csv") if __name__ == "__main__": main()
run_gemm_like 替换为 cublasSgemm 或 cudnnConvolutionForward 即可。configs = [...]定义测试用例集合,包含 batch size、M/K 维度与精度档位,对应正文“测试用例设计”中的组合矩阵。times.append(run_gemm_like(a_shape, b_shape, dtype))对同一用例重复执行,把多次耗时收进列表,供后续取最小稳定值。best = min(times)取最小耗时而不是平均耗时,用来模拟“热身后最优状态”,对应 warmup 后取最优的行业惯例。flops = 2.0 * batch * m * n * k用正文公式FLOPs = 2mnk计算该用例的浮点运算总量。round(tflops, 2)把 TFLOPS 保留两位小数,便于阅读与归档。writer.writerow([...])将每条用例的结果串行写入 CSV,对应“结果记录存储”环节,为性能基线数据库提供原始素材。
常见误区
误区cuDNN 是 NVIDIA 官方库,里面每个算子都经过了深度优化,我直接调它就行了,不需要自己测什么基准。
正解cuDNN 内部为同一个卷积准备了多套算法,如直接卷积、隐式 GEMM、Winograd,它们在 batch size、kernel size、精度不同时表现各异。官方库也不保证每个 shape 都选了最优解。正常情况下需要开启 cudnn.benchmark 模式做算法探试,或者主动用 benchmark 脚本对目标 shape 做网格扫描,才能确定最快算法。
为何常见:初学者容易把“官方优化”和“自适应最优”混为一谈。事实上官方库更像一座装备齐全的厨房,菜刀虽好,也要选对型号才顺手。
误区FP16 数据量是 FP32 的一半,带宽减半,又有张量核心加速,所以同样模型直接换成 FP16,速度必然翻倍。
正解FP16 的收益取决于瓶颈类型。当算子处于计算瓶颈时,张量核心确实能带来接近翻倍的吞吐;但当矩阵很小时,耗时集中在数据搬运和内核启动上,FP16 的优势被带宽瓶颈掩盖,实测可能只快 10% 甚至无提升。另外 FP16 动态范围小,直接使用可能造成数值溢出,需要配合 loss scaling。
为何常见:很多人只看到“半精度=一半数据=一半时间”的表面逻辑,忽略了计算、带宽、启动三类开销的复杂博弈。
误区我今天在 GPU 上测出 GEMM 耗时 2.1 毫秒,明天还应该是 2.1 毫秒,这个数据存下来可以直接用。
正解GPU 的时钟频率会随负载和温度动态变化,相邻进程、显存分配状态也会影响结果。单次测量可能偏离真实值 10% 以上。正确做法是每个用例预热后重复多次,取最小值或中位数,并在记录中附上驱动版本、算子库版本、GPU 型号等环境信息,否则数据无法跨时间对比。
为何常见:人们习惯把计算机的运行视为确定性的,忽略了现代 GPU 的功耗管理与多租户干扰等现实因素。
误区A100 FP16 峰值 312 TFLOPS,我的 benchmark 显示 260 TFLOPS,说明性能已经很好了,不用再优化。
正解峰值是理想状态下的理论上限,真实性能要看算力利用率。小矩阵场景下利用率可能只有 15%,此时瓶颈不在算力而在带宽。应该同时测量耗时的绝对值和利用率,并对比同 shape 下不同算法的表现,才能判断“是计算满了还是等数据等崩了”。
为何常见:厂商宣传口径中峰值最抓眼球,而实际开发中 shape、精度、访存模式才是决定性能的隐藏变量。
面试题
Q1. cuBLAS 算子库最擅长加速以下哪类运算?
- A.卷积神经网络中的卷积
- B.通用矩阵乘法 GEMM← 正确答案
- C.图像缩放与裁剪
- D.文本分词与编码
答案:B。cuBLAS 是 NVIDIA 的 BLAS(基础线性代数)库,核心是矩阵与向量运算,其中 GEMM 是最常用、优化最深的算子。卷积主要由 cuDNN 负责,图像与文本处理则不在它的职责范围内。
Q2. cuDNN 算子库的核心作用是?
- A.加速卷积等深度学习算子← 正确答案
- B.管理 GPU 显存分配
- C.编译 Python 源代码
- D.提供 Web 服务框架
答案:A。cuDNN 全称 CUDA Deep Neural Network library,专门为深度网络中的卷积、池化、归一化等算子提供高性能实现。显存管理由 CUDA runtime 负责,编译与 Web 服务与它无关。
Q3. 性能基准测试最主要的目标是?
- A.建立量化的性能基线← 正确答案
- B.增加 GPU 的显存容量
- C.加速磁盘的读写速度
- D.生成随机的测试种子
答案:A。基准测试是通过受控实验测量算子的吞吐量与耗时,形成可对比的基线数据库。它不能改变硬件参数,而是为算子选型和优化提供定量决策依据。
Q4. 一个 GEMM 的 m=n=k=1024,其浮点运算量约为多少?
- A.约 1G FLOPs
- B.约 2G FLOPs← 正确答案
- C.约 4G FLOPs
- D.约 512M FLOPs
答案:B。FLOPs = 2 × 1024 × 1024 × 1024 = 2,147,483,648 ≈ 2.1 GFLOPs。2 代表乘法和加法各算一次。留意别漏掉系数 2。
Q5. 做算子性能测试时为什么要先 warmup(预热)?
- A.让 GPU 频率拉升且缓存就绪← 正确答案
- B.减少代码编译的时间
- C.增大 GPU 显存空间
- D.防止数据被意外删除
答案:A。预热让 GPU 进入稳定工作状态,避免首轮调用时驱动加载、缓存未命中和频率爬坡造成的虚假慢速。没有预热的数据更像“冷启动成绩”,不能代表真实运行性能。
Q6. FP16 相比 FP32 更快的核心原因是?
- A.数据量减半且张量核心加速← 正确答案
- B.数学上精度更高
- C.不需要占用显存
- D.完全不需要做乘法
答案:A。FP16 每个数占 2 字节,传输带宽与存储占用减半;NVIDIA 张量核心对 FP16 算力是 FP32 的数倍。精度更高是 FP32 的特点,FP16 仍需运算与存储。
Q7. 某 GPU 峰值 100 TFLOPS,实测利用率 30%,则实测算力为?
- A.30 TFLOPS← 正确答案
- B.300 TFLOPS
- C.333 TFLOPS
- D.70 TFLOPS
答案:A。实测算力 = 峰值 × 利用率 = 100 × 0.3 = 30 TFLOPS。利用率 30% 意味着算力严重闲置,需要进一步排查是否处于带宽瓶颈或小矩阵场景。
Q8. cuDNN benchmark 模式做算法探试时,通常需要考虑哪些因素?
- A.输入 tensor 的 shape← 正确答案
- B.数据类型与精度模式← 正确答案
- C.候选算法集合← 正确答案
- D.服务器摆放的位置
答案:ABC。算法探试是在给定 shape 与精度下,遍历所有候选算法实测耗时并选择最优。服务器位置是物理部署因素,与算法选择无关。
Q9. 性能基线数据库可以支撑哪些应用?
- A.检测算子库版本性能回退← 正确答案
- B.为模型算子选择最优配置← 正确答案
- C.指导精度模式的切换← 正确答案
- D.直接增加 GPU 的流处理器
答案:ABC。基线库存的是“哪个配置最快”的历史数据,可做版本 A/B 对比、配置推荐和精度策略参考。它无法改变硬件资源,流处理器数量是硬件的固定属性。
Q10. 算子库升级后做 A/B 回归对比时,需要控制哪些变量以保证结论可靠?
- A.输入 shape 与精度保持一致← 正确答案
- B.使用相同的测量统计口径← 正确答案
- C.固定 GPU 驱动版本与机器状态← 正确答案
- D.故意使用不同的 batch size
答案:ABC。控制变量是基准测试的基石。shape、精度、统计口径与运行环境必须一致,才能把性能差异归因于算子库版本。故意改 batch size 会让结果无法归因,属于无效实验。