raylight —— Raylight(ComfyUI插件) + MiniMax-H3,4卡 FSDP 生产部署
这是什么
minimax-h3/(DiffSynth-Studio + NF4 量化,4卡各自独立 worker,能同时处理4个请求)的替代
方案:改用 ComfyUI + Raylight 插件,把4张 2080Ti
当一个 FSDP 集群跑一个请求(序列并行+权重分片),不再是4个独立worker。
2026-08-21 验证链路通、2026-08-22 完成生产部署验证并接管 h3-console 的默认后端。
详细踩坑记录见 docs/todo/raylight-comfyui-multi-gpu-eval.md,这里只写"现在是什么状态、
怎么起停、怎么回滚"。
跑在哪张卡 / 占多少显存 / 对外端口
| 项 | 值 |
|---|---|
| GPU | 0/1/2/3 全部(FSDP 集群,不是独立worker,任何时刻只处理1个请求) |
| attention后端 | 2026-08-22起SAGE_FP16(原来是TORCH_MATH,见「已知限制」第1条——这是当前最重要的一次架构改动) |
| 显存/时长 | SAGE_FP16上线后,832×480/448×256/1024×576/1344×768 四档实测到的帧数全部成功,没有再找到OOM边界,见「已知限制」第1条和notes.md |
| 对外端口 | raylight-server.py(8100,取代 minimax-h3/gateway.py+api.py两层的对外契约)+ ComfyUI 自身(8188,仅内部调用,不对外) |
| console 接入 | h3-console 8080/8081 两个实例的 H3_GATEWAY_URL 都已指向 http://127.0.0.1:8100,H3_ENABLE_SUPERVISOR 已关闭(分区flip的概念在FSDP集群架构下不适用) |
怎么起
ssh admin@192.168.110.67
bash /data/models/raylight/start.sh
脚本会按端口停掉已在跑的 ComfyUI(8188)/raylight-server(8100),重新拉起两者,轮询健康检查。
照抄的是 minimax-h3/start-all.sh 的模式(按端口 ss -lptn 找PID kill,不用 pkill -f)。
怎么停 / 回滚到旧的 minimax-h3
# 停 raylight
ssh admin@192.168.110.67 'PID=$(ss -lptn "sport = :8100" | grep -o "pid=[0-9]*" | cut -d= -f2); kill $PID
PID=$(ss -lptn "sport = :8188" | grep -o "pid=[0-9]*" | cut -d= -f2); kill $PID'
# 拉回旧的 minimax-h3(代码/权重保留,只是不再默认启动,start-all.sh 还在)
ssh admin@192.168.110.67 'cd /data/models/minimax-h3 && export H3_OFFLOAD=disk && ./start-all.sh'
# h3-console 两个实例改回旧后端(重启时把 H3_GATEWAY_URL 换回 http://127.0.0.1:8000,
# H3_ENABLE_SUPERVISOR=1(8080那个要开),具体环境变量见 h3-console/README.md)
已知限制(如实记录,别重复踩)
- 【已解决,2026-08-22】旧的"832×480×124帧/448×256×362帧/1024×576×60帧都会OOM"结论已过时。
根因找到了:环境里从来没装过给Turing(sm_75)编译的
sageattention,XFuser_attention只能 退回TORCH_MATH——这个后端会把完整attention分数矩阵物化在显存里,显存开销随序列长度平方 增长。装上社区维护的Turing版SageAttention2(github.com/1506086927/SageAttention2_Optimized_Test, 经github.com/Eanya-Tonic/2080ti-minimax-h3的补丁接入,Apache 2.0),把XFuser_attention换成SAGE_FP16后,这三个档位全部通过,而且快很多(124帧从OOM变成100秒)。 之前试过的"换小文本编码器""挪CLIP/VAE到别的GPU""开expandable_segments"这几条路全都没用 (瓶颈根本不在那几个地方)——真正的根因一直是attention kernel选型,不是显存怎么腾挪。 2026-08-23 凌晨补充实测(修完第2条的完成检测bug后,重新摸了一遍真实上限,见notes.md完整数据表):
| 分辨率 | 已验证能跑 | 真实上限 | |---|---|---| | 832×480 | 1125帧(约68分钟) | 1195帧真实CUDA OOM(GPU0耗尽)——离API硬上限1200帧很近,两者离得很近,没细分谁是真正瓶颈 | | 448×256 | 1200帧(50秒视频,约10.8分钟) | 不适用——这个分辨率是画质卡的,不是显存卡的,见下方单独说明 | | 1024×576 | 743帧(约62分钟) | 787帧真实CUDA OOM | | 1344×768(官方默认分辨率) | 412帧(约55分钟) | 431帧真实CUDA OOM |
⚠️ 【2026-08-24补充,重要】上面这张表全部是0张参考图(partition: text)测出来的,
带参考图(ref2va/fl2va)不能直接套用——参考图的conditioning张量会广播到每个FSDP
rank,显存开销明显更高。实测"1344×768×362帧×3参考图"这个精确配置当天5次尝试4次失败
(1次OOM在GPU0,1次OOM在FSDP初始化阶段的GPU2/GPU3,不再是"只有GPU0会先炸"),完整
证据见notes.md「2026-08-24(晚)」一节。带参考图时别直接信这张表的帧数上限,
目前没有专门测过参考图数量×帧数的边界,这是个空白。
所有真实OOM都是GPU0先炸(跟之前"GPU0同时扛文本编码器+DiT分片"的结论一致),且都在提交后
30秒内报错(采样刚开始分配显存就失败,不会跑到一半才炸)。每个done结果都额外SSH核实过
视频文件真实存在、大小合理,不是只信API的status字段。
448×256 是画质限制,不是显存限制——这是故意留着的结论,不是没测完的TODO:技术上把
GenReq.frames的上限从1200调到3000后继续测过,2700帧、3000帧全部成功且GPU显存远没跑满
(3000帧用了约44分钟,没有OOM迹象),说明这个分辨率的真实VRAM上限比3000帧还高,没有真正
探到。但用户实际看过1200帧(50秒/24fps)以上的输出后判定画面质量从50秒往后就不能看了
——所以推荐的实际上限是1200帧/50秒,卡点是主观画质而不是硬件承受能力,继续往上探
VRAM上限没有实际价值,不需要再花时间找。
2026-08-23:这张表已经写进两边代码强制生效,不只是文档——server.py的
RESOLUTION_CEILINGS会在提交阶段直接拒绝超过实测上限的请求(422,报真实上限数字),
h3-console/static/index.html的RESOLUTIONS[].maxFrames同步这份数据,选了分辨率之后
时长下拉框会自动把超上限的档位过滤掉——不用等提交了才知道会OOM。两边数字改动要一起改,
对不上就会出现"前端放行、后端拒绝"或反过来。新建任务默认分辨率改成了1344×768(官方
默认分辨率)/5秒。
- 【2026-08-23发现并修复】
server.py里"ComfyUI已经执行完了,但任务状态卡在running转不成done、单队列被永久堵死"的bug——真根因找到了:_run_job_async的finally里tail_task.cancel()/ws_task.cancel()之后await这些task,用contextlib.suppress (Exception)接——但asyncio.CancelledError在py3.8+是BaseException的子类,不是Exception的子类,压不住,会直接飞出finally绕过worker()的except Exception, 导致status卡死在running但finished_at已经被写上了。跟之前怀疑的"websocket断线/ 丢消息"是同一个bug的不同表现,不是两个问题。已修:改成contextlib.suppress(asyncio.CancelledError);同时把完成判定从"只信websocket的execution_success消息"改成轮询/history/{prompt_id}(websocket现在只用来上报进度, 断了不影响完成判定,双重加固)。用真实console任务验证过修复生效。 - 【2026-08-23修复】
_load_jobs()启动时会崩:sqlite3.OperationalError: database is locked——_load_jobs()用一个连接的SELECT游标还开着的时候,循环里调_persist()拿新连接 写,SQLite默认rollback-journal模式下第二个连接的写commit会被第一个连接的读事务锁住。 已修:_load_jobs()先把SELECT结果整个取成list、关掉连接,再在循环里写。这个bug是这次 测试期间由本目录内另一个并发进程引入的(_load_jobs()加了失败原因落盘的逻辑,改动本身 是对的,只是漏了这个连接冲突),修复已经跑通验证过。 - warm-reuse 效果很有限,别指望"第二次会快很多"。理论上
RayInitializer/RayUNETLoader等重节点的输入值不变,ComfyUI 会跳过重建集群直接复用;实测确实跳过了 NCCL通信测试和 FSDP分片这两步,但文本编码器(CLIPLoader)每次都在重新加载,原因未查——两次64帧测试 耗时分别是 822s(首次,紧跟着服务重启)和 686s(第二次,配置逐字节相同),只省了约17%, 不是"9分钟降到几十秒"那种量级。每个请求实际都要 11-14 分钟,这是当前要接受的真实延迟。 - 并发数从4降到1——这是架构级代价,不是bug。现在4卡是4个独立worker能同时处理4个请求;
这套方案4卡是一个集群,ComfyUI 会排队多个
/prompt提交但严格串行执行。高峰期多人同时 提交会互相排队等,不会像以前那样4个同时跑。 - cfg_scale / negative_prompt / turbo 是 no-op——请求体接受这些字段(跟 console 现有
契约保持兼容),但 Raylight 的 MiniMax H3 pipeline 本身不支持 CFG(
cfg_degree只能是1, Raylight 自己的支持列表也写了 CFG❌),turbo LoRA 还没接到 workflow graph 里,传了也不生效。 - 【2026-08-22修复】进度上报(
step/total_steps)以前是坏的,现在能用了——根因: Raylight 的实际采样循环跑在 Ray 分布式 worker 进程里,不是本地 ComfyUI 进程直接算的, 真实的逐步进度没有通过 ComfyUI 本地的progress_state机制传回来,websocket 那条路永远 拿到total_steps=1这种假数据。退而求其次:轮询子进程 tqdm 直接打到 stdout、被comfyui-server.log捕获的那行文字(_PROGRESS_RE,server.py),只信任total等于 本次请求steps的那次匹配(避免跟FSDP初始化阶段同样打"Model Initializing"但计数是GPU数 的另一个进度条搞混)。这是个ponytail级别的退路方案,上游改了这行日志的措辞就会失效 (退化成进度条不动,不影响任务本身跑成功)。elapsed_seconds(挂钟时间)一直是准的。 - actor 崩溃不会自动恢复——Raylight 的
ensure_fresh_actors(负责检测到模型已加载/actor 崩溃时重建)里有个确认过的 bug:needs_restart = False无条件覆盖了前面刚算出来的检测 结果,这段自愈逻辑现在是死代码。真机测试中一次 OOM 后 GPU1/2/3 卡在接近满载没释放, 必须手动bash start.sh整个重启才能回收——这不是我们这边能修的,是上游代码的问题。 - 【2026-08-24修复】
/health之前不会真的探测 ComfyUI(8188) 存不存活,会骗人报健康—— 跟第8条是同一类事故的另一半:一次 actor 崩溃(ray.exceptions.ActorDiedError)之后, ComfyUI(8188) 进程本身直接退出了,但server.py(8100) 这层壳没死。根因:/health的ready字段只读STATE["model"],这个值在worker()后台线程启动时设一次"ready", 之后再也不更新——不管 8188 是死是活,/health//v1/backends永远报同一个值。真实 故障中这个盲区持续了近20分钟,期间 console 界面一直显示"ready",但提交的每个任务都在 1秒内秒失败(ConnectError: All connection attempts failed,请求根本连不上已经不存在 的8188),是人工发现任务反常才追查到8188已经不在监听。已修:/health现在每次都 用跟start.sh自己就绪检测同一个端点(GET {COMFY_BASE}/system_stats,2秒超时)真的 探测一次8188,探测失败就把ready降级成0、model字段报"comfyui_unreachable",不再 单纯相信内存里那个只写一次的标志位。这条不能顶替第8条——探测准了只是能更快发现 崩溃,8188死了以后还是要手动bash start.sh重启,自愈本身仍然没有。 - 【2026-08-25修复】
watchdog.sh的卡死判定漏了"采样跑完、VAE解码存盘中"这个阶段, 把正常任务当卡死误杀重启——watchdog.sh(2026-08-23新增,按step字段判断任务是否 卡死,见该脚本文件头注释)只区分"FSDP初始化中(step=0,20分钟宽限)"和"denoise采样中 (step>0,10分钟宽限)"两个阶段,没考虑到step到达total_steps后(采样已经跑完)还有 VAE视频/音频解码+存盘这段没有独立进度信号的尾巴,step会一直停在最终值不变。这段被 并入了10分钟宽限那档,实测至少一个1344×768长帧数案例这个阶段本身就超过608秒—— watchdog判定"卡死"、自动执行bash start.sh整机重启,把当时排队中的一整批任务 (h3-showcase-cases批量测试,5个)连带打断,跟"服务被别人手动重启"表现完全一样, 一开始容易误判成人为操作。已修:给step>=total_steps这个"采样完/存盘中"阶段单独 一档STALL_LIMIT_POSTPROC=1800(30分钟宽限),不再跟denoise阶段共用10分钟的紧阈值。 排查经验:见到"进程重启导致任务被取消"这类错误,先查watchdog.log有没有 "卡死判定触发"字样,比先怀疑人为操作或start.sh被谁手动跑了更快定位——这台机器上 至少有两套独立的自动重启机制(这个watchdog + 8月24日排查过的PM2),别再默认是人。
多参考图生视频支持参考视频/独立音频参考(2026-08-24 新增)
MiniMaxH3ReferenceToVideo 节点本身支持的输入比之前接的多——查 ComfyUI /object_info 发现
除了 ref_images(参考图,早就接了),还有 ref_videos(参考视频,最多3段)、
ref_video_audios(对应视频自己的音轨)、ref_audios(独立音频参考,最多3个,不跟任何
视频绑定)。之前只做了参考图这一种,现在都接上了:
GenReq加了reference_video_urls/reference_audio_urls(各自最多3个)。- 参考视频走
LoadVideo→GetVideoComponents拆成 images/audio 两路,images 接ref_videos.ref_video_{i},audio 自动接ref_video_audios.ref_video_audio_{i}——音轨 是从视频文件自己解出来的,不用用户再单独传一份,这是"同一段视频自己的音轨"这个语义 最省事的实现方式。 - 独立音频走
LoadAudio直接接ref_audios.ref_audio_{i}。 - console 的"多参考图生视频"tab 加了两个上传框(参考视频/独立音频参考),
/v1/assets上传接口按扩展名分流校验(视频100MB上限、音频30MB上限,图片还是20MB)。 - 端到端测过:2秒测试视频(带音轨)+独立音频文件,
status正确转done,视频文件正常生成。
LoadVideo/LoadAudio/GetVideoComponents 都是 ComfyUI 核心节点(comfy_extras),不是
自定义节点包,没有额外安装/兼容性风险。
相关文档
docs/todo/raylight-comfyui-multi-gpu-eval.md——完整调研+验证+生产部署过程,踩过的坑 按时间顺序都在里面。notes.md——本目录内,被推翻的假设和精简版实测数据表。minimax-h3/README.md——旧系统(现在是热备份)的架构和踩坑记录,仍然有效。