Henry Jia

同一张 5090 上再装一个出图模型

约 12 分钟显影AI

目录 · 13 节
  1. 一、许可比画质更早把人筛掉
  2. 二、中文招牌是准的
  3. 三、不能靠降分辨率提速
  4. 四、显存只吃四成
  5. 那一列平均值,我第一版抄错了行
  6. 五、量化格式:证据是数出来的
  7. 六、SageAttention 在图片上只剩 1.08×
  8. 七、把 ComfyUI 撑死的真正原因
  9. 八、--cache-none 是补丁,不是选项
  10. 九、正解:让它主动卸载
  11. 十、切模态没有额外代价
  12. 完整数字
  13. 几条经验,不限于这个模型

上一篇在一张 RTX 5090 上把 MiniMax H3 跑到了底,那是视频。这次在同一台机器上加一条图片产线。

选型、装机、跑基线,一天做完。这篇记的是为什么是 HiDream-O1、它在这张卡上什么速度,以及一个查了很久才查明白的老问题:ComfyUI 为什么会把自己撑死。

先说结论:出图比出片轻松得多。2048×2048 四百万像素、40 步,42 秒一张,显存只吃四成,而同一张卡跑视频时是 98%。中文 prompt 直接喂就懂,指定的中文招牌一字不差。


一、许可比画质更早把人筛掉

开源出图模型现在很多。我列了四条判据,要同时满足:画质靠前、许可能商用、ComfyUI 原生支持不用装第三方节点、一个模型同时能出图和吃参考图。

第二条淘汰得最狠。

FLUX.2 [dev] 的许可有个容易读反的结构:产出端是放开的,原文写着 You may use Output for any purpose (including for commercial purposes);卡的是模型端,它对非商业用途的定义明确排除了营收活动。也就是说,你自己玩、出的图自己用,可以;但一旦生成这一步进了营收链条,越线的是用模型这件事,不是用图。

Ideogram 4.0 在盲测榜上是开源第一,但权重在 Hugging Face 上是 gated 的,协议还把限制延伸到产出用途。

筛完只剩一个同时满足四条的:HiDream-O1-Image,8B,MIT 许可,文生图和图片编辑两个榜都排第二。

顺带一个坑:Qwen-Image-2.0 不是开源的。一堆二手博客写它 Apache 2.0,但官方仓库的时间线里它只有 Qwen Chat 和 API,没放权重。开源线最新的是 Qwen-Image-2512。


二、中文招牌是准的

这条超出我的预期,所以先放。

上一轮选型时我以为中文字得靠 Qwen-Image-Edit,还写进了方案。实测把这条推翻了。

prompt 里我指定了两块牌子的内容:门楣的木牌刻灯下书局,下面小黑板写营业 九时至十八时。

旧书店橱窗,门楣木牌上刻着灯下书局四个烫金楷体字,下方小黑板写着营业 九时至十八时

两块牌子的字一字不差,连 prompt 里写的直角引号都照着刻了出来

英文那条同样全对,而且自动分了行、粉笔质感也成立:

旧书店门口,木招牌上是 THE LANTERN PRESS,旁边小黑板用粉笔写着 OPEN 9-6, CLOSED SUNDAY

我只给了文字内容,分行、字体、材质都是模型自己决定的

有个测法上的注意:要测文字渲染,必须在 prompt 里给出要写的文字。不给的话模型会自由发挥写一堆乱码。我早先那几张基准图里,凡是画面里带标签、带书脊的地方字全是糊的,那不算文字渲染失败,因为我根本没要求它写字。


三、不能靠降分辨率提速

出图模型有个通用直觉:想快就出小图,定稿了再出大的。这个模型上不成立,而且官方在节点源码的说明里写死了:

The model was trained at ~4 megapixels; lower resolutions go off-distribution and quality regresses noticeably.

也就是说 1024×1024 不是便宜的预览档,是分布外,画质会明显退化。

官方给的训练分辨率是十一档,方的 2048×2048,横到 3104×1312,竖到 1312×3104,全都是约 400 万像素。

我拿 3104×1312 跑了一条对照:32 秒对 2048² 的 33 秒,峰值显存 13676 对 13964,几乎一样。成本只跟像素数走,跟长宽比无关。

还有一处要留神:节点的 width 和 height 下限是 64,源码不做钳位。你完全可以送一个它没训过的画布进去,它不报错,只是悄悄变差。


四、显存只吃四成

这是和上一篇最大的反差。

同一张卡,H3 跑视频时峰值恒在 97–98%,靠 DynamicVRAM 拼命换出才摁住。HiDream 出图:

档位 采样 平均 总执行 峰值显存
Full · 40 步 · 2048² 33 秒 1.18 it/s 42.17 秒 13964(42.8%)
Full · 40 步 +Sage 31 秒 1.28 it/s 40.1 秒(墙钟) 13324
Dev · 28 步 8 秒 3.38 it/s 16.86 秒 11116
Dev · 28 步 +Sage 7 秒 3.64 it/s 15.0 秒(墙钟) 11502
Dev · BF16 · 28 步 9 秒 2.85 it/s 22.40 秒 17080
Full · 3104×1312 32 秒 1.23 it/s 40.58 秒 13676
Full · 50 步 41 秒 1.20 it/s 49.07 秒 13932

+Sage 表示挂了 SageAttention。全部 2048×2048,只标出与之不同的那一项。

标了墙钟的那两格是我自己脚本量的,5 秒轮询粒度,不是 ComfyUI 报的 Prompt executed——这一批的日志后来被别的实验覆盖了,精确值取不回来。别拿这两个数去和别行算比值。其余五行都是 Prompt executed。

一半以上的卡是空的。顺带两条:50 步对 40 步严格线性(41 ÷ 33 = 1.24,50 ÷ 40 = 1.25);Dev 版是 Full 的四分之一时间,28 步对 40 步只解释一部分,另两条是它用 LCM 采样器而不是 dpmpp_2m_sde_gpu,以及不挂下面这个 seam smoothing。

那一列平均值,我第一版抄错了行

tqdm 结束时打两行,我只 grep 了含 s/it 的那一行:

 32/40 [00:19<00:04,  1.65it/s]   ← 前 32 步
 33/40 [00:20<00:08,  1.24s/it]   ← 第 33 步起,每步 ×2
 37/40 [00:26<00:07,  2.42s/it]   ← 第 37 步起,每步 ×4
 40/40 [00:33<00:00,  2.41s/it]   ← 末段瞬时速率
 40/40 [00:33<00:00,  1.18it/s]   ← 全程平均,这行才是要抄的

拿末段值当平均值,每步耗时会大出近三倍。而且它和步数乘起来会超过总执行时间,这就是自查的抓手:40 × 2.41 = 96 秒,可这一行的总执行只有 42.17 秒。

变速的拐点在第 33 步,正好是 80%。而 HiDreamO1PatchSeamSmoothing 的出厂参数就是 start_percent = 0.8、passes = "ramp_2_4"(RAMP_LEVELS 里 ramp_2_4 = [2, 4]):

步 seam passes 实测 核对
1–32(0–80%) 不生效 0.61 s/it 基准
33–36 2 1.22 s/it 0.61×2 对得上
37–40 4 2.42 s/it 0.61×4 对得上

最后 8 步只占两成,却吃掉 14 秒,占整段采样的 42%,全是 seam smoothing。

Dev 那三行怎么抄都对,因为 Dev 模板根本没挂 seam smoothing,它匀速,两行 tqdm 完全一致(3.38it/s 和 3.38it/s)。这也是它比 Full 快那么多的第三条原因。


五、量化格式:证据是数出来的

权重有三个变体:BF16、FP8 scaled、MXFP8。Blackwell(50 系)对 MXFP8 有硬件级支持,官方说比 BF16 快约三成。

MXFP8 的原理是每 32 个连续权重共享一个 8 位缩放因子,所以平均每个权重约 8.25 位。这句话我一开始是从官方讨论串里读到的,后来发现在文件里能直接数出来:

张量数 dtype 体积
MXFP8 1474 BF16, F8_E4M3, U8 8.31 GiB
BF16 758 BF16 15.24 GiB

那批 U8 就是缩放因子,所以张量数正好多出约一倍;体积比 0.545,和理论上的 8.25/16 = 0.516 对得上,差的部分是有些层保留了 BF16。

实测 MXFP8 比 BF16 快 1.19×、省 6.0 GB 显存。官方说的三成、实测一成九,方向一致量级对得上。

画质我做了同 seed 对照,肉眼判看不出高下:

MXFP8 与 BF16 同一 seed 出图,差异最大区域的 1:1 像素裁切并排对比

自动找出差异最大的那块 512×512 做 1:1 裁切。整幅平均绝对差 17.45/255,一半以上的像素都变了

这里有个容易误判的地方:这不是同一张图,一张糊一点。 量化改的是计算图,同 seed 出来的是两个不同的采样结果。所以判据不是哪张更清晰,而是你想发哪张。


六、SageAttention 在图片上只剩 1.08×

上一篇实测 SageAttention 在 H3 视频上是 1.89×(5 秒)到 2.35×(15 秒),并总结出一条规律:序列越长,收益越大。

这次拿图片验证这条规律,结果比我预期的还低:

不挂 挂 Sage 提速
视频 15 秒 88.25 s/it 37.61 s/it 2.35×
图片 2048² 0.85 s/it 0.78 s/it 1.08×

图片这一行是全程平均(33.9 秒 ÷ 40 步、31.3 秒 ÷ 40 步),不是进度条末尾那个数,原因见上一节。视频那一行 H3 用 res_multistep 匀速、模板里也没有 seam 类节点,进度条上的值直接就是平均值。

图片的 token 序列比 15 秒视频短一个量级,所以收益几乎没有。

而且这 1.08× 没有被上一节那个 seam smoothing 稀释。逐段对照下来,seam 生效前和生效后的提速比是一样的:seam 多跑的那几遍是完整前向,照样过整个 transformer、照样有 attention,Sage 对它一视同仁。

这里差点被总耗时骗过去。Dev 那一对的总执行看着差了好几秒,但采样只有 8 秒到 7 秒,差的是模型装载,不是 Sage。看总耗时会高估加速比,得把固定开销扣掉;何况那一对总执行正是上一节标了墙钟的那两格,5 秒粒度,本来就不该拿来算比值。


七、把 ComfyUI 撑死的真正原因

这是今天最值钱的一条,也是困扰了很久的老问题。

现象在上一篇里就出现过:ComfyUI 会自己挂掉,已经发生过四到六次,每次都是控制台的内存表冲到 90% 多的时候。死状很特别,采样全部跑完,日志停在 VAE 加载那一行,之后一行没有,没有 traceback,进程直接消失。

当时的应对是启动加 --cache-none,有效,但那是知其然不知其所以然。这次查到了机制:

cgroup 真实上限          90 GiB
ComfyUI 以为的总内存    754.5 GiB   ← 宿主机的
ComfyUI 以为的可用       699.1 GiB   ← 容器实际只剩 78 GiB

comfy/model_management.py 里读内存用的是 psutil.virtual_memory(),而 psutil 读的是 /proc/meminfo,在容器里那是宿主机的数字。判断要不要腾内存的那行代码,比较对象也是宿主机的 699 GiB。

它永远算不出内存不够,于是一路缓存,直到 cgroup 把它打死。 grep -rin cgroup comfy/ 零命中,ComfyUI 完全没有 cgroup 感知。

这解释了所有细节:为什么显存 98% 没事而内存 90% 要命,前者有自适应换出、后者没有;为什么加 --cache-none 有效,把缓存整个关掉就绕开了盲区;为什么它死得那么安静,不是 Python 层的 OOM,是被外部杀掉。


八、--cache-none 是补丁,不是选项

想清楚根因之后,--cache-none 的定位就变了。它不是省内存的性能选项,是绕过盲区的补丁,而且代价不小。

去掉它、让模型常驻之后,每次换 seed 逼采样器真跑:

带 --cache-none 不带 收益
图片 固定开销 7.9–8.4 秒 1.6–2.0 秒 省约 6 秒
视频 固定开销 44.5–46.3 秒 10.8–11.4 秒 省约 34 秒
图片 总执行 39 秒 32.6 秒 1.20×
视频 总执行 80–82 秒 47.8–48.4 秒 1.68×

视频那条快了 1.68×,只是因为不用每次重装 40 GB 权重。这是那条 15 秒出片档的产物:

1344×768 · 362 帧 · 20 步 · 挂 SageAttention · 模型常驻。815 秒,比上一篇记录的 829 秒还快 14 秒,省下的正是重装权重的时间

但代价来了:两套模型常驻等于 60–62 GiB / 90。我拿 15 秒出片档试探边界,T2V 那条安然无恙,峰值 66.6 GiB,cgroup 的撞天花板计数器是 0;换成参考图模式,那要再装一个 19.5 GB 的模型,当场复现了上一篇记录的那次崩溃,日志停在同一行、同样没有 traceback。

峰值 86.8 GiB,采样 20/20 全跑完,没有产物,然后 SSH 和 JupyterLab 双双失联,只能在控制台重启。

但这次的数据修正了一个说法。原来以为是长片撑爆的,实测长片本身不是自变量——同样 15 秒、同样 4MP 的 T2V 好好的。真正的自变量是同时缓存了多少模型。

还有一条运维判据:内存 32 秒就从 66 冲到 83 GiB。等你看见控制台报警,手动干预已经来不及了。


九、正解:让它主动卸载

二选一其实是个假选择。ComfyUI 有官方的第三条路,我之前没注意到:

@routes.post("/free")
async def post_free(request):
    unload_models = json_data.get("unload_models", False)
    free_memory   = json_data.get("free_memory", False)

一条 HTTP 请求就能把已缓存的模型卸掉,不用重启服务、也不用牺牲常驻速度。实测:

步骤 内存 显存 固定开销
服务刚起 2.6 GiB 508 MiB —
第 1 张(冷) 13.2 GiB — 8.9 秒
第 2 张(常驻) 13.3 GiB — 1.5 秒
/free 后 3 秒 5.1 GiB 766 MiB —
第 3 张 13.3 GiB — 6.4 秒

三秒内生效,降 8.2 GiB。而且是真卸不是假动作,下一张的固定开销从 1.5 秒回到 6.4 秒,正是重新装载的时间。代价很清楚:一次 /free 等于下次多花约 5 秒,换来 8.2 GiB。

curl -X POST http://127.0.0.1:6006/free -H "Content-Type: application/json" -d '{"unload_models": true, "free_memory": true}'

所以日常不带 --cache-none,享受模型常驻的速度;要跑大任务之前先调一次 /free。

然后我拿它去打那条崩过的 R2V。同一条配置,唯一的差别是起跑前调了一次 /free:

不调 /free 先调 /free
起跑时内存 66.5 GiB 5.2 GiB
峰值内存 86.8 GiB 55.0 GiB
cgroup 压力计数 high 涨到 9538 全部 0
采样 20/20 跑完 20/20 跑完
产物 没有 5.6 MB
总耗时 崩了,没有 888 秒
服务 进程消失、机器失联 HTTP 200,活着

峰值低了 31.8 GiB,压力计数器从九千多降到一次都没撞。二选一那道题就此不存在了。

这一轮我自己的检查脚本还谎报了一次进程崩了,因为前面为了修 PATH 把启动命令换成了 /root/miniconda3/bin/python 绝对路径,而检查脚本还在用 awk '$2=="python"' 匹配。改了启动方式,配套的检查也得跟着改,否则它会稳定地骗你。


十、切模态没有额外代价

一个原本担心的问题:用户在界面上先出张图、再出条片,中间要换模型,会不会很卡?

实测不会,而且两种配置下都不会,虽然理由不是同一个。

带 --cache-none:

顺序 固定开销
视频 44.5 秒
切回来出图 8.4 秒
紧接着再出一张图 7.9 秒
再切回视频 46.3 秒

切回来 8.4 秒对连着出 7.9 秒,差 0.5 秒,在噪声里。这一组的理由很直白:每次生成本来就要重装模型,所以切模态和连续同模态根本是同一件事。

不带 --cache-none,也就是第九节推荐的那套,也一样:切回来 2.0 秒对连着出 1.6–1.8 秒。这一组的理由不同,两套模型加起来 60–62 GiB,在 90 GiB 里同时待得下,切模态压根没触发卸载。

后一条有前提:起跑前调过 /free 就不成立了,那时候确实要重装,回到第九节那张表里 /free 之后的 6.4 秒那一档。

先出图再出片,并不比连出两条片慢。所以界面上要做的只有一件事:转圈时把状态说清楚,是正在装载模型还是第 12/40 步。剩下的都是过度设计。


完整数字

单张 RTX 5090 32GB,ComfyUI 0.30.0,HiDream-O1-Image MXFP8,官方模板值。自测数据,官方没有任何速度或显存基线可对照。

第四节开头那张表就是全部七条。补几个别处的数字:

权重下载 43.3 GB,17 MiB/s,约 45 分钟
两套模型常驻 60–62 GiB / 90 GiB
出图峰值显存 11116–17080 MiB / 32607(34–52%)
带卡总时长 约 3 小时,约 ¥9

装机、下载、拆模板、写提交脚本全程在无卡模式下做,¥0.1/时。这条经验来自上一篇:省钱省的是开着卡干不吃卡的活,这次执行得比上次好。


几条经验,不限于这个模型

先看许可再看画质。榜首那两个都因为许可出局了,画质对比根本没轮到。判据的顺序会决定你浪费多少时间。

官方源码里的一句话,可能推翻你整个方案。4MP 以下跑到分布外这句写在节点的 description 字段里,不在文档、不在模型卡。读源码的性价比常常高于读文档。

规律要拿新场景去验,别只在原场景里复述。SageAttention 那条序列越长收益越大,在图片上验出 1.08×,而验证成功的方式恰恰是它给出了一个我原本不会猜到的低值。

看总耗时会高估加速比,而且两头得是同一个钟。固定开销不扣掉,几乎没用的 1.08× 会被读成像模像样的加速;更隐蔽的是口径,这张主表的总执行我一开始混了 ComfyUI 报的时间和自己脚本的墙钟,混着算出来的比值全是假的。

进度条末尾那个速率是瞬时值,不是全程平均。这个模型最后 20% 的步真的慢 4 倍,抄错一行,每步耗时就大出近三倍。自查的抓手很便宜:每步耗时乘步数,不能大于总执行时间。

知其然不知其所以然的补丁,迟早要还。--cache-none 挂了很久都有效,直到想省那 34 秒才发现它的代价,而根因其实一行 grep 就能查到。

测缓存复用必须换随机种子。我第一版实验有三条 0.9 秒完成,看着像天大的优化,其实是 prompt 完全相同、ComfyUI 把上次的产物直接返回了,采样器根本没跑。

最后一条还是给自己的:这一天里,比七个速度数字更值钱的是那个 psutil 的根因。 速度换台机器就得重测,而容器里的进程看不见自己的天花板这件事,会在别的地方一再遇到。