来源:2026-09-28 ~ 09-29,《一直很安静》竖屏 MV 项目(45 镜 AI 生成 + 1 镜 ffmpeg 尾卡,连续运行约 24 小时) 任务量:45 个分镜,单镜约 28 分钟,总产出含重跑 记录人:DSH 会话(含用户多次指正) 结论一句话:这次真正的坑只有两个——一个是我"优化"出来的(模型不卸载),一个是我写错的(提示词注入 bug);其余都是参考图卫生与提示词写法问题。
| # | 教训 | 代价 |
|---|---|---|
| 1 | 不要"优化"工作流的内存管理。把 cleanup_after_run 从工作流原值「卸载模型」改成「不处理」,导致 35GB 模型在 30GB 内存机器上跨镜累积,当天整机硬挂 4 次 | 4 次硬挂 + 数小时重跑 |
| 2 | 生成的提示词必须与实际输入对账。build_prompt 把「场景描述文本」当成「场景图列表」用,每个提示词被注入 25~60 条引用不存在图片的 <Picture N> 声明 | 三人镜连续失效,排查数轮 |
| 3 | 参考图必须"净化"到单一主体、无文字。整张三视图→拼贴;带「背面/BACK」标签的裁图→标签渗进画面 | 2 镜重跑 |
| 4 | 负面提示词会反向诱导。写「不要悬浮的人脸」→ 脸变得更大;写「不要拼贴」→ 更容易拼贴 | 1 镜白跑 2 轮 |
| 5 | 长跑必须配自愈监督脚本(探活→取证→拉起→断点续跑),而不是人盯着 | 否则 6 小时 timeout 后无人接管 |
当天 GPU 机(RTX 5060 Ti 16GB / 30GB RAM)硬挂 4 次:02:26、05:04、11:33、约 16:10。症状均为:ping 100% 丢包、SSH No route to host、ARP INCOMPLETE,且不触发 kernel.panic 自动重启,需人工断电。
AICG3D 工作流(AICG3D-13-渲染器二采高清放大-可用版.json)里,两个渲染节点的 cleanup_after_run 原值是 「卸载模型」。
我为了"避免每镜重新加载模型、提速",把它改成了 「不处理」。后果:
该工作流要加载:UNET 20GB + 文本编码器 15GB = 35GB
宿主机内存:30GB
「不处理」→ 模型跨镜持续驻留不释放 → 内存压力累积 → 整机冻结
时间线完全吻合:
03:49-04:17 S01(用「卸载模型」) → ✅ 成功
04:17 我改成「不处理」 ← 错误决定
04:18-04:50 S02 → 勉强通过
04:50-05:04 S03 → ❌ 硬挂(第 2 次)
11:29-11:33 S03 重跑 → ❌ 硬挂(第 3 次,仅 3 分钟)
(改回「卸载模型」后)
12:16-次日 连续 24 小时+ / 45 镜 → ✅ 零崩溃,内存压力告警 0
第 3 次硬挂后我一度断言:"内存压力是元凶,加了 --disable-pinned-memory 就能稳定跑完"。这个结论被证伪——第 3 次硬挂恰恰发生在该参数已生效、内存压力告警为 0 的情况下。
教训:不要把单次相关性当因果。当时唯一抓到的内核异常是内存压力,但真正的原因(模型不卸载)我还没意识到。正确说法应是"内存压力是一个可疑因素,但不足以解释全部"。
运维记录里长期写着"这台机器没有崩溃捕获能力"。这个结论是错的:/var/log/journal 是持久化的,journalctl --list-boots 能列出历史启动,journalctl -b -1 能读上一次启动的完整内核日志。
关键做法:在机器恢复的"第一时间"抓(错过就被新启动的日志冲淡)。已封装为 ~/.tools/capture_after_recovery.sh,抓 9 项:
1. uptime / who / last -x reboot(判断自动重启还是人工断电)
2. 上一次启动的完整内核日志 journalctl -k -b -1
3. 上次启动的 Xid|nvrm|gsp|fell off|pcie|aer|oom|hung_task|BUG:|panic
4. 当前启动的内核错误
5. journald 是否持久化(-b -1 是否可用)
6. nvidia-smi -q 的 PCIe 代际/降速、Retired/Remapped 页、温度、功耗上限
7. NVreg_DynamicPowerManagement 与 kernel.panic* 设置
8. ComfyUI 日志尾部(死亡时的最后动作)
9. ram_guard 痕迹与日志轮转情况
实际抓到的东西(第二次硬挂):
04:19:31 systemd-journald: Under memory pressure, flushing caches.
04:56:30 systemd-journald: Under memory pressure, flushing caches.
05:04:39 ← 日志戛然而止:无 panic、无 Xid、无 OOM、无正常关机
「日志直接断掉」= 内核冻结,这就是"硬挂"的定义。
cd /home/zyw/ComfyUI && setsid nohup env \
PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:512,expandable_segments:True \
CUDA_VISIBLE_DEVICES=0 OMP_NUM_THREADS=4 \
venv/bin/python main.py --listen 0.0.0.0 --port 8189 \
--lowvram --force-fp16 --reserve-vram 1.0 --fast-disk \
--cache-lru 2 --disable-pinned-memory --preview-method none \
> /home/zyw/comfyui_8189_opt.log 2>&1 < /dev/null &
--disable-pinned-memory:必须有。ComfyUI 默认会按公式 pin 住约 27GB「不可换页」宿主内存,而机器只有 30GB(09-15 取证报告的结论)--cache-lru 2:降缓存占用--fast-disk:偏向磁盘后备、少占非 pinned RAM# 调用点
chars, scene, sdesc, motion, sound = SHOTS[sid]
prompt = build_prompt(sid, chars, sdesc, motion, sound)
# ^^^^^ 第 3 个位置传的是「场景描述文本」
def build_prompt(sid, chars, scene, motion, sound):
...
for j in range(len(scene)): # scene 这里是字符串,len() = 文本字数
defs.append(f"<Picture {len(chars)+j+1}> 是 [镜头1] 的环境与光照依据。")
场景描述有多少字,就往提示词里塞多少条「环境依据」图片声明。实测每个提示词被注入 25~60 条引用不存在的图片,而实际只传了 2~4 张图。
模型的反应:把参考图当作独立元素去拼贴,而不是组合进同一个场景。表现为三人镜「某个角色整个消失」或「三人被生硬拼贴、比例悬殊」。
def build_prompt(sid, chars, scene_imgs, sdesc, motion, sound):
"""scene_imgs = 场景图路径列表;sdesc = 场景描述文本(两者必须分开)"""
验证方法(应固化为习惯):生成提示词后,数一遍 <Picture N> 声明的条数,必须等于实际传入的图片数。
# 自检
decl = [l for l in prompt.split('\n') if l.startswith('<Picture')]
assert len([d for d in decl if '环境依据' in d]) == len(scene_imgs)
| 事故 | 素材问题 | 现象 |
|---|---|---|
| 拼贴 | 用整张三视图(含正面/侧面/背面)作参考 | 画面出现「大幅正面像里嵌着缩小背影」 |
| 标签渗出 | 裁三视图时把「背面 / BACK」中英文标签一起裁进去 | 成片画面上方出现「背面 / BACK」文字 |
| 主体冲突 | 用正面参考图,但镜头要求「走离、只给背影」 | 天空出现巨大悬浮人脸 |
参考图里的任何文字、标注、多余主体,模型都可能原样复制到成片里。 参考图的视角必须与镜头的机位一致。
人物参考/正面单图/<角色>_front.jpg
人物参考/侧面单图/<角色>_side.jpg ← 记得裁掉顶部 8% 的「侧面/SIDE」标签
人物参考/背影单图/<角色>_back.jpg ← 同样裁掉「背面/BACK」标签| 我写的 | 结果 |
|---|---|
| 「只给背影」+ 加「不要出现悬浮的人脸、人像叠影、或天空中的面孔」 | 浮脸变得更大更清晰 |
| 「画面中只出现这一个实例,不要出现第二个身影、不要拼贴、不要镜像」 | 拼贴概率似乎上升 |
| 「画面只见纸张的背面」(想规避文字) | 模型仍渲染纸张正面并生成乱码汉字 |
STYLE 里的通用否定(不要文字/水印/现代元素)保留即可,但对具体名词(告示、符文、牌匾)无效「告示 / 符文 / 牌匾 / 匾额」→ 模型一定生成汉字,且几乎都是乱码。
三种处置(按优先级):
drawtext)——符合原定原则「文字后期叠加,不让图像模型生成」三人 + 场景(4 张参考图)时,模型会把每个 <Subject> 当成要"展示"的独立元素拼贴,而不是组合进同一场景。
<Picture 1>,人物参考依次排后 —— 先给场景锚点
def scene_first(sid, chars, scene):
return len(chars) >= 3 and len(scene) >= 1
注意:subject_definitions / retention_analysis 里的 <Picture N> 编号与 <Subject N> 编号要同步重排
detailed_description 里显式点名每个人物的位置
画面中三个人物同时出现在同一镜头里:
<Subject 2>(少年剑客)与 <Subject 3>(蓝衣少女)并肩走在画面前景、身体微微相靠;
<Subject 1>(紫衣少女)走在他们身后数步、位于画面左侧的中景处,处于浅景深之外、略微虚焦。
三人的身高比例一致。实测:只做 1+2 就能让 S16/S25 通过;S12/S13 加上 3 后也通过。
S19 的动作设计只描述了狐妖女,但我误把林月如也放进了 chars → 林月如喧宾夺主,狐妖女被挡。
镜头里不该出现的人,绝不能放进
chars。
建议固化为审计(本次就是这么抓出来的):
for sid in SHOT_ORDER:
chars, scene, sd, mo, so = SHOTS[sid]
hit = []
if '紫衣少女' in mo: hit.append('L')
if '少年' in mo or '剑客' in mo: hit.append('X')
if '蓝衣少女' in mo: hit.append('Z')
if chars and not hit: print(f'{sid} ⚠️ 传了角色但动作里没提到')
if not chars and hit: print(f'{sid} ⚠️ 没传角色但动作里提到了')
(注意:用「三人」「两名主角」这类泛称时会误报,需人工复核)
~/.tools/mv_aicg3d_supervisor.sh 的循环:
每 60s 一轮:
探活 ping
├─ 不可达 → 标记 WAS_DOWN=1,等 60s 重试
└─ 可达
├─ WAS_DOWN=1 → **第一时间跑 capture_after_recovery.sh 取证**
├─ ComfyUI 未就绪 → 自动拉起(含正确参数)
└─ 断点续跑 mv_aicg3d.py
本次实测有效两次:
05:03:21 批次被 6h timeout 杀掉(rc=124)
05:04:21 监督脚本第 93 轮:已完成 19/45 → 自动重启批次
05:04:22 按顺序跳过已完成,补跑 S15
11:04:21 批次被 6h timeout 杀掉(rc=124)
11:05:21 监督脚本第 94 轮 → 自动重启
11:05:22 补跑 S24(用去标签版参考图)
17:05 同样机制补跑 S34
配合
timeout 21600(6 小时)使用:批次定期被杀重启,既避免单次运行过长,又自动捡起所有缺失镜头——这成了本次的自愈核心。
启动时先 ssh 列一遍 GPU 机产出目录,已有的直接下载跳过,不重复提交:
def scan_remote_existing():
"""扫 GPU 机 output/video 里已有的 AICG3D_Pass2_<镜号>_*.mp4,返回 {镜号: 文件名}"""
...
注意:若要重跑某镜,必须同时删除 ① 本机产出 ② 运行日志里的记录 ③ GPU 机上的产出文件 —— 否则续传扫描会把它重新下载回来、跳过重跑。
坑 1:AICG3D 的 SaveVideo 在 /history 里把结果放在 images 键,不是 videos/gifs:
for key in ("images", "videos", "gifs", "animated"):
for v in (out.get(key) or []):
if isinstance(v, dict) and v.get("filename"):
files.append(v)
坑 2:等待上限太小会漏文件。S24 生成超过 40 分钟,超出 wait_and_fetch(pid, timeout_min=40),批次记成 FAIL 1files,只下载了 Pass1、Pass2 留在 GPU 上。
修法:上限提到 90 分钟;并校验下载文件数是否等于预期的 SaveVideo 节点数(本工作流是 2)。
1. 确认 ComfyUI 队列为空(等间隔期,不要在采样中途动手)
2. 移走本机产出到 问题记录/
3. 从 运行日志.tsv 删掉该镜记录
4. 删掉 GPU 机上的产出文件
5. kill 批次进程(监督脚本会在下一轮自动重启并补跑该镜)
⚠️ 一定按 PID kill,不要用 pkill 模式
不要清空 ComfyUI 队列——那会强制 35GB 模型重新加载,制造内存/IO 尖峰(这正是第 3 次硬挂的可疑诱因)。
| # | 错误 | 后果 | 正确做法 |
|---|---|---|---|
| 1 | 为"提速"改 cleanup_after_run | 4 次硬挂 | 不要动工作流已调好的内存管理参数 |
| 2 | 过早宣布根因("内存压力+缺 pinned 参数") | 结论被第 3 次硬挂证伪,需公开更正 | 区分"相关"与"因果";标注置信度 |
| 3 | 为换参考图清空 ComfyUI 队列 | 强制模型重载,疑为硬挂诱因 | 改动一律等批次间隔期 |
| 4 | pkill -f "mv_aicg3d_superviso[r].sh" | 匹配到自己的命令行、把自己杀了,新旧监督脚本一起死 | 按 PID kill;或确保模式不出现在自己的命令行里 |
| 5 | 一开始只挂了一个 SaveVideo | 用户要求"一采也要落盘"后才补 | 先对齐工作流的完整节点结构(工作流有 2 个 SaveVideo) |
| 6 | 为修缺陷多次重启批次 | 拖长了单镜完成时间 | 先攒够问题,再一次性处理 |
| 7 | 漏了 --disable-pinned-memory | 前两次硬挂时该参数缺失 | 启动参数对照运维记录的修复项逐条核对 |
| 项 | 值 |
|---|---|
| 工作流 | AICG3D-13-渲染器二采高清放大-可用版.json |
| 模式 | reference(REF2VA) |
| 分辨率 | 768P / 9:16 / 768×1344 → Pass2 ×1.2 → 928×1664 |
| 帧率 | 24 fps |
| 时长 | 5 秒 |
| Pass1 | res_multistep + simple,3 步,denoise 1.0 |
| Pass2 | res_multistep + simple,8 步,second_pass_denoise 0.5 |
| 潜空间放大 | minimax_h3_latent_upscaler_3d_fp16.safetensors,upscale_scale 1.2 |
cleanup_after_run | 卸载模型(两个节点都要) |
| LoRA | minimax/taomate_h3_3step_comfy.safetensors,strength 1.0 |
| 模型 | UNET minimax_h3_{fl2va,ref2va}_pruned_int8_convrot / 文本编码器 qwen3vl_32b_minimax_h3_nvfp4_awq |
| 单镜耗时 | 约 28 分钟 |
POST /prompt 提交工作流(API 图)
GET /history/<pid> 查结果 → outputs[<node>].images[] ← **注意是 images**
POST /upload/image 上传参考图
GET /view?filename=&subfolder=&type=output 下载产出
POST /interrupt 中断当前任务
POST /queue {"clear":true} 清空队列(⚠️ 慎用,见 §6.4)
GET /system_stats 探活
GET /object_info/<节点> 查节点输入规格
# 逐镜:统一 1080x1920 + 按目标时长裁切(S08 从 1.2s 起)
ffmpeg -y -nostdin -i "$f" \
-vf "scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,fps=30,\
trim=start=${off},setpts=PTS-STARTPTS,tpad=stop_mode=clone:stop_duration=20" \
-t "$dur" -an -c:v libx264 -crf 16 -preset veryfast -pix_fmt yuv420p "$out"
# 拼接
ffmpeg -y -f concat -safe 0 -i concat.txt -c copy v_noa.mp4
# 混音乐
ffmpeg -y -i v_noa.mp4 -i "$SONG" -map 0:v:0 -map 1:a:0 -c:v copy -c:a aac -b:a 192k -shortest v_music.mp4
# 烧字幕
ffmpeg -y -i v_music.mp4 -vf "subtitles=字幕.srt:force_style='FontName=Source Han Serif SC,\
FontSize=17,PrimaryColour=&H00FFFFFF,OutlineColour=&H80000000,BorderStyle=1,Outline=2,Shadow=1,MarginV=150'" \
-c:v libx264 -crf 17 -preset medium -c:a copy 成片.mp4
⚠️ 循环里的 ffmpeg 必须加
< /dev/null(或-nostdin),否则它会吃掉while read的 stdin,导致循环只处理前几条。
本次所有"跑很多轮才修好"的事故,根源都是没有在下游对账上游:
<Picture> 条数(§2)建议固化为 checklist:
提交前:提示词里的 <Picture N> 条数 == 传入图片数?
参考图无文字/标签/邻图?视角与镜头机位一致?
chars 里的每个人都在动作设计里出现了?
提交后:SaveVideo 节点数 == 下载到的文件数?
画面里有没有不该有的元素(文字/浮脸/多余人)?
45 镜如果逐镜细看,成本极高。有效做法是"分组抽帧做对比图":
# 每镜取 3 个时间点拼一行,多镜拼成一张总览
ffmpeg -pattern_type glob -i "$tmp/*.jpg" -filter_complex "tile=3x2:padding=5" -frames:v 1 复核.jpg
本次靠这个方法在半小时内复核完全部 45 镜,抓出 4 处缺陷。
待修复清单.md 的格式(问题 / 根因 / 修法 / 状态)在整个过程中反复被引用,是有价值的。特别是:
我在会话中一次把"内存压力"当成确定结论向用户断言,随后被证伪。在取证报告里专设了「重要修正」一节,写明原结论、被证伪的证据、以及新的假设。
这比悄悄改口更有价值——后来者能看到"这个假设已经被排除过"。
| 内容 | 路径 |
|---|---|
| 成片 | dl-hub/62-仙剑1MV素材/MV制作/MV-一直很安静-竖屏成片.mp4 |
| 完成报告 | dl-hub/62-仙剑1MV素材/MV制作/竖屏MV制作完成报告.md |
| 缺陷清单(含 8 处修复全过程) | dl-hub/62-仙剑1MV素材/MV制作/AICG3D-竖屏分镜/待修复清单.md |
| 缺陷证据与坏产物存档 | dl-hub/62-仙剑1MV素材/MV制作/AICG3D-竖屏分镜/问题记录/ |
| 崩溃取证报告(含更正) | dl-hub/06-ComfyUI-H3运维记录/崩溃取证/2026-09-28-首次崩溃取证报告.md |
| 崩溃原始现场 | dl-hub/06-ComfyUI-H3运维记录/崩溃取证/recovery_20260928_*.txt |
| 监督脚本 | ~/.tools/mv_aicg3d_supervisor.sh |
| 取证脚本 | ~/.tools/capture_after_recovery.sh |
| 逐镜生成脚本 | ~/.tools/mv_aicg3d.py |
| 合成脚本 | ~/.tools/mv_assemble_final.sh |