LingBot-VLA 2.0 跑推理时显存不够 / 先看序列长度和批大小哪个在吃内存

文章导读
LingBot-VLA 2.0 推理 OOM 时,先别急着降精度或者换卡。把显存占用拆成“静态部分”和“随输入增长的部分”,再看峰值显存跟着序列长度走还是跟着批大小走,通常几分钟就能定位。定位顺序建议是:先在只加载模型、不跑推理的状态下记录一次静态占用,确认权重本身占了多少;然后固定批大小为 1、只拉长序列长度,记录峰值;再固定序列长度、只加批大小,记录峰值。两组曲线一对比,谁在吃内存就很清楚了。
📋 目录
  1. Ⅰ 用 nvidia-smi 或 torch.cuda.memory_sum
  2. Ⅱ 固定批大小为 1,逐步增加序列长度,记录峰值显存
  3. Ⅲ 固定序列长度,逐步增加批大小,记录峰值显存
  4. Ⅳ 检查是否开启梯度计算或缓存
  5. Ⅴ 根据实验数据给出调整优先级
A A

LingBot-VLA 2.0 推理 OOM 时,先别急着降精度或者换卡。把显存占用拆成“静态部分”和“随输入增长的部分”,再看峰值显存跟着序列长度走还是跟着批大小走,通常几分钟就能定位。定位顺序建议是:先在只加载模型、不跑推理的状态下记录一次静态占用,确认权重本身占了多少;然后固定批大小为 1、只拉长序列长度,记录峰值;再固定序列长度、只加批大小,记录峰值。两组曲线一对比,谁在吃内存就很清楚了。

先量静态占用,再分别单独扫描序列长度和批大小,看峰值显存随哪一个增长更快。序列维度若呈接近平方的增长,先压序列长度;批维度接近线性且序列已经压短,再压批大小。推理场景不建议用梯度累积替代批大小,那个只对训练有意义。量化、关 KV 缓存属于定位和验证手段,是否采用要结合延迟和输出质量一起看。

用 nvidia-smi 或 torch.cuda.memory_sum

这一步只做一件事:把模型权重和框架本身的显存占用先量出来,作为后面所有增量对比的基线。建议在模型加载完成、eval() 之后、任何推理发生之前采样。

# 终端侧:看进程级占用,1 秒一次刷新
nvidia-smi `--query-gpu`=memory.used,memory.total `--format`=csv -l 1

# 只想看本进程用了多少
nvidia-smi `--query-compute-apps`=pid,used_memory `--format`=csv
import torch

model = load_lingbot_vla()   # 替换成本仓库实际的加载方式
model.eval()
model.to("cuda")

def report(tag):
    torch.cuda.synchronize()
    print(
        tag,
        "allocated(MiB)=", round(torch.cuda.memory_allocated() / 2**20, 1),
        "reserved(MiB)=", round(torch.cuda.memory_reserved() / 2**20, 1),
    )

report("after_load")           # 静态基线
print(torch.cuda.memory_summary(abbreviated=True))

解读方式:memory_allocated 是张量真正占用的量,memory_reserved 是缓存分配器向驱动申请并保留的量,后者通常大于前者,差值属于可被复用的保留块,不等于泄漏。权重占用可以粗算:参数量乘以每参数字节数(fp32 约 4 字节、fp16/bf16 约 2 字节),再留出框架上下文和通信缓冲的余量。如果静态基线就已经贴近显存上限,那 OOM 的根因不在输入,而在权重加载方式或显卡容量。

占用来源主要由什么决定怎么观察是否随输入变化
模型权重参数量、精度(fp32/fp16/int8 等)加载后 memory_allocated否
KV 缓存层数、注意力头维度、序列长度、批大小开启/关闭 use_cache 对比峰值是,随两者增长
激活与中间张量单次前向的输入形状逐步加大序列或批观察峰值是
框架与 CUDA 上下文驱动、算子库版本空进程跑一次 memory_summary否
分配器保留块与碎片分配/释放节奏reserved 与 allocated 的差值间接

固定批大小为 1,逐步增加序列长度,记录峰值显存

把批大小锁死为 1,只改输入序列长度,这样测出来的增量基本就是序列维度带来的。配置项位置通常在这几处,按你仓库的实际命名替换:

# 通用骨架:按仓库实际键名替换
infer:
  batch_size: 1
  max_new_tokens: 64
  max_input_len: 256   # 依次改成 512 / 1024 / 2048
for seq_len in [256, 512, 1024, 2048]:
    torch.cuda.empty_cache()
    torch.cuda.reset_peak_memory_stats()
    with torch.inference_mode():
        out = model.generate(build_batch(batch_size=1, input_len=seq_len))
    torch.cuda.synchronize()
    print(seq_len, round(torch.cuda.max_memory_allocated() / 2**20, 1))

关键动作是每次跑之前调用 reset_peak_memory_stats(),否则读到的是历史峰值。max_memory_allocated 才是这一轮的峰值,memory_allocated 在跑完后往往已经回落,不能拿来当峰值用。注意把 max_new_tokens 也固定住,否则生成长度变化会混进结果。

固定序列长度,逐步增加批大小,记录峰值显存

这一轮反过来:序列长度固定在一个你实际会用的值,只改批大小。批大小的入口一般在推理脚本参数、服务化配置或者 dataloader 的 batch 维度上。

# 通用骨架:按仓库实际键名替换
infer:
  batch_size: 1        # 依次改成 2 / 4 / 8
  max_input_len: 512
  max_new_tokens: 64
for bs in [1, 2, 4, 8]:
    torch.cuda.empty_cache()
    torch.cuda.reset_peak_memory_stats()
    with torch.inference_mode():
        out = model.generate(build_batch(batch_size=bs, input_len=512))
    torch.cuda.synchronize()
    print(bs, round(torch.cuda.max_memory_allocated() / 2**20, 1))

同样固定 max_new_tokens 和输入长度。如果批大小翻倍时峰值显存也大致翻倍,说明这一维是近线性消耗,压缩批大小立竿见影;如果峰值涨得比批大小还快,需要回头检查有没有额外的广播或拼接操作把中间张量放大了。

LingBot-VLA 2.0 跑推理时显存不够 / 先看序列长度和批大小哪个在吃内存

检查是否开启梯度计算或缓存

推理时多占显存,常见原因是这段代码还跑在训练模式或梯度开启状态下。逐项确认:

print("model.training =", model.training)          # 期望 False
print("grad enabled =", torch.is_grad_enabled())   # 期望 False
print("any requires_grad =", any(p.requires_grad for p in model.parameters()))

建议用 with torch.inference_mode(): 包住前向和生成调用,它比 no_grad() 更彻底,会自动跳过一部分版本计数相关开销。如果代码里显式写了 model.train() 或者手动 backward(),先把这些去掉再复测。

KV 缓存方面:开启缓存会随序列长度和批大小增长,但关闭后解码阶段要反复重算,延迟会明显上升。所以关缓存适合用来验证“峰值显存里有多少来自缓存”,而不是长期运行方案。在支持的实现里通常通过生成参数控制,例如 use_cache=False 或模型配置里的对应开关,具体键名需要按你使用的版本确认。

根据实验数据给出调整优先级

把两轮数据摆在一起看增长趋势,再决定先动哪个:

  • 序列长度从 512 加到 2048,峰值显存涨幅明显超过四倍,说明注意力相关的开销在放大。这种形状下优先压序列长度,比如截断输入、分块处理长输入、减少 max_new_tokens。
  • 序列长度涨 4 倍、峰值显存也大致涨 4 倍左右,说明这一维是线性的,压序列收益稳定但不大;此时如果批大小翻倍能让峰值翻倍,先压批大小更直接。
  • 批大小为 1 时就已经 OOM,那问题在静态占用或单条最长输入上,先回到第一步确认权重占用,再看是否需要权重量化或换更小的实例。
  • 批大小和序列长度都不能再压、延迟又不能变,才考虑量化、分页 KV 缓存或把部分层放到 CPU,这些都会带来额外成本,需要单独评估。

关于梯度累积:它通过多次前向累积梯度来等效更大 batch,只对训练有意义。纯推理阶段没有梯度,也就没有可累积的对象,用梯度累积来替代批大小不成立,需要的是批处理调度或请求排队。另外,临时把张量换出到内存、依赖统一内存或调小分配器预留块,都只能当止血手段,判断根因仍然要靠上面两轮扫描。