NVlink为什么那么快?你知道PCIe和NVlink的区别吗?
"A CPU is a sprinter; a GPU is a marching band." —— 某 NVIDIA 工程师的内部比喻
你看 H100 的规格表:16,896 个 CUDA Core,再乘上 Tensor Core 的加成,算力高达 989 TFLOPS。脑子里可能出现了一个画面:将近两万个处理器同时为你效命,每个都在飞速处理数据。
这个画面有一半是对的。但另一半,可能会让你对 GPU 做出一些根本性的误判。
CPU 的 8 个核心和 GPU 的 16,896 个核心,是两种截然不同的东西。理解这个差异,是理解所有 AI 系统性能问题的前提。
2.1 CPU:为低延迟设计的复杂控制者
CPU 的设计目标只有一个:让单条指令尽可能快地跑完。为了实现这个目标,工程师们在一块指甲盖大小的芯片里,塞入了极其复杂的控制逻辑。
流水线(Pipeline):一条装配线
在数学上,a = b + c 是一步操作。但 CPU 内部,这行代码被拆成了多个微步骤:

图:左侧"无流水线"——4 条指令严格串行,每条占 4 个周期,共需 16 周期。右侧"有流水线"——4 条指令错位交叠,只需 7 个周期;从 T4 起,每个时钟周期都有一条指令完成(红色箭头标出)。延迟没变,但吞吐量提升了 2.3 倍。
流水线的关键洞察:延迟(Latency)没有变短,但吞吐量(Throughput)提高了。单条指令还是要走完 4 个阶段,但多条指令可以像工厂装配线一样流水作业,平均每个周期产出一条结果。
流水线最怕两件事:
- 数据依赖(Data Dependency):
b = a + 1必须等a = ...算完才能开始,流水线被迫停顿(Stall)。 - 分支跳转(Branch):遇到
if-else,CPU 不知道该去取哪条路的指令——这就引出了下面的机制。
分支预测(Branch Prediction):CPU 的赌博
遇到 if (x > 0) 时,CPU 有两个选择:
- 等:等算出
x > 0再决定取哪条路的指令。但这样流水线就空转了。 - 猜:根据历史记录,预测你会走哪条路,提前把那边的指令塞进流水线。
现代 CPU 选择猜,而且猜对率高达 95% 以上。
- 猜对:流水线满载运行,性能飙升。
- 猜错:Pipeline Flush——已经提前跑的指令全部作废,清空流水线重来,代价是几十个时钟周期。
科普:这就是著名的"为什么处理排好序的数组比未排序的快"的根本原因。排序后数组的
if (a[i] > 0)结果先是一长串false再是一长串true,分支预测器轻松猜中;而随机数据让预测器每次都在掷硬币。
超标量(Superscalar)与 SIMD
现代 CPU 在流水线基础上再叠了两层加速:
- 超标量:一个 CPU 核心内部有多条流水线,每个时钟周期能同时发射多条独立指令。你的 8 核 CPU 实际上远不止 8 条流水线。
- SIMD(AVX/NEON):CPU 拥有 256-bit(AVX2)甚至 512-bit(AVX-512)的超宽寄存器。一条
vaddps指令,把 8 对 float32 数字同时相加,而不是一对一对地加。
NumPy 的速度秘密之一就在这里:a + b 底层调用的是 SIMD 指令。你写 Python for 循环,就是放弃了 SIMD,回到了一对一对地加的标量模式。
2.2 GPU:为高吞吐设计的暴力美学
看一眼 CPU 和 GPU 的芯片面积分配,两种设计哲学一目了然:

图:CPU 把大量面积留给控制逻辑(分支预测、乱序执行引擎)和 Cache;GPU 把 70% 的面积给了计算单元,控制逻辑极度精简。
CPU 花大量晶体管造"聪明的大脑",让单线程跑得尽量快;GPU 砍掉大部分控制逻辑,用这些晶体管堆出数以万计的简单计算核心。
SIMT:一声令下,万核齐动
GPU 的执行模型叫 SIMT(Single Instruction, Multiple Threads)。
想象一个方阵:指挥官(控制单元)喊"向前走!",所有士兵(线程)同时迈步。GPU 的线程调度就是这样工作的。
GPU 线程的组织层次:
- Thread(线程):最小执行单位,对应你 CUDA Kernel 里的一个
threadIdx。 - Warp(线程束):32 个线程组成一个 Warp,这是硬件调度的最小粒度。同一个 Warp 里的 32 个线程,在同一时刻执行同一条指令。
- Block(线程块):若干 Warp 组成一个 Block,共享同一块 Shared Memory。
- Grid(网格):所有 Block 组成 Grid,对应一次 Kernel 启动。
Warp Divergence:GPU 最怕 if-else
既然 32 个线程必须同时执行同一条指令,那写了 if-else 会怎样?
# CUDA Kernel 里的伪代码
if thread_id < 16:
do_A() # 前 16 个线程走这里
else:
do_B() # 后 16 个线程走这里
GPU 的处理方式:
- 执行
do_A()——前 16 个线程干活,后 16 个线程被强制屏蔽(Masked Off),发呆等待。 - 执行
do_B()——后 16 个线程干活,前 16 个线程发呆等待。
总耗时 = do_A() 的时间 + do_B() 的时间,硬件利用率直接砍半。这就是 Warp Divergence(线程束发散)。
AI 启示:这解释了为什么神经网络算子几乎不用条件判断,而是用数学手段代替。
ReLU(x)不写成if x > 0: return x else: return 0,而是max(0, x)——纯粹的数学运算,所有线程走同一条路,没有 Divergence。Dropout用掩码矩阵乘以 0,而不是跳过某些神经元。
常见误区:很多人以为 GPU 的 16,896 个 CUDA Core 是 16,896 个独立的"小 CPU",可以各自执行不同代码。实际上它们被分成若干个 Warp,每个 Warp 内的 32 个线程必须步调一致。GPU 擅长的是"同一段代码,作用于海量不同数据",而不是"海量不同代码并行跑"。
Tensor Core:为矩阵乘法造的专用硬件
普通 CUDA Core 是通用计算单元,能做加减乘除、三角函数等任意运算。但深度学习 99% 的计算量是矩阵乘加:。
NVIDIA 从 Volta 架构(V100)开始引入 Tensor Core:一个在单个时钟周期内完成 矩阵乘加的专用硬件电路,吞吐量比普通 CUDA Core 高出数倍到数十倍。
使用 Tensor Core 的条件:
| 条件 | 说明 |
|---|---|
| 数据类型 | FP16、BF16、TF32、FP8、INT8 |
| 矩阵维度 | 必须是 8 或 16 的倍数(取决于精度和架构) |
| 内存对齐 | 数据在内存中需要连续且对齐 |
TF32(TensorFloat-32) 是 Ampere(A100)架构引入的一种特殊格式:它使用 FP32 的指数位(8 bit)保留动态范围,但只保留 FP16 的尾数位精度(10 bit),共 19 bit。矩阵乘法的输入输出仍然是 FP32,但在 Tensor Core 内部计算时以 TF32 精度处理——对用户完全透明,无需改代码,精度损失通常可忽略(神经网络对精度鲁棒),速度却接近 FP16。
# A100/H100 默认开启 TF32 加速 FP32 矩阵乘法
# 如果显式关闭过,可以这样重新开启:
torch.backends.cuda.matmul.allow_tf32 = True # 矩阵乘法
torch.backends.cudnn.allow_tf32 = True # 卷积
"不支持 FP32 直接加速"的说法特指原生 FP32(32 bit 全精度)——直接把 FP32 张量送进 Tensor Core 必须先转换。TF32 正是这个"桥梁":外部接口 FP32,内部计算 TF32 精度。
这就是为什么你会在各种 AI 工程建议里看到"batch size 设成 8 的倍数""hidden dim 设成 64 的倍数"——不是玄学,是为了命中 Tensor Core。
动手试试:检查你的 PyTorch 是否真的在用 Tensor Core:
import torch # 开启 TF32(A100/H100 自动使用 Tensor Core 加速 FP32 矩阵乘法) torch.backends.cuda.matmul.allow_tf32 = True torch.backends.cudnn.allow_tf32 = True # 验证矩阵尺寸对齐(维度为 8 的倍数时才能命中 Tensor Core) a = torch.randn(1024, 1024, device='cuda', dtype=torch.float16) b = torch.randn(1024, 1024, device='cuda', dtype=torch.float16) c = torch.matmul(a, b) # 这会走 Tensor Core
2.3 CPU 与 GPU 的互联
CPU 和 GPU 并不是一体的,它们通过总线连接,数据必须在两者之间搬运。

图:CPU 通过 PCIe 连接 GPU(128 GB/s),GPU 之间通过 NVLink 直连(900 GB/s)。PCIe 是明显的瓶颈。
PCIe:细水管
CPU(Host)和 GPU(Device)之间通过 PCIe 总线连接:
| 规格 | 带宽 |
|---|---|
| PCIe 4.0 x16 | ~64 GB/s |
| PCIe 5.0 x16 | ~128 GB/s |
| H100 HBM3(内部) | 3,350 GB/s |
GPU 内部带宽是 PCIe 的 26 倍。 每次 tensor.cpu() 或 tensor.cuda() 都要过这根细水管。
# ❌ 常见性能杀手:训练循环里频繁 CPU↔GPU 传输
for step, (x, y) in enumerate(dataloader):
loss = model(x.cuda())
print(loss.item()) # .item() 触发 GPU→CPU 同步传输!
writer.add_scalar(loss.item(), step) # 每步都在传
# ✅ 正确做法:在 GPU 上积累,批量传输
losses = []
for step, (x, y) in enumerate(dataloader):
loss = model(x.cuda())
losses.append(loss.detach()) # 保留在 GPU
if step % 100 == 0:
print(torch.stack(losses).mean().item()) # 每 100 步才传一次
原则:数据上了 GPU 就别让它轻易下来。
NVLink / NVSwitch:GPU 间的宽带公路
单张 GPU 装不下大模型时,需要多卡协同。如果 GPU 之间也走 PCIe,带宽只有 128 GB/s,完全撑不起分布式训练的梯度同步需求。
NVLink 是 NVIDIA 专有的 GPU 互联技术,让 GPU 之间绕过 CPU 直接通信:
| 技术 | 带宽 | 典型场景 |
|---|---|---|
| PCIe 5.0 | 128 GB/s | CPU↔GPU 数据传输 |
| NVLink 4.0(H100) | 900 GB/s | GPU↔GPU 梯度同步 |
| NVSwitch(NVL8/NVL16) | 全带宽互联 | 8/16 卡全连接 |
NVSwitch 进一步让机箱内所有 GPU 实现全连接(每对 GPU 之间都有直连通路),8 张 H100 在实践中可以当作一块拥有 640 GB 显存的"超级 GPU"来用。
科普:8 卡以内靠 NVSwitch 全连接;跨节点(机箱之间)就只能走 InfiniBand 网络了,速度下降一个数量级,这就是为什么分布式训练要仔细区分 intra-node(节点内)和 inter-node(节点间)通信——我们会在第六部分详细讨论。
核心结论:CPU 和 GPU 的区别不是"快"和"慢",而是两种截然不同的设计哲学——CPU 追求单线程的极速响应,GPU 追求海量线程的并行吞吐。在 AI 系统里,CPU 负责逻辑调度,GPU 负责数值计算;PCIe 是两者之间不可忽视的带宽瓶颈,NVLink 则是多 GPU 协同的基础设施。