第二章:硬件基础与计算模型——CPU/GPU/NPU架构差异、内存层次结构、计算与访存比

各位同学,咱们今天聊点硬核的。算子优化这事儿,说白了就是跟硬件打交道。你如果不了解CPU、GPU、NPU这三兄弟的脾气秉性,那优化起来就像闭着眼睛开车——能跑,但迟早要翻。

我个人习惯,在动手写任何算子之前,先花半小时把目标硬件的架构图翻出来看看。别嫌麻烦,这半小时能帮你省下后面几天的调试时间。好,咱们开始。

2.1 CPU:通用但“娇气”的调度大师

CPU的设计哲学是“什么都能干,但一次干好一件”。它的核心数不多,但每个核心都配备了完整的控制单元、缓存和强大的分支预测能力。

架构特点:

  • 控制单元占比高:大约40%的芯片面积用于控制逻辑和缓存。为什么?因为它要处理复杂的指令流、分支跳转、乱序执行。
  • 缓存层次深:L1(32KB左右)、L2(256KB-1MB)、L3(8MB-30MB)。每一级延迟差异巨大,L1访问只要1ns,L3可能要10-20ns。
  • SIMD向量化:AVX-512、SSE等指令集,一次能处理8个float或16个int。但说实话,编译器自动向量化经常翻车,我建议你手写intrinsic。

核心指标:计算与访存比

CPU的算力(FLOPS)和内存带宽(GB/s)的比值通常较低。举个例子,一颗Intel Xeon Platinum 8380,双精度算力约2.3 TFLOPS,内存带宽约200 GB/s。算一下:2.3T / 200G ≈ 11.5。这意味着每从内存读一个float(4字节),你只能做11.5次浮点运算。如果算子计算密度低于这个比值,就会变成“访存瓶颈”。

我的经验:在CPU上优化矩阵乘法时,我遇到过一个问题:明明算力够,但跑起来就是慢。后来用perf工具一查,发现L1 cache miss率高达40%。原因是我把大矩阵直接塞进了循环,没有做分块(tiling)。改成16x16的小块后,性能直接翻倍。记住:CPU怕的是“数据喂不饱计算单元”。

2.2 GPU:并行怪兽,但怕“分支”和“小批量”

GPU的设计哲学是“简单的事情重复做,做一万遍”。它有成百上千个流处理器(SM/CU),每个核心都很“笨”,但堆数量。

架构特点:

  • SIMT执行模型:32个线程组成一个warp,它们必须执行同一条指令。如果遇到if-else分支,一半线程干活,另一半闲着——这就是所谓的“warp divergence”。
  • 内存层次更复杂:全局显存(HBM2e/HBM3,带宽可达2TB/s)、L2缓存(几MB)、共享内存(每个SM几十KB到上百KB)、寄存器。共享内存是程序员可以手动控制的“片上SRAM”,延迟极低(约5ns),但容量有限。
  • 计算与访存比更高:以NVIDIA A100为例,FP16 Tensor Core算力约312 TFLOPS,显存带宽约2 TB/s。比值:312T / 2T ≈ 156。比CPU高了一个数量级。

避坑指南:我曾经在优化一个卷积算子时,天真地把所有数据都放在全局显存里,结果kernel跑起来只有理论峰值的5%。后来把输入特征图和权重分批加载到共享内存,再用寄存器缓存中间结果,性能直接拉到60%。GPU优化的核心就一句话:让数据尽可能靠近计算单元。

内存层次结构对比(GPU视角):

层级 容量 延迟 带宽 程序员控制
全局显存 40-80 GB ~400 ns ~2 TB/s 显式分配/释放
L2缓存 40 MB ~200 ns ~4 TB/s 透明
共享内存 48-164 KB/SM ~5 ns ~20 TB/s 显式声明
寄存器 256 KB/SM ~1 ns 极高 编译器分配

2.3 NPU:为AI而生的“专用加速器”

NPU(比如华为昇腾、Google TPU)是专门为神经网络设计的。它放弃了通用性,换来了极致的能效比。

架构特点:

  • 脉动阵列(Systolic Array):这是NPU的灵魂。一个二维的乘法器阵列,数据像波浪一样在阵列中流动,每个时钟周期完成一次矩阵乘法。以昇腾910为例,它的Cube单元是16x16x16的3D阵列,一次完成4096个MAC操作。
  • 片上缓存巨大:NPU通常有几十MB的片上SRAM(比如TPU v4有144MB),可以放下整个模型的权重。这意味着很多情况下不需要频繁访问片外DRAM。
  • 计算与访存比极高:NPU的算力带宽比可以轻松超过1000。以昇腾910为例,FP16算力约256 TFLOPS,HBM带宽约1.2 TB/s,比值约213。但实际中,由于脉动阵列的利用率问题,这个比值会打折扣。

关键差异:CPU是“冯·诺依曼”架构,指令和数据分开存储;GPU是“SIMT”架构,靠大量线程隐藏延迟;NPU是“数据流”架构,计算由数据驱动。你想想看,这三种架构对算子优化的要求完全不同。

2.4 计算与访存比:算子优化的“罗盘”

这个比值(Arithmetic Intensity,简称AI)决定了你的算子是“计算密集型”还是“访存密集型”。

怎么算?

Arithmetic Intensity = 总计算量(FLOPs) / 总数据访问量(Bytes)

举个例子,一个矩阵乘法 C = A * B,A是MxK,B是KxN,C是MxN。

  • 计算量:2 * M * N * K(乘加各算一次)
  • 访存量:M*K + K*N + M*N(读A、B,写C),每个元素4字节(FP32)
  • AI = (2*M*N*K) / (4*(M*K + K*N + M*N))

当M=N=K=1024时,AI ≈ 170。这个值远高于CPU的11.5,但低于GPU的156。嗯,这里要注意:实际运行时,由于缓存命中率、带宽利用率等因素,有效AI会低于理论值。

我的判断方法:拿到一个算子,先算AI。如果AI < 10,那大概率是访存瓶颈,优化方向是减少数据搬运(比如算子融合、内存复用)。如果AI > 100,那就是计算瓶颈,优化方向是提高计算密度(比如向量化、循环展开、使用Tensor Core)。

2.5 一张图看懂三者的核心差异

下面这张SVG图,我画了三者的核心架构对比。你仔细看看,就能明白为什么同一个算子在不同硬件上表现天差地别。

CPU / GPU / NPU 架构核心差异 CPU 控制单元 (40%) 分支预测、乱序执行 L1/L2/L3 缓存 延迟 1ns ~ 20ns ALU + SIMD AVX-512, 8 float/cycle 核心数: 8~64 算力/带宽比: ~10 适合: 控制流复杂、小批量 GPU SM / CU (80个+) 每个SM含多个CUDA Core 共享内存 (48KB/SM) 程序员手动管理 Tensor Core 4x4x4 矩阵乘 (FP16) HBM2e 显存 (2TB/s) 核心数: 数千个 算力/带宽比: ~150 适合: 大规模并行、矩阵运算 NPU 脉动阵列 (Systolic Array) 16x16x16 3D MAC阵列 片上SRAM (几十MB) 可放下整个模型权重 数据流驱动 无指令取指开销 核心: 专用计算单元 算力/带宽比: >200 适合: 固定形状、高密度计算 从左到右:通用性递减,专用性递增;能效比递增,编程灵活性递减

2.6 实战中的“计算与访存比”应用

我记得有一次优化一个LayerNorm算子。原始实现是逐元素操作,每个元素都要读一次、写一次。算下来AI只有可怜的2.3。在GPU上跑,显存带宽利用率只有30%。

后来我做了两件事:

  1. 算子融合:把多个逐元素操作合并成一个kernel,减少中间结果的读写。
  2. 向量化访存:用float4一次性读4个元素,提高带宽利用率。

优化后AI提升到了8.5,带宽利用率到了70%,整体性能快了3倍。你看,有时候不需要动算法,把数据流理顺了,效果立竿见影。

曾经踩过的坑:我在一个项目里,为了追求极致的计算密度,把矩阵分块做得特别小(4x4)。结果呢?计算单元确实跑满了,但地址计算和循环开销占了总时间的40%。这就是典型的“过度优化”。记住:计算与访存比是一个指导指标,不是目标。最终要看端到端的延迟。

好了,这一章的内容就到这里。硬件基础是算子优化的地基,地基不稳,上层建筑再漂亮也白搭。下一章我们会深入具体的算子优化技巧,到时候这些硬件知识会反复用到。

想要更多资料,联系可可