先量清显存和延迟再挑卡——Cosmos 3 Edge 本地部署的实际取舍

文章导读
先分清卡在哪一块,再决定换不换卡。用同一段输入分别记录冷启动和热推理耗时,同时抓 torch.cuda.memory_allocated() 与 torch.cuda.memory_reserved() 的峰值,才能判断瓶颈出在显存容量、显存带宽,还是 CPU 侧的预处理让 GPU 空等。Cosmos 3 Edge 这类视频或世界模型推理,显存峰值常由激活和中间缓存撑起来,权重只占其中一部分;只看
📋 目录
  1. 壹 用同一段输入分别测冷启动与热推理耗时
  2. 贰 把显存峰值拆成权重、激活、缓存三块
  3. 叁 固定输入只改一个变量做对照实验
  4. 肆 确认数据预处理有没有让 GPU 空等
  5. 伍 按实测结果给出选卡判断顺序
A A

先分清卡在哪一块,再决定换不换卡。用同一段输入分别记录冷启动和热推理耗时,同时抓 torch.cuda.memory_allocated() 与 torch.cuda.memory_reserved() 的峰值,才能判断瓶颈出在显存容量、显存带宽,还是 CPU 侧的预处理让 GPU 空等。Cosmos 3 Edge 这类视频或世界模型推理,显存峰值常由激活和中间缓存撑起来,权重只占其中一部分;只看参数量挑卡,容易买到大显存却仍被带宽和输入规模卡住。下面给出的测量顺序和判断分支可以先在自己机器上跑一遍,阈值需要结合你的分辨率、帧数和批大小确认。

判断顺序建议:先测同一段输入的冷启动与热推理耗时,把权重加载和单次推理分开;再记录加载后、首次推理后、稳态第 N 次推理后的 allocated 与 reserved,看显存峰值来自权重、激活还是缓存;最后固定输入,只改帧数、分辨率、batch 中的一个变量做对照。显存随输入线性上涨,就先看容量;显存够但耗时随输入放大明显,就看带宽与算力;两段计时里 GPU 长时间空闲,先改预处理与数据搬运,不必急着换卡。

用同一段输入分别测冷启动与热推理耗时

计时必须钉在同一段输入上:prompt、步数、随机种子、分辨率、帧数全部不变,否则冷启动与热推理的差值没有可比性。建议划四个计时点:进程启动到权重加载前、权重加载完成后、首次推理结束、第 N 次推理结束。前两点之差是加载开销,后两点之差是单次推理开销。

首次推理通常包含 kernel 编译、显存池扩张,个别实现还有图捕获,这一次的数字不能当稳态值。可以先跑若干次预热,再取后几次的中位数,同时保留最小值作为参考。

import time, torch

t = {}
t['t0_before_load'] = time.perf_counter()
pipe = load_pipeline(ckpt_path='...', device='cuda')   # 换成你的加载入口
torch.cuda.synchronize()
t['t1_after_load'] = time.perf_counter()

def timed_run(prompt, steps):
    torch.cuda.synchronize()
    s = time.perf_counter()
    out = pipe(prompt=prompt, num_inference_steps=steps)
    torch.cuda.synchronize()          # 必须同步,否则只测到入队
    return out, time.perf_counter() - s

_, t['t2_first'] = timed_run(PROMPT, STEPS)          # 首次推理
runs = []
for i in range(N):
    _, dt = timed_run(PROMPT, STEPS)
    runs.append(dt)
    t[f't3_run_{i}'] = dt

print('加载耗时', t['t1_after_load'] - t['t0_before_load'])
print('首次推理', t['t2_first'])
print('稳态中位数', sorted(runs)[len(runs)//2])

验证方式:把加载耗时和稳态单次推理耗时对比。加载占大头,说明服务常驻比反复冷启动更划算;单次推理本身就慢,问题在计算或数据侧,换更大显存不一定有用。

先量清显存和延迟再挑卡——Cosmos 3 Edge 本地部署的实际取舍

把显存峰值拆成权重、激活、缓存三块

memory_allocated 是当前张量实际占用的显存,memory_reserved 是缓存分配器向驱动申请下来的总量,包含碎片和尚未复用的缓存。前者回答“模型真需要多少”,后者回答“进程在卡上占住了多少”。两个数一起看,才能判断是容量不够还是碎片太多。

def snap(tag):
    return {
        'tag': tag,
        'allocated_mb': round(torch.cuda.memory_allocated()/1024**2, 1),
        'reserved_mb': round(torch.cuda.memory_reserved()/1024**2, 1),
        'max_allocated_mb': round(torch.cuda.max_memory_allocated()/1024**2, 1),
    }

snap('权重加载后')                       # 这一档近似权重占用
torch.cuda.reset_peak_memory_stats()
timed_run(PROMPT, STEPS)
snap('首次推理后')                       # max_allocated 近似权重+激活峰值
for _ in range(N):
    timed_run(PROMPT, STEPS)
snap('稳态第N次后')                      # 看 reserved 是否持续爬升

采集位置固定在四处:权重加载后、峰值统计重置后、稳态若干次之后、以及卸载或进程退出前。记录表建议包含这些字段:实验编号、帧数、分辨率、batch、步数、权重加载后 allocated、峰值 max_allocated、稳态 reserved、单次推理耗时中位数、备注。三块的粗略拆法是:加载后的 allocated 约等于权重,峰值减去权重约等于激活,reserved 减去 allocated 是缓存与碎片。

固定输入只改一个变量做对照实验

每轮只动一个维度,其余全部锁死,否则耗时变化归因不了。三组实验可以这样设计:

先量清显存和延迟再挑卡——Cosmos 3 Edge 本地部署的实际取舍
  • 帧数组:固定分辨率与 batch,帧数取 1、8、16 这类跨度明显的几档。观察点是显存峰值和耗时的增长形态:近似线性上涨,说明激活随序列长度线性膨胀;耗时增长快于帧数增长,说明注意力部分的计算与带宽压力在放大。
  • 分辨率组:固定帧数与 batch,把宽高按 1/2、1/1 两档跑。观察点是显存峰值是否随像素面积成比例变化,以及耗时下降幅度是否明显大于帧数组中看到的幅度。
  • batch 组:仅在推理接口确实支持 batch 时做,batch 取 1 和 2 对照。观察点是权重部分是否基本不变、激活是否成倍上涨,以及单样本耗时是否真的下降;没有下降就不必为了吞吐去堆 batch。

每组实验都保留同一份输入(同一段视频或同一组图片),避免解码差异混进结果。做完三组,通常能看出对耗时和显存最敏感的那个维度是哪一类,也就能判断买卡时该优先显存容量还是带宽。

确认数据预处理有没有让 GPU 空等

很多“推理卡顿”其实是解码、缩放、归一化在 CPU 上排队。在预处理和推理两段之间各打一个时间戳,能把等待暴露出来。

先量清显存和延迟再挑卡——Cosmos 3 Edge 本地部署的实际取舍
t0 = time.perf_counter()
frames = decode_and_resize(video_path, size=(H, W))   # CPU 侧预处理
t1 = time.perf_counter()
x = to_tensor(frames).to('cuda', non_blocking=True)
torch.cuda.synchronize()
t2 = time.perf_counter()
out = pipe(x, num_inference_steps=STEPS)
torch.cuda.synchronize()
t3 = time.perf_counter()
print('预处理', t1-t0, '搬运', t2-t1, '推理', t3-t2)

观察指标:在另一终端跑 nvidia-smi dmon 或 nvitop,看推理间隔里 GPU 利用率是否掉到接近空闲;同时看 CPU 是否单核打满、内存拷贝是否吃满。若预处理与搬运加起来占据主要时间,而 GPU 大量空闲,改多进程解码、提前把预处理结果落成缓存、或调整分辨率,收益通常比换卡更直接。

按实测结果给出选卡判断顺序

走到这一步,判断可以按三条分支依次确认,先排除成本最低的一条。

  1. 显存不足分支:现象是 OOM,或峰值 max_allocated 已经贴近卡容量。
    验证动作:先把帧数或分辨率降一档,看 OOM 是否消失;再看 reserved 是否明显高于 allocated,若是,试试设置 PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True 后复跑,观察碎片是否缓解。
    硬件关注点:单卡显存容量。把权重卸载到内存或换更小的精度能缓解容量,但会拉长单次耗时,这属于取舍,不是提速手段。
  2. 带宽不足分支:现象是显存够用,但耗时随帧数、分辨率放大很快,GPU 利用率高而计算单元没有跑满。
    验证动作:把输入规模减半复跑,看耗时下降幅度是否与输入缩减幅度接近;再确认当前是否已启用高效的注意力实现。
    硬件关注点:显存带宽与算力规格,而不是单纯容量。同容量下带宽更高的卡,在长序列、高分辨率场景更值得考虑。
  3. 预处理慢分支:现象是推理间隔里 GPU 空等,CPU 侧占用高。
    验证动作:把预处理结果固化成张量文件复跑一次,若总耗时明显下降,说明瓶颈确在数据侧;也可临时把解码挪到独立进程观察 GPU 利用率是否被填满。
    硬件关注点:CPU 单核性能、内存带宽、存储读取速度。先改流程,再考虑整机配置。

三条分支都排除后仍然慢,剩下的才是“算力不够”这一类。此时用前面同一段输入和同一组参数,把候选卡按“显存峰值是否放得下、稳态单次耗时是否可接受”两列打分,比只盯着显存数字更稳;最终数值需要你在自己的输入上复测确认,不要直接套用别人的配置结论。