本地跑 Ming-3.0 推理时显存很快被吃满,先别急着同时改批大小和数值精度。这两个变量的作用位置不一样:批大小主要抬高推理阶段的中间结果(激活、缓存、并发请求的临时张量),数值精度主要压低权重和算子计算的单位开销。同时改,峰值降下来了也不知道是谁的功劳,下次换一段更长的音频又会反弹。稳妥的顺序是:先按时间点把显存拆成「空载 / 权重 / 推理」三段,确认增量落在哪一段;再用批大小 1 起逐级加,看峰值随批次怎么长;然后固定同一段音频只换精度重跑,比对显存和输出;最后用显存上限把真实边界压出来。
Ming-3.0 显存吃满,判断路径建议按「分段量显存 → 批大小找斜率 → 换精度做对照 → 限显存压边界」走。若增量出现在权重加载段,说明模型本身尺寸和精度档位是主因,调批大小收益有限;若增量出现在开始推理之后并随音频长度、批大小同步上涨,优先降批大小。精度下调只作为第二步,且必须用同一段音频比对输出文本、时间戳和数字专名是否漂移。边界条件以限制显存后是否仍触发分配失败为准,不以某次跑通为准。
先量空载、加载权重后、开始推理后三个时间点的显存
这一步只做观测,不改任何配置。开一个终端按秒轮询显存,把输出重定向到文件,方便事后对齐时间点:
nvidia-smi `--query-gpu`=timestamp,memory.used,memory.total `--format`=csv,noheader -lms 1000 | tee mem.log
如果轮询间隔想放宽到每秒一次,把 -lms 1000 换成 -l 1 即可;AMD 卡对应换用 rocm-smi 的显存字段。轮询开起来之后,按顺序触发三个动作,并记录每段的稳定值,不要只记瞬时抖动:
| 时间点 | 触发动作 | memory.used 记录 | 与上一段的差值 | 怎么读 |
|---|---|---|---|---|
| T0 空载 | 进程未启动,只有驱动和桌面占用 | 填入实测值 | — | 这是基线,后面所有差值都减它 |
| T1 加载权重后 | 模型加载完成、尚未喂入音频 | 填入实测值 | T1−T0 | 这段增量基本固定,换音频长度不变 |
| T2 开始推理后 | 同一段音频跑完一次前向 | 填入实测值(取峰值) | T2−T1 | 这段增量随音频时长、批大小变化 |
判断方法很直接:把 T2−T1 和音频时长、批大小做对比,如果这段增量随输入明显变化,说明瓶颈在推理中间结果,调批大小是有效方向;如果 T1−T0 已经占掉了大部分可用显存,而 T2−T1 很小,那再怎么调批大小也只是在窄缝里挪动,应该先看精度档位和模型加载方式。另外注意把日志里的加载完成时间和 nvidia-smi 的时间戳对齐,否则很容易把权重加载阶段误判成推理阶段。
把批大小从 1 开始逐级加,记录峰值
批大小配置项的字段名各家不同,通用骨架如下,替换成实际配置文件里的键名即可:
inference:
batch_size: 1 # 本次实验的变量
max_batch_size: 1 # 部分实现会用它做上限校验
num_workers: 1 # 保持不变,避免干扰
实验时只动 batch_size,其他项全部冻结。从 1 开始按 1、2、4、8 逐级加,每一档都固定同一段音频、单进程运行,跑之前先确认上一个进程已经退出(nvidia-smi 里看不到残留进程),否则峰值会串档。每档记录成下面这样:
- 批大小取值、峰值 memory.used、峰值出现时刻(推理中还是收尾阶段)
- 单次推理耗时,只用于判断是否已经开始换页或降频
- 是否成功输出、输出条数是否等于批大小
把几档的峰值画在纸上或表格里,看增长斜率:如果每翻一倍批大小,峰值大致翻倍,说明中间结果随批次线性增长,需要保守设定上限;如果翻倍后峰值涨得很少,说明这一档还没到显存临界点,可以继续加。峰值超过上限时的复现步骤固定成四步:记录当前批大小和音频文件路径 → 确认显存已释放 → 单独重跑这一档 → 抓取进程退出前后的 mem.log 和完整报错行。复现不出来的那一档不要写进结论,那多半是残留进程或缓存造成的假象。
换数值精度再跑同一段音频,对比显存与输出
精度配置同样是骨架式写法,具体键名以实际配置为准:
precision: bf16 # 候选值:fp32 / fp16 / bf16,逐次只改这一行
fallback_precision: fp32
换精度时必须回到批大小 1,否则显存差值里混着批次变化。跑完一轮后,除了看 mem.log 的峰值,输出侧要比对这些可观察项:转写或生成文本是否与 fp32 结果逐句对齐、时间戳是否整体偏移、数字和专有名词是否出错、有没有出现重复片段或提前截断、句子长度分布是否异常偏短。建议把两档输出保存成两个文件再用 diff 看,肉眼扫一遍容易漏掉个别字。
风险边界要说清楚:fp16 的数值表示范围更窄,遇到幅值较大的中间结果容易溢出成 inf 或 nan,表现为输出突然空白或全是重复词;bf16 的动态范围接近 fp32,但有效位数少,细微的数值差异可能体现在低置信度片段的用词上。如果比对下来精度档位能省出明显显存余量而输出无明显漂移,那它可以作为长期配置;如果输出出现上述任何一类异常,就回到上一档,改为从批大小上找空间。
用显存上限限制的方式压出真实边界
前面测到的峰值只是「跑通了一次」,不等于边界。把可用显存人为压小,才能知道在什么条件下会真正触发分配失败。配置骨架示例:
runtime:
device: 0
gpu_memory_fraction: 0.6 # 只允许使用该比例,字段名以实际实现为准
如果实现里没有这个字段,可以用环境变量限制可见设备再配合进程级显存上限的方式近似:
CUDA_VISIBLE_DEVICES=0 python run_infer.py `--config` cfg.yaml
建议比例从较低值开始,每次上调一档,同一段音频、同一批大小重复跑,记录「第一次出现失败时的比例」。失败时的日志要保留这几类关键行:报错中的 out of memory 字样、请求分配的字节数、当前已分配与保留的字节数、出错时涉及的张量形状或 batch 索引、以及进程退出码。如果报错发生在权重加载阶段,说明限制压到了模型本身体积,跟推理无关;如果发生在推理阶段且张量形状里带着批维度,说明这一档批大小已经越界。这个失败点才是可写进配置说明的边界,也建议在配置文件里把批大小设在这个边界之下留出余量,而不是贴着失败点跑。
最后提醒一点:用显存上限、缩小分辨率、临时降级到更小模型这类做法都属于压边界或止血手段,能跑通不等于性能更好,也不要在整理记录时把它们写成优化收益。