NVlink为什么那么快?你知道PCIe和NVlink的区别吗?

0 阅读9分钟

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 个阶段,但多条指令可以像工厂装配线一样流水作业,平均每个周期产出一条结果。

流水线最怕两件事:

  1. 数据依赖(Data Dependency)b = a + 1 必须等 a = ... 算完才能开始,流水线被迫停顿(Stall)。
  2. 分支跳转(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 的处理方式:

  1. 执行 do_A()——前 16 个线程干活,后 16 个线程被强制屏蔽(Masked Off),发呆等待
  2. 执行 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% 的计算量是矩阵乘加:D=A×B+CD = A \times B + C

NVIDIA 从 Volta 架构(V100)开始引入 Tensor Core:一个在单个时钟周期内完成 4×44 \times 4 矩阵乘加的专用硬件电路,吞吐量比普通 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.0128 GB/sCPU↔GPU 数据传输
NVLink 4.0(H100)900 GB/sGPU↔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 协同的基础设施。