notes.md 26.9 KB

踩坑记录 / 被推翻的假设

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/<pid>/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 <module>,不要依赖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.<node_id>.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秒内就报错——说明失败发生在采样 刚开始分配显存的时候,不是跑到一半才炸,比较干脆,不存在"跑了很久才失败"的浪费。