发布日期:2026-09-18
说明:本文提供可复现的多实例调度方法。文中的算家云(suanjiayun.com) 是关联演示环境;未对特定视频模型做 4 卡或 8 卡性能实测,不提供吞吐与稳定性保证。
如果目标是批量生成大量独立视频,稳定方案通常不是让一个 ComfyUI 工作流强行"吃满"8张卡,而是每张 GPU 启动一个 ComfyUI 实例,再由 Python 调度器把任务分发到空闲队列。
一、多张 4090 的显存不能简单相加
RTX 4090 单卡具有 24GB GDDR6X 显存,并且不支持 NVLink。NVIDIA 官方规格对此有明确说明。
因此,4张4090虽然合计安装了96GB显存,但对普通 ComfyUI 工作流来说,并不等于获得一张"96GB显卡"。8张卡也不能直接当作192GB统一显存使用。
需要先区分两种需求:
| 需求 | 合适方案 |
|---|---|
| 一条视频工作流超过24GB显存 | 降低分辨率、帧数、批量数,启用模型卸载,或改用更大显存GPU |
| 同时生成很多条独立视频 | 每张GPU运行一个ComfyUI实例,外层队列并发调度 |
| 一个模型需要跨卡切分 | 依赖具体模型、节点或分布式框架,不属于通用ComfyUI多实例方案 |
| 多套不同工作流同时运行 | 按GPU划分实例、模型和任务队列 |
本文解决的是第二种:提高批量任务吞吐。
二、4卡和8卡架构怎么设计
整体架构如下:
text
prompts.txt / 任务数据库
│
▼
Python任务调度器
│ │ │
▼ ▼ ▼
:8188 :8189 :8190 ... :8195
GPU0 GPU1 GPU2 GPU7
│ │ │
└─────┴─────┴──────────────┐
▼
独立输出目录 + 结果索引
每个实例只负责一张GPU:
text
GPU 0 → ComfyUI端口8188
GPU 1 → ComfyUI端口8189
GPU 2 → ComfyUI端口8190
GPU 3 → ComfyUI端口8191
8卡时继续增加到端口8195即可。
ComfyUI 官方启动参数支持 --cuda-device、--port、--listen 和独立输出目录。需要注意,给单个进程指定多个可见设备,并不代表普通工作流会自动把节点均匀拆到多张卡上。ComfyUI启动参数源码
三、启动4个或8个ComfyUI实例
先确认系统识别到的GPU:
bash
nvidia-smi -L
预期应看到连续的GPU编号,例如:
text
GPU 0: NVIDIA GeForce RTX 4090
GPU 1: NVIDIA GeForce RTX 4090
GPU 2: NVIDIA GeForce RTX 4090
GPU 3: NVIDIA GeForce RTX 4090
在ComfyUI目录外创建启动脚本:
bash
#!/usr/bin/env bash
set -euo pipefail
COMFY_DIR="/workspace/ComfyUI"
RUN_ROOT="/workspace/comfy-multi"
GPU_COUNT="${GPU_COUNT:-4}"
BASE_PORT=8188
mkdir -p \
"$RUN_ROOT/logs" \
"$RUN_ROOT/pids" \
"$RUN_ROOT/outputs" \
"$RUN_ROOT/users"
for ((gpu=0; gpu<GPU_COUNT; gpu++)); do
port=$((BASE_PORT + gpu))
mkdir -p \
"$RUN_ROOT/outputs/gpu-$gpu" \
"$RUN_ROOT/users/gpu-$gpu"
(
cd "$COMFY_DIR"
python main.py \
--listen 127.0.0.1 \
--port "$port" \
--cuda-device "$gpu" \
--models-directory "$COMFY_DIR/models" \
--output-directory "$RUN_ROOT/outputs/gpu-$gpu" \
--user-directory "$RUN_ROOT/users/gpu-$gpu" \
--disable-auto-launch
) >"$RUN_ROOT/logs/comfy-gpu-$gpu.log" 2>&1 &
pid=$!
echo "$pid" >"$RUN_ROOT/pids/comfy-gpu-$gpu.pid"
echo "GPU=$gpu PORT=$port PID=$pid"
done
wait
4卡启动:
bash
GPU_COUNT=4 bash start_comfy_multi.sh
8卡启动:
bash
GPU_COUNT=8 bash start_comfy_multi.sh
这里让所有实例共享模型目录,但将用户数据、日志和输出目录分开。这样可以减少模型文件的重复存储,也能避免不同任务使用相同文件名前缀时互相覆盖。
如果当前ComfyUI版本不支持某个目录参数,先执行:
bash
cd /workspace/ComfyUI
python main.py --help
再根据当前版本调整启动命令,不要直接照搬旧版参数。
四、检查每个实例是否启动成功
另开终端执行:
bash
for port in 8188 8189 8190 8191; do
curl -fsS "http://127.0.0.1:${port}/system_stats" >/dev/null \
&& echo "$port OK" \
|| echo "$port FAILED"
done
8卡时将端口补到8195。
同时检查日志:
bash
tail -n 50 /workspace/comfy-multi/logs/comfy-gpu-0.log
日志中应确认:
- 加载的是预期GPU;
- 模型与自定义节点没有缺失;
- 端口没有冲突;
- 没有CUDA OOM;
- 没有多个实例写入同一个输出文件。
ComfyUI服务端提供 /system_stats、/prompt、/queue、/history/{prompt_id} 和 /ws 等接口,可分别用于健康检查、提交任务、查询队列和跟踪执行状态。ComfyUI Server API文档
五、先把视频工作流导出为API格式
在ComfyUI中完成并验证单条视频工作流,然后导出API格式JSON。
不要直接把普通界面工作流JSON提交给 /prompt。官方示例要求使用API格式工作流,并将整个工作流放进:
json
{
"prompt": {
"...": {
"class_type": "...",
"inputs": {}
}
}
}
可参考 ComfyUI官方API示例。
导出后,找到下面几个节点ID:
- 正向提示词节点;
- 随机种子节点;
- 保存视频或保存图片节点。
例如:
python
PROMPT_NODE_ID = "6"
SAMPLER_NODE_ID = "3"
OUTPUT_NODE_ID = "9"
不同视频工作流的节点ID不同,必须以自己的API JSON为准。
六、用Python把任务分发到最空闲的GPU
准备两个文件:
text
workflow_api.json
prompts.txt
prompts.txt 每行放一个视频任务:
text
雨夜城市街道,镜头缓慢向前移动,霓虹灯反射在地面
雪山日出,云层从山谷中流动,延时摄影风格
产品展示镜头,白色背景,镜头围绕主体缓慢旋转
调度脚本如下:
python
import copy
import json
import random
import time
import uuid
from pathlib import Path
from urllib import request
SERVERS = [
"http://127.0.0.1:8188",
"http://127.0.0.1:8189",
"http://127.0.0.1:8190",
"http://127.0.0.1:8191",
]
WORKFLOW_FILE = Path("workflow_api.json")
PROMPTS_FILE = Path("prompts.txt")
RESULT_FILE = Path("submitted_jobs.jsonl")
# 改成自己API工作流里的节点ID
PROMPT_NODE_ID = "6"
SAMPLER_NODE_ID = "3"
OUTPUT_NODE_ID = "9"
MAX_QUEUED_PER_GPU = 2
random_source = random.SystemRandom()
def get_json(url, timeout=10):
with request.urlopen(url, timeout=timeout) as response:
return json.loads(response.read())
def queue_size(server):
state = get_json(f"{server}/queue")
running = len(state.get("queue_running", []))
pending = len(state.get("queue_pending", []))
return running + pending
def submit_workflow(server, workflow):
payload = json.dumps(
{
"prompt": workflow,
"client_id": str(uuid.uuid4()),
}
).encode("utf-8")
req = request.Request(
f"{server}/prompt",
data=payload,
headers={"Content-Type": "application/json"},
method="POST",
)
with request.urlopen(req, timeout=30) as response:
return json.loads(response.read())
def select_server():
while True:
loads = [(queue_size(server), server) for server in SERVERS]
loads.sort(key=lambda item: item[0])
size, server = loads[0]
if size < MAX_QUEUED_PER_GPU:
return server, size
print("所有GPU队列均已达到上限,等待空闲实例......")
time.sleep(2)
def build_workflow(template, prompt_text, job_index):
workflow = copy.deepcopy(template)
workflow[PROMPT_NODE_ID]["inputs"]["text"] = prompt_text
workflow[SAMPLER_NODE_ID]["inputs"]["seed"] = (
random_source.randrange(0, 2**63 - 1)
)
# 保存节点字段名称可能因节点而异
workflow[OUTPUT_NODE_ID]["inputs"]["filename_prefix"] = (
f"video/job-{job_index:05d}"
)
return workflow
def main():
template = json.loads(WORKFLOW_FILE.read_text(encoding="utf-8"))
prompts = [
line.strip()
for line in PROMPTS_FILE.read_text(encoding="utf-8").splitlines()
if line.strip()
]
with RESULT_FILE.open("a", encoding="utf-8") as result_file:
for index, prompt_text in enumerate(prompts, start=1):
server, old_size = select_server()
workflow = build_workflow(template, prompt_text, index)
try:
result = submit_workflow(server, workflow)
except Exception as exc:
print(f"[FAILED] job={index} server={server} error={exc}")
continue
record = {
"job_index": index,
"server": server,
"queue_size_before_submit": old_size,
"prompt_id": result.get("prompt_id"),
"prompt": prompt_text,
}
result_file.write(
json.dumps(record, ensure_ascii=False) + "\n"
)
result_file.flush()
print(
f"[SUBMITTED] job={index} "
f"server={server} "
f"prompt_id={result.get('prompt_id')}"
)
if __name__ == "__main__":
main()
8卡只需要扩展地址:
python
SERVERS = [
f"http://127.0.0.1:{port}"
for port in range(8188, 8196)
]
这个版本采用"当前队列最短优先",比简单轮询更适合视频任务,因为不同提示词、帧数和采样参数可能导致执行时间差异。
七、正式批量生成前,先做三轮验收
1. 单卡验收
只启动一个实例,提交一条任务,确认:
- 模型能够完整加载;
- 输出视频可播放;
-帧数、分辨率和时长正确; - 没有缺失节点;
- 峰值显存未触发OOM。
2. 每卡一条任务
4卡先提交4条,8卡先提交8条:
bash
watch -n 1 nvidia-smi
观察是否每张卡都有独立进程和显存占用。不要一开始就提交几百条任务,否则节点错误、输出冲突或存储问题会被成倍放大。
3. 小批量吞吐验收
建议先跑16~32条代表性任务,记录:
text
任务总数
成功数
失败数
总耗时
单条视频平均耗时
峰值显存
GPU利用率
输出文件大小
重试次数
吞吐量可按下面计算:
text
合格视频吞吐 = 成功且通过验收的视频数 ÷ 总运行小时数
多卡扩展效率:
text
扩展效率 = N卡吞吐 ÷(N × 单卡吞吐)
如果从4卡增加到8卡后吞吐提升很小,应优先检查:
- 模型文件是否反复从网络盘加载;
- 系统内存是否不足;
- CPU解码、预处理或视频编码是否成为瓶颈;
- 输出盘IO是否饱和;
- 调度器是否把大量任务堆在少数实例;
- 自定义节点是否包含全局锁或共享临时文件。
八、常见故障
1. 只有GPU 0在运行
先确认每个实例的启动参数和日志:
bash
ps -ef | rg "python main.py"
每个进程都应有不同的 --cuda-device 和 --port。
2. 第一个实例正常,第二个端口启动失败
通常是端口重复,或旧进程没有退出:
bash
ss -lntp | rg "8188|8189|8190|8191"
3. 多卡后反而频繁OOM
检查是否误把多个实例绑定到同一张GPU,以及任务参数是否一致。视频任务的分辨率、帧数、批量数和上下文长度都可能改变显存峰值。
多实例提高的是并发吞吐,不会替单条工作流扩大显存上限。
4. 输出被覆盖
为每张GPU设置独立输出目录,并给每个任务设置唯一的 filename_prefix。任务索引、种子、工作流版本和模型版本应一起写入结果记录。
5. 同一提示词生成结果不一致
需要同时固定:
- ComfyUI版本或Git提交;
- 自定义节点版本;
- 模型文件及哈希;
- 工作流API JSON;
- seed;
- sampler、scheduler和steps;
- 输入图片、视频及预处理参数。
只固定seed,不代表跨版本结果一定完全一致。
九、以算家云为例操作演示
这类4卡或8卡持续批量生成任务,更适合按长期生产、推理训练和稳定运行定位的专业版 Pro,而不是只根据某个起步价选择资源。

创建实例前,应在算家云创建页面实时核对:
- 当前区域是否有RTX 4090或对应24GB卡型;
- 单卡和多卡数量是否满足任务;
- CPU、系统内存和本地存储是否够用;
- 是否能为每个GPU实例分配独立端口和输出目录;
- 模型、输入素材和成片应放在哪类存储中;
- 关机、释放与数据保留规则。
GPU库存、区域和可选卡数属于动态信息,不能把某次看到的配置写成长期固定资源。算家云镜像社区提供多个ComfyUI镜像,但具体版本和节点仍应以创建时页面为准。
对于8卡任务,先确认是"8张卡处理8条独立任务",还是要求单个模型跨卡运行。前者适合本文的多实例队列;后者需要进一步核对模型并行方式和实例拓扑,不能只靠增加ComfyUI进程解决。
建议先用4张卡跑一轮16~32条小批量PoC,记录成功率、峰值显存、吞吐和IO,再决定是否扩到8卡。
十、总结
ComfyUI调用4张或8张RTX 4090,最容易稳定落地的方式是:
- 每张GPU启动一个独立ComfyUI实例;
- 每个实例使用不同端口、日志和输出目录;
- 通过
/prompt提交API工作流; - 根据
/queue长度把任务发到较空闲的GPU; - 先完成单卡、每卡一任务和小批量三轮验收;
- 用合格成片吞吐衡量扩容效果,不假设8卡一定获得8倍速度。
这套架构主要解决批量视频吞吐,不解决单条工作流超过24GB显存的问题。
FAQ
1. --cuda-device 0,1,2,3 能让一个工作流自动使用4张卡吗?
不能据此推断工作流会自动拆分。该参数控制进程可见的CUDA设备;具体节点是否跨卡,取决于模型、节点实现和工作流。
2. 4张4090是不是等于96GB显存?
对于普通ComfyUI工作流,不是。每张卡仍有独立显存,多实例方案让不同任务分别占用不同GPU。
3. 4卡和8卡需要准备两套代码吗?
不需要。启动脚本修改 GPU_COUNT,调度器扩展服务端口列表即可。
4. 为什么不直接把ComfyUI暴露到公网?
示例默认监听 127.0.0.1,避免未鉴权接口直接暴露。远程使用时应通过SSH隧道、受控反向代理或平台安全访问方式开放。
5. 什么情况下不建议直接上8卡?
任务数量不足、单卡仍频繁OOM、模型盘读取慢、系统内存不足,或者还没有验证工作流成功率时,都不适合直接扩到8卡。