Nvidia cuDNN 面试准备
CUDA 优化核心
CUDA 优化核心是让计算单元尽量忙起来,同时减少 memory stall。通常从 memory coalescing、shared memory reuse、减少 bank conflict、提高 occupancy、减少分支发散、合理使用 vectorized load/store 和 asynchronous copy 等方向优化。
memory stall
Memory stall指的是 CPU/GPU/加速器在执行程序时,因为等待内存数据返回而被迫停下来,不能继续执行指令的现象。
常见原因
- Cache miss:数据不在 L1/L2/L3 cache,需要访问主存。
- 内存带宽不足:多个核心、线程或算子同时访问内存,带宽被打满。
- 访存不连续:随机访问、stride 太大、数据 layout 不合理,导致 cache 利用率差。
优化方向
- 提高 cache locality,比如让访问更连续;
- 减少重复访存,比如复用中间结果;
- 改数据布局,比如 NHWC / NCHW / blocking;
- 对 GPU,优化 shared/local memory 使用,减少 global memory 访问;
Memory coalescing
Memory coalescing指的是 GPU 里多个线程访问内存时,硬件把这些访问合并成更少、更大的连续内存事务,从而提高访存效率。
也就是说:一组线程一起读写连续地址,硬件可以一次性批量搬数据;如果地址很散,就只能拆成很多次访问,性能会差很多。
bank conflict
Bank conflict通常指 GPU 的shared memory / local memory访问冲突:多个线程同时访问同一个 memory bank,硬件没法并行服务,只能把访问拆开串行执行,导致变慢。
优化
- 加 padding
- 调整访问模式,尽量让同一个 warp 内的线程访问不同 bank
occupancy
Occupancy在 GPU profiling 里一般指:一个计算单元上实际驻留的线程/warp/wave 数量,占硬件理论最大可驻留数量的比例。
occupancy 不是越高越好。
很多高性能 kernel 可能 occupancy 只有 30%~60%,但因为每个线程做了更多计算、数据复用更好、寄存器 tile 更大,反而更快。
asynchronous copy
Asynchronous copy指的是:把数据从一块内存搬到另一块内存时,发起 copy 后不立刻等待它完成,而是让计算继续执行,等真正需要数据时再同步。
比如做矩阵乘法、卷积、attention 时,经常要先把一块 tile 从 global memory 搬到 shared memory,再用这块 tile 做计算。
核心收益是:把访存延迟藏到计算后面
什么是 epilogue fusion
epilogue fusion 一般指在 GEMM / matmul / conv 这类主计算 kernel 的“收尾阶段”直接融合后处理操作。比如:bias、activation、scale、cast、quantize 等。
这样做的好处是:避免中间结果写入 global memory 再读回来,也减少额外 kernel launch 开销。
Tensor Core
Tensor Core 是专门加速矩阵乘累加的硬件单元,可以在一个指令中完成小矩阵 MMA 操作,比如 FP16 输入、FP32 accumulate。深度学习中 GEMM 和 convolution 都可以映射到 MMA,所以 Tensor Core 是 cuDNN 性能优化的核心之一。
GEMM
优化
naive matmul 每个线程计算一个 C 元素,重复从 global memory 读取 A 和 B,memory bandwidth 浪费严重。优化方式是按 tile 把 A、B 加载到 shared memory,让 block 内线程复用数据,再进一步用 register tiling、warp-level tiling 和 tensor core 提升计算效率。
small GEMM 为什么难优化
核心原因是:矩阵太小,GPU 还没来得及把算力吃满,开销和访存就已经占了大头。
batched GEMM
batched GEMM 指的是一次 kernel 调用里计算一批形状相同或规则相同的 GEMM。
A: [B, M, K] B: [B, K, N] C: [B, M, N]grouped GEMM
grouped GEMM指一次 kernel 调用里计算一组形状可能不同的 GEMM。
GEMM 0: [16, 64] @ [64, 32] GEMM 1: [32, 64] @ [64, 16] GEMM 2: [8, 128] @ [128, 64] GEMM 3: [64, 32] @ [32, 32]grouped GEMM 的难点是:不同 GEMM 的 tile 数量不同,不能简单按 batch index 平均分配工作。
更合理的是把每个 GEMM 拆成 tiles,然后全局调度
Convolution
优化
卷积本质上可以转化为矩阵乘。常见实现包括 direct convolution、im2col + GEMM、implicit GEMM、Winograd 和 FFT。实际 cuDNN 会根据 shape、数据类型、layout、hardware 选择不同 algorithm。
输出 shape 计算
Conv2D
Input: [N, C_in, H_in, W_in] Weight: [C_out, C_in/groups, K_h, K_w] Output: [N, C_out, H_out, W_out]H_out = floor((H_in + 2 * P_h - D_h * (K_h - 1) - 1) / S_h + 1) W_out = floor((W_in + 2 * P_w - D_w * (K_w - 1) - 1) / S_w + 1)Conv3D
Input: [N, C_in, D_in, H_in, W_in] Weight: [C_out, C_in/groups, K_d, K_h, K_w] Output: [N, C_out, D_out, H_out, W_out]D_out = floor((D_in + 2 * P_d - Dilation_d * (K_d - 1) - 1) / S_d + 1) H_out = floor((H_in + 2 * P_h - Dilation_h * (K_h - 1) - 1) / S_h + 1) W_out = floor((W_in + 2 * P_w - Dilation_w * (K_w - 1) - 1) / S_w + 1)groups 对输出空间 shape 的影响
groups不影响 H/W/D 的输出尺寸,只影响通道连接方式和 weight shape
C_in % groups == 0 C_out % groups == 0implicit GEMM
implicit GEMM通常指:把卷积 Conv 看成 GEMM 来算,但不真的显式生成 im2col 矩阵。
1×1 conv
1×1 conv 本质上就是对每个空间位置做一次矩阵乘法,所以优化重点通常不是卷积窗口,而是GEMM / layout / memory access / fusion。
X: [N, C_in, H, W] W: [C_out, C_in, 1, 1] Y: [N, C_out, H, W]Y[n, :, h, w] = W[:, :] × X[n, :, h, w]更适合 NHWC
优先转成 GEMM
最常见优化是直接走高性能 GEMM
避免 im2col
可能会产生不必要的 memory copy。
Depthwise conv
Depthwise convolution是一种“每个输入通道单独做卷积”的卷积方式。
普通卷积会同时混合空间信息和通道信息;Depthwise conv 只做每个通道内部的空间卷积,不做通道之间的混合。
实际模型里经常和1×1 conv搭配,这里的1×1 conv 就是为了混合不同 channel 信息
Attention
Self-Attention 复杂度
时间复杂度:O(n² · d) 空间复杂度:O(n²) n = sequence length d = hidden sizeMulti-Head Attention
Multi-Head Attention就是把一次 attention 拆成多组小 attention 并行做,每一组叫一个head。
单头 attention 只用一种方式看上下文;多头 attention 允许模型从多个角度同时看上下文。
好处
- 表达能力更强,不同 head 可以学习不同类型的 token 关系。
- 更适合并行,多个 head 可以并行计算,GPU 上通常会把它们 batch 到一起做矩阵乘法。
GQA 与 MQA
标准 MHA,每个 Q head 都对应自己的一组 K/V head。
MQA = 多个 Q head 共享同一组 K/V head。
GQA = 把多个 Q head 分成若干组,每组共享一组 K/V head。
FlashAttention
FlashAttention 快的核心原因:不是减少了 Self-Attention 的理论计算量,而是大幅减少了 HBM/global memory 读写。
标准 attention 里会显式生成一个很大的 attention matrix:
S = QKᵀ // [seq_len, seq_len] P = softmax(S) // [seq_len, seq_len] O = P V把 Q / K / V 切成 block 每次只算一小块 QKᵀ 立刻做局部 softmax 统计 立刻乘 V 累加到输出 O 丢掉当前 attention block 继续处理下一块C++
vector 与 list 区别
| 特性 | vector | list |
|---|---|---|
| 底层结构 | 连续数组 | 双向链表 |
| 随机访问 | O(1) | O(n) |
| 尾部插入 | 平均 O(1) | O(1) |
| 中间插入/删除 | O(n) | 已知位置 O(1) |
| 遍历性能 | 通常很快 | 通常较慢 |
| cache locality | 好 | 差 |
| 内存开销 | 小 | 大 |
| iterator 稳定性 | 较差 | 较好 |
为什么 list 内存开销大
list内存开销大,主要因为它不是只存数据,而是每个元素都要单独包装成一个节点 node。
unordered_map和map区别
| 维度 | std::map | std::unordered_map |
|---|---|---|
| 底层结构 | 红黑树 / 平衡二叉搜索树 | 哈希表 |
| 元素顺序 | 按 key 自动排序 | 无序,遍历顺序不保证 |
| 查找复杂度 | O(log n) | 平均O(1),最坏O(n) |
| 插入复杂度 | O(log n) | 平均O(1),最坏O(n) |
| 删除复杂度 | O(log n) | 平均O(1),最坏O(n) |
是否支持lower_bound/upper_bound | 支持 | 不支持有序意义上的范围查询 |
| 是否适合范围查询 | 适合 | 不适合 |
| 性能稳定性 | 稳定,基本一直是O(log n) | 依赖 hash 质量,冲突多会退化 |
| 内存开销 | 每个节点有父/左/右指针、颜色位等 | 有 bucket 数组 + 节点/链表开销 |
| cache locality | 一般,树节点分散 | 一般,也可能分散 |
| 典型用途 | 排序、区间查询、找最近 key | 词频统计、缓存、ID 映射、快速查找 |
| 遍历结果 | key 从小到大 | 不确定顺序 |
| 自定义 key 要求 | 需要能比较大小,比如operator<或 comparator | 需要 hash 函数和相等判断,比如hash和== |
匿名函数
C++ 匿名函数就是lambda
[capture](parameters) -> return_type { // body };捕获方式主要写在[]里。
不捕获的话,lambda 内部不能使用外部局部变量。
值捕获:[x]
int x = 10; auto f = [x]() { return x; }; x = 20; std::cout << f() << std::endl; // 10[x]表示把x的值拷贝进 lambda 对象里。
注意:捕获发生在 lambda 创建时,不是在调用时。
值捕获默认不能修改
如果想修改 lambda 内部那份拷贝,要加mutable:
int x = 10; auto f = [x]() mutable { x++; return x; }; std::cout << f() << std::endl; // 11 std::cout << x << std::endl; // 10,外部 x 不变引用捕获:[&x]
int x = 10; auto f = [&x]() { return x; }; x = 20; std::cout << f() << std::endl; // 20[&x]表示 lambda 内部引用外部的x。要注意生命周期
默认值捕获:[=]
int a = 1; int b = 2; auto f = [=]() { return a + b; };[=]表示:lambda 里用到的外部局部变量,默认按值捕获。
默认引用捕获:[&]
int a = 1; int b = 2; auto f = [&]() { a++; b++; };[&]表示:lambda 里用到的外部局部变量,默认按引用捕获。
this捕获
在成员函数里,lambda 如果要访问成员变量,通常会捕获this。
class Foo { private: int x_ = 10; public: void run() { auto f = [this]() { return x_; }; } };