# raylight —— Raylight(ComfyUI插件) + MiniMax-H3,4卡 FSDP 生产部署 ## 这是什么 `minimax-h3/`(DiffSynth-Studio + NF4 量化,4卡各自独立 worker,能同时处理4个请求)的替代 方案:改用 ComfyUI + [Raylight](https://github.com/komikndr/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集群架构下不适用) | ## 怎么起 ```bash 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 ```bash # 停 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) ``` ## 已知限制(如实记录,别重复踩) 1. **【已解决,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 | 所有真实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秒。 2. **【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任务验证过修复生效。 3. **【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()`加了失败原因落盘的逻辑,改动本身 是对的,只是漏了这个连接冲突),修复已经跑通验证过。 4. **warm-reuse 效果很有限**,别指望"第二次会快很多"。理论上 `RayInitializer`/`RayUNETLoader` 等重节点的输入值不变,ComfyUI 会跳过重建集群直接复用;实测确实跳过了 NCCL通信测试和 FSDP分片这两步,但**文本编码器(CLIPLoader)每次都在重新加载**,原因未查——两次64帧测试 耗时分别是 822s(首次,紧跟着服务重启)和 686s(第二次,配置逐字节相同),只省了约17%, 不是"9分钟降到几十秒"那种量级。每个请求实际都要 11-14 分钟,这是当前要接受的真实延迟。 5. **并发数从4降到1**——这是架构级代价,不是bug。现在4卡是4个独立worker能同时处理4个请求; 这套方案4卡是一个集群,ComfyUI 会排队多个 `/prompt` 提交但严格串行执行。高峰期多人同时 提交会互相排队等,不会像以前那样4个同时跑。 6. **cfg_scale / negative_prompt / turbo 是 no-op**——请求体接受这些字段(跟 console 现有 契约保持兼容),但 Raylight 的 MiniMax H3 pipeline 本身不支持 CFG(`cfg_degree` 只能是1, Raylight 自己的支持列表也写了 CFG❌),turbo LoRA 还没接到 workflow graph 里,传了也不生效。 7. **【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`(挂钟时间)一直是准的。 8. **actor 崩溃不会自动恢复**——Raylight 的 `ensure_fresh_actors`(负责检测到模型已加载/actor 崩溃时重建)里有个确认过的 bug:`needs_restart = False` 无条件覆盖了前面刚算出来的检测 结果,这段自愈逻辑现在是死代码。真机测试中一次 OOM 后 GPU1/2/3 卡在接近满载没释放, 必须手动 `bash start.sh` 整个重启才能回收——这不是我们这边能修的,是上游代码的问题。 ## 相关文档 - `docs/todo/raylight-comfyui-multi-gpu-eval.md`——完整调研+验证+生产部署过程,踩过的坑 按时间顺序都在里面。 - `notes.md`——本目录内,被推翻的假设和精简版实测数据表。 - `minimax-h3/README.md`——旧系统(现在是热备份)的架构和踩坑记录,仍然有效。