CUDA 优化核心CUDA 优化核心是让计算单元尽量忙起来同时减少 memory stall。通常从 memory coalescing、shared memory reuse、减少 bank conflict、提高 occupancy、减少分支发散、合理使用 vectorized load/store 和 asynchronous copy 等方向优化。memory stallMemory stall指的是 CPU/GPU/加速器在执行程序时因为等待内存数据返回而被迫停下来不能继续执行指令的现象。常见原因Cache miss数据不在 L1/L2/L3 cache需要访问主存。内存带宽不足多个核心、线程或算子同时访问内存带宽被打满。访存不连续随机访问、stride 太大、数据 layout 不合理导致 cache 利用率差。优化方向提高 cache locality比如让访问更连续减少重复访存比如复用中间结果改数据布局比如 NHWC / NCHW / blocking对 GPU优化 shared/local memory 使用减少 global memory 访问Memory coalescingMemory coalescing指的是 GPU 里多个线程访问内存时硬件把这些访问合并成更少、更大的连续内存事务从而提高访存效率。也就是说一组线程一起读写连续地址硬件可以一次性批量搬数据如果地址很散就只能拆成很多次访问性能会差很多。bank conflictBank conflict通常指 GPU 的shared memory / local memory访问冲突多个线程同时访问同一个 memory bank硬件没法并行服务只能把访问拆开串行执行导致变慢。优化加 padding调整访问模式尽量让同一个 warp 内的线程访问不同 bankoccupancyOccupancy在 GPU profiling 里一般指一个计算单元上实际驻留的线程/warp/wave 数量占硬件理论最大可驻留数量的比例。occupancy 不是越高越好。很多高性能 kernel 可能 occupancy 只有 30%60%但因为每个线程做了更多计算、数据复用更好、寄存器 tile 更大反而更快。asynchronous copyAsynchronous copy指的是把数据从一块内存搬到另一块内存时发起 copy 后不立刻等待它完成而是让计算继续执行等真正需要数据时再同步。比如做矩阵乘法、卷积、attention 时经常要先把一块 tile 从 global memory 搬到 shared memory再用这块 tile 做计算。核心收益是把访存延迟藏到计算后面什么是 epilogue fusionepilogue fusion 一般指在 GEMM / matmul / conv 这类主计算 kernel 的“收尾阶段”直接融合后处理操作。比如bias、activation、scale、cast、quantize 等。这样做的好处是避免中间结果写入 global memory 再读回来也减少额外 kernel launch 开销。Tensor CoreTensor Core 是专门加速矩阵乘累加的硬件单元可以在一个指令中完成小矩阵 MMA 操作比如 FP16 输入、FP32 accumulate。深度学习中 GEMM 和 convolution 都可以映射到 MMA所以 Tensor Core 是 cuDNN 性能优化的核心之一。GEMM优化naive matmul 每个线程计算一个 C 元素重复从 global memory 读取 A 和 Bmemory bandwidth 浪费严重。优化方式是按 tile 把 A、B 加载到 shared memory让 block 内线程复用数据再进一步用 register tiling、warp-level tiling 和 tensor core 提升计算效率。small GEMM 为什么难优化核心原因是矩阵太小GPU 还没来得及把算力吃满开销和访存就已经占了大头。batched GEMMbatched GEMM 指的是一次 kernel 调用里计算一批形状相同或规则相同的 GEMM。A: [B, M, K] B: [B, K, N] C: [B, M, N]grouped GEMMgrouped 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 计算Conv2DInput: [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)Conv3DInput: [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 shapeC_in % groups 0 C_out % groups 0implicit GEMMimplicit GEMM通常指把卷积 Conv 看成 GEMM 来算但不真的显式生成 im2col 矩阵。1×1 conv1×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 convDepthwise convolution是一种“每个输入通道单独做卷积”的卷积方式。普通卷积会同时混合空间信息和通道信息Depthwise conv 只做每个通道内部的空间卷积不做通道之间的混合。实际模型里经常和1×1 conv搭配这里的1×1 conv 就是为了混合不同 channel 信息AttentionSelf-Attention 复杂度时间复杂度O(n² · d) 空间复杂度O(n²) n sequence length d hidden sizeMulti-Head AttentionMulti-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。FlashAttentionFlashAttention 快的核心原因不是减少了 Self-Attention 的理论计算量而是大幅减少了 HBM/global memory 读写。标准 attention 里会显式生成一个很大的 attention matrixS 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 继续处理下一块Cvector 与 list 区别特性vectorlist底层结构连续数组双向链表随机访问O(1)O(n)尾部插入平均 O(1)O(1)中间插入/删除O(n)已知位置 O(1)遍历性能通常很快通常较慢cache locality好差内存开销小大iterator 稳定性较差较好为什么 list 内存开销大list内存开销大主要因为它不是只存数据而是每个元素都要单独包装成一个节点 node。unordered_map和map区别维度std::mapstd::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 内部那份拷贝要加mutableint 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_; }; } };