这条产线最早是在一张 24G 的卡上跑起来的。语音、口型、动画、合成,四个模型,理论上加起来 19G 出头,应该塞得下。
实际第一次跑,OOM。第二次把批大小砍到 1,还是 OOM。
我当时的第一反应和大多数人一样:换个小一点的模型。 幸好没有立刻动手,因为后来发现,模型根本不是问题所在。
显存不是「加起来」的,是「同时」的
把每个阶段的峰值显存单独测了一遍,一个反直觉的数字跳出来:四个模型里最大的那个,稳态只占 7G,但加载和第一次前向的瞬间峰值是 13G。那多出来的 6G 是什么?是框架为了加速预分配的工作区、是 kv 缓存的预留、是解码时临时的中间张量。
而我的编排逻辑,是四个模型同时驻留,等着上一步的产物一到就立刻接手。这个「随时待命」的设计在显存充足的机器上很漂亮,在 24G 的卡上就是四个峰值叠在一起,随便哪一刻踩到就炸。
问题不在任何一个模型,在我让它们同时存在。
第一刀:把「同时」改成「排队」
改动本身很无趣——把编排改成严格串行,前一个模型算完就释放,再加载下一个。
代价是每个环节之间多了几秒的加载时间。一条五分钟的成片,端到端慢了大概四十秒。
收益是:峭壁消失了。 显存曲线从四座山叠在一起,变成四座山排成一行,最高点就是那个 13G 的加载峰值。24G 的卡从「随时会炸」变成「绰绰有余」。
四十秒换一个不炸的系统,这笔账不用算。
第二刀:把「加载」这件事本身变便宜
串行化之后,瓶颈变成了反复加载。同一个模型一天要加载几百次,每次几秒,加起来是实实在在的一小时。
这里有两个我试过的办法:
- 权重常驻内存,显存按需搬。系统内存比显存便宜得多,把权重放在主机内存里,用的时候拷进显存,用完拷出来。加载从「读磁盘」变成「拷内存」,快了一个量级。
- 同类任务攒批。与其「一条视频跑完四个阶段」,不如「二十条视频的第一阶段一起跑,再一起跑第二阶段」。每个模型只加载一次,处理二十份产物。
第二个办法效果更好,但它改变了产线的形态——从「流式」变成了「批式」。这个取舍后面会单独写。
第三刀才是砍模型,而且砍的不是我以为的那个
做完前两步,显存已经不是瓶颈了。但我还是回头看了一眼模型选择,发现真正值得替换的不是最大的那个,而是一个很小、但被调用次数最多的辅助模型——它每次调用都要重建一部分状态,累计的开销比大模型还高。
换成一个更轻的实现,产线整体又快了 15%。
如果一开始就按直觉「砍最大的那个」,我会花一周替换一个根本不该动的模型,然后发现 OOM 照旧。
我从这件事里记住的
显存问题先看时间轴,再看模型列表。 把每个阶段的峰值画在同一条时间线上,哪些峰叠在一起一眼就能看出来。叠在一起的才是问题,单独看谁都不是问题。
还有一句更泛一点的:系统里最贵的往往不是最大的那个组件,而是你以为理所当然、从来没单独量过的那部分。


SERVICES.waline即生效。想说话可以先去 留言板,那边我每条都回。