第一次替人选 GPU 主机,我在一张表格里卡了很久。不是价格难算,是三个卖家用三种名字卖三种东西:一家叫"独占实例",一家在"Kubernetes 上跑 GPU Pod",第三家在控制台放了个"抢占式"开关。我按"每小时单价从低到高"排了序,差点直接照着最便宜的报上去。
后来我按卡的归属权重画了一遍表,思路立刻就清楚了。
独占卡:一整块 GPU 跟着实例走
最接近裸金属的形态,实例里那块卡是独占的,能装驱动、能跑 NCCL、能做张量并行。
容易忽略的细节是计费粒度。同一家厂商里,"独占卡"往往对应三种不同的付费方式:按秒、按小时、包年包月。按秒的那种最适合跑完就释放的临时任务,包年的适合常驻服务。但有个限制我一开始没注意:实例创建之后卡型基本不能改,想换显存更大的卡只能重建,数据得自己搬。
弹性容器:卡是集群的,容器分到的是"时间片"
托管容器或 K8s 形态,每个容器申请 nvidia.com/gpu: 1,看着跟独占一样,底下可能完全不同。常见实现是两种:
- 时间分片:一张物理卡轮流分给多个容器(MPS 或纯上下文切换)
- MIG 硬件切分:从一张卡上切出若干独立实例,硬件级隔离
坑在容器里跑 nvidia-smi 时特别明显:输出的是宿主机上全部 GPU 的状态,不是你分到的那块。判断自己拿到哪块,要看宿主机注入的 NVIDIA_VISIBLE_DEVICES,而不是容器内进程用的 CUDA_VISIBLE_DEVICES。
更要命的是显存。如果是时间分片,两个容器各自 nvidia-smi 都能看到"用了 40GB / 80GB",你会以为自己还有 40GB 余量,实际两个进程在抢同一块物理显存。表现是训练跑到第三个 epoch 突然 OOM,日志里找不到任何"另一个容器"的痕迹。
MIG 则没有这个问题,切出来的实例有独立显存配额和独立算力单元。代价是切分档位固定,而且跨 MIG 实例做不了 NCCL 通信——多卡张量并行直接用不了。对训练来说,这基本等于单卡。
竞价实例:便宜,但必须按"随时会断"来设计
抢占式/竞价实例的价格通常是按需的一小截,机制一般是这样:你的出价高于当前市场价就自动开机,市场价超过出价就被回收。回收有两种通知方式——提前几分钟发 SIGTERM,或者直接停机不通知。
坑不在"便宜",在训练进度。我第一次跑微调用的就是竞价实例,checkpoint 周期设的 15 分钟。某天早上发现实例被回收了,训练停在上次保存的地方,白跑两小时。
后来改成监听 SIGTERM 同步保存:
import signal
import sys
def _bye(signum, frame):
print("收到中断信号,先存 checkpoint 再退", flush=True)
trainer.save_model(f"{out_dir}/checkpoint-last")
sys.exit(0)
signal.signal(signal.SIGTERM, _bye)
这只是优雅离场的第一半。另一半是别把唯一一份权重放在会跟着实例一起被回收的盘上。我把 checkpoint 统一写到对象存储,回收前先上传,恢复时从对象存储拉。竞价实例的本地盘是否随实例一起释放,各家规则不同,这一条得单独确认。
还有个反直觉的点:折扣率不是固定的。闲时可能只有两三折,繁忙时段等于按需价。所以"用竞价能省八成"这句话在旺季不成立,真实账单要看实际时段。
一张对照表
| 维度 | 独占卡实例 | 弹性容器 | 竞价实例 |
|---|---|---|---|
| 显存归属 | 整卡独占 | 时间分片共享 或 MIG 硬件切 | 整卡独占(多数厂商) |
容器内 nvidia-smi | 本机几张 | 宿主机全部(易误判) | 本机几张 |
| 被回收风险 | 无 | 无 | 有 |
| 多卡通信 | 支持 | MIG 下不支持 | 支持 |
| 适合 | 训练、多卡推理 | 微调、在线推理、弹性扩缩 | 可中断任务、离线批处理 |
| 卡型可否事后改 | 通常不可 | 可,Pod 重调度 | 重新申请即可 |
我现在怎么选
三条判断,顺序不能颠倒:
- 这个任务断了会丢多少进度? 丢几分钟 → 竞价;丢几小时 → 按需。
- 需要几张卡协同? 是 → 独占卡,别在容器和 MIG 上折腾。
- 负载有没有明显波峰波谷? 有 → 弹性容器按需扩缩;没有 → 长期独占更省。
最后补一句老话:上面提到的单价和折扣都是量级示意,不是报价。同样的卡在不同套餐里给的规格、散热、网络都不一样,价格也会调整,真实数字以官方页面为准。