# 踩坑记录 / 被推翻的假设 ## 2026-08-31 — 调研 FastVideo/FastH3(MiniMax H3 的 4-step 蒸馏版),结论:暂不可行,不建议投入 ### 是什么 `https://huggingface.co/collections/FastVideo/fastvideo-fasth3`——Hao AI Lab(FastVideo项目) 把 MiniMax H3 做的 DMD2 蒸馏:复用 H3-Base 的文本编码器/VAE/tokenizer/scheduler,去噪步数从 49 步蒸馏到 4 步,号称 B200 上最高 14x 加速。collection 里 5 个 checkpoint,核心是 `Dense-DataFree`(33B,标准dense attention)和几个 `VSA-*`(35B,带90%稀疏attention, 需要额外的 VSA kernel)。还有一个单独的 LoRA 版本(只需下载蒸馏adapter,不用整份权重)。 ### 卡我们的三条,不是单独一条,是三条一起卡死 1. **只发布 BF16,没有量化版本**。跟 `turing-bf16-attention-dead.md` 是同一个坑:sm_75 没有 高效 bf16 attention kernel,PyTorch 会静默掉到 MATH 后端物化整个attention矩阵然后 OOM。 对照组:FastVideo 家的另一个模型 FastHunyuan 是发了 NF4/LLM-INT8(bitsandbytes)量化版的, 单张 RTX 4090 20GB 就能跑——**但 FastH3 没有这个待遇**,NF4/量化目前只在官方 roadmap 里 (跟 NVFP4、RTX/DGX Spark/Apple MLX 优化写在一起,都是"未来工作",不是现在能下载到的东西)。 2. **VSA 加速的 kernel 是 Blackwell 专属**。官方发布的是 `sm100a`(B200)编译产物,非 Blackwell 多卡系统建议 fallback 到 `--vsa-kernel triton`,但这条路径官方自己都没验证过 Turing——roadmap 原文把 "RTX GPUs" 列为未来工作,不是当前支持范围。`Dense-DataFree` 这个 不依赖 VSA 的版本理论上绕开了这条,但绕不开第1条(bf16)。 3. **不是 drop-in checkpoint,是要接官方 FastVideo 推理栈**。官方原话:"FastH3 is a FastVideo-stack release rather than a drop-in checkpoint"——依赖 FastVideo 自己的 VSA-H3 backend + launcher(`uv` 装包、CUDA 13、Python 3.12),不是通用 LoRA loader, 也**不是 ComfyUI-native**(官方原话:ComfyUI-compatible 路径"worth watching",即还没做)。 这意味着即使解决了前两条,也没法直接塞进我们现有的 `raylight/` FSDP + ComfyUI 管线, 得单独起一套 FastVideo 自己的推理服务,工作量不是换个权重文件那么简单。 ### 结论 **三条骨牌效应**:没有量化权重 → bf16 在 Turing 上跑不动 attention;VSA 加速官方明确只支持 Blackwell;就算只用不依赖 VSA 的 dense 版本绕开第2条,还是绕不开第1条的 bf16 问题,而且还得 额外接一套跟 `raylight/` 不兼容的独立推理栈。三条里任何一条单独都可能想办法workaround(比如 当年给base H3装社区版SageAttention2解决了bf16问题),但三条叠在一起、且其中两条依赖上游 还没发布的东西(量化权重、ComfyUI适配),现在投入去啃性价比很低。 **什么时候值得重新看**:FastVideo 官方 roadmap 里的 NVFP4量化/RTX支持 或 ComfyUI-compatible 路径任一项实际发布之后,可以重新调研一次——那时候可能只需要解决 bf16→fp16 这一条, 跟当年 base H3 上 SAGE_FP16 那次工作量类似。在此之前不建议投入,我们已经有 `raylight/` 跑通的 base MiniMax H3(4-step蒸馏能省的时间,比不上现在这套稳定管线的确定性)。 ### 补充:即使上面三条全部解决,目标"N秒视频在1)。算了一下 数量级差距,这条比兼容性问题更硬,不是"肯搞"就能绕过去的: **FastH3官方自己的benchmark(B200,最理想情况)也没稳定做到 RTF>1**: | 时长 | 配置 | 生成耗时 | RTF | |---|---|---|---| | 5s | 1×B200 | 16.2s | 0.31x | | 10s | 1×B200 | 31.1s | 0.32x | | 15s | 1×B200 | 47.2s | 0.32x | | 15s | **8×B200** | 15.5s | **0.97x**(最接近,但仍不到1) | 即数据中心顶配单卡也只做到约1/3实时速度,要8张B200堆到接近(仍未越过)实时线。 **我们现有 base H3(raylight, 4×2080Ti, 49步)的RTF**(从`README.md`已知限制表反推, 帧数/24fps=视频时长,除以实测耗时):1344×768是**0.0052x(慢192倍)**,1024×576是 0.0083x(慢120倍),832×480是0.0114x(慢88倍),448×256是0.0774x(慢13倍)。 **假设FastH3的三条全部被我们解决,只拿"4步蒸馏"这一项(12.25x步数缩减,VSA那部分 Blackwell专属加速不算,因为在我们卡上用不了)套到现有耗时上**:1344×768推算269s (RTF=0.064x,仍慢15.7倍)、1024×576推算304s(RTF=0.10x,仍慢9.8倍)、832×480推算335s (RTF=0.14x,仍慢7.2倍)、**448×256推算53s(RTF=0.95x,接近但仍不到1)**。 **结论**:只有画质已知有妥协(448×256,`README.md`已知限制第1条记过"这是画质限制的 分辨率")这一档,乐观推算下勉强逼近实时,其余可用分辨率(832×480起步)哪怕拿到FastH3 全部好处也还差一个数量级(7-16倍)。这不是靠攒工程投入能补的差距,是2080Ti算力本身的 天花板——**在这台机器上,"N秒视频/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秒内就报错——说明失败发生在采样 刚开始分配显存的时候,不是跑到一半才炸,比较干脆,不存在"跑了很久才失败"的浪费。