ComfyUI 怎么同时调用 4 张/8 张 RTX 4090?AI 视频批量生成的多实例队列与 Python 调度方案

发布日期: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,最容易稳定落地的方式是:

  1. 每张GPU启动一个独立ComfyUI实例;
  2. 每个实例使用不同端口、日志和输出目录;
  3. 通过 /prompt 提交API工作流;
  4. 根据 /queue 长度把任务发到较空闲的GPU;
  5. 先完成单卡、每卡一任务和小批量三轮验收;
  6. 用合格成片吞吐衡量扩容效果,不假设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卡。


相关推荐
泡海椒1 小时前
评分系统最佳实践:JQuick-Java实现权重、阈值动态配置评分
java·人工智能·python
云上工程笔记1 小时前
星图AstraFlow接入MiniMax-H3/H3-Max实战:文字/单图生成视频怎么用?附提示词与选型
人工智能
weixin199701080161 小时前
《二手ERP对接的对账机制:按日巡检 + 自动补偿,消灭“幽灵订单“》(附Python源码)
python
m0_547486661 小时前
《Python数据分析与实践》全套PPT课件(杭州电子科技大学)
python·数据分析
7177771 小时前
国内组件安全扫描工具选哪家:2026年主流方案对比与选型指南
人工智能·gitee
OKkankan1 小时前
Python 基础进阶(三):从函数、类到 asyncio 与 FastAPI 后端开发实战
开发语言·python
智购科技自动售卖机厂家1 小时前
设备一到夏天就频繁跳闸,从启动电流追到压缩机电容~YH
数据结构·人工智能·python·eclipse
风合星语1 小时前
2026 机器人行业观察(三):机器人走进酒店和门店——完成一次演示,和顶一个班差在哪?
人工智能·机器人·具身智能·人形机器人·人机协作
优氙费控2 小时前
电子发票报销怎么管理?采集、验真、报销、归档全流程
大数据·人工智能