2024年3月,公司要批量生成1000张产品图,每张需要不同角度、不同光照。我用SD WebUI跑,一张图平均4.2秒(RTX4090,1024x1024,50步DPM++ 2M Karras)。
算一下:1000张 × 4.2秒 = 4200秒 ≈ 70分钟。加上手动调参、重试,实际花了3小时。
同事用ComfyUI,同样配置,一张图2.5秒。1000张只要41分钟。差距在哪?节点式工作流+显存复用。
我当场决定迁移。这篇文章就是迁移过程的全记录。
SD WebUI(v1.9.0)的问题:
ComfyUI(v0.2.7)的解决思路:
测试环境:
测试参数:
| 指标 | SD WebUI 1.9.0 | ComfyUI 0.2.7 | 提升 |
|---|---|---|---|
| 单张耗时 | 4.2s | 2.5s | 40.5% |
| 批量4张耗时 | 16.8s | 8.2s | 51.2% |
| 显存峰值 | 22.1GB | 12.3GB | 44.3% |
| 显存泄漏 | 有(200张后OOM) | 无 | - |
| 首次加载时间 | 12.3s | 8.1s | 34.1% |
| ControlNet支持 | 需插件 | 原生节点 | - |
| 工作流可复用性 | 低(UI操作) | 高(JSON导出) | - |
# 克隆仓库
git clone https://github.com/comfyanonymous/ComfyUI.git
cd ComfyUI
# 创建虚拟环境
python3 -m venv venv
source venv/bin/activate
# 安装依赖
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
pip install -r requirements.txt
# 下载模型(以SDXL为例)
mkdir -p models/checkpoints
wget -O models/checkpoints/sdxl_base.safetensors https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors
# 启动
python main.py --listen 0.0.0.0 --port 8188
这是一个文生图工作流,包含:模型加载、正向提示词、负向提示词、采样器、VAE解码、保存图像。
{
"last_node_id": 10,
"last_link_id": 12,
"nodes": [
{
"id": 3,
"type": "CheckpointLoaderSimple",
"pos": [100, 100],
"size": [315, 106],
"inputs": {
"ckpt_name": "sdxl_base.safetensors"
}
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [500, 100],
"size": [422, 180],
"inputs": {
"text": "a beautiful landscape, mountains, sunset, highly detailed, 8k",
"clip": ["3", 1]
}
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [500, 350],
"size": [422, 180],
"inputs": {
"text": "blurry, low quality, distorted, ugly",
"clip": ["3", 1]
}
},
{
"id": 5,
"type": "KSampler",
"pos": [1000, 200],
"size": [315, 262],
"inputs": {
"seed": 42,
"steps": 50,
"cfg": 7.0,
"sampler_name": "dpmpp_2m",
"scheduler": "karras",
"denoise": 1.0,
"model": ["3", 0],
"positive": ["6", 0],
"negative": ["7", 0],
"latent_image": ["8", 0]
}
},
{
"id": 8,
"type": "EmptyLatentImage",
"pos": [1000, 50],
"size": [315, 106],
"inputs": {
"width": 1024,
"height": 1024,
"batch_size": 1
}
},
{
"id": 9,
"type": "VAEDecode",
"pos": [1400, 200],
"size": [210, 46],
"inputs": {
"samples": ["5", 0],
"vae": ["3", 2]
}
},
{
"id": 10,
"type": "SaveImage",
"pos": [1700, 200],
"size": [210, 46],
"inputs": {
"images": ["9", 0],
"filename_prefix": "ComfyUI"
}
}
],
"links": [
[3, 3, 0, 5, 0, "MODEL"],
[4, 3, 1, 6, 1, "CLIP"],
[5, 3, 1, 7, 1, "CLIP"],
[6, 6, 0, 5, 1, "CONDITIONING"],
[7, 7, 0, 5, 2, "CONDITIONING"],
[8, 8, 0, 5, 3, "LATENT"],
[9, 5, 0, 9, 0, "LATENT"],
[10, 9, 0, 10, 0, "IMAGE"],
[11, 3, 2, 9, 1, "VAE"]
]
}
用Python调用ComfyUI API,批量生成100张图。
import json
import requests
import time
import os
# ComfyUI API地址
API_URL = "http://localhost:8188"
def queue_prompt(workflow):
"""发送工作流到队列"""
payload = {
"prompt": workflow,
"client_id": "batch_generator"
}
response = requests.post(f"{API_URL}/prompt", json=payload)
return response.json()["prompt_id"]
def get_history(prompt_id):
"""获取生成结果"""
response = requests.get(f"{API_URL}/history/{prompt_id}")
return response.json()
def generate_batch(workflow_template, seeds, output_dir):
"""批量生成"""
os.makedirs(output_dir, exist_ok=True)
total = len(seeds)
start_time = time.time()
for i, seed in enumerate(seeds):
# 修改工作流中的seed
workflow = json.loads(json.dumps(workflow_template))
workflow["5"]["inputs"]["seed"] = seed
# 提交任务
prompt_id = queue_prompt(workflow)
# 等待完成
while True:
history = get_history(prompt_id)
if prompt_id in history:
break
time.sleep(0.1)
# 获取输出
outputs = history[prompt_id]["outputs"]
for node_id, node_output in outputs.items():
for img in node_output.get("images", []):
img_url = f"{API_URL}/view?filename={img['filename']}&subfolder={img['subfolder']}&type={img['type']}"
img_data = requests.get(img_url).content
with open(f"{output_dir}/img_{i:04d}.png", "wb") as f:
f.write(img_data)
elapsed = time.time() - start_time
print(f"[{i+1}/{total}] Seed {seed} done. Elapsed: {elapsed:.2f}s")
total_time = time.time() - start_time
print(f"Total: {total} images in {total_time:.2f}s, avg {total_time/total:.2f}s per image")
# 加载工作流模板
with open("workflow.json", "r") as f:
workflow_template = json.load(f)
# 生成100个随机种子
import random
seeds = [random.randint(0, 2**32 - 1) for _ in range(100)]
# 执行批量生成
generate_batch(workflow_template, seeds, "output_batch")
{
"last_node_id": 15,
"nodes": [
{
"id": 3,
"type": "CheckpointLoaderSimple",
"pos": [100, 100],
"inputs": {"ckpt_name": "sdxl_base.safetensors"}
},
{
"id": 11,
"type": "LoadImage",
"pos": [100, 300],
"inputs": {"image": "input_sketch.png"}
},
{
"id": 12,
"type": "ControlNetLoader",
"pos": [100, 500],
"inputs": {"control_net_name": "control_v11p_sd15_canny.pth"}
},
{
"id": 13,
"type": "CannyEdgePreprocessor",
"pos": [400, 300],
"inputs": {
"images": ["11", 0],
"low_threshold": 100,
"high_threshold": 200
}
},
{
"id": 14,
"type": "ControlNetApply",
"pos": [700, 400],
"inputs": {
"conditioning": ["6", 0],
"control_net": ["12", 0],
"image": ["13", 0],
"strength": 0.8
}
},
{
"id": 5,
"type": "KSampler",
"pos": [1000, 300],
"inputs": {
"seed": 42,
"steps": 50,
"cfg": 7.0,
"sampler_name": "dpmpp_2m",
"scheduler": "karras",
"denoise": 0.7,
"model": ["3", 0],
"positive": ["14", 0],
"negative": ["7", 0],
"latent_image": ["8", 0]
}
}
]
}
#!/bin/bash
# 监控GPU使用率
while true; do
clear
echo "=== GPU Monitor ==="
nvidia-smi --query-gpu=index,temperature.gpu,utilization.gpu,memory.used,memory.total --format=csv,noheader
echo ""
echo "=== ComfyUI Process ==="
ps aux | grep "python main.py" | grep -v grep
sleep 2
done
实测结果(RTX4090, SDXL, 50步, 1024x1024):
| 场景 | ComfyUI | SD WebUI | 提升 |
|---|---|---|---|
| 单张文生图 | 2.5s | 4.2s | 40.5% |
| 批量4张 | 8.2s | 16.8s | 51.2% |
| 批量100张 | 205s | 420s | 51.2% |
| ControlNet图生图 | 3.8s | 6.5s | 41.5% |
| 显存峰值 | 12.3GB | 22.1GB | 44.3% |
| 200张后显存 | 12.5GB | OOM | - |
关键发现:
我踩过的坑,直接列出来:
models/checkpoints,不是models/Stable-diffusion。我一开始把SDXL模型放错目录,报错FileNotFoundError。解决方案:用软链接ln -s /path/to/models models。CUDA error: no kernel image is available for execution on the device。解决方案:pip install torch==2.1.2+cu121 torchvision==0.16.2+cu121 --index-url https://download.pytorch.org/whl/cu121。LatentUpscale节点,把latent缩小再解码,或者用TAESD解码器(轻量级VAE)。control-lora-canny-rank128.safetensors。get_history返回空字典。原因是prompt_id拼写错误(大小写敏感)。解决方案:打印response.json()调试,确认字段名。JSONDecodeError。解决方案:用jsonlint验证:cat workflow.json | python -m json.tool。euler_a在SDXL上生成噪点图。解决方案:用官方推荐的dpmpp_2m或dpmpp_sde。--gpu-only 0,1,但实测多卡提升不大(通信开销),建议单卡跑。核心三点:
torch.cuda.empty_cache()回收碎片。WebUI是每个节点独立申请显存,不释放。数据说话:用nvidia-smi监控,ComfyUI的GPU利用率稳定在95-98%,WebUI只有60-75%。
ComfyUI不是银弹,但如果你需要批量生成、复杂工作流、或者显存不够,它比WebUI强太多。我的建议:
代码都在上面,直接复制跑。有问题去ComfyUI GitHub Issues搜,别问我。
专注技术分享与实战