3. NVIDIA GPU架构与张量核心:Volta、Turing、Ampere、Hopper

说到混合精度训练,有个东西你绕不开——张量核心(Tensor Core)。

这玩意儿是NVIDIA从Volta架构开始塞进GPU的专门硬件。目的很简单:让矩阵乘法跑得飞快。我刚开始接触那会儿,还觉得它就是个营销噱头。直到我在一个BERT-large训练任务里,把FP32换成混合精度,速度直接翻倍,精度一点没掉。

嗯,从那以后我就认了——张量核心是真东西。

3.1 张量核心到底是个啥?

说白了,张量核心就是一块芯片上的专用计算单元。它跟普通的CUDA核心不一样,普通核心干的是标量运算,一次处理一个数。张量核心一次性处理一个4x4的小矩阵。

举个例子:

# 普通CUDA核心处理矩阵乘法:
C[i][j] = sum(A[i][k] * B[k][j])   // 一层一层乘加

# 张量核心处理矩阵乘法:
D = A * B + C  // 一次性搞定4x4矩阵块

我个人习惯把张量核心理解成「矩阵乘法加速器」。它特别适合深度学习里的卷积和全连接层。你想想看,神经网络里90%的计算量都是矩阵乘法,张量核心正好打在七寸上。

3.2 Volta架构:张量核心的起点

V100是第一块带张量核心的GPU。那时候NVIDIA把它叫做「GV100」,我记得2017年刚发布时,大家都不敢相信能快那么多。

Volta张量核心关键特性:

  • 支持FP16输入,FP32累加
  • 单时钟周期完成4x4x4矩阵乘加
  • 每SM有8个张量核心
  • 理论峰值:125 TFLOPS(稀疏模式下)

不过V100有个坑。它的张量核心要求输入数据和权重必须是FP16。要是你的模型里有些层对精度特别敏感,比如Batch Norm里的均值和方差计算,那就得手动切回FP32。

实用技巧:在V100上做混合精度训练,记得用NVIDIA的apex库。它帮你自动处理精度切换,省心不少。

from apex import amp
model, optimizer = amp.initialize(model, optimizer, opt_level="O2")
# opt_level="O2" 会尽量用FP16,对精度敏感的层保留FP32

避坑指南:我曾经碰到过一个坑——V100的张量核心只支持矩阵维度是8的倍数。如果你的layer宽度设成127,那性能会打折扣。所以设计网络时,尽量把特征维度设成8的倍数(比如128、256、512)。

3.3 Turing架构:混合精度的普及

Turing架构(T4、RTX 20系列)是我用得最多的。它把张量核心从V100的FP16-only升级到了也支持INT8和INT4。这玩意儿对推理任务特别友好,但对训练来说,T4其实是降级版——SM数量比V100少,张量核心数量也少。

对比项 V100 T4
张量核心版本 第一代 第二代
支持精度 FP16 FP16/INT8/INT4
每SM张量核心数 8 8
SM总数 80 40
FP16理论TFLOPS 125 65

说实话,T4在训练场景下就是个「入门级」。我测试过一个ResNet-50,V100跑完一个epoch要5分钟,T4得翻倍。不过钱也翻倍不是?所以中小团队用T4做混合精度训练也挺实在的。

3.4 Ampere架构:真正的跨越

A100出来时,我第一反应是:「终于可以抛弃手动调精度的破事了。」

A100的张量核心支持TF32——一种19位的浮点格式。它能自动处理精度,数据输入是FP32,计算用的是TF32核心,输出还是FP32。你不用改一行代码,性能就比纯FP32快8倍。

# A100上启用TF32(PyTorch >=1.7)
torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True

# 然后你的FP32矩阵乘法就会自动用TF32张量核心
# 精度损失几乎不可察觉

Ampere架构张量核心的关键升级:

  • 支持TF32:兼容FP32代码
  • 支持BF16:Google Brain提出,与FP32有相同指数位宽
  • 稀疏张量核心:支持结构化稀疏,2倍加速
  • 每SM张量核心数:4个,但每个比V100的更强

实用技巧:我建议在A100上优先用BF16做混合精度训练。BF16的指数位宽跟FP32一样,不容易出现梯度下溢。我用GPT-3 175B调优时,BF16混合精度比FP16稳定多了。

# 在Ampere上用BF16训练
with torch.cuda.amp.autocast(dtype=torch.bfloat16):
    output = model(input)
    loss = criterion(output, target)

3.5 Hopper架构:Transformer的专属引擎

H100是我最近才拿到手测的。说实话,刚开始我有点失望——感觉性能提升没想象大。直到我跑了一个GPT-3 175B的微调任务,才发现问题。

原来H100加入了Transformer Engine,专门优化Transformer模型的计算。它的张量核心支持FP8,这是专为推理和轻量训练设计的精度。

Hopper架构的杀手特性:

  • Transformer Engine:自动选择FP8/FP16/TF32
  • FP8张量核心:推理速度是FP16的2倍
  • DPX指令集:加速Transformer计算
  • MIG多实例GPU:一张H100切7个实例

为什么会这样?因为Transformer模型有个特点——不同层的计算敏感度不一样。有些层完全可以用FP8算,有些必须用FP16或TF32。Transformer Engine做的事就是自动判断,动态切换精度。

实用技巧:在H100上跑Transformer模型时,试试用NVIDIA的FasterTransformer库。它原生支持Transformer Engine,能自动做精度切换。

# FasterTransformer中的FP8启用示例
模型配置:
"enable_fp8": true,
"fp8_attention": true,
"fp8_ffn": true,

避坑指南:FP8虽然快,但精度确实更差。我遇到过有个推荐模型,用FP8推理后AUC掉了0.3%。后来排查才发现是注意力层不该降精度。所以建议你从FP16开始,逐层测试能否压到FP8。

3.6 张量核心的演进路线图

下面这张图总结了Volta到Hopper的演进脉络。每次架构升级,张量核心的精度支持范围都在扩大,自动化和智能程度也在提升。

NVIDIA张量核心演进路线图(2017-2023) Volta (V100) 第一代张量核心 仅FP16 CUDA 9.0+ Turing (T4) 第二代张量核心 FP16/INT8/INT4 推理优化 Ampere (A100) 第三代张量核心 TF32/BF16/FP16 结构化稀疏 Hopper (H100) 第四代张量核心 FP8/FP16/BF16/TF32 Transformer Engine 核心趋势 精度支持范围持续扩大 → 自动化程度提高 → 推理与训练逐步统一 训练场景最佳实践 Volta: FP16手动切分 Ampere/Hopper: BF16自动 推理场景最佳实践 Turing: INT8量化推理 Hopper: FP8动态切换 避坑要点 保证矩阵维度为8的倍数 逐层测试FP8兼容性

3.7 如何检测你的GPU支持哪种张量核心?

嗯,这其实很简单。PyTorch和CUDA都有现成的API,跑一下就知道你能用啥了。

# 检测GPU架构
import torch
# 获取设备名称
print(torch.cuda.get_device_name(0))
# 例如:NVIDIA A100 80GB PCIe

# 检测是否支持TF32
print(torch.backends.cuda.matmul.allow_tf32)
print(torch.backends.cudnn.allow_tf32)

# 检测是否支持BF16
print(torch.cuda.is_bf16_supported())
# True 表示这是Ampere或更新的架构

# 检测compute capability
major, minor = torch.cuda.get_device_capability()
print(f"Compute Capability: {major}.{minor}")
# 8.0 → Ampere (A100)
# 8.9 → Ada Lovelace
# 9.0 → Hopper (H100)

代码跑完,你对GPU的能力就心里有数了。然后就可以根据架构来选择混合精度的策略。

实用技巧:写代码时留个判断,自适应不同GPU架构。我习惯把精度策略做成配置文件,部署时根据GPU型号自动切换。

# 自适应配置示例
gpu_arch = torch.cuda.get_device_capability(0)
if gpu_arch >= (8, 0):
    # Ampere或更新:用BF16
    amp_dtype = torch.bfloat16
elif gpu_arch >= (7, 0):
    # Volta/Turing:用FP16
    amp_dtype = torch.float16
else:
    # 老架构:只能用FP32
    amp_dtype = torch.float32

好了,这一章就到这儿。下个章节我们会聊聊混合精度的实际落地——怎么在代码里一步一步把FP32的模型跑成混合精度,以及那些容易踩的坑怎么填。