第一次在本地显卡上跑一个像样的视觉模型时,我差点被一张报错截图劝退:CUDA out of memory。后来回头看,问题不在显卡太差,而在我根本没把“显存预算”当成一个工程指标来算。浪费了整整一晚上搞清楚单精度、半精度这些概念之后,我才发现同样一份训练任务,用FP32和用FP16去跑,显存占用可能差出一倍,训练速度也不在一个量级。
这篇我不会把精度格式讲成玄学,而是直接把账摊开:前半部分把单精度(FP32)、半精度(FP16)以及BF16这几个词背后的原理、适用边界、加速逻辑讲透,后半部分用智星云这类GPU租用平台走一遍完整实操——怎么选卡、怎么开实例、怎么跑通一个训练任务、怎么处理计费和存储。无论你是刚学深度学习的初学者,还是准备租GPU跑任务又不想在底层细节上交太多学费的实操党,这篇都能直接用上。
1. 精度格式背后:从FP32到FP16/BF16
1.1 一张表看懂FP64、FP32、FP16、BF16
计算机里的浮点数,无论精度高低,底层都是由三部分组成的:符号位、指数位、尾数位。一句话记住就是:范围看指数,精度看尾数。指数位越长,能表示的数越大;尾数位越长,同一数量级下能区分出来的细节越多。
| 格式 | 符号位 | 指数位 | 尾数位 | 表示范围(约) | 有效精度(十进制,约) |
|---|---|---|---|---|---|
| FP64 | 1 | 11 | 52 | ±1.8e308 | 15~17位 |
| FP32 | 1 | 8 | 23 | ±3.4e38 | 7位 |
| FP16 | 1 | 5 | 10 | ±65504 | 3位 |
| BF16 | 1 | 8 | 7 | ±3.4e38 | 2~3位 |
FP32就是我们常说的单精度,也是深度学习中几十年来的默认格式。FP16是半精度,只有FP32的一半位数,所以显存占用天然少一半。BF16是一种更特殊的格式,它的指数位跟FP32一样长,所以能表示的范围跟FP32几乎一致,但尾数位砍得更狠,有效精度只有大约2到3位十进制数。
用尺子来类比会直观很多:FP32像一把毫米刻度的尺子,量桌面、量细小差异都靠谱;FP16像一把厘米刻度的卷尺,量大致尺寸没问题,但要精确到毫米就力不从心;BF16则像一把只标到米刻度的皮尺,量马路的长度很方便,但要量房间里的家具尺寸就太粗了。
1.2 为什么深度学习基本只看FP32和FP16
你可能发现上面表格里还有FP64,但在深度学习里它几乎不出现。FP64主要用于科学计算、物理模拟这类对数值稳定性极其敏感的领域,代价是显存占用和计算量都翻倍。游戏和图形渲染一般走的是整数或专用浮点格式,这和模型训练是两个话题。
深度学习真正的主战场是FP32和FP16。传统训练流程里,模型权重默认用FP32存储和更新,前向推理和反向传播中涉及的大量中间张量才是精度格式发挥作用的地方。很多人以为“用FP16训练”就是把所有东西都换成FP16,这个理解是不完整的。成熟的做法叫混合精度训练,主权重保留FP32,计算过程中用FP16加速,这也是PyTorch里amp模块的基本逻辑。
1.3 精度格式在你的显存账单上怎么体现
为什么我们要关心这些细节?因为精度格式直接决定显存占用。我算一个常见的例子:一个10亿(1B)参数的模型,单参数用FP32占4字节,光模型权重就需要4GB;反向后产生的梯度又占4GB;如果用AdamW优化器,还要额外存一阶动量和二阶动量各4GB。仅仅是“权重+梯度+优化器状态”这三项,FP32下就已经是16GB。
换成混合精度之后,主权重通常还是要保留一份FP32副本,所以这三项没有显著减少。真正省下来的是训练过程中的中间激活和临时张量——这些在长序列、大batch size下往往是显存大头。所以,混合精度不是直接把参数砍半,而是把计算过程中那些“用完就丢”的数据大幅瘦身。对于推理场景就简单多了,模型可以直接以FP16格式存储和加载,权重从4GB变成2GB,部署成本和显存压力一起降下来。
1.4 一个来自实操的判断原则
看了这么多格式,可能有点晕。我给一个可以抄作业的结论:
- 训练阶段优先开混合精度,也就是FP32主权重+FP16计算。绝大多数CV、NLP模型都能直接受益。
- 如果训练过程中频繁出现Loss变成NaN、Inf,优先排查数值溢出,再考虑切到BF16,或让部分对精度敏感的模块回退FP32。
- 推理阶段能FP16就FP16,如果模型允许,甚至可以进一步做INT8量化,但一定要用验证集确认精度回退在可接受范围内。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么半精度能让训练变快:核心加速逻辑
2.1 显存带宽:深度学习里最容易忽视的瓶颈
很多人以为GPU性能只看算力单位,比如FP32能达到多少TFLOPS,但实际训练大模型时,最卡脖子的往往不是算力,而是显存带宽。GPU要反复读取权重、激活值、梯度,数据和计算单元之间的搬运速度跟不上,芯片再能算也是白搭。
FP16的核心红利就在这里:每个数字占2字节,是FP32的一半。带宽固定的情况下,同样一次搬运能搬两倍数量的数字。用生活化的说法,FP32像是卡车每次运一个集装箱,里面堆满了细节;FP16像是同一辆卡车每次能运两个集装箱,但每个箱子里装的东西比较稀疏。对于大批量矩阵运算来说,后者的整体吞吐明显更高。
这也是为什么很多显卡在FP16下的速率能远超FP32——不仅仅是精度格式变短了,还因为它的硬件路径从一开始就为这种更小的数据类型做了优化设计。
2.2 Tensor Core:专为半精度矩阵乘法设计的计算单元
提到半精度,就绕不开硬件里的Tensor Core。这是显卡厂商从Volta这一代架构开始引入的专用计算单元,专门为低精度、大块头的矩阵乘法服务。传统CUDA核心更适合通用计算,但深度学习里超过九成的计算都集中在矩阵乘法上,于是硬件厂商专门设计了一条“高速公路”。
Tensor Core一次可以直接算一个小矩阵的乘加融合,输入往往是FP16。这也是为什么很多消费级显卡标注的半精度算力比单精度高出一倍以上,游戏玩家感受不深,但深度学习用户很容易在跑混合精度训练时发现训练速度明显提升。没有Tensor Core的时代,半精度加速只能在软件层面抠数值,收益有限;有了这套硬件,混合精度才真正成为训练加速的默认选项。
2.3 混合精度不是单纯把精度砍半:Master Weight与Loss Scaling
既然FP16这么好,为什么不全部换成FP16?因为训练过程的稳定性扛不住。反向传播计算出来的梯度往往数值非常小,小到FP16的精度表示不了,会直接变成0。梯度变0,权重就永远不更新,模型彻底停摆。
混合精度方案解决这个问题靠两个关键机制。第一是Master Weight,也就是主权重始终以FP32保存和更新,FP16只是参与前向和反向计算的临时副本,每次更新时用FP32的数值来计算,不会让微小更新丢失。第二是Loss Scaling,在反向传播之前,把损失值先放大几百甚至几千倍,这样梯度也跟着放大,落到FP16能表示的安全范围内,等更新权重时再缩回来。
这两件事听着复杂,但在PyTorch里已经被封装成一个GradScaler,你不需要手动去乘那些放大系数。
2.4 用PyTorch AMP把迁移成本降到最低
实际写代码时,切换混合精度比你想象中简单。这里给出一段可以直接套用的核心代码:
python复制from torch.cuda.amp import autocast, GradScaler
model = model.cuda()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3)
scaler = GradScaler()
for inputs, labels in train_loader:
inputs, labels = inputs.cuda(), labels.cuda()
optimizer.zero_grad()
with autocast():
outputs = model(inputs)
loss = criterion(outputs, labels)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
改动只有三处:把前向计算和Loss计算包进autocast(),把loss.backward()换成scaler.scale(loss).backward(),把optimizer.step()换成scaler.step(optimizer)并加上scaler.update()。只要你的网络里没有太多自定义的、对精度极其敏感的算子,这几行改动就能直接跑起来。
用AMP跑之前,我的习惯是先在一个小数据集上做几十步实验,确认Loss曲线能正常下降,再正式开跑。如果发现用了Amp之后曲线不收敛,优先检查是不是scaler没有调用update(),这是一个非常容易踩的坑。
3. 半精度不是银弹:溢出、精度回退与验证流程
3.1 FP16动态范围的上限只有65504
FP16能表示的最大正数是65504。这个数在数学上看起来不小,但在深度学习里,一个注意力分数乘以一个很大的中间值,分分钟就能冲破65504变成Inf。一旦某个中间张量变成Inf,后续所有计算都会被污染,Loss直接变成NaN。
我自己遇到过一种典型情况:某一个视觉模型用FP16的Amp训练时,前几步一切正常,跑到某个特定batch突然Loss变成NaN。排查了很久才发现,模型里某个自定义注意力模块在特定输入下会计算出超大数值,正好撞上FP16的65504上限。解决办法是把这个模块排除在autocast之外,或者干脆切换成BF16格式。
BF16能在相同动态范围下比FP16稳定,就是因为它的指数位跟FP32一样长。代价是尾数更少、精度更粗。对很多大模型训练场景来说,稳定性比那一点精度重要得多。
3.2 哪些任务更适合全精度,哪些可以放心用半精度
不是说所有模型都适合FP16。根据我的经验,可以大致分三类:
- 常规图像分类、目标检测、常见NLP任务:FP16通常安全,Amp基本是无脑收益。
- 强化学习、GAN、扩散模型这类训练本身就不稳定的任务:可以先全精度跑通,再开Amp,避免混合精度带来的额外干扰。
- 科学计算、物理仿真、数值分析类任务:老老实实用FP64或FP32,这不是精度格式的问题,是整个数值体系都对这些东西敏感。
还有一个容易被忽视的点:有些模型在FP16下推理掉点不明显,但训练时却迟迟不收敛。因为训练涉及梯度传播的微小变化,比推理更敏感,所以“推理能用FP16”不代表“训练能用FP16”,这两个场景要分开判断。
3.3 怎么判断半精度适不适合你的模型:先跑对照实验
我的建议是:不要赌,直接验证。在正式跑大任务之前,花一个小时做一次小规模对照实验。用同一个模型、同一个数据子集,分别跑FP32全精度和Amp混合精度各几十步,把Loss曲线画出来对比。
如果两条曲线下降趋势一致、最终Loss接近,就放心用混合精度。如果曲线有明显发散或震荡,不要急着硬调Loss Scaling,先回到全精度确认问题不在模型本身,再尝试BF16或者分层精度方案——让某些模块保持FP32,某些模块用FP16。
这套验证流程虽然原始,但能帮你避开很多“半精度玄学”的坑。毕竟算力平台上每一分钟都在计费,与其在正式任务里反复试错,不如先在小范围里快速验证。
3.4 推理部署时还可以更激进一点
训练时你追求的是梯度数值稳定,推理时追求的就是吞吐量。FP16推理对大部分模型都是安全的,模型体积直接减半,显存占用降低,推理吞吐量提升。在很多推理框架里,开启FP16就是一行配置的事。
如果还想进一步压榨性能,可以考虑INT8量化。量化的思路跟FP16类似,但更激进:把浮点数变成8位整数,模型体积再砍一半。不过INT8需要一小部分校准数据,用它来观察模型在低精度下的输出变化,如果精度回退在可接受范围,再用它部署。
4. 智星云上怎么选卡、怎么开实例:完整流程
4.1 GPU租用解决的是“买不起也等不起”的问题
专业级显卡动辄上万甚至几十万,本地消费级显卡的显存又撑不起稍大的模型。此外,很多同学面临的是实验室或公司的GPU排队问题,明明任务就几个小时,排队却要等几天。GPU租赁平台解决的就是这种短时、弹性、高性价比的需求。
智星云这类平台比较适合两类用途:一类是按小时租一张卡做代码调试和模型验证,避免为一次性任务购买硬件;另一类是短时间跑大批量任务,比如训练一个模型、跑一组超参搜索,用完了就停,不用承担硬件折旧成本。
4.2 开实例的通用五步流程
各家平台的网页布局不一样,但开一个GPU实例的核心流程基本一致:
- 注册账号并完成支付配置,确保余额充足。
- 选择显卡型号和数量,单卡起步,按需求扩容。
- 选择镜像环境,一般有PyTorch、TensorFlow、纯净CUDA等选项。
- 设置存储空间,至少要有系统盘和数据盘两个概念。
- 选择计费模式,按量或包时,创建实例等待调度。
第一次使用这类平台时,我最纠结的是镜像选择。后来养成一个习惯:优先选已经预装好目标框架的官方镜像,不要选择纯净镜像再手动装环境,能省下大量“装包三小时、运行五分钟”的时间。
4.3 选卡不是越大越好:按显存和算力需求倒推
很多人一上来就想租最大号显卡,但“大”只是在显存上大,算力适配未必适合你。选卡之前先做一道估算题。
快速估算训练显存的基本公式:
- 模型权重大小:参数量 × 单个参数字节数
- 梯度大小:通常与权重同规模
- 优化器状态:AdamW这种带两个动量的优化器,需要约2倍参数大小的额外空间
- 中间激活:取决于batch size、序列长度、模型深度,往往是最大的变量
举个例子,7B参数模型,如果以FP32保存权重就是28GB,加上梯度和优化器状态轻松超过80GB,单张卡根本放不下。这种大模型要么拆分到多卡,要么考虑量化或模型并行。反过来,一个1B参数模型如果数据量较小,FP16下可能只需要十几GB显卡,这样就不用为了它特意租超大显存卡。
除了显存,还要看显存带宽。同样标称24GB显存的两款卡,带宽可能差出30%到50%,实际训练速度差距很大。如果平台展示的型号参数里有“带宽”这一栏,建议优先选带宽更高的。
4.4 第一次登录实例:SSH和Web终端
实例创建成功后,常见登录方式有两种。一种是SSH连接,适用于习惯命令行操作的用户:
bash复制ssh -p 端口号 user@实例IP
另一种是平台自带的Web终端或Jupyter界面,适合快速验证、写简单脚本。
登录后的第一件事永远是确认环境状态:
bash复制nvidia-smi
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"
这两条命令能告诉你驱动是否正常、PyTorch是否真的能用GPU。先确认基础环境是通的,再去跑模型,不要等到训练脚本报错才开始排查。
5. 第一次跑通一个PyTorch训练任务:从上传代码到看日志
5.1 写一个包含Amp且支持断点续训的最小训练脚本
在算力平台上跑训练,我的建议是不要拿本地的长脚本直接上传,而是先写一个带Amp开关、带断点续训的最小脚本。一来方便验证环境,二来万一实例被释放,不会白跑。
给你一个最小骨架:
python复制import argparse
import torch
import torch.nn as nn
from torch.cuda.amp import autocast, GradScaler
parser = argparse.ArgumentParser()
parser.add_argument("--amp", action="store_true")
parser.add_argument("--batch_size", type=int, default=64)
parser.add_argument("--resume", type=str, default=None)
args = parser.parse_args()
model = nn.Sequential(
nn.Flatten(),
nn.Linear(28 * 28, 256),
nn.ReLU(),
nn.Linear(256, 10),
).cuda()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3)
scaler = GradScaler(enabled=args.amp)
start_epoch = 0
if args.resume:
checkpoint = torch.load(args.resume)
model.load_state_dict(checkpoint["model"])
optimizer.load_state_dict(checkpoint["optimizer"])
start_epoch = checkpoint["epoch"] + 1
# 省略数据加载代码,核心训练循环如下
for epoch in range(start_epoch, 10):
for inputs, labels in train_loader:
inputs, labels = inputs.cuda(), labels.cuda()
optimizer.zero_grad()
with autocast(enabled=args.amp):
loss = criterion(model(inputs), labels)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
# 每个epoch保存一次,便于断点续训
torch.save({
"epoch": epoch,
"model": model.state_dict(),
"optimizer": optimizer.state_dict(),
}, f"checkpoint_epoch_{epoch}.pt")
这个脚本把amp做成了参数。验证环境时用--amp跑几十步,对比的时候去掉--amp,同一份代码就能完成对照组实验,省得维护两套脚本。
5.2 上传数据和代码:scp与对象存储
数据量不大时,最简单的上传方式就是scp。如果数据在本地是文件夹,可以先压缩再传,传输效率更高:
bash复制tar czvf data.tar.gz ./data
scp -P 端口号 data.tar.gz user@实例IP:~/
数据量达到几个GB甚至几十GB时,scp就不太现实了。这种情况下,我习惯先把数据集传到对象存储,再在实例里下载。很多云算力平台本身就提供对象存储服务,数据集放那边,之后每次开新实例都能直接拉取,不用重新上传。
这个环节最容易忽略的问题不是“怎么传”,而是“忘了传”。开实例前先想清楚数据和代码放在哪里,别等GPU实例已经启动了才去找数据集,每一分钟都是在计费。
5.3 用tmux挂后台任务,优雅地看日志
直接在SSH终端里跑python train.py,只要终端一关,训练就断了。正确的做法是用tmux把训练会话挂在后台:
bash复制tmux new -s train
conda activate pytorch
python train.py --amp --batch_size 64
然后按Ctrl+B再按D,把会话分离到后台。之后随时可以重新连上去看进度:
bash复制tmux attach -t train
如果只是快速看日志,也可以直接:
bash复制tail -f train.log
监控GPU资源同样重要。我会用watch -n 2 nvidia-smi实时刷新GPU占用情况,或者用nvidia-smi dmon看更细粒度的利用率变化。
5.4 遇到“CUDA out of memory”之后的排查顺序
跑训练几乎一定会遇到OOM,尤其是第一次在云平台上跑。我的排查顺序是固定的:
- 先看报错堆栈,确定是在哪一行炸的,是模型前向、loss计算还是优化器更新。
- 如果只是显存不够,优先调小batch size。大多数情况下,batch size减半就能解决问题。
- 检查模型是否支持
gradient_checkpointing,也就是梯度检查点。开启后可以用少量计算换大量显存节省。 - 如果还没开Amp,先开Amp,可能直接省下30%以上的中间激活显存。
- 实在不行再考虑梯度累积或模型并行,最后才考虑换更大显存的卡。
调小时batch size后注意学习率可能需要同步调整,这是另一个容易踩的坑。如果之前用大batch size配的学习率,突然小batch size会显得学习率过大,训练震荡明显,可以按比例适当降低学习率。
6. 计费、存储与多卡训练:算力平台的进阶操作
6.1 计费模式怎么选,才能少花冤枉钱
算力平台的计费一般分为按量计费、包时/包日计费和特惠实例几种。按量计费灵活,按小时甚至按秒结算,适合调试代码、跑几个小实验。包时或包日适合已经确定要连续跑几天的任务,单位时间价格通常更低。特惠实例价格优势大,但可能随时被抢占回收,适合能容忍中断的实验。
我自己的习惯是:白天调试用按量计费,确认代码没问题、任务能稳定跑之后,如果平台支持切换计费方式,就转到包时模式。特惠实例只用来跑那些跑了很久没出结果也无所谓的探索性实验。这里一定要看清楚平台的计费规则,尤其是“关机是否停止计费”和“实例释放后存储是否还会收费”这两条,不少人在这里吃过亏。
6.2 多卡训练:DDP才是默认选择
如果需要多卡训练,不要自己手写DataParallel,直接用PyTorch的DistributedDataParallel。DDp的启动方式是通过torchrun:
bash复制torchrun --nproc_per_node=4 train_ddp.py
代码里需要做对应初始化:
python复制import torch.distributed as dist
dist.init_process_group(backend="nccl")
model = model.cuda()
model = torch.nn.parallel.DistributedDataParallel(model)
torchrun会自动给每个进程设置RANK、WORLD_SIZE、LOCAL_RANK这些环境变量,你不需要手动分配。多卡训练最怕的是通信瓶颈,如果两张卡之间走的是PCIe而不是高速互连接口,同步梯度的耗时可能会吃掉计算加速的收益。租多卡实例前,先看平台说明里的互联方式。
还有一种更省钱的思路:如果只是并行跑多个不同超参的实验,开两张单卡实例分开跑往往比开一张双卡实例更灵活、也更容易调度。
6.3 数据存储:不要把鸡蛋放在一个篮子里
在算力平台上,实例本身通常是“临时”的,关机再开可能还在,但释放后系统盘数据就可能消失。我的习惯是:
- 数据文件放到独立的数据盘或对象存储里。
- 模型checkpoint每隔固定步数就保存一份到持久化存储,不要只放在本地。
- 训练日志如果重要,也要定期备份。
这个习惯救过我一次。有一次夜间训练跑到第三天,实例被意外回收,系统盘里的checkpoint全部丢失。后来我改成每2000步就自动往对象存储上传一次checkpoint,虽然麻烦了一点,但即使任务中断,也能从最近的checkpoint断点续训,不会白跑。
6.4 无卡模式:省钱的数据预处理方案
很多算力平台支持纯CPU的无卡实例,价格远低于GPU实例。第一次用的时候我没意识到这个选项的意义,结果在GPU实例上干了好几个小时静态数据解压和预处理的活儿,现在想想全是冤枉钱。
正确做法是:先用无卡实例把环境配好,把大数据集上传、解压、预处理、裁剪到合适尺寸,再启动GPU实例直接拉取就绪的数据。无卡模式处理这些I/O密集型的活儿绰绰有余,GPU的每一秒钟都应该花在真正需要算力的训练上,而不是浪费在tar解压和图片缩放上。
拿这次实操来说,整个流程走下来,最大的感触是:精度格式不是教科书里那些干巴巴的名词,它直接对应显存账单和训练速度。租来的GPU每一分钟都在计费,做实验前花十分钟算清楚模型需要多少显存、该用哪种精度格式、值不值得上多卡,经常比盲目调半天学习率划算得多。希望这篇笔记能让你在第一次租GPU时少走几步弯路。
