matmul 复杂度多少?tiling 怎么做?
CPU 和 GPU 算 matmul 有什么区别?roofline 怎么用?

英文问法What's the complexity of matmul, and how do you optimize it? · How does matmul differ on a CPU and a GPU? · Explain the roofline model.

考过NVIDIA DevTech NG 2026(matmul 复杂度多少?过程 优化? tilling咋做的、CPU GPU算matmul的区别,1p3a 1161658)· NVIDIA GPU 性能岗(making a GEMM then explaining how to optimize the heck out of it for throughput, cache, and compiler on CPU/GPU,Reddit)· NVIDIA 中国 DevTech(手写 CUDA reduce、matmul,写完分析怎么继续优化)

先弄清楚:每个概念是什么

FLOPs:为什么是 2MNK
C 有 \(M\times N\) 个元素,每个是长度 \(K\) 的 dot product,\(K\) 次乘 + \(K\) 次加。例:\(M=N=K=4096\) → \(2\cdot 4096^3\approx 1.37\times10^{11}\) FLOPs。
Arithmetic intensity
每从 HBM 搬 1 byte 做几次 FLOP(基础见 GPU 与 NumPy)。matmul 数据是 \(n^2\) 级、计算是 \(n^3\) 级,本来很高。但 naive kernel 每次乘加都去 HBM 现读两个数,FP32 只有 0.25 FLOP/B。
Tiling
把 C 切成 \(T\times T\) 小块,一个 thread block 负责一块。沿 K 每次把 A、B 各一个 tile 搬进 shared memory,里面每个数被用 \(T\) 次。例:FP32、\(T=32\):intensity 从 0.25 涨到 8 FLOP/B。
Roofline 与 ridge point
性能上限 = min(峰值算力, intensity × bandwidth)。先是一条斜线(memory-bound),再是一条平线(compute-bound),拐点叫 ridge point,intensity 在它左边时加算力没用。
Tensor Core
专做小矩阵块乘加的单元,FP16 / BF16 输入、FP32 累加。例:A100 的 BF16 Tensor Core 312 TFLOPS,普通 FP32 只有 19.5 TFLOPS。elementwise 运算用不上它。

看图:roofline 上谁卡在哪,tiling 怎么搬数据

0.1110100100010⁴0.11101001000可达性能(TFLOPS,对数刻度)arithmetic intensity I(FLOP/B,对数刻度)ridge ≈ 153峰值 312 TFLOPS(BF16)I × 2,039 GB/s(HBM)memory-boundcompute-boundelementwise add1/6 FLOP/B → 0.34 TFLOPSGEMV(decode)≈ 1 FLOP/B → 2.0 TFLOPS4096³ GEMM≈ 1365 FLOP/B
图 1:A100 的 roofline。斜线(橙红)是 HBM bandwidth × intensity,平线(蓝)是 Tensor Core 峰值,交点是 ridge point。三个点是下面表里的操作。
shared memorySM 上的 SRAMAsBsA:M × KB:K × NC:M × Nacc搬进搬进B 的列 stripk 往下走A 的行 strip:k 往右走C tile(T × T)一个 thread block 负责acc 在 register 里累加
图 2:一个 thread block 负责 C 的一个 T × T tile,要读 A 的一条行 strip 和 B 的一条列 strip(蓝色)。沿 K 每步把 A、B 各一个 tile(橙红)搬进 shared memory,乘积累加到 register 里的 acc。

30 秒回答

Multiplying M×K by K×N takes MNK multiply-adds, so 2MNK FLOPs, or O(n³). A naive kernel rereads operands from memory for every multiply-add, so it's memory-bound. With tiling, each thread block owns a tile of C, walks along K, stages tiles of A and B in shared memory and reuses each value T times, so intensity grows with T. Then come register tiling, vectorized loads, double buffering and Tensor Cores. CPUs do the same with cache blocking and SIMD; GPUs use thousands of threads and hide latency by switching warps. Roofline: attainable FLOPs = min(peak, intensity × bandwidth).

推导 / 关键公式

tiling 为什么有用

一个 \(T\times T\) 的 C tile 沿 K 每走一步:搬 A、B 各 \(T^2\) 个数,做 \(2T^3\) FLOPs。FP32(4 bytes):

\[ I_{\text{tile}} = \frac{2T^3}{4\cdot 2T^2} = \frac{T}{4} \]

\(T\) 受 shared memory、register 大小限制。tile 太大则块数太少,填不满所有 SM。

roofline

\[ P = \min\big(P_{\text{peak}},\ I\times BW\big) \] \[ I^{*} = P_{\text{peak}} / BW \]

整个 GEMM 理想情况下 A、B、C 各过一次 HBM(BF16,2 bytes):

\[ I = \frac{2MNK}{2\,(MK + KN + MN)} \]

手算例子:A100 上谁是 memory-bound

A100 80GB SXM:BF16 dense 312 TFLOPS,HBM 2,039 GB/s → ridge point \(I^{*}\approx 153\) FLOP/B。

操作(BF16)intensity上限瓶颈
\(4096^3\) GEMM≈ 1365312 TFLOPScompute
GEMV(decode)≈ 1≈ 2.0 TFLOPS(0.65%)memory
elementwise add1/6≈ 0.34 TFLOPSmemory → fusion

CPU vs GPU

CPUGPU
并行十来个强 core + SIMD(AVX)上千 thread,32 个一组成 warp
分块放哪A 块放 L2,B 的窄条放 L1,C 的小块放 registerblock tile 放 shared memory,thread tile 放 register
藏 latency大 cache + prefetchwarp 等 memory 时切另一个

手撕代码(CUDA,shared memory tiling)

#define T 32
__global__ void matmul(const float* A, const float* B, float* C,
                       int M, int N, int K) {
  __shared__ float As[T][T], Bs[T][T];
  int tx = threadIdx.x, ty = threadIdx.y;
  int r = blockIdx.y * T + ty, c = blockIdx.x * T + tx;
  float acc = 0.f;
  for (int k0 = 0; k0 < K; k0 += T) {    // 沿 K 一块一块走
    As[ty][tx] = (r < M && k0 + tx < K) ? A[r * K + k0 + tx] : 0.f;
    Bs[ty][tx] = (c < N && k0 + ty < K) ? B[(k0 + ty) * N + c] : 0.f;
    __syncthreads();                     // tile 搬完再算
    for (int k = 0; k < T; ++k)          // 每个数在 SRAM 里用 T 次
      acc += As[ty][k] * Bs[k][tx];
    __syncthreads();                     // 算完再搬下一块
  }
  if (r < M && c < N) C[r * N + c] = acc;
}

读 A、B 时相邻 thread 读相邻地址(见 coalescing)。

必答点

追问

shared memory tiling 之后还能怎么优化?

register tiling 让每个 thread 算一小块 C(如 8×8)。float4 一次读 16 bytes,double buffering 让下一块的搬运和这一块的计算 overlap。Tensor Core(WMMA / MMA)的 FP16 维度取 8 的倍数(A100 上最好 64)。

有比 \(O(n^3)\) 更快的算法吗?

Strassen 是 \(O(n^{\log_2 7})\approx O(n^{2.81})\),代价是很多额外的矩阵加法和临时矩阵。cuBLAS 的 GEMM 仍按 \(2MNK\) 计 FLOPs。

怎么判断一个 kernel 卡在哪?

算 achieved FLOP/s 和 achieved bandwidth 各占峰值多少,接近上限的那个就是瓶颈。Nsight Compute 能画出 kernel 在 roofline 上的位置。

来源NVIDIA Matrix Multiplication Background §2、§2.1–2.3 · NVIDIA GPU Performance Background §2、§4 · A100 datasheet(p.1)· Williams et al., CACM 2009(p.67)· Goto & van de Geijn 2008 §4、§6.1 · Nsight Compute Profiling Guide §2.9