
1. 从单跑一条到批量出片的真实痛点做AI漫剧推文短视频的朋友应该都有同感单条视频跑通并不难难的是稳定批量出片。我这边做的是把推文、漫画、配音、字幕全部AI化合成一条有剧情、有旁白、有音效的漫剧短视频。前期单跑一条效果还很理想等真正要上量每天产出上百条的时候问题就来了。最直观的表现是显卡内存疯狂打架。同一台机器既要跑文生图、图生视频又要跑转绘和超分每个环节都是吃显存大户。最开始我用的是串行方案一张卡上按顺序跑完一个任务再跑下一个数据安全但效率低得让人发慌GPU利用率能掉到30%以下。后来改成并行又频繁出现显存碎片化导致的OOM一崩就是整批任务重头再来。后来我重新设计了整套生成流水线核心就两个关键词显存池化、并发调度。这篇文章详细讲一下当时的思路、踩坑和最终落地方案希望能给正在做AI漫剧、AI短剧批量生产的同行一些参考。2. 项目整体架构把散枪打鸟改成流水线作业2.1 漫剧推文短视频的生成链路拆解AI漫剧推文短视频本质上是一条工业化的内容生产线。从输入一段小说原文或剧本台词到输出一条完整的竖屏短视频中间至少要经历这几个关键节点文本解析与分镜设计。把推文切成若干分镜每个分镜需要匹配对应的画面描述、旁白文本、情绪语气。这一步不仅是正则切句还得做语义段落合并否则后面生成的旁白会断句奇怪。角色一致性生成。漫剧最怕角色脸崩。同一个角色在分镜1和分镜5里长得完全不像这条片子基本就废了。所以要提前对每个角色做定妆照再基于定妆照做姿势、表情、场景的扩展生成。场景图生成。每个分镜需要一张或几张底图走文生图或者图生图。不同分镜之间要保持光影风格一致这时候一般会用ControlNet注入法线或边缘结构再用风格LoRA锁风格。动态化处理。底图不是静态就能用的还要让画面动起来。要么用图生视频模型做局部运动要么做镜头推拉、粒子特效和关键帧动画。这里面最吃显存的就是这一步分辨率稍高一点显存占用就往上跳几个G。旁白TTS与音效合成。旁白生成、背景音乐选择、音效插入最后统一混音。字幕渲染与视频合成。把有旁白的音频转成字幕时间轴再和动态画面、转场特效合成最终MP4。上面每一环都可以拆成独立的微服务或独立进程如果用一台多卡机器做批量生产串行跑非常浪费并行跑又需要一套聪明的资源分配机制。2.2 为什么说显存是批量生成的最大瓶颈做批量AI视频生成CPU和内存一般不是瓶颈显卡算力也不是最稀缺的真正卡脖子的是显存容量。GPU要处理大尺寸特征图显存一旦不够再多算力也使不出来。举个例子一张1024x1024的底图在SD系列模型下仅UNet部分的中间激活至少需要2~4GB显存如果开启ControlNet和图像拼接直接把批量大小拉高8GB显存基本瞬间见底。到了图生视频环节单条数秒片段的中间特征缓存可以达到6~12GB显存占用非常恐怖。所以在整个批量生成系统中谁能把显存利用率提上去谁就能在同成本下多跑几路任务。显存池化的价值就在这里它不是在算法层面给你省显存而是在工程层面把显存这块公共资源管起来避免浪费、碎片和争抢。2.3 整体架构分了三层我的最终架构大致可以分成三层管理调度层。负责任务拆分、优先级排队、状态管理、失败重试。这一层不碰显卡纯粹是CPU和内存上的逻辑控制。执行器层。每个执行器对应一张GPU内部维护一个显存池、一个任务队列和一个生命周期管理器。执行器从调度层领取任务在当前GPU上执行具体生成操作。资源池层。也就是显存池本身。负责分配和回收各算子的显存块避免反复调用cudaMalloc同时监控每个进程的显存水位。这种分层的好处是每一层都能独立演进。调度层可以随时调整排队策略执行器层可以单独重启而不影响全局显存池则专注于底层的显存复用和碎片整理。3. 显存池化设计给每张显卡装上内存管家3.1 显存池化的核心思想所谓显存池化一句话解释就是把显存当成一个可复用的大仓库而不是每次要用就临时去操作系统申请一大块用完再还回去。反复调用cudaMalloc和cudaFree是非常昂贵的操作尤其在高并发下频繁分配和释放会带来两个坏处一是分配开销大拖慢单任务速度二是产生显存碎片虽然总量够但连续大块显存不足OOM照出不误。显存池化做的事情就是任务开始时从池子里借用显存任务结束后不还给CUDA层而是还给池子。后续任务可以直接复用同样尺寸的显存块省去分配的时间也避免碎片化累积。3.2 自建简单的显存池实现我做的是一个进程内的显存池核心结构可以用下面这段Python伪码表示import threading from collections import defaultdict import torch class CUDAMemoryPool: def __init__(self): self._free_blocks defaultdict(list) # key: block_size - list of (ptr, device) self._active set() self._lock threading.Lock() def alloc(self, size: int, device: str, align256): size self._align_up(size, align) with self._lock: if size in self._free_blocks and self._free_blocks[size]: ptr, dev self._free_blocks[size].pop() if dev device: block torch.cuda.FloatTensor(1).new().storage()._new_shared(size * 4).type(torch.float32).view(-1) self._active.add(ptr) return ptr ptr torch.cuda.cudaHostAllocator.new(size) self._active.add(ptr) return ptr def free(self, ptr, size, device): size self._align_up(size, 256) with self._lock: self._active.discard(ptr) self._free_blocks[size].append((ptr, device))当然这只是工程上的示意实际中更好的选择是直接使用PyTorch原生的CUDA Caching Allocator。PyTorch从1.9开始默认就带了一个基于cudaMalloc的缓存分配器它会把释放的显存块缓存起来复用效果比我们手写轮子好得多。我们要做的是让PyTorch的缓存分配器能够更高效地应对多变的任务尺寸而不是完全重写。所以在这个项目里我做了一层任务级显存规划在调用模型前后主动告诉缓存分配器接下来大概需要多大的显存区段必要时调用torch.cuda.empty_cache()来收缩已经无用的缓存块。与此同时我把每个子任务的显存占用做了预测量提前保留足够的池内存量有效减少了并发任务之间的显存争抢。3.3 关键参数池容量、分块粒度、释放策略显存池最关键的三个参数我实测下来是这样调优的池容量一般设置为单卡可用显存的80%。显存总共24G的话池子规划20G预留一部分给模型权重、CUDA context和系统开销。如果池子容量设到100%系统本身会因为分页和上下文产生少量显存超支直接触发OOM。分块粒度块大小按256MB递进。小于256MB的小对象统一当256MB分配减少块数量后续碎片更少。实测表明粒度256时的碎片率比64时降低了一个数量级代价是内部碎片浪费多了1~2%完全可接受。释放策略使用两级释放。池内空闲块超过总池容量的20%时才会把多余部分还给CUDA。短时间内的突发任务不会触发大量反复释放整体更稳定。注意显存池不是万能灵药。如果你的任务中某些算子需要非常大的连续显存池化分块反而会害了你因为大显存被切成了小块。这时应该对大块请求单独开一条大块直通路径绕过池子直接分配。3.4 池化后的显存水位变化我做了一个简单的监控脚本每2秒记录一次显存使用量。池化前的记录是典型的锯齿状曲线一个任务开始就冲顶任务结束瞬间掉下来再冲顶再掉。池化后曲线变成了相对稳定的平直线最高峰和最低谷的差值明显收窄。一组真实的对比数据单卡RTX 3090跑100条漫剧分镜指标池化前池化后任务平均峰值显存20.8 GB17.4 GBGPU利用率均值54%71%平均单条生成耗时95秒72秒OOM崩溃次数7次0次池化前后不只是显存占用下来了连单条耗时都在下降。原因很简单显存不再反复申请释放分配等待时间大幅缩短同时CUDA上下文切换更少计算管线更连续。4. 并发调度从抢闸到红绿灯4.1 调度模型选择队列比抢占更可靠显存池解决了资源侧的效率问题但任务侧还有一个更头疼的问题多个任务同时到达谁先谁后怎么防止一个任务占满显存导致其他任务饿死。在最开始的版本里我采用了一种抢占式调度新任务来了如果当前显存不够就把正在跑的任务挂起释放显存给新任务。结果很糟糕——频繁挂起和恢复导致模型上下文反复保存和加载显存释放不彻底比串行还慢。后来我换成了FIFO 动态优先级的队列调度。所有任务进来先排队调度器根据任务类型、预估显存占用、预估耗时、优先级权重综合评分然后决定下一个放行哪个任务。4.2 两级队列设计我在每个执行器内部维护了两个队列等待队列。存放还没开始执行的用户任务。按优先级排序同一优先级内按提交时间排序。就绪队列。从等待队列中被调度器点名可以执行的任务。执行器线程按照就绪队列的顺序逐个领取执行。调度器每隔N秒我设的是1秒扫描一次等待队列把能够放进当前显存池的任务转移到就绪队列。放行条件很简单该任务预估显存峰值 当前已占用显存 池容量上限。这样做的好处是不会出现一个8G显存耗尽的超大任务堵住所有小任务的情况。8G任务进不来那4个2G任务可以先走。4.3 优先级如何打分任务优先级不是简单地按人给的而是综合了几个因素def compute_priority(task, queue_state): score 0.0 # 任务自身价值权重 score task.user_weight * 10.0 # 等待时间惩罚 score min(task.wait_time / 60.0, 10.0) * 5.0 # 资源匹配度奖励显存预估越接近可用余量越优先 if task.est_mem queue_state.available_mem * 0.7: score 30.0 # 大任务降权避免阻塞 if task.est_mem queue_state.total_mem * 0.6: score - 50.0 return score这套评分很粗糙但很实用。核心逻辑是保证小任务不被大任务饿死同时等待久的任务能逐渐获得更高优先级最终强制放行一次。4.4 与显存池的联动预处理预扣机制调度器和显存池不是独立工作的二者之间有一个关键机制预扣显存。当一个任务从等待队列转移到就绪队列时调度器不会把所有资源全部锁死而是先从显存池中预扣该任务的预估峰值显存。这样后面的大任务就不会错误地把这块空间分配出去导致就绪队列里正在执行的任务OOM。预估峰值显存怎么来的我采用了两阶段估算法离线阶段。把常见的分镜任务按照分辨率、批量大小、模型类型三个维度做一次摸底测试记录每个组合下的峰值显存形成一张基准表。在线阶段。新任务进来时先查基准表如果找不到完全匹配的就取最接近且偏大的记录再乘以1.15的安全系数。这套预处理与实际显存误差基本控制在10%以内为后面稳定批量生产提供了很好的护栏。5. 并发度与显存分配策略的联动优化5.1 单卡多并发 vs 多卡并行很多工程团队喜欢一上来就搞多卡并行一张卡跑一个任务简单可靠。但漫剧生产有个特点不同环节对算力需求差异很大有的环节吃显存但不算重有的环节重算但是显存小。我最后采用的是单卡多任务并发 多卡负载均衡的组合策略对于显存占用大、计算量相对低的环节如文生图、图生图单卡上并发跑2~3个任务只要显存池容量允许。对于计算密集的环节如动效渲染、超分单卡上跑1个任务避免多个计算密集型任务互相抢占SM算力。多张卡之间按照执行器空闲率和显存剩余量动态分配任务不让某张卡累死某张卡闲死。5.2 动态批量大小的自适应调整在批量生成场景里经常要做动态batch_size调整。Number of batches越大训练或推理效率越高但显存占用也越大。我做了一个自适应批量大小模块每次任务开始时先测一个短脉冲尝试提升批量大小如果显存池分配成功且耗时没有明显劣化就继续加大如果分配失败或者推理速度反而下降了就回退。这部分的实现逻辑大致是这样def adaptive_batch_size(model, input_tensor, pool, base_size1, max_size8): current base_size while current max_size: mem_needed predict_memory(current * 2) if not pool.can_alloc(mem_needed): break start_time time.time() result infer(model, [input_tensor] * current) infer_time time.time() - start_time # 如果翻倍后的耗时没有成比例上升说明并行效益明显 if infer_time base_time * current * 0.8: break current * 2 return infer(model, [input_tensor] * current)实际效果是在显存充足时批量大小能稳定翻到4~8在显存紧张时自动降到1~2保障任务不OOM。5.3 多任务混跑时的显存水位控制我们生产过程中经常遇到一种情况一个长视频动效任务正在跑中间插入几个图文生成小任务。如果不控制大任务会占满显存导致小任务全部卡死。解决办法是在显存池层面加了一个水位上限机制。池子内部把显存分成两个区高优先级保留区和普通区。大任务允许使用普通区但必须保留至少2GB的高优先级区给后续小任务。小任务来了优先使用高优先级区保证快速完成、快速释放。如果是特别大的视频任务开始前我会强制设置一个环境变量限制所有tokenizer和缓存的预留量防止任务运行到一半突然申请2G临时显存找不到空间。6. 实操中遇到的3个顽固问题及排查实录6.1 问题一显存池退避不及时OOM仍然偶发现象池化后1小时才出现一次OOM但清掉之后跑一阵又复现。一步步排查下来发现问题不在池子本身而在PyTorch的CUDA缓存分配器和池子之间的协作。有个算子执行后生成了大量小的临时缓存块这些块被PyTorch缓存住没有回到我们的池子我们的池子还傻乎乎地以为显存很充足结果新任务分配失败。解法在关键节点主动调用torch.cuda.empty_cache()并且对返回的缓存块做trim。实际上相当于告诉PyTorch我这边池子里的空间够用你不用替我省那么多次要状态不释放的缓存。注意empty_cache()不能高频调用它本身是个代价较高的操作频繁起不到缓存复用的效果反而会拖慢速度。经验值是每执行完20~50个任务调用一次。6.2 问题二调度器放行的任务实际跑起来显存超预估现象调度器评估任务预估峰值15GB实际跑到一半直接20GB直接把池子撑爆了。排查定位后发现问题出在模型参数上。同样是文生图任务不同提示词的长度会影响CLIP模型的显存占用尤其开启长文本编码时序列长度比默认值大好几倍显存用量陡增。解法预估显存增加一个提示词长度的校正项。提示词超过100个token预估显存额外加10%超过200个token加25%。同时调度器放行的阈值从85%降到80%多留了一点安全余量。6.3 问题三生成节点进程意外崩溃池内显存没有被回收现象执行器进程被OOM killer杀掉之后显存池状态一直异常新任务无法分配显存。说实话这个问题我排查了最久。最终发现是显存池的状态没有做持久化进程崩溃后没有机制去扫描和重置池子里的所有块。后来在池子外层加了一个健康检查线程定期汇报池子状态和进程存活情况。一旦发现进程异常退出直接清理该进程的所有池内记录并且通过调度器把这个执行器标记为重建中等它恢复后再接收新任务。7. 最终实现效果与关键数据整套方案落地后我用24GB显存的RTX 3090做了压测。任务是每天批量生产100条漫剧推文短视频每条视频平均包含大约25个分镜镜头。最终结果单卡平均每天完成120条完整短视频超过预期。GPU利用率峰值稳定在82%~90%平均值从原来的54%拉到了75%。显存池块复用率达到91%cudaMalloc调用次数减少了78%。并发调度下任务排队平均等待时间从原来的4分半降到了35秒。连续运行72小时后未见OOM崩溃整体非常稳定。不管是从吞吐量、稳定性还是资源利用率来看显存池化加上并发调度的组合对整个AI漫剧批量生成系统都是决定性的提升。过去想要提高产量第一反应就是加卡、加机器、多开几个任务但做了池化和调度后发现先把手头卡规划好了比盲目堆资源划算得多。如果现在让我重新设计一套AI漫剧短剧批产系统我仍然会把显存池化和并发调度放在架构图的最中心位置。它们不是最前沿的算法创新却是我在实际生产中感受最明显的两个工程支点。