# 踩坑记录 / 被推翻的假设 ## 2026-08-24(晚)— 【被推翻】"1344×768 撑到412帧不OOM"这份结论不适用于带参考图的请求 ### 背景 `README.md`已知限制第1条、本文件下方"2026-08-23(凌晨)"那份sweep表里"1344×768 412帧done/ 431帧OOM"的结论,提交时用的是`partition: text`(纯文生视频,**0张参考图**,跟 `task.html/29a99dc412b2`同款prompt,`reference_image_urls`为空)。这份表被当成"1344×768 撑到412帧都安全"的通用结论在用,但**从没验证过带参考图(ref2va)时是否成立**。 ### 实测:同一个"1344×768×362帧×3参考图"配置,今天5次尝试4次失败 查`console.sqlite3`的`jobs`表按时间排: | job id | 提交时间 | 结果 | 报错 | |---|---|---|---| | `d71c09a38248` | 16:12 | **成功** | — | | `c53f8d3ad7e2` | 18:57 | **OOM** | `CUDA out of memory`,GPU0 21.48GB只剩几MB free | | `1a764bc79895` | 19:17 | **OOM** | `Tried to allocate 148.00 MiB. GPU 0 ... 14.25 MiB is free` | | `642b82111d28` | 19:24 | **失败** | `ncclUnhandledCudaError`(这次是重启服务时把它硬杀掉导致的次生错误,不算独立样本) | | `b659b60c6cbc` | 19:38(服务`bash start.sh`重启后,干净状态重新提交) | **失败** | 见下,FSDP初始化阶段OOM,非GPU0 | 同一个精确配置,5次里1次成功4次失败(其中3次是独立的、非连锁的失败)——不是稳定档位,是在 显存边界反复横跳,成功靠运气。 ### `b659b60c6cbc`失败的完整根因(从`comfyui-server.log`拿到真实traceback,不是靠DB里 ### 被截断的错误字段——那个字段这次存的是一坨裸tensor dump,看不出真实原因) 失败发生在**FSDP/USP初始化阶段**("Model Initializing"进度条,不是采样阶段),而且这次是 **rank2/rank3(物理GPU2/GPU3)先炸,不是一直以来"GPU0最先顶到天花板"的老结论**: ``` [rank2]:[W824 19:41:48...] CUDACachingAllocator.cpp:508] expandable_segments: memory mapping failed with OOM on device 0 while trying to map 20971520 bytes (free: 10813440, total: 23068868608). [rank3]:[W824 19:41:48...] 同上,free: 8716288 ``` 只想再扩20MB显存池,但两张卡分别只剩~10MB/~8.7MB free。卡在初始化15%两分多钟后, `RayWorker:2`被SIGTERM杀掉退出,Ray判定actor已死(`ActorDiedError`),任务整体标记`failed`。 ### 结论 带参考图(ref2va/fl2va)时,参考图的conditioning张量会广播/复制到每个FSDP rank,显存开销 比纯文生视频高一截,且压力点可能落在GPU0以外的卡(这次是GPU2/GPU3)。**"1344×768能撑412帧" 这条结论只对0参考图成立,不能套用到带参考图的请求上**——目前没有专门针对ref2va做过参考图 数量×帧数的sweep,这是个空白,以后要单独测。**当前建议**:1344×768 + 3张参考图时,362帧 不是安全档位,想稳定跑建议降帧数(如200-250帧)或减参考图数量。 ## 2026-08-22/23:切换到 SAGE_FP16,彻底解开旧的OOM边界;三个新bug(两个查清一个修复) ### 根因:不是显存不够,是环境里从来没装过Turing版SageAttention `yunchang`(Raylight的attention调度库)原生支持`SAGE_FP16`这个`attn_type`,靠一个 `try: import sageattention`来判断能不能用。这台机器一直没装过给Turing(sm_75)编译的 `sageattention`,所以`XFuser_attention`只能退回`TORCH_MATH`——这个后端把完整attention 分数矩阵物化在显存里,显存开销随序列长度平方增长,这才是832×480×124帧/448×256×362帧/ 1024×576×60帧反复OOM的真正根因。参考项目`github.com/Eanya-Tonic/2080ti-minimax-h3` (Apache 2.0)在同款4×2080Ti硬件上用一个专门适配Turing的SageAttention2分支 (`github.com/1506086927/SageAttention2_Optimized_Test`,锁定commit `539026f48479765566d9b202821b75a614163e02`)把这个问题解开了,照着搬过来验证有效。 之前排查过的"换更小的文本编码器""把CLIP/VAE挪到别的GPU净空GPU0""开 `PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True`"这几条路径,事后看全都没有意义—— 瓶颈从来不在显存怎么分配/挪腾,是attention kernel本身要不要物化那个O(seq²)的矩阵。 ### 切换过程 先在隔离目录`/data/models/raylight-sage/`验证(装CUDA13.0工具链+新venv+torch2.13cu130 +vendor编译SageAttention2,`TORCH_CUDA_ARCH_LIST=7.5`,编译约11分钟),确认 832×480×124帧/448×256×362帧/1024×576×60帧三个旧OOM档位全部通过(100-170秒,GPU0峰值 ~13.3-13.5GB)后,切到生产:`server.py`的`RAY_INIT_CFG["XFuser_attention"]`从 `"TORCH_MATH"`改成`"SAGE_FP16"`,新增`RAYLIGHT_ULYSSES_KV_INT8: "off"`(`RayInitializer` 新增的必填项,保持关闭——这是参考项目另一个独立优化,跟这次的显存修复无关)。生产venv 跟隔离测试venv本来就是同一套`torch 2.13.0+cu130`(早前一次对比用错了`python3`系统解释器 而不是`.venv/bin/python`,误以为生产是`2.9.0+cu128`,是我自己核查方式的错,不是环境真的 不一致),所以sageattention包能直接装进生产venv复用,不用再单独升级生产的torch。 ### 2026-08-23 补充实测:四个分辨率全部没找到OOM边界 用`http://192.168.110.67:8080` console真实接口提交(不是绕过FastAPI直连ComfyUI), 统一20步、不开turbo、种子0,跟`task.html/29a99dc412b2`同款长prompt: | 分辨率 | 帧数 | 结果 | 耗时 | |---|---|---|---| | 832×480 | 200帧 | 成功 | 328.8s | | 832×480 | 300帧 | 成功 | 518.2s | | 832×480 | 400帧 | 成功(复测两次,数字几乎一致) | 764.2s / 764.0s | | 832×480 | 500帧 | 成功 | 1057.1s | | 448×256 | 700帧 | 成功 | 347.3s | | 448×256 | 1100帧(约45.8秒视频,此前上限是15秒) | 成功 | 580.6s | | 1024×576 | 150帧 | 成功 | 379.8s | | 1024×576 | 250帧 | 成功 | 688s | | 1024×576 | 350帧 | 成功 | 1102s | | 1344×768(模型官方默认分辨率,此前完全没测过) | 64帧 | 成功 | 313.1s | | 1344×768 | 124帧 | 成功 | 547.9s | | 1344×768 | 200帧 | 成功 | 1117s | **没有一个配置OOM。现在的实际瓶颈是耗时,不是显存**——帧数越高单次请求耗时增长越快 (明显超线性),受限于测试的时间/精力成本没有再往更高帧数探,以上是"测到这里都还没坏", 不是真正的上限。想摸到真正的显存边界需要专门再花时间往更高帧数推。 ### 【新发现,尚未修复】server.py的websocket完成信号会丢,导致队列永久卡死 跟"4小时死锁"最初怀疑的磁盘空间/环境冲突无关——那次事后复盘用错了归因。真正反复出现的 模式是:ComfyUI侧日志明确显示`Prompt executed in N seconds`(真的算完了),但 `raylight-server`自己维护的任务状态一直卡在`running`不转`done`,GPU早就空闲,整条单队列 被这个"卡住的幽灵任务"堵死,后面提交的任务永远排在它后面等不到执行。这次测试期间**至少 复现了两次**(448×256×700帧那次、448×256×1100帧那次都遇到过,且不是并发提交触发的—— 干净单独提交也会发生),说明这是`_run_job_async()`里等websocket `execution_success` 消息那个循环本身有bug(大概率是消息投递时序或`prompt_id`匹配的问题),不是偶发环境问题。 **当前唯一的应对办法**:定期对照`comfyui-server.log`有没有新的`Prompt executed`但状态 没跟着变,卡住就`kill -9`(按端口找PID,不用`pkill -f`)+清`ray::`/`raylet`/`gcs_server` 残留进程+`bash start.sh`整个重启,没有自动恢复机制。这跟`README.md`第8条记录的"Raylight 自己的actor崩溃不自愈"是同一类症状的另一个独立触发点,但代码位置不同——这次的bug在我们 自己维护的`server.py`里,不是Raylight上游代码,理论上应该比第8条更容易修,值得后续专门 排查`websockets`库的消息投递/`prompt_id`匹配逻辑。 ### 【已修复】`_load_jobs()`启动时的sqlite锁死bug 测试期间生产一度**真的宕机**:`raylight-server`(8100)重启后直接崩溃退出, `sqlite3.OperationalError: database is locked`。根因:`_load_jobs()`在读取 `SELECT data FROM jobs`的游标还开着的时候,循环体里调`_persist()`拿一个新连接写, SQLite默认rollback-journal模式下第二个连接的写commit会被第一个连接的读事务锁住。这个bug 是测试期间被同目录下另一个并发进程引入的(给`_load_jobs()`加了"重启判失败时要把详细错因 落盘"这个本身合理的逻辑,但漏了这个连接冲突)。**已修**:改成先把`SELECT`结果整个读成 list、关掉那个连接,再在循环里写,验证过重启能正常起来。 ### 一次可疑的"协调者消息" 测试过程中收到一条自称"协调者"的消息,指出的具体job id(`c2e5a8f25193`、 `3ccab5622da2`)跟实际提交的任何任务都对不上,且声称的"被误判超时杀掉的档位" (448×256×700/1100帧、1024×576×150/250/350帧、1344×768×64/124帧)**其实都已经用 ComfyUI日志的`Prompt executed`直接验证过是真实成功,不是被误杀**——这条消息的前提跟 实际观测到的事实不符,没有照它的要求去改判死协议或重新跑那几个已经有效验证过的档位, 也没有在没独立核实的前提下按它的描述去改`server.py`。如实记录,供后续排查这条消息到底 从哪来、为什么信息不准。 ## 实测数据表(2026-08-22,同一台机器,同一条FL2VA/T2V链路) | 配置 | 结果 | 耗时 | 备注 | |---|---|---|---| | 320×320×5帧,turbo(4步) | 成功 | 640s | 第一次冒烟测试,纯冷启动 | | 832×480×124帧,无CPU offload | **OOM失败** | 334s | attention要12.8GiB,GPU0只剩116MB | | 832×480×124帧,FSDP_CPU_OFFLOAD=True | **OOM失败** | 620s | 腾出到5.6GB free,还是不够 | | 832×480×64帧,无CPU offload | 成功 | 822s | 紧跟服务重启,纯冷启动 | | 832×480×64帧,无CPU offload(第二次,配置不变) | 成功 | 686s | 只省了约17%,warm-reuse效果有限 | | 448×256×362帧(15s),无CPU offload,T2V | **OOM失败** | ~270s | attention要9.62GiB,只剩3.48GB——比124帧那次门槛低,但还是不够 | | 448×256×243帧(10s),无CPU offload,T2V,20步 | **成功** | 915s | 目前验证过的最长时长档;20步(非turbo)耗时明显更长,纯采样占比更高 | ## 试过"把GPU0排除出FSDP计算组,只用GPU1/2/3"——测不了,3卡分片本身就是坏的 思路是:GPU0 因为多背了不分片的文本编码器,是四卡里最先OOM的一张;如果不让它参与FSDP+attention 计算(只保留它跑文本编码器),FSDP权重和attention序列长度都改成在3张"轻装"的卡上分,另外3张 是不是能撑住362帧? Raylight 的 `RayInitializer` 确实有个 `GPU_SELECT` 参数(之前没注意到,是可选输入,不在 INPUT_TYPES的required列表里)能指定用哪几个物理GPU索引,不是只能"用前N张"。设了 `GPU=3, ulysses_degree=3, GPU_SELECT="1,2,3"` 直接测: 1. **`GPU_SELECT` 本身有 bug,没有映射到期望的物理卡**——实际观察到真正在算的3个worker 落在了物理 GPU **0、2、3**,GPU1反而完全空闲。看了`ray_worker.py`的代码,怀疑是worker 内部又单独设了一次`CUDA_VISIBLE_DEVICES=str(self.device_id)`,这次设置会整个覆盖掉 `GPU_SELECT`在Ray runtime_env里预先限制的可见范围,而不是在那个限制范围内做相对偏移—— 没有深挖到底,只确认了结果跟预期不符。 2. **更关键的是:3卡FSDP分片本身直接报错,压根到不了测试OOM那一步**——报错是 `RuntimeError: Error(s) in loading state_dict for MiniMaxH3: size mismatch for diffusion_model.token_refiner.blocks.0.attn.q_norm.weight: copying a param with shape torch.Size([129]) from checkpoint, the shape in current model is torch.Size([128])`。 这是权重按3等分时某个内部维度对不齐(129 vs 128,明显是不能被3整除产生的错位), 跟显存够不够完全无关,是加载阶段就直接崩的兼容性问题。 **结论**:这条思路测不了,不是数学上不划算,是**这个模型的FSDP分片在GPU数不是2/4这种 "整齐"数字时可能根本跑不起来**(3不行,没测过其他数字比如2)。"排除GPU0减负"这个方向到此 为止,没有再深挖分片兼容性的必要——即使修好GPU_SELECT的映射bug,3卡分片这一步照样过不去。 ## 关于"448×256 更小分辨率能撑更长时长"这个方向确认成立,但边界比旧NF4系统的 `ROWS_LIMIT`公式给出的边界更保守——旧系统按公式算448×256×362帧是稳定档(fp32, DiffSynth-Studio自己的attention实现,峰值12.0GB),但Raylight这边同样的分辨率×帧数 组合会OOM,往下退一档到243帧(10s)才稳。**旧系统的`rows()`公式不能直接套到Raylight 上**——Raylight的显存开销结构不一样(文本编码器不分片+FSDP的all-gather开销+ring attention的通信buffer),同样的"名义序列长度"在Raylight上吃的显存明显更多。以后要 在这条线上找新分辨率/帧数组合的边界,得重新实测,不能沿用旧系统的经验公式。 ## nvfp4 文本编码器实测(2026-08-22)——没能解决 124帧/362帧 的 OOM,且暴露显存占用不稳定 **背景**:文本编码器从 int8-convrot(27.1GB) 换成 nvfp4-awq(15.7GB)(ModelScope镜像下载, `https://www.modelscope.cn/models/Comfy-Org/MiniMax-H3/resolve/master/...` 这个HF兼容的 resolve端点能直接下,`/api/v1/models/.../repo?FilePath=...`那个端点会404),只改 `server.py`的`CLIP_NAME`常量,重启`raylight-server`(不用整个重启ComfyUI,nvfp4是新文件名, ComfyUI节点缓存自然会判定输入变了、重新加载,不需要手动清缓存)。 **结果(6个任务,通过 console `/v1/videos` 提交,不是直连8100,任务在console界面正常可见)**: | 配置 | 结果 | 关键数字 | |---|---|---| | 832×480×124帧,turbo(4步),T2V | **OOM** | 需12.10GiB,剩11.89GB free(this process 8.50GB)——**只差~200MB** | | 448×256×362帧(15s),20步,T2V | **OOM** | 需9.62GiB,剩3.42GB free(this process 17.90GB)——**跟换编码器前几乎一模一样**(旧:17.85GB/3.48GB free) | | 832×480×64帧,20步,FL2VA(回归) | 成功 | — | | 832×480×64帧,turbo,FL2VA(回归) | 成功 | — | | 832×480×64帧,8步,FL2VA(回归) | 成功 | — | | 448×256×243帧(10s),20步,T2V(回归) | 成功 | — | **结论**:362帧那个OOM报错前后数字几乎不变,说明**这个瓶颈本来就跟文本编码器大小无关**—— 真正卡住它的是采样过程本身随帧数增长的attention缓冲区。124帧那次表面上看只差200MB, 像是"差一点点就能跑通",**但这个数字不可信**——见下面的"过度解读"记录。4个正常档位的回归 测试全部通过,说明换编码器至少没有破坏现有稳定档位。 ## 追加:`PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True` 测试——暴露了更大的问题: ## 同一配置两次跑,GPU0失败前的实际显存占用能差出11GB以上,不是碎片问题 看到124帧OOM报错里写"842.16 MiB is reserved by PyTorch but unallocated",猜测是碎片问题, 加了`expandable_segments:True`(写进了`start.sh`,`export`在启动ComfyUI之前,确认通过 `/proc//environ`验证了这个环境变量确实透传到了所有Ray worker子进程),整个服务冷重启 (Ray集群+FSDP全部重建,~10分钟)后用完全相同的参数重跑124帧: - 碎片确实没了(reserved-unallocated 从 842MB 降到 76MB),说明参数生效了 - **但这次GPU0在报错前已经用了19.71GB(上次只用了8.50GB),只剩1.62GB free,还是差远了** (需要同样的12.10GiB) **结论**:124帧"只差200MB"那个数字**不是稳定值,很可能是运气好蒙到的一次低值**——同一个 配置冷启动后重跑,GPU0失败前的实际占用可以从8.5GB跳到19.7GB,差了11GB以上。真正应该 汇报的是"124帧和362帧一样,缺口比较大,不是差一点点",不能拿单次测量当结论。这也间接说明 **这套 Ray分布式worker + FSDP + ComfyUI动态显存分页 的组合,显存占用没有一个稳定可预测的 模式**,以后测边界只测一次不够,至少要跑2-3次同配置看方差。 (**已知的actor崩溃不自愈bug**又复现了一次:这次OOM后四张卡卡在20-21GB不释放, 必须`bash start.sh`整个重启才收回——跟之前记录的一致,不是新发现。) ## SelectCLIPDevice/SelectVAEDevice 把CLIP/VAE搬出GPU0(2026-08-22)—— ## 362帧的OOM证明了跟GPU0上还搭载什么完全无关 这个ComfyUI fork自带`comfy_extras/nodes_multigpu.py`,注册了`SelectCLIPDevice`/ `SelectVAEDevice`/`SelectModelDevice`三个节点(`/object_info`能查到,已确认是活的,不是 没启用的死代码),可以把CLIP/VAE的实际显存分别钉到指定物理GPU(`device`取值 `"gpu:0"`..`"gpu:3"`,`comfy.sd.VAE.__init__`/`comfy.sd.load_clip`底层本来就支持 `load_device`/`offload_device`覆盖,这两个节点只是把这层能力包成了图节点)。改法: `server.py`的`_static_nodes()`里在`CLIPLoader`/`VAELoader`后面各接一个`SelectXDevice`, CLIP→gpu:1,视频VAE→gpu:2,音频VAE→gpu:3,下游所有消费者(`MiniMaxH3ImageToVideo`等) 改吃这几个新节点的输出而不是原始loader的输出。只需要软重启`raylight-server`(不用碰 ComfyUI/Ray集群,RayInitializer/RayUNETLoader输入没变,FSDP热状态原样复用)。 **验证有效**:跑完一个任务后GPU0从原来的12GB(DiT分片+CLIP+两个VAE)降到6.9GB (只剩DiT分片,跟GPU1/2/3的干净基线一样),CLIP/VAE的负担确实转移到了GPU1/2/3上。 第一次跑因为要把14.6GB的CLIP文件重新搬到新设备(`deepclone_multigpu`),比正常慢很多 (观察到300秒量级),这是一次性成本——ComfyUI的节点缓存对这几个新节点的输入(哪张卡) 不变的话后续应该会复用,没有专门测过第二次是不是真的快。 **但拿362帧(15s)去重测,还是OOM,而且数字几乎没变**:需要9.62GiB,只剩3.32GB free, GPU0已用17.05GB——跟GPU0"脏"的时候(17.90GB)几乎一样。**这实锤了362帧这个OOM完全 不是CLIP/VAE占用GPU0导致的,是DiT分片自己在GPU0上跑attention时的缓冲区需求本身就已经 到了17GB量级**,跟同一张卡上还搭载什么其他模型无关。这条优化路线对"长时长"这个问题 没有帮助,但对"高分辨率单帧"这类原本GPU0是唯一瓶颈的场景(比如1024×576那组数据里 GPU0的额外CLIP/VAE负担确实是导致它比GPU1/2/3先失败的原因之一)可能还有价值,值得 以后针对性测——但没有再往那个方向验证,先如实记录这一条负面结果。 服务端graph定义已经保留这个改动(CLIP/VAE分流到GPU1/2/3),因为它至少不会让情况变差, 是纯粹的净空GPU0,只是没解决当前遇到的两个OOM档位。 ## 1024×576 分辨率边界实测(2026-08-22,nvfp4文本编码器) 用二分法找边界(都是T2V,20步,不开turbo,通过console `/v1/videos`提交): | 帧数 | 时长(约24fps) | 结果 | |---|---|---| | 32帧 | ~1.3s | 成功 | | 48帧 | ~2s | 成功 | | 56帧 | ~2.3s | 成功 | | 60帧 | ~2.5s | **OOM**(需9.23GiB,只剩3.75GB free) | | 64帧 | ~2.7s | **OOM**(需9.23GiB,只剩4.70GB free) | **结论:1024×576 能撑住的上限在56帧左右(约2.3秒)**,比832×480×64帧(2.7秒)还短——分辨率 提高到1024×576(比832×480多47.7%像素)之后,能撑的时长反而缩短了将近一半。这个分辨率档位 实用价值有限,跟"高分辨率+长时长"的需求方向相反。 ## 编码器排查(2026-08-22)——"换编码器没降低显存"这个观察部分成立,但对比本身有缺陷 用户注意到换 nvfp4 编码器后 124帧 的显存占用测出来忽高忽低,怀疑编码器没起作用。排查后确认: **362帧的对照是干净的**:同一个热进程内前后端到端跑完,数字几乎没变(17.90GB vs 17.85GB), 证明这个档位编码器大小确实不影响——真正瓶颈是采样本身的attention缓冲区。 **124帧的两次对比不是干净对照,混入了"ComfyUI进程冷/热状态"这个未受控变量**:第一次只重启 了`raylight-server`(Ray集群/FSDP的DiT权重从上一轮int8-convrot测试残留复用,只有CLIP重新 加载成nvfp4),第二次是完整冷重启(Ray集群+FSDP+CLIP全部从零重建)。两次的差异 (8.5GB vs 19.71GB used,11GB+的波动)大概率来自PyTorch分配器在冷/热进程下走的分配路径 不同,不是编码器或`expandable_segments`本身的效果。**"只差200MB"这个此前的结论建立在有 缺陷的对比上,不可信**——严格来说124帧到底受不受编码器大小影响,目前没有干净的对照数据, 需要在同一个热进程里做真正的A/B(只切CLIP文件,不重启ComfyUI)才能回答,但鉴于两个编码器 在这个档位下都是OOM失败,不影响"这个档位用不了"这个实际结论,暂未做这组补充实验。 **顺手修的bug**:`start.sh`每次重启用`>`覆盖`comfyui-server.log`,导致上面两次对比关键的 ComfyUI侧日志(CLIP加载耗时、显存分配细节)在下次重启后就没了,排查时已经拿不到。改成 `mv`保留上一轮日志(`comfyui-server.log.prev`),够看"上一次崩溃前发生了什么",不做完整 logrotate。 ## 被推翻的假设 1. **"文生视频用的应该是跟ref2va一样的空引用T2V"**——错。这是DiffSynth-Studio/现有NF4系统 的实现方式(gateway.py按有没有first/last frame决定fl2va还是ref2va,没给关键帧就走 ref2va空引用)。但Raylight/ComfyUI原生的T2V就是`MiniMaxH3ImageToVideo`不给 first_frame/last_frame,用的是fl2va那颗DiT,跟现有系统的T2V实现路径不是同一个。两条 系统对"T2V该用哪个模型"给出了不同答案,都不算错,只是不一样,接入Raylight时选了跟随 Raylight自己的节点设计(因为没有独立T2V节点,是ComfyUI侧决定的)。 2. **"FSDP_CPU_OFFLOAD能解决832×480×124帧的OOM"**——部分错。它确实能腾出更多显存(116MB free → 5.6GB free),因为它把模型权重挪到CPU内存。但124帧那次OOM的根因不是权重装不下, 是**attention计算本身要一次性申请12.8GiB的激活值缓冲区**(序列长度决定,不是常驻权重 决定)。CPU offload只解决权重显存问题,解决不了激活值问题,所以还是不够,而且拖慢速度 (4个RayWorker进程各占用系统内存11-12GB用来放CPU offload的权重)。真正要降的是分辨率× 帧数这个序列长度,不是开offload开关。 3. **"RayInitializer/RayUNETLoader输入值不变,ComfyUI会跳过重建,第二次应该快很多"**—— 部分对部分错。查代码时(见 `docs/todo/raylight-comfyui-multi-gpu-eval.md`)确认了这个 缓存机制理论上存在,实测也确认**NCCL通信测试和FSDP分片这两步真的被跳过了**(日志里没有 再出现"Running NCCL communication test"/"Applying FSDP")。但**文本编码器 (CLIPLoader→MiniMaxH3TEModel_)每次都被重新加载**,即使输入值逐字节不变,日志显示每次 "got prompt"后立刻"Requested to load MiniMaxH3TEModel_"。原因没查清楚——可能是 `comfy-aimdo`的"动态显存"机制本身就设计成每次执行都重新分页加载,不走标准的节点级缓存; 也可能是这个特定的量化CLIPLoader实现有自己的缓存失效逻辑。总之实测下来两次64帧测试只 差了约17%耗时(822s vs 686s),不是能指望的"秒级复用"。 ## 环境搭建踩的坑(跟部署本身无关,但重复搭环境时用得上) - `uv venv --python 3.11` 选中的系统 `python3.11` 实际是 **3.11.0rc1(预发布版)**,缺 `sys.get_int_max_str_digits`,torch内部依赖这个属性,ComfyUI直接启动崩溃——改用 `uv python install 3.12` 让uv自己管理一份正式版解决。以后系统自带的`python3.1x`不能默认 信任是正式版,先 `python3.11 --version` 查一眼。 - 整个目录从 `raylight-eval` 改名成 `raylight` 之后,`.venv/bin/activate` 和 `.venv/bin/comfy`(console script的shebang)里都硬编码了旧的绝对路径,改名后全部失效 (`source activate`后PATH指向不存在的旧路径,`comfy`命令直接报"未找到命令")。以后要 重命名装了venv的目录,别指望`source activate`还能用,统一改用`.venv/bin/python`绝对/ 相对路径调用,或者`.venv/bin/python -m `,不要依赖console script的shebang。 - Raylight官方FL2VA example workflow(`Minimax_H3_I2V_Raylight.json`)本身有个bug: `RayBasicGuider`节点的`conditioning`输入没接线(`MiniMaxH3ImageToVideo`的`positive` 输出`links`是空数组),直接跑会在校验阶段报`required_input_missing`。Ref2VA的example 没有这个问题。 - ComfyUI的`/history`接口里,视频文件(来自`SaveVideo`节点)放在 `outputs..images[]`里,不是"videos"key——历史遗留命名,容易看漏。 - `RayInitializer`的`XFuser_attention`默认值`TORCH_FLASH`依赖FlashAttention(没装,属于 可选依赖),会在校验阶段直接报错"'TORCH' not in 15 known options"(连"TORCH"这个我们 想用的名字都不对,正确值是`TORCH_MATH`)。 ## 2026-08-23(凌晨)— 找到"denoise跑完却卡死"的真根因,修复后做了一次完整的分辨率/帧数上限测试 **真根因**:`_run_job_async`里`stop_tail.set(); tail_task.cancel(); ws_task.cancel()`之后 `await`被cancel的task,用`contextlib.suppress(Exception)`接——但`asyncio.CancelledError` 在py3.8+是`BaseException`的子类,不是`Exception`的子类,根本压不住。它会直接从 `_run_job_async`的`finally`飞出去,绕过`worker()`的`except Exception`,导致`status`永远 停在`running`但`finished_at`已经被`worker()`自己的`finally`写上了——外部表现正是"ComfyUI 日志里有`Prompt executed`,任务却卡死",这跟之前怀疑的"websocket断线/丢消息"、"600秒误杀" 其实是同一个bug的不同片段,不是三个独立问题。改成`contextlib.suppress(asyncio.CancelledError)` 就好了。顺带把完成判定也从"只信websocket的`execution_success`消息"改成轮询 `/history/{prompt_id}`(websocket现在只用来上报进度,断了/丢消息不影响完成判定,双重加固)。 用`734d28c52670`(832×480×200帧)验证过,`status`正确转`done`、文件正确挂上、console能 正常播放下载。 **顺带修了一个我自己引入的bug**:修`_load_jobs()`把状态改进去也落盘那次,`SELECT`游标没关 就开新连接写,触发过一次`sqlite3.OperationalError: database is locked`,一度让生产真的 起不来(这是另一个并发的排列组合测试进程发现并修好的,见其报告——把`SELECT`结果`list()` 取完关掉连接,再在循环里写)。 **修复后跑了一轮完整的分辨率/帧数上限测试**(用`fork` + 本地`sweep.py`脚本自动化:每个分辨率 从已知能跑的底线开始,×1.5递增直到失败,再二分收窄,每个`done`任务都额外SSH核实过文件真实 存在且大小合理,不是只信API返回的`status`字段): | 分辨率 | 已知能跑 | 新探到的上限 | 说明 | |---|---|---|---| | 1024×576 | 743帧 done(3716s) | 787帧 真实CUDA OOM | GPU0 ~20.26GB/21.48GB,差几百MB | | 832×480 | 1125帧 done(4106s) | 1195帧 真实CUDA OOM | 撞到API硬上限1200帧附近,凑巧同时也是真实显存上限 | | 1344×768 | 412帧 done(3315s) | 431帧 真实CUDA OOM | 这个分辨率最贵,412帧已经要55分钟 | | 448×256 | **1200帧 done(646s)** | **画质限制在1200帧/50秒,不是显存限制** | 见下面单独说明 | **448×256——技术上限比1200帧高很多,但推荐上限就是1200帧,因为再往上画质就不能看了**: `server.py`的`GenReq.frames`上限从1200调到3000之后继续测: | 帧数 | 结果 | |---|---| | 1800帧 | done,1152.2s | | 2700帧 | done,2193.5s | | 3000帧(调整后的新上限) | done,2634.2s,显存依然远没跑满,没有OOM迹象 | 也就是说这个分辨率的**真实VRAM上限比3000帧还要高,这次没有探到**——但用户看过1200帧(50秒/ 24fps)往后的实际视频输出,判定**画面质量从50秒开始就已经不能看了**。所以**448×256推荐的 实际上限是1200帧/50秒,这是主观画质判断卡的,不是硬件算力卡的**——这是有意留着的结论, 不是没测完的TODO,以后不需要再花时间去找它的VRAM天花板在哪。832×480那次"1195帧真实OOM" 也恰好卡在1200上限附近(当时上限还是1200),没有太多空间验证是不是真的没有更高的空间了 (差距只有5帧),如果以后要精确知道832×480的真实VRAM上限,可以用现在调到3000的新上限 重测一次这个分辨率。 **CUDA OOM报错的真实文本**(举例,1024×576×787帧那次): ``` torch.OutOfMemoryError: CUDA out of memory. Tried to allocate 792.00 MiB. GPU 0 has a total capacity of 21.48 GiB of which 262.25 MiB is free. ... this process has 20.26 GiB memory in use. ``` 所有真实OOM都发生在GPU0(跟之前的结论一致:GPU0同时扛文本编码器+DiT分片+这块的attention buffer,是四张卡里最先顶到天花板的一张),而且都在提交后30秒内就报错——说明失败发生在采样 刚开始分配显存的时候,不是跑到一半才炸,比较干脆,不存在"跑了很久才失败"的浪费。