AI漫剧推文生产Docker化实践:多项目环境隔离与GPU资源调度

发布时间:2026/9/17 5:43:09
AI漫剧推文生产Docker化实践:多项目环境隔离与GPU资源调度 上个月我把一套已经稳定跑了三个月的AI漫剧推文短视频流水线整个推倒重来。原因说起来有点丢人三个账号项目在同一个Linux服务器上跑得好好的某天为了给其中一个项目加上“素材自动去重”的小功能随手pip install了一个第三方库第二天另外两个项目凌晨的定时任务全线崩溃报错从undefined symbol到CUDA out of memory五花八门。我花了一整天回滚环境、重装依赖、重新出图才真正意识到一件事AI漫剧推文短视频这种听着很“轻”的内容生产背后不仅有文生图、OCR、TTS、视频合成这些模型环节还绕不开Docker层面的环境封装和GPU资源调度。这套方案走通之后我把手上所有项目都迁进了容器用Docker做了彻底的多项目隔离从此三个项目互不污染显卡显存也终于按需分配、互不抢食。这篇文章就聊聊我在这个迁移过程中沉淀下来的完整实践包括镜像分层怎么做、GPU资源在多容器之间怎么调度、多项目目录怎么规划、以及连续跑了三个月之后遇到的那些坑。不管你是自己一个人做漫剧账号还是帮团队搭一套多人共用的AI生产服务器这套思路都可以直接照抄。1. 为什么AI漫剧推文这种“轻生产”也逃不过环境隔离1.1 一条漫剧流水线其实由大量“环境敏感”组件组成很多人提起AI漫剧第一反应就是“用AI画图嘛”但实际上一条能稳定输出的漫剧推文流水线至少包含这几个环节小说文本切片、大模型改写分镜文案、Stable Diffusion生成漫画分镜、OCR识别原图或字幕、TTS配音、最后用FFmpeg或MoviePy把分镜、字幕、音频合成短视频。听起来都是现成的模型和工具可问题恰恰出在“现成”这两个字上。这些环节在Python世界里全都是有依赖的库torch、transformers、diffusers、paddlepaddle、paddleocr、opencv、moviepy、ffmpeg-python每个库对CUDA版本、numpy版本、opencv版本都有自己的要求。我当时的项目A在跑SDXL加LoRA微调就是热词里说的“gpu微调大模型”项目B在用Stable Diffusion 1.5出普通漫画分镜项目C主要跑PaddleOCR做字幕提取和自动去重。三个项目堆在同一台机器上pip安装的依赖都在同一个site-packages里互相挤兑不出事才是运气好。1.2 多项目并行时冲突爆发在三个层面第一个层面是Python依赖冲突。项目A依赖PyTorch 2.1和transformers 4.36项目B和项目C依赖paddlepaddle-gpu 2.6和paddleocr 2.7这两套大库都绑定了各自的numpy、protobuf、opencv版本。一旦某个项目为了临时功能升级了其中一个库另一个项目可能立刻跑不起来。Conda虚拟环境虽然能隔离site-packages但隔离不了后面两个更麻烦的层面。第二个层面是CUDA运行时错配。PyTorch这边需要CUDA 12.1的运行时库而paddlepaddle-gpu当时用的是CUDA 11.8的包两个运行时在系统层面不是完全兼容的。如果通过修改环境变量强行切来切去很容易出现“一个项目能跑、另一个项目报CUDA driver version is insufficient”的尴尬局面。第三个层面是资源争抢。生成动画分镜的定时任务往往集中在凌晨一个项目把显存占满之后另一个项目的任务立刻OOM再加上定时脚本互相重叠整台机器就像早高峰的地铁。这三个层面的问题叠加在一起已经不是“手动维护几套环境变量”能解决的了。1.3 用Docker隔离换来的四个实际收益把项目迁到Docker之后我最直观的体会是环境随项目走。镜像里固化了Python版本、CUDA运行时、所有依赖库项目A的镜像不会影响项目B从开发机拷贝到生产机行为完全一致。独立重启不牵连。项目A的进程卡死或显存泄漏我只需要重启manju-a这个容器项目B和C还在正常跑。资源可见可控。每个容器消耗多少CPU、内存、显存docker stats和nvidia-smi一查就清楚不存在“谁把显存吃光了”这种罗生门。多版本CUDA共存。CUDA 11.8的项目和CUDA 12.1的项目在同一台机器上和平共处这在非容器环境下几乎是不可维护的。甚至定时任务调度这种看似与模型无关的环节在Docker里也成了依赖管理的一部分。这跟我们做漫剧推文时用青龙面板管理多账号脚本是同一个思路——只不过现在把依赖管理从脚本层下沉到了容器层彻底根治了环境互相污染的问题。2. 镜像封装分层方案从CUDA基础镜像到业务代码的落地细节2.1 为什么底层直接选官方PyTorch镜像而不是自己拼装刚开始我自己写过一个“从零装CUDA”的Dockerfile先装CUDA Toolkit再装cuDNN再装PyTorch最后还遇到驱动和运行为不匹配的问题白白折腾了两天。后来想明白一个道理官方pytorch/pytorch镜像就是在NVIDIA CUDA基础镜像之上装好PyTorch、并且把cuDNN版本配套调好的现成底座我为什么还要重复造轮子我用的基础镜像是pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime。注意我选的是带runtime后缀的版本而不是devel。devel镜像包含了完整的编译工具链体积大很多适合要编译自定义算子的场景而漫剧推文项目虽然涉及LoRA微调但使用的diffusers官方训练脚本已经编译好了所有算子runtime镜像足够用镜像体积能小差不多2GB推送到服务器和拉取都快很多。2.2 一份可以直接复用的Dockerfile拆解下面这个Dockerfile是我项目A在用的版本稍微脱敏后贴出来FROM pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime ENV LANGC.UTF-8 \ TZAsia/Shanghai \ PYTHONUNBUFFERED1 \ PIP_NO_CACHE_DIR1 RUN apt-get update apt-get install -y --no-install-recommends \ ffmpeg \ fonts-wqy-zenhei \ libgl1 \ libglib2.0-0 \ rm -rf /var/lib/apt/lists/* WORKDIR /workspace COPY requirements.txt . RUN pip install --upgrade pip \ pip install -r requirements.txt \ -i https://pypi.tuna.tsinghua.edu.cn/simple COPY src/ /workspace/src/ CMD [python, /workspace/src/run.py]每一段的意图说一下ffmpeg是视频合成绕不开的工具AI漫剧最后一步的音频视频合成、抽帧、加片头片尾都靠它。fonts-wqy-zenhei是文泉驿正黑字体如果容器里没有中文字体漫剧的字幕、标题、水印在出图时会变成方块或乱码这个问题我后面专门踩过。libgl1和libglib2.0-0是opencv的运行时依赖跑paddleocr和Pillow处理图片时经常报“libGL.so.1: cannot open shared object file”提前装好可以省掉一大半兼容性报错。依赖安装顺序也讲究。先把requirements.txt复制进镜像、装完依赖、再接后续的源码复制这样可以充分利用Docker的层缓存。后面改业务代码时只要requirements没变Docker就能直接复用之前构建好的依赖层重新build的时间从十几分钟降到十几秒。这个习惯能帮你节省大量重复等待。2.3 requirements版本锁定与跨项目错配的规避一个容易被忽略的细节是requirements.txt必须精确锁版本。如果不锁版本今天构建出来的镜像能用三个月后重新构建可能因为某个小版本升级直接跑不起来。我的项目A和项目B是两个不同的镜像两个项目的requirements如下项目ASDXL LoRA微调torch2.1.0 diffusers0.24.0 transformers4.36.2 accelerate0.25.0 peft0.7.1 opencv-python-headless4.9.0.80 pillow10.1.0 moviepy1.0.3 fastapi0.109.0 uvicorn0.25.0项目BSD 1.5出图 PaddleOCRpaddlepaddle-gpu2.6.0 paddleocr2.7.3 opencv-python-headless4.9.0.80 diffusers0.24.0 transformers4.36.2 pillow10.1.0两个项目都有opencv需求但我统一用opencv-python-headless而不是opencv-python因为后者依赖libGL等图形库在精简容器里最容易报错。项目A和项目B分别使用PyTorch和PaddlePaddle两套深度学习框架彼此不在同一个镜像里所以CUDA运行时错配的问题直接从源头消失了。3. GPU资源调度多容器抢卡时的分配策略与真实参数3.1 底层机制容器做的是设备透传不是显存隔离先说一个最容易误解的点Docker配合NVIDIA Container Toolkit以前叫nvidia-docker2把宿主机GPU设备“透传”给容器容器里的CUDA程序其实是在直接操作物理GPUDocker本身并没有提供“给容器分配多少显存”的配置项。所以不要以为docker run --gpus 0是把GPU 0上的多少GB显存划给了容器它只是把完整的GPU 0传给了这个容器。多个容器同时使用同一张卡时它们共享这张卡的显存和算力谁的程序申请显存失败就抛OOM。真正的硬件级显存隔离只有MIGMulti-Instance GPU能做到但MIG只在A100、A30这类数据中心卡上支持我们做漫剧推文的生产机基本都是RTX 4090或双卡消费卡压根不支持。所以在这个场景下靠的是“软分区”方案也就是通过应用层控制每个项目实际使用的显存大小。3.2 单卡多项目共享的软分区实操我当时是三台项目共用两台RTX 4090每张卡24GB显存。项目A的SDXL加LoRA微调峰值显存约14GB独占卡0。项目B用SD 1.5推理峰值约10GB项目C是PaddleOCR加TTS显存峰值约2GB这两个项目放在卡1上共享正好凑满24GB。要让两个项目在同一张卡上和平相处光靠Docker命令是不够的必须在容器环境变量里做配置。项目B的容器设置NVIDIA_VISIBLE_DEVICES1 CUDA_VISIBLE_DEVICES1 PYTORCH_CUDA_ALLOC_CONFmax_split_size_mb:128项目C的容器设置NVIDIA_VISIBLE_DEVICES1 CUDA_VISIBLE_DEVICES1 FLAGS_allocator_strategyauto_growthNVIDIA_VISIBLE_DEVICES是NVIDIA Container Toolkit识别物理设备用的CUDA_VISIBLE_DEVICES是容器内CUDA运行时看到的设备编号两者都设成1确保容器内只看到一张物理卡。PYTORCH_CUDA_ALLOC_CONFmax_split_size_mb:128的作用是减少PyTorch显存分配器产生的碎片防止显存明明还有余量但申请大块显存失败。Paddle的FLAGS_allocator_strategyauto_growth则让显存按需增长而不是训练或推理开始时一下子预留整张卡。这两套配置组合下来项目B实际显存峰值约10GB项目C约2GB加起来离24GB的物理上限还有很宽裕的空间两个项目的任务同时跑也不会互相踩踏。3.3 多卡分配与Compose编排的完整配置docker-compose.yml是整套多项目隔离的指挥中心。我用的compose版本是v2.x下面是简化后的完整配置services: manju-a: build: ./projects/project-a container_name: ai-manju-a shm_size: 2gb environment: - NVIDIA_VISIBLE_DEVICES0 - CUDA_VISIBLE_DEVICES0 - PYTORCH_CUDA_ALLOC_CONFmax_split_size_mb:128 - TZAsia/Shanghai volumes: - ./projects/project-a/src:/workspace/src - ./models:/workspace/models:ro - ./projects/project-a/output:/workspace/output ports: - 127.0.0.1:7861:7860 restart: unless-stopped manju-b: build: ./projects/project-b container_name: ai-manju-b shm_size: 2gb environment: - NVIDIA_VISIBLE_DEVICES1 - CUDA_VISIBLE_DEVICES1 - PYTORCH_CUDA_ALLOC_CONFmax_split_size_mb:128 - TZAsia/Shanghai volumes: - ./projects/project-b/src:/workspace/src - ./models:/workspace/models:ro - ./projects/project-b/output:/workspace/output ports: - 127.0.0.1:7862:7860 restart: unless-stopped manju-c: build: ./projects/project-c container_name: ai-manju-c shm_size: 2gb environment: - NVIDIA_VISIBLE_DEVICES1 - CUDA_VISIBLE_DEVICES1 - FLAGS_allocator_strategyauto_growth - TZAsia/Shanghai volumes: - ./projects/project-c/src:/workspace/src - ./projects/project-c/output:/workspace/output ports: - 127.0.0.1:7863:7860 restart: unless-stoppedshm_size: 2gb一定要加。容器默认的/dev/shm只有64MBPyTorch的DataLoader在跑多进程数据加载时经常因为共享内存不足直接崩溃这个坑我细讲放在后面。端口映射我特意绑定到127.0.0.1这样只有本机能访问各个容器的推理服务避免服务器上还有别人能把请求打到你的出图服务上算是个安全习惯。关于compose里GPU的写法不同版本差异比较大。老版本用runtime: nvidia加环境变量新版本推荐deploy: resources: reservations: devices: - driver: nvidia device_ids: [0] capabilities: [gpu]我自己的经验是既然团队用的Docker版本已经固定就用统一写法不要在生产环境混用两种配置不然不同成员改配置时行为不一致排查起来非常痛苦。3.4 监控资源是不是真的按预期走配置做完不等于万事大吉监控才是长期稳定的保障。我最常用的三招nvidia-smi查看每张卡上有哪些进程、占用了多少显存能快速定位“谁在偷吃显存”。docker stats查看每个容器的CPU和内存占用排查是否有内存泄漏。定时任务错峰。所有项目的生成任务不要一股脑塞在凌晨零点我通常把项目A排到0点项目B排到1点项目C排到2点再配合GPU软分区基本不会出现资源挤兑。如果管理的机器数量上去了还可以接dcgm-exporter把指标导到Prometheus和Grafana但这套对单机双卡场景来说有点重我目前都是靠上面三个轻量手段。4. 多项目隔离工程化目录设计、部署脚本与日常运维4.1 一套清晰的项目目录是隔离的基石镜像层面的隔离只解决了依赖环境文件层面的隔离同样重要。我最开始把所有项目的素材、模型、输出都混在一个大目录里后来整理了一次最终的结构长这样ai-manju-studio/ ├── projects/ │ ├── project-a/ │ │ ├── Dockerfile │ │ ├── requirements.txt │ │ ├── src/ │ │ ├── data/ │ │ └── output/ │ ├── project-b/ │ │ ├── Dockerfile │ │ ├── requirements.txt │ │ ├── src/ │ │ ├── data/ │ │ └── output/ │ └── project-c/ │ ├── Dockerfile │ ├── requirements.txt │ ├── src/ │ ├── data/ │ └── output/ ├── models/ │ ├── sdxl-base/ │ └── sd15-base/ ├── docker-compose.yml └── scripts/ ├── deploy.sh └── logs.sh每个项目都有自己的src、data、output三层目录。source代码通过volume挂载进容器改代码不用重新构建镜像data存项目自己的原始小说文本、分镜文案和临时素材output放最终生成的视频和图片。models目录只读挂载给需要底模的项目这样SDXL和SD 1.5的基础模型可以共用一份不用每个项目重复占磁盘。把源码用volume挂载而不是COPY进镜像开发期是高效的但发布到生产或给别的机器部署时我就把代码COPY进镜像再构建一次保证生产环境跑的是和镜像绑定的确定版本两边逻辑不冲突。4.2 构建与启动的一键脚本三个项目加起来的镜像在首次构建时总共要下载好几个GB的依赖手动一条条敲docker命令很容易出错。我写了一个deploy.sh#!/usr/bin/env bash set -e cd $(dirname $0) echo 构建镜像 docker compose build --parallel echo 启动服务 docker compose up -d echo 当前状态 docker compose ps echo 最近日志 docker compose logs -f --tail50--parallel可以在多核服务器上并行构建多个镜像首建时间能缩短将近一半。脚本放在git仓库里成员clone下来直接执行就能拉起整套环境不存在“我本地能跑你本地跑不了”的问题。4.3 日常更新与排查流程日常运营中遇到最多的操作有三类第一类是只改业务代码。源码是挂载的改完宿主机文件在容器里重启对应的Python进程就行。我用的是docker compose exec manju-a supervisorctl restart all这类操作或者直接docker compose restart manju-a几秒钟就完成。第二类是改了依赖需要重新构建镜像。执行docker compose build manju-a docker compose up -d manju-a由于第2章说的层缓存除非requirements变了否则构建非常快。第三类是查日志。docker compose logs -f --tail200 manju-b一条命令就能看到最近的运行记录不用再SSH进去翻文件。排查GPU相关问题时我习惯先跑nvidia-smi看宿主机侧进程再进容器跑python -c import torch; print(torch.cuda.is_available())确认容器内确实能看到GPU这样可以快速把故障定位到“驱动层”还是“容器层”。4.4 Windows本地开发与Linux生产环境的差异这套方案在Windows上也能跑但只能用来开发调试不适合做生产。Docker Desktop用的是WSL2后端GPU透传依赖WSL2自带的vGPU支持效果上能跑通SD推理但性能相比物理Linux会打折扣而且重启Docker Desktop之后的设备号偶尔会飘。经常有人遇到的问题是在Windows上启动Docker Desktop时提示“virtualization support not detected”通常是因为BIOS里没开VT-x或AMD-V或者Windows的Hyper-V和虚拟机平台功能没启用。这不是Docker的问题是虚拟化基础没准备好去BIOS开启虚拟化并启用Windows功能里的“虚拟机平台”和“适用于Linux的Windows子系统”基本就能解决。生产服务器我强烈建议直接用Linux宿主机加NVIDIA官方驱动加nvidia-container-toolkit稳定性和性能都远优于Windows加WSL2的组合。如果团队里有人用Windows做本地开发只需要把Dockerfile和compose配置保持在同一套开发结果和生产就几乎一致。5. 实测数据与踩坑修复三个项目连续运行三个月的复盘5.1 资源占用实测表整套体系稳定运行三个月之后我拉了一次各容器的资源占用数据大致情况如下项目容器名GPU分配平均显存峰值显存内存占用主要负载项目Aai-manju-a卡011GB14GB4.2GBSDXL LoRA微调项目Bai-manju-b卡18.5GB10GB3.6GBSD 1.5批量出图项目Cai-manju-c卡1共享1.2GB2GB1.1GBPaddleOCR TTS每个项目一天大约产出3到5条60秒的漫剧短视频整台双卡服务器基本满负荷运行但从早到晚没有发生过一次因为项目间抢占资源导致的OOM事故。部署之前那种“不知哪个任务把显存吃满”的焦虑感彻底消失了。5.2 坑一/dev/shm不足导致DataLoader崩溃项目A第一次在容器里跑批量数据加载时出现了这样的报错RuntimeError: DataLoader worker (pid 1234) is killed by signal: Bus error原因很隐蔽容器默认的/dev/shm只有64MB而PyTorch的DataLoader在多个worker进程之间传输数据时会用到共享内存64MB很快就被写满进程直接被系统杀掉。网上很多中文教程没有提到这一点我在compose里加上shm_size: 2gb之后问题立刻消失。这个参数对任何跑PyTorch数据加载的项目都建议直接配上。5.3 坑二容器里没有中文字体导致漫剧字幕乱码迁移到容器后的第三天运营反馈漫剧视频里的字幕变成了方块。排查了很久才想到是字体问题宿主机上有完整的中文字体库但容器是从精简镜像构建的里面一个中文字体都没有。PIL和OpenCV在出图时找不到合适字体只能用系统默认字体渲染自然就出现了方块。解决方式是在Dockerfile里安装fonts-wqy-zenhei同时确保fontconfig配置正确。从那以后我再没遇到过乱码问题。如果你用的是西文或日文素材也建议在镜像里提前装好fonts-noto-cjk这类开源字体族一套搞定中日韩。5.4 坑三CUDA driver version is insufficient for runtime version有一次我把本地构建好的镜像推到服务器上启动容器后项目B直接报错CUDA driver version is insufficient for CUDA runtime version排查链路是这样的先在服务器上跑nvidia-smi查看驱动版本再在容器里执行python -c import torch; print(torch.version.cuda)查看镜像内置的CUDA运行时版本最后确认宿主机驱动能支持的最高CUDA版本低于容器内置的12.1两者不匹配。根因是开发机和服务器驱动版本不一致。解决方案有两个升级服务器NVIDIA驱动或者把镜像基础层从CUDA 12.1降到CUDA 11.8对应的PyTorch镜像。我当时为了保住项目A的SDXL依赖选择升级了服务器驱动。这也提醒我镜像在一个环境构建后到另一个环境部署前一定要先核对驱动版本不然启动即报错。5.5 坑四paddle与torch的opencv冲突项目B只用了paddlepaddle但我在调试时发现它和另一个项目共用的镜像层里出现了libGL.so.1: cannot open shared object file的ImportError。原因是paddleocr的依赖中包含了opencv-python而cv2导入时会去找libGL精简容器镜像里没有安装图形库。解决方式两条线一是安装libgl1和libglib2.0-0系统库二是把requirements里的opencv-python统一替换成opencv-python-headless。我用的是后者因为headless版本不依赖图形环境在服务器和容器场景下更干净。这也正好说明为什么不同项目要构建不同镜像一个镜像里可以放心用headless另一个镜像如果确实需要GUI功能也有独立调整的空间互不干扰。5.6 踩过碎坑之后沉淀下来的几条经验基础镜像和requirements版本一定第一时间固定能锁多死就锁多死别用和做弹性范围。所有项目的目录挂载点语义要统一比如源码都挂到/workspace/src数据都挂到/workspace/data。统一语义之后写脚本和排查问题都省力很多。容器端口默认只绑定127.0.0.1不要暴露到局域网。AI漫剧的推理服务本质上就是一个HTTP接口暴露出去容易被别人扫到并滥用。定时任务统一从宿主机crontab触发通过docker compose exec进容器执行具体脚本。容器内不要再挂一个cron守护进程否则容器重启时容易造成任务重复或漏跑。回头再看这次从“环境混乱”到“容器隔离”的迁移最大的感受是AI漫剧推文短视频虽然看起来只是“出图、配音、剪辑”的组合但一旦把生产节奏拉起来它就是一套完整的分布式系统级应用。Docker在这里解决的不仅仅是技术问题更是一个团队协作和内容生产稳定性的问题。现在新接一个漫剧项目我只需要复制一份project目录改改Dockerfile里的底模路径和requirements版本启动一个全新容器十分钟就能给这个项目一个完全独立、干干净净的生产环境。