mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-05-30 02:47:24 +08:00
Compare commits
36 Commits
3bec51725e
...
35879d106a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35879d106a | ||
|
|
3e3ed8cc2a | ||
|
|
67f6cb3527 | ||
|
|
0230e0e7cc | ||
|
|
b5921c8ac2 | ||
|
|
63103d519e | ||
|
|
cf758bd256 | ||
|
|
10b45a71cd | ||
|
|
fa7553138e | ||
|
|
875bdc4015 | ||
|
|
1d64200d2e | ||
|
|
9ea25780c6 | ||
|
|
a2204ec976 | ||
|
|
54ced2923b | ||
|
|
c39f7ea76c | ||
|
|
ba978bc0e2 | ||
|
|
79825dbd32 | ||
|
|
139d4a7e86 | ||
|
|
6251350cf4 | ||
|
|
69f6272edc | ||
|
|
059b346966 | ||
|
|
ca56e224a0 | ||
|
|
512deb3cd6 | ||
|
|
09f03107c2 | ||
|
|
08411a1d65 | ||
|
|
125ed0be4b | ||
|
|
d407d82350 | ||
|
|
ff5e379cc2 | ||
|
|
52da6933b4 | ||
|
|
978b962300 | ||
|
|
8136fbbb4a | ||
|
|
28bbdb0031 | ||
|
|
c02b5d4c1e | ||
|
|
00379b4acf | ||
|
|
96363fa74a | ||
|
|
ac85d7887f |
@ -1,2 +1,2 @@
|
||||
.\python_embeded\python.exe -s ComfyUI\main.py --windows-standalone-build --disable-smart-memory
|
||||
.\python_embeded\python.exe -s ComfyUI\main.py --windows-standalone-build --enable-dynamic-vram
|
||||
pause
|
||||
@ -1,2 +1,2 @@
|
||||
# Admins
|
||||
* @comfyanonymous @kosinkadink @guill @alexisrolland @rattus128
|
||||
* @comfyanonymous @kosinkadink @guill @alexisrolland @rattus128 @kijai
|
||||
|
||||
@ -193,13 +193,15 @@ If you have trouble extracting it, right click the file -> properties -> unblock
|
||||
|
||||
The portable above currently comes with python 3.13 and pytorch cuda 13.0. Update your Nvidia drivers if it doesn't start.
|
||||
|
||||
#### Alternative Downloads:
|
||||
#### All Official Portable Downloads:
|
||||
|
||||
[Portable for AMD GPUs](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_amd.7z)
|
||||
|
||||
[Experimental portable for Intel GPUs](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_intel.7z)
|
||||
[Portable for Intel GPUs](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_intel.7z)
|
||||
|
||||
[Portable with pytorch cuda 12.6 and python 3.12](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_nvidia_cu126.7z) (Supports Nvidia 10 series and older GPUs).
|
||||
[Portable for Nvidia GPUs](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_nvidia.7z) (supports 20 series and above).
|
||||
|
||||
[Portable for Nvidia GPUs with pytorch cuda 12.6 and python 3.12](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_nvidia_cu126.7z) (Supports Nvidia 10 series and older GPUs).
|
||||
|
||||
#### How do I share models between another UI and ComfyUI?
|
||||
|
||||
|
||||
127
benchmarks/README.md
Normal file
127
benchmarks/README.md
Normal file
@ -0,0 +1,127 @@
|
||||
# ComfyUI Serving Benchmarks
|
||||
|
||||
Measures latency and throughput of a running ComfyUI server by submitting
|
||||
concurrent prompt requests and collecting results from the history API.
|
||||
|
||||
## Dependencies
|
||||
|
||||
```bash
|
||||
pip install aiohttp tqdm gdown
|
||||
```
|
||||
|
||||
## Supported models / tasks
|
||||
|
||||
| Model | Task | Description |
|
||||
|-------|------|-------------|
|
||||
| `wan22` | `i2v` | Wan 2.2 Image-to-Video — LightX2V 4-step, 720×720, 81 frames |
|
||||
|
||||
To add a new model/task: drop a workflow JSON in `workflows/` (with
|
||||
`__INPUT_IMAGE__` as the image placeholder) and add an entry to
|
||||
`_MODEL_REGISTRY` in `benchmark_comfyui_serving.py`.
|
||||
|
||||
## How it works
|
||||
|
||||
On each run the script:
|
||||
|
||||
1. Downloads model weights into the ComfyUI `models/` directory (only if
|
||||
`--download-models` is passed).
|
||||
2. Downloads the [VBench I2V](https://github.com/Vchitect/VBench) image
|
||||
dataset via `gdown` into ComfyUI's `input/` folder.
|
||||
3. Generates one prompt JSON per input image under
|
||||
`benchmarks/prompts/<model>_<task>/`.
|
||||
4. Submits `--num-requests` prompts to the server, cycling through the
|
||||
generated prompt files in round-robin order.
|
||||
5. Polls `/history/{prompt_id}` for completion and prints a latency /
|
||||
throughput summary.
|
||||
|
||||
Per-node execution times are available when the server is started with
|
||||
`--benchmark-server-only`.
|
||||
|
||||
## Usage
|
||||
|
||||
### Start the server
|
||||
|
||||
```bash
|
||||
python main.py --listen 127.0.0.1 --port 8188 --benchmark-server-only
|
||||
```
|
||||
|
||||
### Run the benchmark
|
||||
|
||||
```bash
|
||||
# From the ComfyUI root directory:
|
||||
python3 benchmarks/benchmark_comfyui_serving.py \
|
||||
--model wan22 --task i2v \
|
||||
--num-requests 50 --max-concurrency 4 \
|
||||
--host http://127.0.0.1:8188
|
||||
```
|
||||
|
||||
Include model weight download on first run:
|
||||
|
||||
```bash
|
||||
python3 benchmarks/benchmark_comfyui_serving.py \
|
||||
--model wan22 --task i2v \
|
||||
--download-models --comfyui-base-dir /path/to/ComfyUI \
|
||||
--num-requests 50 --max-concurrency 4 \
|
||||
--host http://127.0.0.1:8188
|
||||
```
|
||||
|
||||
### All flags
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `--model` | *(required)* | Model name (e.g. `wan22`) |
|
||||
| `--task` | *(required)* | Task type (e.g. `i2v`) |
|
||||
| `--host` | `http://127.0.0.1:8188` | ComfyUI base URL |
|
||||
| `--num-requests` | `50` | Total requests to submit |
|
||||
| `--max-concurrency` | `8` | Max in-flight requests |
|
||||
| `--request-rate` | `0` | Requests/sec; `0` = fire immediately |
|
||||
| `--poisson` | off | Poisson inter-arrival when `--request-rate > 0` |
|
||||
| `--num-images` | `20` | Synthetic images if VBench download unavailable |
|
||||
| `--prompts-dir` | `benchmarks/prompts/<model>_<task>/` | Prompt JSON output directory |
|
||||
| `--download-models` | off | Download model weights before benchmarking |
|
||||
| `--comfyui-base-dir` | — | ComfyUI root (required with `--download-models`) |
|
||||
| `--output-json` | — | Write full per-request results to a JSON file |
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
benchmark: 100%|█████████████| 5/5 [02:58<00:00, 35.73s/req, succeeded=5]
|
||||
|
||||
=== ComfyUI Serving Benchmark Summary ===
|
||||
requests_total: 5
|
||||
requests_success: 5
|
||||
requests_failed: 0
|
||||
wall_time_s: 178.652
|
||||
throughput_req_s: 0.028
|
||||
latency_p50_s: 109.594
|
||||
latency_p90_s: 164.840
|
||||
latency_p95_s: 171.744
|
||||
latency_p99_s: 177.266
|
||||
latency_mean_s: 109.781
|
||||
latency_max_s: 178.647
|
||||
execution_mean_ms: 35465.21
|
||||
execution_p95_ms: 39685.06
|
||||
|
||||
--- Per-node execution time (mean ms across successful requests) ---
|
||||
KSamplerAdvanced (130:110): mean=12827.5 p95=14264.0 n=5
|
||||
KSamplerAdvanced (130:111): mean=12726.4 p95=13822.2 n=5
|
||||
VAEDecode (130:129): mean=3439.0 p95=3467.6 n=5
|
||||
SaveVideo (108): mean=2844.7 p95=3280.0 n=5
|
||||
WanImageToVideo (130:128): mean=2367.7 p95=2595.9 n=5
|
||||
CLIPTextEncode (130:125): mean=1785.0 p95=1785.0 n=1
|
||||
CLIPLoader (130:105): mean=700.7 p95=700.7 n=1
|
||||
LoadImage (97): mean=518.4 p95=970.0 n=5
|
||||
VAELoader (130:106): mean=507.7 p95=507.7 n=1
|
||||
CLIPTextEncode (130:107): mean=223.4 p95=223.4 n=1
|
||||
UNETLoader (130:122): mean=122.2 p95=122.2 n=1
|
||||
LoraLoaderModelOnly (130:126): mean=68.1 p95=68.1 n=1
|
||||
UNETLoader (130:123): mean=65.9 p95=65.9 n=1
|
||||
LoraLoaderModelOnly (130:127): mean=36.2 p95=36.2 n=1
|
||||
ModelSamplingSD3 (130:109): mean=1.0 p95=1.0 n=1
|
||||
ModelSamplingSD3 (130:124): mean=0.9 p95=0.9 n=1
|
||||
CreateVideo (130:117): mean=0.7 p95=1.1 n=5
|
||||
```
|
||||
|
||||
> **Note:** Nodes with `n=1` (e.g. model loaders) are cached by ComfyUI after
|
||||
> the first request and skipped in subsequent executions, so they only appear
|
||||
> once across the benchmark run.
|
||||
685
benchmarks/benchmark_comfyui_serving.py
Normal file
685
benchmarks/benchmark_comfyui_serving.py
Normal file
@ -0,0 +1,685 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ComfyUI model serving benchmark.
|
||||
|
||||
Submits prompts concurrently to a running ComfyUI server and reports
|
||||
latency/throughput metrics. Input images and prompt files are prepared
|
||||
automatically (and cached for reuse) before the benchmark starts.
|
||||
|
||||
On first run the script will:
|
||||
1. Download model weights (if --download-models is set).
|
||||
2. Download the VBench I2V image dataset (requires: pip install gdown),
|
||||
or generate synthetic placeholder images as a fallback.
|
||||
3. Write one prompt JSON per input image under benchmarks/prompts/<model>_<task>/.
|
||||
|
||||
On subsequent runs all three steps are skipped if the files already exist.
|
||||
Requests are distributed across prompt files in round-robin order.
|
||||
|
||||
Supported models / tasks
|
||||
------------------------
|
||||
wan22 / i2v — Wan 2.2 Image-to-Video (LightX2V 4-step, 720×720, 81 frames)
|
||||
|
||||
Usage
|
||||
-----
|
||||
python3 benchmarks/benchmark_comfyui_serving.py \\
|
||||
--model wan22 --task i2v \\
|
||||
--num-requests 50 --max-concurrency 4 \\
|
||||
--host http://127.0.0.1:8188
|
||||
|
||||
# Also download model weights (run from ComfyUI root):
|
||||
python3 benchmarks/benchmark_comfyui_serving.py \\
|
||||
--model wan22 --task i2v \\
|
||||
--download-models --comfyui-base-dir /path/to/ComfyUI \\
|
||||
--num-requests 50 --max-concurrency 4 \\
|
||||
--host http://127.0.0.1:8188
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.request
|
||||
import uuid
|
||||
from dataclasses import dataclass, asdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Benchmark setup helpers
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# Workflow JSON files live in benchmarks/workflows/<model>_<task>.json.
|
||||
_WORKFLOWS_DIR = Path(__file__).parent / "workflows"
|
||||
|
||||
# Placeholder in workflow JSON files that is replaced with the actual image filename.
|
||||
_IMAGE_PLACEHOLDER = "__INPUT_IMAGE__"
|
||||
|
||||
# Model weight downloads for wan22/i2v.
|
||||
_WAN22_I2V_MODELS: list[tuple[str, str]] = [
|
||||
(
|
||||
"models/diffusion_models/wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
"https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
),
|
||||
(
|
||||
"models/diffusion_models/wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
"https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
),
|
||||
(
|
||||
"models/loras/wan2.2_i2v_lightx2v_4steps_lora_v1_high_noise.safetensors",
|
||||
"https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/loras/wan2.2_i2v_lightx2v_4steps_lora_v1_high_noise.safetensors",
|
||||
),
|
||||
(
|
||||
"models/loras/wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors",
|
||||
"https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/loras/wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors",
|
||||
),
|
||||
(
|
||||
"models/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
),
|
||||
(
|
||||
"models/vae/wan_2.1_vae.safetensors",
|
||||
"https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/vae/wan_2.1_vae.safetensors",
|
||||
),
|
||||
]
|
||||
|
||||
# Google Drive file IDs from VBench's vbench2_beta_i2v/download_data.sh
|
||||
_VBENCH_ORIGIN_ZIP_GDRIVE_ID = "1qhkLCSBkzll0dkKpwlDTwLL0nxdQ4nrY"
|
||||
|
||||
# Registry mapping (model, task) → benchmark configuration.
|
||||
# To add a new model/task: drop a workflow JSON in benchmarks/workflows/ and
|
||||
# add an entry here.
|
||||
_MODEL_REGISTRY: dict[tuple[str, str], dict[str, Any]] = {
|
||||
("wan22", "i2v"): {
|
||||
"workflow_file": "wan22_i2v.json",
|
||||
"model_files": _WAN22_I2V_MODELS,
|
||||
"image_source": "vbench_i2v",
|
||||
},
|
||||
}
|
||||
|
||||
_VALID_MODELS = sorted({m for m, _ in _MODEL_REGISTRY})
|
||||
_VALID_TASKS = sorted({t for _, t in _MODEL_REGISTRY})
|
||||
|
||||
|
||||
def _replace_in_graph(obj: Any, placeholder: str, value: str) -> None:
|
||||
"""Recursively replace every occurrence of *placeholder* with *value* in-place."""
|
||||
if isinstance(obj, dict):
|
||||
for k, v in obj.items():
|
||||
if v == placeholder:
|
||||
obj[k] = value
|
||||
else:
|
||||
_replace_in_graph(v, placeholder, value)
|
||||
elif isinstance(obj, list):
|
||||
for i, item in enumerate(obj):
|
||||
if item == placeholder:
|
||||
obj[i] = value
|
||||
else:
|
||||
_replace_in_graph(item, placeholder, value)
|
||||
|
||||
|
||||
def download_models(base_dir: Path, model: str, task: str) -> None:
|
||||
"""Download model weights for *model*/*task* into *base_dir* using wget."""
|
||||
key = (model, task)
|
||||
if key not in _MODEL_REGISTRY:
|
||||
raise ValueError(f"No model files registered for {model}/{task}")
|
||||
for rel_path, url in _MODEL_REGISTRY[key]["model_files"]:
|
||||
dest = base_dir / rel_path
|
||||
if dest.exists():
|
||||
print(f"[setup] already exists, skipping: {dest}")
|
||||
continue
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
print(f"[setup] downloading {dest.name} ...")
|
||||
subprocess.run(["wget", "-O", str(dest), url], check=True)
|
||||
|
||||
|
||||
def _try_download_vbench_i2v(input_dir: Path) -> list[str]:
|
||||
"""
|
||||
Download VBench I2V origin images from Google Drive via gdown (pip install gdown).
|
||||
Raises on any failure.
|
||||
"""
|
||||
import gdown # type: ignore; raises ImportError if not installed
|
||||
|
||||
import zipfile
|
||||
|
||||
zip_path = input_dir / "origin.zip"
|
||||
try:
|
||||
if not zip_path.exists():
|
||||
print("[setup] downloading VBench I2V origin images from Google Drive ...")
|
||||
gdown.download(id=_VBENCH_ORIGIN_ZIP_GDRIVE_ID, output=str(zip_path), quiet=False)
|
||||
print("[setup] extracting origin.zip ...")
|
||||
with zipfile.ZipFile(zip_path, "r") as zf:
|
||||
zf.extractall(str(input_dir))
|
||||
zip_path.unlink()
|
||||
except Exception:
|
||||
if zip_path.exists():
|
||||
zip_path.unlink()
|
||||
raise
|
||||
|
||||
image_exts = {".png", ".jpg", ".jpeg", ".webp"}
|
||||
filenames = sorted(
|
||||
p.relative_to(input_dir).as_posix()
|
||||
for p in input_dir.rglob("*")
|
||||
if p.suffix.lower() in image_exts
|
||||
)
|
||||
print(f"[setup] prepared {len(filenames)} VBench I2V images in {input_dir}")
|
||||
return filenames
|
||||
|
||||
|
||||
def _generate_synthetic_images(input_dir: Path, num_images: int) -> list[str]:
|
||||
"""Generate synthetic 720×720 white PNG placeholders; returns filenames."""
|
||||
try:
|
||||
from PIL import Image as PILImage # type: ignore
|
||||
except ImportError:
|
||||
raise RuntimeError(
|
||||
"Pillow is required for synthetic image generation. "
|
||||
"Install it with: pip install Pillow"
|
||||
)
|
||||
|
||||
filenames: list[str] = []
|
||||
for i in range(num_images):
|
||||
fname = f"benchmark_input_{i:04d}.png"
|
||||
dest = input_dir / fname
|
||||
if not dest.exists():
|
||||
PILImage.new("RGB", (720, 720), color=(255, 255, 255)).save(str(dest))
|
||||
filenames.append(fname)
|
||||
return filenames
|
||||
|
||||
|
||||
def prepare_input_images(
|
||||
input_dir: Path,
|
||||
num_images: int = 20,
|
||||
image_source: str = "vbench_i2v",
|
||||
) -> list[str]:
|
||||
"""
|
||||
Prepare benchmark input images in *input_dir*.
|
||||
For "vbench_i2v", downloads from Google Drive and raises on failure.
|
||||
Falls back to synthetic images only when image_source is not "vbench_i2v".
|
||||
Returns a list of image paths relative to *input_dir*.
|
||||
"""
|
||||
input_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if image_source == "vbench_i2v":
|
||||
return _try_download_vbench_i2v(input_dir)
|
||||
|
||||
print(f"[setup] generating {num_images} synthetic 720×720 placeholder images ...")
|
||||
return _generate_synthetic_images(input_dir, num_images)
|
||||
|
||||
|
||||
def generate_prompt_file(
|
||||
output_path: Path,
|
||||
workflow_path: Path,
|
||||
image_filename: str,
|
||||
) -> None:
|
||||
"""
|
||||
Write a single ComfyUI prompt JSON to *output_path* from *workflow_path*.
|
||||
|
||||
Replaces every occurrence of the sentinel string "__INPUT_IMAGE__" in the
|
||||
workflow graph with *image_filename*.
|
||||
"""
|
||||
graph: dict[str, Any] = json.loads(workflow_path.read_text())
|
||||
_replace_in_graph(graph, _IMAGE_PLACEHOLDER, image_filename)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(json.dumps({"prompt": graph}, indent=2))
|
||||
|
||||
|
||||
def generate_prompt_files(
|
||||
model: str,
|
||||
task: str,
|
||||
output_dir: Path,
|
||||
input_dir: Path,
|
||||
num_images: int = 20,
|
||||
download_model_weights: bool = False,
|
||||
comfyui_base_dir: Path | None = None,
|
||||
) -> list[Path]:
|
||||
"""
|
||||
Full benchmark setup for a given *model*/*task*:
|
||||
|
||||
1. Optionally download model weights into *comfyui_base_dir*.
|
||||
2. Prepare input images in *input_dir* (skipped if images already exist).
|
||||
3. Generate one prompt JSON per input image in *output_dir*
|
||||
(skipped if prompt files already exist).
|
||||
|
||||
Returns the list of prompt file paths.
|
||||
"""
|
||||
key = (model, task)
|
||||
if key not in _MODEL_REGISTRY:
|
||||
available = ", ".join(f"{m}/{t}" for m, t in _MODEL_REGISTRY)
|
||||
raise ValueError(f"Unknown --model {model!r} --task {task!r}. Available: {available}")
|
||||
|
||||
cfg = _MODEL_REGISTRY[key]
|
||||
|
||||
if download_model_weights:
|
||||
if comfyui_base_dir is None:
|
||||
raise ValueError("--comfyui-base-dir is required when --download-models is set")
|
||||
download_models(comfyui_base_dir, model, task)
|
||||
|
||||
image_filenames = prepare_input_images(
|
||||
input_dir,
|
||||
num_images=num_images,
|
||||
image_source=cfg.get("image_source", "synthetic"),
|
||||
)
|
||||
if not image_filenames:
|
||||
raise RuntimeError(f"No input images available in {input_dir}")
|
||||
|
||||
workflow_path = _WORKFLOWS_DIR / cfg["workflow_file"]
|
||||
if not workflow_path.exists():
|
||||
raise FileNotFoundError(f"Workflow file not found: {workflow_path}")
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
generated: list[Path] = []
|
||||
for i, image_name in enumerate(image_filenames):
|
||||
prompt_path = output_dir / f"{model}_{task}_prompt_{i:04d}.json"
|
||||
generate_prompt_file(prompt_path, workflow_path, image_name)
|
||||
generated.append(prompt_path)
|
||||
|
||||
print(f"[setup] generated {len(generated)} prompt files in {output_dir}")
|
||||
return generated
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@dataclass
|
||||
class RequestResult:
|
||||
request_index: int
|
||||
prompt_id: str | None
|
||||
ok: bool
|
||||
error: str | None
|
||||
queued_at: float
|
||||
started_at: float
|
||||
finished_at: float
|
||||
end_to_end_s: float
|
||||
execution_ms: float | None
|
||||
node_timing_ms: dict[str, dict] | None
|
||||
|
||||
|
||||
def percentile(values: list[float], pct: float) -> float:
|
||||
if not values:
|
||||
return float("nan")
|
||||
if len(values) == 1:
|
||||
return values[0]
|
||||
values = sorted(values)
|
||||
rank = (len(values) - 1) * (pct / 100.0)
|
||||
lower = math.floor(rank)
|
||||
upper = math.ceil(rank)
|
||||
if lower == upper:
|
||||
return values[lower]
|
||||
weight = rank - lower
|
||||
return values[lower] * (1.0 - weight) + values[upper] * weight
|
||||
|
||||
|
||||
def patch_seed_in_prompt(prompt: dict[str, Any], seed: int, seed_path: str | None) -> dict[str, Any]:
|
||||
"""
|
||||
Patch prompt seed in-place for common sampler nodes.
|
||||
seed_path format: "<node_id>.<input_name>".
|
||||
"""
|
||||
if seed_path:
|
||||
try:
|
||||
node_id, input_name = seed_path.split(".", 1)
|
||||
prompt[node_id]["inputs"][input_name] = seed
|
||||
return prompt
|
||||
except Exception as exc:
|
||||
raise ValueError(f"Invalid --seed-path '{seed_path}': {exc}") from exc
|
||||
|
||||
# Best-effort fallback: update any input key named 'seed' or 'noise_seed'
|
||||
for node in prompt.values():
|
||||
if not isinstance(node, dict):
|
||||
continue
|
||||
inputs = node.get("inputs")
|
||||
if not isinstance(inputs, dict):
|
||||
continue
|
||||
if "seed" in inputs:
|
||||
inputs["seed"] = seed
|
||||
if "noise_seed" in inputs:
|
||||
inputs["noise_seed"] = seed
|
||||
return prompt
|
||||
|
||||
|
||||
def load_prompt_template(path: Path) -> dict[str, Any]:
|
||||
data = json.loads(path.read_text())
|
||||
if "prompt" in data and isinstance(data["prompt"], dict):
|
||||
return data
|
||||
if isinstance(data, dict):
|
||||
return {"prompt": data}
|
||||
raise ValueError("Prompt file must be a JSON object (prompt graph or wrapper with 'prompt').")
|
||||
|
||||
|
||||
async def submit_prompt(
|
||||
session: aiohttp.ClientSession,
|
||||
base_url: str,
|
||||
endpoint: str,
|
||||
payload: dict[str, Any],
|
||||
timeout_s: float,
|
||||
) -> str:
|
||||
url = f"{base_url}{endpoint}"
|
||||
async with session.post(url, json=payload, timeout=timeout_s) as resp:
|
||||
text = await resp.text()
|
||||
if resp.status != 200:
|
||||
raise RuntimeError(f"submit failed [{resp.status}] {text}")
|
||||
body = json.loads(text)
|
||||
prompt_id = body.get("prompt_id")
|
||||
if not prompt_id:
|
||||
raise RuntimeError(f"missing prompt_id in response: {body}")
|
||||
return prompt_id
|
||||
|
||||
|
||||
async def wait_for_prompt_done(
|
||||
session: aiohttp.ClientSession,
|
||||
base_url: str,
|
||||
prompt_id: str,
|
||||
poll_interval_s: float,
|
||||
timeout_s: float,
|
||||
) -> tuple[float | None, dict | None]:
|
||||
"""
|
||||
Returns (execution_ms, node_timing_ms) from history_item["benchmark"].
|
||||
Falls back to (None, None) if unavailable.
|
||||
"""
|
||||
deadline = time.perf_counter() + timeout_s
|
||||
history_url = f"{base_url}/history/{prompt_id}"
|
||||
|
||||
while time.perf_counter() < deadline:
|
||||
async with session.get(history_url, timeout=timeout_s) as resp:
|
||||
if resp.status != 200:
|
||||
text = await resp.text()
|
||||
raise RuntimeError(f"history failed [{resp.status}] {text}")
|
||||
|
||||
payload = await resp.json()
|
||||
if not payload:
|
||||
await asyncio.sleep(poll_interval_s)
|
||||
continue
|
||||
|
||||
history_item = payload.get(prompt_id)
|
||||
if history_item is None:
|
||||
await asyncio.sleep(poll_interval_s)
|
||||
continue
|
||||
|
||||
status = history_item.get("status", {})
|
||||
if status.get("status_str") not in ("success", "error"):
|
||||
await asyncio.sleep(poll_interval_s)
|
||||
continue
|
||||
|
||||
benchmark = history_item.get("benchmark", {})
|
||||
return (
|
||||
benchmark.get("execution_ms"),
|
||||
benchmark.get("nodes"),
|
||||
)
|
||||
|
||||
await asyncio.sleep(poll_interval_s)
|
||||
|
||||
raise TimeoutError(f"timed out waiting for prompt_id={prompt_id}")
|
||||
|
||||
|
||||
def build_arrival_schedule(num_requests: int, request_rate: float, poisson: bool, seed: int) -> list[float]:
|
||||
"""
|
||||
Returns absolute offsets (seconds from benchmark start) for each request.
|
||||
"""
|
||||
if request_rate <= 0:
|
||||
return [0.0] * num_requests
|
||||
|
||||
rnd = random.Random(seed)
|
||||
offsets: list[float] = []
|
||||
t = 0.0
|
||||
for _ in range(num_requests):
|
||||
if poisson:
|
||||
delta = rnd.expovariate(request_rate)
|
||||
else:
|
||||
delta = 1.0 / request_rate
|
||||
t += delta
|
||||
offsets.append(t)
|
||||
return offsets
|
||||
|
||||
|
||||
async def run_request(
|
||||
idx: int,
|
||||
start_time: float,
|
||||
scheduled_offset_s: float,
|
||||
semaphore: asyncio.Semaphore,
|
||||
session: aiohttp.ClientSession,
|
||||
args: argparse.Namespace,
|
||||
prompt_templates: list[dict[str, Any]],
|
||||
) -> RequestResult:
|
||||
await asyncio.sleep(max(0.0, (start_time + scheduled_offset_s) - time.perf_counter()))
|
||||
queued_at = time.perf_counter()
|
||||
|
||||
async with semaphore:
|
||||
started_at = time.perf_counter()
|
||||
prompt_id = None
|
||||
try:
|
||||
payload = json.loads(json.dumps(prompt_templates[idx % len(prompt_templates)]))
|
||||
payload.setdefault("extra_data", {})
|
||||
payload["client_id"] = args.client_id
|
||||
|
||||
seed = args.base_seed + idx
|
||||
payload["prompt"] = patch_seed_in_prompt(payload["prompt"], seed, args.seed_path)
|
||||
|
||||
prompt_id = await submit_prompt(
|
||||
session=session,
|
||||
base_url=args.host,
|
||||
endpoint=args.endpoint,
|
||||
payload=payload,
|
||||
timeout_s=args.request_timeout_s,
|
||||
)
|
||||
|
||||
execution_ms, node_timing_ms = await wait_for_prompt_done(
|
||||
session=session,
|
||||
base_url=args.host,
|
||||
prompt_id=prompt_id,
|
||||
poll_interval_s=args.poll_interval_s,
|
||||
timeout_s=args.request_timeout_s,
|
||||
)
|
||||
finished_at = time.perf_counter()
|
||||
return RequestResult(
|
||||
request_index=idx,
|
||||
prompt_id=prompt_id,
|
||||
ok=True,
|
||||
error=None,
|
||||
queued_at=queued_at,
|
||||
started_at=started_at,
|
||||
finished_at=finished_at,
|
||||
end_to_end_s=finished_at - queued_at,
|
||||
execution_ms=execution_ms,
|
||||
node_timing_ms=node_timing_ms,
|
||||
)
|
||||
except Exception as exc:
|
||||
finished_at = time.perf_counter()
|
||||
return RequestResult(
|
||||
request_index=idx,
|
||||
prompt_id=prompt_id,
|
||||
ok=False,
|
||||
error=repr(exc),
|
||||
queued_at=queued_at,
|
||||
started_at=started_at,
|
||||
finished_at=finished_at,
|
||||
end_to_end_s=finished_at - queued_at,
|
||||
execution_ms=None,
|
||||
node_timing_ms=None,
|
||||
)
|
||||
|
||||
|
||||
def print_summary(results: list[RequestResult], wall_s: float) -> None:
|
||||
success = [r for r in results if r.ok]
|
||||
fail = [r for r in results if not r.ok]
|
||||
lat_s = [r.end_to_end_s for r in success]
|
||||
exec_ms = [r.execution_ms for r in success if r.execution_ms is not None]
|
||||
|
||||
throughput = (len(success) / wall_s) if wall_s > 0 else 0.0
|
||||
print("\n=== ComfyUI Serving Benchmark Summary ===")
|
||||
print(f"requests_total: {len(results)}")
|
||||
print(f"requests_success: {len(success)}")
|
||||
print(f"requests_failed: {len(fail)}")
|
||||
print(f"wall_time_s: {wall_s:.3f}")
|
||||
print(f"throughput_req_s: {throughput:.3f}")
|
||||
|
||||
if lat_s:
|
||||
print(f"latency_p50_s: {percentile(lat_s, 50):.3f}")
|
||||
print(f"latency_p90_s: {percentile(lat_s, 90):.3f}")
|
||||
print(f"latency_p95_s: {percentile(lat_s, 95):.3f}")
|
||||
print(f"latency_p99_s: {percentile(lat_s, 99):.3f}")
|
||||
print(f"latency_mean_s: {statistics.mean(lat_s):.3f}")
|
||||
print(f"latency_max_s: {max(lat_s):.3f}")
|
||||
|
||||
if exec_ms:
|
||||
print(f"execution_mean_ms: {statistics.mean(exec_ms):.2f}")
|
||||
print(f"execution_p95_ms: {percentile(exec_ms, 95):.2f}")
|
||||
|
||||
# Per-node timing: aggregate execution_ms across all successful results.
|
||||
node_totals: dict[str, list[float]] = {}
|
||||
for r in success:
|
||||
if not r.node_timing_ms:
|
||||
continue
|
||||
for node_id, info in r.node_timing_ms.items():
|
||||
key = f"{info.get('class_type', 'unknown')} ({node_id})"
|
||||
node_totals.setdefault(key, []).append(info.get("execution_ms", 0.0))
|
||||
if node_totals:
|
||||
print("\n--- Per-node execution time (mean ms across successful requests) ---")
|
||||
for key, times in sorted(node_totals.items(), key=lambda x: -statistics.mean(x[1])):
|
||||
print(f" {key}: mean={statistics.mean(times):.1f} p95={percentile(times, 95):.1f} n={len(times)}")
|
||||
|
||||
if fail:
|
||||
print("\nSample failures:")
|
||||
for r in fail[:5]:
|
||||
print(f" idx={r.request_index} prompt_id={r.prompt_id} error={r.error}")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(description="Benchmark ComfyUI request serving.")
|
||||
p.add_argument("--host", type=str, default="http://127.0.0.1:8188", help="ComfyUI base URL.")
|
||||
p.add_argument(
|
||||
"--endpoint",
|
||||
type=str,
|
||||
default="/prompt",
|
||||
choices=("/prompt", "/bench/prompt"),
|
||||
help="Submission endpoint.",
|
||||
)
|
||||
p.add_argument(
|
||||
"--model",
|
||||
choices=_VALID_MODELS,
|
||||
required=True,
|
||||
help=f"Model to benchmark. Choices: {_VALID_MODELS}.",
|
||||
)
|
||||
p.add_argument(
|
||||
"--task",
|
||||
choices=_VALID_TASKS,
|
||||
required=True,
|
||||
help=f"Task type. Choices: {_VALID_TASKS}.",
|
||||
)
|
||||
p.add_argument(
|
||||
"--prompts-dir",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Directory where generated prompt JSON files are written (default: benchmarks/prompts/<model>_<task>/).",
|
||||
)
|
||||
p.add_argument(
|
||||
"--num-images",
|
||||
type=int,
|
||||
default=20,
|
||||
help="Number of synthetic images to generate when dataset download is unavailable (default: 20).",
|
||||
)
|
||||
p.add_argument(
|
||||
"--download-models",
|
||||
action="store_true",
|
||||
help="Download model weights before generating prompts (requires --comfyui-base-dir).",
|
||||
)
|
||||
p.add_argument(
|
||||
"--comfyui-base-dir",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="ComfyUI root directory used as the base for model downloads.",
|
||||
)
|
||||
p.add_argument("--num-requests", type=int, default=50)
|
||||
p.add_argument("--max-concurrency", type=int, default=8)
|
||||
p.add_argument("--request-rate", type=float, default=0.0, help="Requests/sec. 0 = fire immediately.")
|
||||
p.add_argument("--poisson", action="store_true", help="Use Poisson inter-arrival when request-rate > 0.")
|
||||
p.add_argument("--base-seed", type=int, default=1234)
|
||||
p.add_argument(
|
||||
"--seed-path",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Optional path to seed field in prompt: <node_id>.<input_name> (e.g. 3.seed).",
|
||||
)
|
||||
p.add_argument("--client-id", type=str, default=f"bench-{uuid.uuid4().hex[:12]}")
|
||||
p.add_argument("--request-timeout-s", type=float, default=600.0)
|
||||
p.add_argument("--poll-interval-s", type=float, default=0.2)
|
||||
p.add_argument("--output-json", type=Path, default=None, help="Write detailed result JSON.")
|
||||
p.add_argument("--seed", type=int, default=0, help="RNG seed for schedule generation.")
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
async def async_main(args: argparse.Namespace) -> None:
|
||||
prompts_dir = args.prompts_dir or Path("benchmarks/prompts") / f"{args.model}_{args.task}"
|
||||
prompt_paths = generate_prompt_files(
|
||||
model=args.model,
|
||||
task=args.task,
|
||||
output_dir=prompts_dir,
|
||||
input_dir=Path("input"),
|
||||
num_images=args.num_images,
|
||||
download_model_weights=args.download_models,
|
||||
comfyui_base_dir=args.comfyui_base_dir,
|
||||
)
|
||||
prompt_templates = [load_prompt_template(p) for p in prompt_paths]
|
||||
print(f"[bench] loaded {len(prompt_templates)} prompt templates, round-robining over {args.num_requests} requests")
|
||||
|
||||
schedule = build_arrival_schedule(
|
||||
num_requests=args.num_requests,
|
||||
request_rate=args.request_rate,
|
||||
poisson=args.poisson,
|
||||
seed=args.seed,
|
||||
)
|
||||
semaphore = asyncio.Semaphore(args.max_concurrency)
|
||||
connector = aiohttp.TCPConnector(limit=max(args.max_concurrency * 2, 32))
|
||||
|
||||
started = time.perf_counter()
|
||||
async with aiohttp.ClientSession(connector=connector) as session:
|
||||
tasks = [
|
||||
asyncio.create_task(
|
||||
run_request(
|
||||
idx=i,
|
||||
start_time=started,
|
||||
scheduled_offset_s=schedule[i],
|
||||
semaphore=semaphore,
|
||||
session=session,
|
||||
args=args,
|
||||
prompt_templates=prompt_templates,
|
||||
)
|
||||
)
|
||||
for i in range(args.num_requests)
|
||||
]
|
||||
results = []
|
||||
with tqdm(total=args.num_requests, unit="req", desc="benchmark") as pbar:
|
||||
for coro in asyncio.as_completed(tasks):
|
||||
result = await coro
|
||||
results.append(result)
|
||||
pbar.update(1)
|
||||
if result.ok:
|
||||
pbar.set_postfix(succeeded=sum(r.ok for r in results))
|
||||
wall_s = time.perf_counter() - started
|
||||
|
||||
print_summary(results, wall_s)
|
||||
|
||||
if args.output_json is not None:
|
||||
out = {
|
||||
"config": vars(args),
|
||||
"wall_time_s": wall_s,
|
||||
"results": [asdict(r) for r in sorted(results, key=lambda x: x.request_index)],
|
||||
}
|
||||
args.output_json.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output_json.write_text(json.dumps(out, indent=2))
|
||||
print(f"\nWrote results to: {args.output_json}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
asyncio.run(async_main(args))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
154
benchmarks/workflows/wan22_i2v.json
Normal file
154
benchmarks/workflows/wan22_i2v.json
Normal file
@ -0,0 +1,154 @@
|
||||
{
|
||||
"97": {
|
||||
"inputs": {"image": "__INPUT_IMAGE__"},
|
||||
"class_type": "LoadImage",
|
||||
"_meta": {"title": "Start Frame Image"}
|
||||
},
|
||||
"108": {
|
||||
"inputs": {
|
||||
"filename_prefix": "video/Wan2.2_image_to_video",
|
||||
"format": "auto",
|
||||
"codec": "auto",
|
||||
"video-preview": "",
|
||||
"video": ["130:117", 0]
|
||||
},
|
||||
"class_type": "SaveVideo",
|
||||
"_meta": {"title": "Save Video"}
|
||||
},
|
||||
"130:105": {
|
||||
"inputs": {
|
||||
"clip_name": "umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"type": "wan",
|
||||
"device": "default"
|
||||
},
|
||||
"class_type": "CLIPLoader",
|
||||
"_meta": {"title": "Load CLIP"}
|
||||
},
|
||||
"130:106": {
|
||||
"inputs": {"vae_name": "wan_2.1_vae.safetensors"},
|
||||
"class_type": "VAELoader",
|
||||
"_meta": {"title": "Load VAE"}
|
||||
},
|
||||
"130:107": {
|
||||
"inputs": {
|
||||
"text": "A felt-style little eagle cashier greeting, waving, and smiling at the camera.",
|
||||
"clip": ["130:105", 0]
|
||||
},
|
||||
"class_type": "CLIPTextEncode",
|
||||
"_meta": {"title": "CLIP Text Encode (Positive Prompt)"}
|
||||
},
|
||||
"130:109": {
|
||||
"inputs": {"shift": 5.000000000000001, "model": ["130:126", 0]},
|
||||
"class_type": "ModelSamplingSD3",
|
||||
"_meta": {"title": "ModelSamplingSD3"}
|
||||
},
|
||||
"130:110": {
|
||||
"inputs": {
|
||||
"add_noise": "enable",
|
||||
"noise_seed": 636787045983965,
|
||||
"steps": 4,
|
||||
"cfg": 1,
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "simple",
|
||||
"start_at_step": 0,
|
||||
"end_at_step": 2,
|
||||
"return_with_leftover_noise": "enable",
|
||||
"model": ["130:109", 0],
|
||||
"positive": ["130:128", 0],
|
||||
"negative": ["130:128", 1],
|
||||
"latent_image": ["130:128", 2]
|
||||
},
|
||||
"class_type": "KSamplerAdvanced",
|
||||
"_meta": {"title": "KSampler (Advanced)"}
|
||||
},
|
||||
"130:111": {
|
||||
"inputs": {
|
||||
"add_noise": "disable",
|
||||
"noise_seed": 0,
|
||||
"steps": 4,
|
||||
"cfg": 1,
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "simple",
|
||||
"start_at_step": 2,
|
||||
"end_at_step": 4,
|
||||
"return_with_leftover_noise": "disable",
|
||||
"model": ["130:124", 0],
|
||||
"positive": ["130:128", 0],
|
||||
"negative": ["130:128", 1],
|
||||
"latent_image": ["130:110", 0]
|
||||
},
|
||||
"class_type": "KSamplerAdvanced",
|
||||
"_meta": {"title": "KSampler (Advanced)"}
|
||||
},
|
||||
"130:117": {
|
||||
"inputs": {"fps": 16, "images": ["130:129", 0]},
|
||||
"class_type": "CreateVideo",
|
||||
"_meta": {"title": "Create Video"}
|
||||
},
|
||||
"130:122": {
|
||||
"inputs": {
|
||||
"unet_name": "wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
"weight_dtype": "default"
|
||||
},
|
||||
"class_type": "UNETLoader",
|
||||
"_meta": {"title": "Load Diffusion Model"}
|
||||
},
|
||||
"130:123": {
|
||||
"inputs": {
|
||||
"unet_name": "wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
"weight_dtype": "default"
|
||||
},
|
||||
"class_type": "UNETLoader",
|
||||
"_meta": {"title": "Load Diffusion Model"}
|
||||
},
|
||||
"130:124": {
|
||||
"inputs": {"shift": 5.000000000000001, "model": ["130:127", 0]},
|
||||
"class_type": "ModelSamplingSD3",
|
||||
"_meta": {"title": "ModelSamplingSD3"}
|
||||
},
|
||||
"130:125": {
|
||||
"inputs": {
|
||||
"text": "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走",
|
||||
"clip": ["130:105", 0]
|
||||
},
|
||||
"class_type": "CLIPTextEncode",
|
||||
"_meta": {"title": "CLIP Text Encode (Negative Prompt)"}
|
||||
},
|
||||
"130:126": {
|
||||
"inputs": {
|
||||
"lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_high_noise.safetensors",
|
||||
"strength_model": 1.0000000000000002,
|
||||
"model": ["130:122", 0]
|
||||
},
|
||||
"class_type": "LoraLoaderModelOnly",
|
||||
"_meta": {"title": "Load LoRA"}
|
||||
},
|
||||
"130:127": {
|
||||
"inputs": {
|
||||
"lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors",
|
||||
"strength_model": 1.0000000000000002,
|
||||
"model": ["130:123", 0]
|
||||
},
|
||||
"class_type": "LoraLoaderModelOnly",
|
||||
"_meta": {"title": "Load LoRA"}
|
||||
},
|
||||
"130:128": {
|
||||
"inputs": {
|
||||
"width": 720,
|
||||
"height": 720,
|
||||
"length": 81,
|
||||
"batch_size": 1,
|
||||
"positive": ["130:107", 0],
|
||||
"negative": ["130:125", 0],
|
||||
"vae": ["130:106", 0],
|
||||
"start_image": ["97", 0]
|
||||
},
|
||||
"class_type": "WanImageToVideo",
|
||||
"_meta": {"title": "WanImageToVideo"}
|
||||
},
|
||||
"130:129": {
|
||||
"inputs": {"samples": ["130:111", 0], "vae": ["130:106", 0]},
|
||||
"class_type": "VAEDecode",
|
||||
"_meta": {"title": "VAE Decode"}
|
||||
}
|
||||
}
|
||||
@ -90,7 +90,6 @@ parser.add_argument("--force-channels-last", action="store_true", help="Force ch
|
||||
parser.add_argument("--directml", type=int, nargs="?", metavar="DIRECTML_DEVICE", const=-1, help="Use torch-directml.")
|
||||
|
||||
parser.add_argument("--oneapi-device-selector", type=str, default=None, metavar="SELECTOR_STRING", help="Sets the oneAPI device(s) this instance will use.")
|
||||
parser.add_argument("--disable-ipex-optimize", action="store_true", help="Disables ipex.optimize default when loading models with Intel's Extension for Pytorch.")
|
||||
parser.add_argument("--supports-fp8-compute", action="store_true", help="ComfyUI will act like if the device supports fp8 compute.")
|
||||
|
||||
class LatentPreviewMethod(enum.Enum):
|
||||
@ -225,6 +224,7 @@ parser.add_argument(
|
||||
parser.add_argument("--user-directory", type=is_valid_directory, default=None, help="Set the ComfyUI user directory with an absolute path. Overrides --base-directory.")
|
||||
|
||||
parser.add_argument("--enable-compress-response-body", action="store_true", help="Enable compressing response body.")
|
||||
parser.add_argument("--benchmark-server-only", action="store_true", help="Enable lightweight benchmark routes and worker fast-paths focused on model serving throughput/latency.")
|
||||
|
||||
parser.add_argument(
|
||||
"--comfy-api-base",
|
||||
|
||||
@ -112,10 +112,6 @@ if args.directml is not None:
|
||||
# torch_directml.disable_tiled_resources(True)
|
||||
lowvram_available = False #TODO: need to find a way to get free memory in directml before this can be enabled by default.
|
||||
|
||||
try:
|
||||
import intel_extension_for_pytorch as ipex # noqa: F401
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
_ = torch.xpu.device_count()
|
||||
@ -583,9 +579,6 @@ class LoadedModel:
|
||||
|
||||
real_model = self.model.model
|
||||
|
||||
if is_intel_xpu() and not args.disable_ipex_optimize and 'ipex' in globals() and real_model is not None:
|
||||
with torch.no_grad():
|
||||
real_model = ipex.optimize(real_model.eval(), inplace=True, graph_mode=True, concat_linear=True)
|
||||
|
||||
self.real_model = weakref.ref(real_model)
|
||||
self.model_finalizer = weakref.finalize(real_model, cleanup_models)
|
||||
@ -1581,10 +1574,7 @@ def should_use_fp16(device=None, model_params=0, prioritize_performance=True, ma
|
||||
return False
|
||||
|
||||
if is_intel_xpu():
|
||||
if torch_version_numeric < (2, 3):
|
||||
return True
|
||||
else:
|
||||
return torch.xpu.get_device_properties(device).has_fp16
|
||||
return torch.xpu.get_device_properties(device).has_fp16
|
||||
|
||||
if is_ascend_npu():
|
||||
return True
|
||||
@ -1650,10 +1640,7 @@ def should_use_bf16(device=None, model_params=0, prioritize_performance=True, ma
|
||||
return False
|
||||
|
||||
if is_intel_xpu():
|
||||
if torch_version_numeric < (2, 3):
|
||||
return True
|
||||
else:
|
||||
return torch.xpu.is_bf16_supported()
|
||||
return torch.xpu.is_bf16_supported()
|
||||
|
||||
if is_ascend_npu():
|
||||
return True
|
||||
@ -1784,6 +1771,7 @@ def soft_empty_cache(force=False):
|
||||
if cpu_state == CPUState.MPS:
|
||||
torch.mps.empty_cache()
|
||||
elif is_intel_xpu():
|
||||
torch.xpu.synchronize()
|
||||
torch.xpu.empty_cache()
|
||||
elif is_ascend_npu():
|
||||
torch.npu.empty_cache()
|
||||
|
||||
@ -1,152 +0,0 @@
|
||||
from enum import Enum
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from pydantic import BaseModel, Field, StrictBytes
|
||||
|
||||
|
||||
class MoonvalleyPromptResponse(BaseModel):
|
||||
error: Optional[Dict[str, Any]] = None
|
||||
frame_conditioning: Optional[Dict[str, Any]] = None
|
||||
id: Optional[str] = None
|
||||
inference_params: Optional[Dict[str, Any]] = None
|
||||
meta: Optional[Dict[str, Any]] = None
|
||||
model_params: Optional[Dict[str, Any]] = None
|
||||
output_url: Optional[str] = None
|
||||
prompt_text: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
|
||||
class MoonvalleyTextToVideoInferenceParams(BaseModel):
|
||||
add_quality_guidance: Optional[bool] = Field(
|
||||
True, description='Whether to add quality guidance'
|
||||
)
|
||||
caching_coefficient: Optional[float] = Field(
|
||||
0.3, description='Caching coefficient for optimization'
|
||||
)
|
||||
caching_cooldown: Optional[int] = Field(
|
||||
3, description='Number of caching cooldown steps'
|
||||
)
|
||||
caching_warmup: Optional[int] = Field(
|
||||
3, description='Number of caching warmup steps'
|
||||
)
|
||||
clip_value: Optional[float] = Field(
|
||||
3, description='CLIP value for generation control'
|
||||
)
|
||||
conditioning_frame_index: Optional[int] = Field(
|
||||
0, description='Index of the conditioning frame'
|
||||
)
|
||||
cooldown_steps: Optional[int] = Field(
|
||||
75, description='Number of cooldown steps (calculated based on num_frames)'
|
||||
)
|
||||
fps: Optional[int] = Field(
|
||||
24, description='Frames per second of the generated video'
|
||||
)
|
||||
guidance_scale: Optional[float] = Field(
|
||||
10, description='Guidance scale for generation control'
|
||||
)
|
||||
height: Optional[int] = Field(
|
||||
1080, description='Height of the generated video in pixels'
|
||||
)
|
||||
negative_prompt: Optional[str] = Field(None, description='Negative prompt text')
|
||||
num_frames: Optional[int] = Field(64, description='Number of frames to generate')
|
||||
seed: Optional[int] = Field(
|
||||
None, description='Random seed for generation (default: random)'
|
||||
)
|
||||
shift_value: Optional[float] = Field(
|
||||
3, description='Shift value for generation control'
|
||||
)
|
||||
steps: Optional[int] = Field(80, description='Number of denoising steps')
|
||||
use_guidance_schedule: Optional[bool] = Field(
|
||||
True, description='Whether to use guidance scheduling'
|
||||
)
|
||||
use_negative_prompts: Optional[bool] = Field(
|
||||
False, description='Whether to use negative prompts'
|
||||
)
|
||||
use_timestep_transform: Optional[bool] = Field(
|
||||
True, description='Whether to use timestep transformation'
|
||||
)
|
||||
warmup_steps: Optional[int] = Field(
|
||||
0, description='Number of warmup steps (calculated based on num_frames)'
|
||||
)
|
||||
width: Optional[int] = Field(
|
||||
1920, description='Width of the generated video in pixels'
|
||||
)
|
||||
|
||||
|
||||
class MoonvalleyTextToVideoRequest(BaseModel):
|
||||
image_url: Optional[str] = None
|
||||
inference_params: Optional[MoonvalleyTextToVideoInferenceParams] = None
|
||||
prompt_text: Optional[str] = None
|
||||
webhook_url: Optional[str] = None
|
||||
|
||||
|
||||
class MoonvalleyUploadFileRequest(BaseModel):
|
||||
file: Optional[StrictBytes] = None
|
||||
|
||||
|
||||
class MoonvalleyUploadFileResponse(BaseModel):
|
||||
access_url: Optional[str] = None
|
||||
|
||||
|
||||
class MoonvalleyVideoToVideoInferenceParams(BaseModel):
|
||||
add_quality_guidance: Optional[bool] = Field(
|
||||
True, description='Whether to add quality guidance'
|
||||
)
|
||||
caching_coefficient: Optional[float] = Field(
|
||||
0.3, description='Caching coefficient for optimization'
|
||||
)
|
||||
caching_cooldown: Optional[int] = Field(
|
||||
3, description='Number of caching cooldown steps'
|
||||
)
|
||||
caching_warmup: Optional[int] = Field(
|
||||
3, description='Number of caching warmup steps'
|
||||
)
|
||||
clip_value: Optional[float] = Field(
|
||||
3, description='CLIP value for generation control'
|
||||
)
|
||||
conditioning_frame_index: Optional[int] = Field(
|
||||
0, description='Index of the conditioning frame'
|
||||
)
|
||||
cooldown_steps: Optional[int] = Field(
|
||||
36, description='Number of cooldown steps (calculated based on num_frames)'
|
||||
)
|
||||
guidance_scale: Optional[float] = Field(
|
||||
15, description='Guidance scale for generation control'
|
||||
)
|
||||
negative_prompt: Optional[str] = Field(None, description='Negative prompt text')
|
||||
seed: Optional[int] = Field(
|
||||
None, description='Random seed for generation (default: random)'
|
||||
)
|
||||
shift_value: Optional[float] = Field(
|
||||
3, description='Shift value for generation control'
|
||||
)
|
||||
steps: Optional[int] = Field(80, description='Number of denoising steps')
|
||||
use_guidance_schedule: Optional[bool] = Field(
|
||||
True, description='Whether to use guidance scheduling'
|
||||
)
|
||||
use_negative_prompts: Optional[bool] = Field(
|
||||
False, description='Whether to use negative prompts'
|
||||
)
|
||||
use_timestep_transform: Optional[bool] = Field(
|
||||
True, description='Whether to use timestep transformation'
|
||||
)
|
||||
warmup_steps: Optional[int] = Field(
|
||||
24, description='Number of warmup steps (calculated based on num_frames)'
|
||||
)
|
||||
|
||||
|
||||
class ControlType(str, Enum):
|
||||
motion_control = 'motion_control'
|
||||
pose_control = 'pose_control'
|
||||
|
||||
|
||||
class MoonvalleyVideoToVideoRequest(BaseModel):
|
||||
control_type: ControlType = Field(
|
||||
..., description='Supported types for video control'
|
||||
)
|
||||
inference_params: Optional[MoonvalleyVideoToVideoInferenceParams] = None
|
||||
prompt_text: str = Field(..., description='Describes the video to generate')
|
||||
video_url: str = Field(..., description='Url to control video')
|
||||
webhook_url: Optional[str] = Field(
|
||||
None, description='Optional webhook URL for notifications'
|
||||
)
|
||||
@ -1403,7 +1403,6 @@ class ByteDance2TextToVideoNode(IO.ComfyNode):
|
||||
status_extractor=lambda r: r.status,
|
||||
price_extractor=_seedance2_price_extractor(model_id, has_video_input=False),
|
||||
poll_interval=9,
|
||||
max_poll_attempts=180,
|
||||
)
|
||||
return IO.NodeOutput(await download_url_to_video_output(response.content.video_url))
|
||||
|
||||
@ -1585,7 +1584,6 @@ class ByteDance2FirstLastFrameNode(IO.ComfyNode):
|
||||
status_extractor=lambda r: r.status,
|
||||
price_extractor=_seedance2_price_extractor(model_id, has_video_input=False),
|
||||
poll_interval=9,
|
||||
max_poll_attempts=180,
|
||||
)
|
||||
return IO.NodeOutput(await download_url_to_video_output(response.content.video_url))
|
||||
|
||||
@ -1907,7 +1905,6 @@ class ByteDance2ReferenceNode(IO.ComfyNode):
|
||||
status_extractor=lambda r: r.status,
|
||||
price_extractor=_seedance2_price_extractor(model_id, has_video_input=has_video_input),
|
||||
poll_interval=9,
|
||||
max_poll_attempts=180,
|
||||
)
|
||||
return IO.NodeOutput(await download_url_to_video_output(response.content.video_url))
|
||||
|
||||
|
||||
@ -178,7 +178,6 @@ class HitPawGeneralImageEnhance(IO.ComfyNode):
|
||||
status_extractor=lambda x: x.data.status,
|
||||
price_extractor=lambda x: request_price,
|
||||
poll_interval=10.0,
|
||||
max_poll_attempts=480,
|
||||
)
|
||||
return IO.NodeOutput(await download_url_to_image_tensor(final_response.data.res_url))
|
||||
|
||||
@ -324,7 +323,6 @@ class HitPawVideoEnhance(IO.ComfyNode):
|
||||
status_extractor=lambda x: x.data.status,
|
||||
price_extractor=lambda x: request_price,
|
||||
poll_interval=10.0,
|
||||
max_poll_attempts=320,
|
||||
)
|
||||
return IO.NodeOutput(await download_url_to_video_output(final_response.data.res_url))
|
||||
|
||||
|
||||
@ -276,7 +276,6 @@ async def finish_omni_video_task(cls: type[IO.ComfyNode], response: TaskStatusRe
|
||||
cls,
|
||||
ApiEndpoint(path=f"/proxy/kling/v1/videos/omni-video/{response.data.task_id}"),
|
||||
response_model=TaskStatusResponse,
|
||||
max_poll_attempts=280,
|
||||
status_extractor=lambda r: (r.data.task_status if r.data else None),
|
||||
)
|
||||
return IO.NodeOutput(await download_url_to_video_output(final_response.data.task_result.videos[0].url))
|
||||
@ -3062,7 +3061,6 @@ class KlingVideoNode(IO.ComfyNode):
|
||||
cls,
|
||||
ApiEndpoint(path=poll_path),
|
||||
response_model=TaskStatusResponse,
|
||||
max_poll_attempts=280,
|
||||
status_extractor=lambda r: (r.data.task_status if r.data else None),
|
||||
)
|
||||
return IO.NodeOutput(await download_url_to_video_output(final_response.data.task_result.videos[0].url))
|
||||
@ -3188,7 +3186,6 @@ class KlingFirstLastFrameNode(IO.ComfyNode):
|
||||
cls,
|
||||
ApiEndpoint(path=f"/proxy/kling/v1/videos/image2video/{response.data.task_id}"),
|
||||
response_model=TaskStatusResponse,
|
||||
max_poll_attempts=280,
|
||||
status_extractor=lambda r: (r.data.task_status if r.data else None),
|
||||
)
|
||||
return IO.NodeOutput(await download_url_to_video_output(final_response.data.task_result.videos[0].url))
|
||||
|
||||
@ -230,7 +230,6 @@ class MagnificImageUpscalerCreativeNode(IO.ComfyNode):
|
||||
status_extractor=lambda x: x.status,
|
||||
price_extractor=lambda _: price_usd,
|
||||
poll_interval=10.0,
|
||||
max_poll_attempts=480,
|
||||
)
|
||||
return IO.NodeOutput(await download_url_to_image_tensor(final_response.generated[0]))
|
||||
|
||||
@ -391,7 +390,6 @@ class MagnificImageUpscalerPreciseV2Node(IO.ComfyNode):
|
||||
status_extractor=lambda x: x.status,
|
||||
price_extractor=lambda _: price_usd,
|
||||
poll_interval=10.0,
|
||||
max_poll_attempts=480,
|
||||
)
|
||||
return IO.NodeOutput(await download_url_to_image_tensor(final_response.generated[0]))
|
||||
|
||||
@ -541,7 +539,6 @@ class MagnificImageStyleTransferNode(IO.ComfyNode):
|
||||
response_model=TaskResponse,
|
||||
status_extractor=lambda x: x.status,
|
||||
poll_interval=10.0,
|
||||
max_poll_attempts=480,
|
||||
)
|
||||
return IO.NodeOutput(await download_url_to_image_tensor(final_response.generated[0]))
|
||||
|
||||
@ -782,7 +779,6 @@ class MagnificImageRelightNode(IO.ComfyNode):
|
||||
response_model=TaskResponse,
|
||||
status_extractor=lambda x: x.status,
|
||||
poll_interval=10.0,
|
||||
max_poll_attempts=480,
|
||||
)
|
||||
return IO.NodeOutput(await download_url_to_image_tensor(final_response.generated[0]))
|
||||
|
||||
@ -924,7 +920,6 @@ class MagnificImageSkinEnhancerNode(IO.ComfyNode):
|
||||
response_model=TaskResponse,
|
||||
status_extractor=lambda x: x.status,
|
||||
poll_interval=10.0,
|
||||
max_poll_attempts=480,
|
||||
)
|
||||
return IO.NodeOutput(await download_url_to_image_tensor(final_response.generated[0]))
|
||||
|
||||
|
||||
@ -1,534 +0,0 @@
|
||||
import logging
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
from comfy_api.latest import IO, ComfyExtension, Input
|
||||
from comfy_api_nodes.apis.moonvalley import (
|
||||
MoonvalleyPromptResponse,
|
||||
MoonvalleyTextToVideoInferenceParams,
|
||||
MoonvalleyTextToVideoRequest,
|
||||
MoonvalleyVideoToVideoInferenceParams,
|
||||
MoonvalleyVideoToVideoRequest,
|
||||
)
|
||||
from comfy_api_nodes.util import (
|
||||
ApiEndpoint,
|
||||
download_url_to_video_output,
|
||||
poll_op,
|
||||
sync_op,
|
||||
trim_video,
|
||||
upload_images_to_comfyapi,
|
||||
upload_video_to_comfyapi,
|
||||
validate_container_format_is_mp4,
|
||||
validate_image_dimensions,
|
||||
validate_string,
|
||||
)
|
||||
|
||||
API_UPLOADS_ENDPOINT = "/proxy/moonvalley/uploads"
|
||||
API_PROMPTS_ENDPOINT = "/proxy/moonvalley/prompts"
|
||||
API_VIDEO2VIDEO_ENDPOINT = "/proxy/moonvalley/prompts/video-to-video"
|
||||
API_TXT2VIDEO_ENDPOINT = "/proxy/moonvalley/prompts/text-to-video"
|
||||
API_IMG2VIDEO_ENDPOINT = "/proxy/moonvalley/prompts/image-to-video"
|
||||
|
||||
MIN_WIDTH = 300
|
||||
MIN_HEIGHT = 300
|
||||
|
||||
MAX_WIDTH = 10000
|
||||
MAX_HEIGHT = 10000
|
||||
|
||||
MIN_VID_WIDTH = 300
|
||||
MIN_VID_HEIGHT = 300
|
||||
|
||||
MAX_VID_WIDTH = 10000
|
||||
MAX_VID_HEIGHT = 10000
|
||||
|
||||
MAX_VIDEO_SIZE = 1024 * 1024 * 1024 # 1 GB max for in-memory video processing
|
||||
|
||||
MOONVALLEY_MAREY_MAX_PROMPT_LENGTH = 5000
|
||||
|
||||
|
||||
def is_valid_task_creation_response(response: MoonvalleyPromptResponse) -> bool:
|
||||
"""Verifies that the initial response contains a task ID."""
|
||||
return bool(response.id)
|
||||
|
||||
|
||||
def validate_task_creation_response(response) -> None:
|
||||
if not is_valid_task_creation_response(response):
|
||||
error_msg = f"Moonvalley Marey API: Initial request failed. Code: {response.code}, Message: {response.message}, Data: {response}"
|
||||
logging.error(error_msg)
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
|
||||
def validate_video_to_video_input(video: Input.Video) -> Input.Video:
|
||||
"""
|
||||
Validates and processes video input for Moonvalley Video-to-Video generation.
|
||||
|
||||
Args:
|
||||
video: Input video to validate
|
||||
|
||||
Returns:
|
||||
Validated and potentially trimmed video
|
||||
|
||||
Raises:
|
||||
ValueError: If video doesn't meet requirements
|
||||
MoonvalleyApiError: If video duration is too short
|
||||
"""
|
||||
width, height = _get_video_dimensions(video)
|
||||
_validate_video_dimensions(width, height)
|
||||
validate_container_format_is_mp4(video)
|
||||
|
||||
return _validate_and_trim_duration(video)
|
||||
|
||||
|
||||
def _get_video_dimensions(video: Input.Video) -> tuple[int, int]:
|
||||
"""Extracts video dimensions with error handling."""
|
||||
try:
|
||||
return video.get_dimensions()
|
||||
except Exception as e:
|
||||
logging.error("Error getting dimensions of video: %s", e)
|
||||
raise ValueError(f"Cannot get video dimensions: {e}") from e
|
||||
|
||||
|
||||
def _validate_video_dimensions(width: int, height: int) -> None:
|
||||
"""Validates video dimensions meet Moonvalley V2V requirements."""
|
||||
supported_resolutions = {
|
||||
(1920, 1080),
|
||||
(1080, 1920),
|
||||
(1152, 1152),
|
||||
(1536, 1152),
|
||||
(1152, 1536),
|
||||
}
|
||||
|
||||
if (width, height) not in supported_resolutions:
|
||||
supported_list = ", ".join([f"{w}x{h}" for w, h in sorted(supported_resolutions)])
|
||||
raise ValueError(f"Resolution {width}x{height} not supported. Supported: {supported_list}")
|
||||
|
||||
|
||||
def _validate_and_trim_duration(video: Input.Video) -> Input.Video:
|
||||
"""Validates video duration and trims to 5 seconds if needed."""
|
||||
duration = video.get_duration()
|
||||
_validate_minimum_duration(duration)
|
||||
return _trim_if_too_long(video, duration)
|
||||
|
||||
|
||||
def _validate_minimum_duration(duration: float) -> None:
|
||||
"""Ensures video is at least 5 seconds long."""
|
||||
if duration < 5:
|
||||
raise ValueError("Input video must be at least 5 seconds long.")
|
||||
|
||||
|
||||
def _trim_if_too_long(video: Input.Video, duration: float) -> Input.Video:
|
||||
"""Trims video to 5 seconds if longer."""
|
||||
if duration > 5:
|
||||
return trim_video(video, 5)
|
||||
return video
|
||||
|
||||
|
||||
def parse_width_height_from_res(resolution: str):
|
||||
# Accepts a string like "16:9 (1920 x 1080)" and returns width, height as a dict
|
||||
res_map = {
|
||||
"16:9 (1920 x 1080)": {"width": 1920, "height": 1080},
|
||||
"9:16 (1080 x 1920)": {"width": 1080, "height": 1920},
|
||||
"1:1 (1152 x 1152)": {"width": 1152, "height": 1152},
|
||||
"4:3 (1536 x 1152)": {"width": 1536, "height": 1152},
|
||||
"3:4 (1152 x 1536)": {"width": 1152, "height": 1536},
|
||||
# "21:9 (2560 x 1080)": {"width": 2560, "height": 1080},
|
||||
}
|
||||
return res_map.get(resolution, {"width": 1920, "height": 1080})
|
||||
|
||||
|
||||
def parse_control_parameter(value):
|
||||
control_map = {
|
||||
"Motion Transfer": "motion_control",
|
||||
"Canny": "canny_control",
|
||||
"Pose Transfer": "pose_control",
|
||||
"Depth": "depth_control",
|
||||
}
|
||||
return control_map.get(value, control_map["Motion Transfer"])
|
||||
|
||||
|
||||
async def get_response(cls: type[IO.ComfyNode], task_id: str) -> MoonvalleyPromptResponse:
|
||||
return await poll_op(
|
||||
cls,
|
||||
ApiEndpoint(path=f"{API_PROMPTS_ENDPOINT}/{task_id}"),
|
||||
response_model=MoonvalleyPromptResponse,
|
||||
status_extractor=lambda r: (r.status if r and r.status else None),
|
||||
poll_interval=16.0,
|
||||
max_poll_attempts=240,
|
||||
)
|
||||
|
||||
|
||||
class MoonvalleyImg2VideoNode(IO.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls) -> IO.Schema:
|
||||
return IO.Schema(
|
||||
node_id="MoonvalleyImg2VideoNode",
|
||||
display_name="Moonvalley Marey Image to Video",
|
||||
category="api node/video/Moonvalley Marey",
|
||||
description="Moonvalley Marey Image to Video Node",
|
||||
inputs=[
|
||||
IO.Image.Input(
|
||||
"image",
|
||||
tooltip="The reference image used to generate the video",
|
||||
),
|
||||
IO.String.Input(
|
||||
"prompt",
|
||||
multiline=True,
|
||||
),
|
||||
IO.String.Input(
|
||||
"negative_prompt",
|
||||
multiline=True,
|
||||
default="<synthetic> <scene cut> gopro, bright, contrast, static, overexposed, vignette, "
|
||||
"artifacts, still, noise, texture, scanlines, videogame, 360 camera, VR, transition, "
|
||||
"flare, saturation, distorted, warped, wide angle, saturated, vibrant, glowing, "
|
||||
"cross dissolve, cheesy, ugly hands, mutated hands, mutant, disfigured, extra fingers, "
|
||||
"blown out, horrible, blurry, worst quality, bad, dissolve, melt, fade in, fade out, "
|
||||
"wobbly, weird, low quality, plastic, stock footage, video camera, boring",
|
||||
tooltip="Negative prompt text",
|
||||
),
|
||||
IO.Combo.Input(
|
||||
"resolution",
|
||||
options=[
|
||||
"16:9 (1920 x 1080)",
|
||||
"9:16 (1080 x 1920)",
|
||||
"1:1 (1152 x 1152)",
|
||||
"4:3 (1536 x 1152)",
|
||||
"3:4 (1152 x 1536)",
|
||||
# "21:9 (2560 x 1080)",
|
||||
],
|
||||
default="16:9 (1920 x 1080)",
|
||||
tooltip="Resolution of the output video",
|
||||
),
|
||||
IO.Float.Input(
|
||||
"prompt_adherence",
|
||||
default=4.5,
|
||||
min=1.0,
|
||||
max=20.0,
|
||||
step=1.0,
|
||||
tooltip="Guidance scale for generation control",
|
||||
),
|
||||
IO.Int.Input(
|
||||
"seed",
|
||||
default=9,
|
||||
min=0,
|
||||
max=4294967295,
|
||||
step=1,
|
||||
display_mode=IO.NumberDisplay.number,
|
||||
tooltip="Random seed value",
|
||||
control_after_generate=True,
|
||||
),
|
||||
IO.Int.Input(
|
||||
"steps",
|
||||
default=80,
|
||||
min=75, # steps should be greater or equal to cooldown_steps(75) + warmup_steps(0)
|
||||
max=100,
|
||||
step=1,
|
||||
tooltip="Number of denoising steps",
|
||||
),
|
||||
],
|
||||
outputs=[IO.Video.Output()],
|
||||
hidden=[
|
||||
IO.Hidden.auth_token_comfy_org,
|
||||
IO.Hidden.api_key_comfy_org,
|
||||
IO.Hidden.unique_id,
|
||||
],
|
||||
is_api_node=True,
|
||||
price_badge=IO.PriceBadge(
|
||||
depends_on=IO.PriceBadgeDepends(),
|
||||
expr="""{"type":"usd","usd": 1.5}""",
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(
|
||||
cls,
|
||||
image: Input.Image,
|
||||
prompt: str,
|
||||
negative_prompt: str,
|
||||
resolution: str,
|
||||
prompt_adherence: float,
|
||||
seed: int,
|
||||
steps: int,
|
||||
) -> IO.NodeOutput:
|
||||
validate_image_dimensions(image, min_width=300, min_height=300, max_height=MAX_HEIGHT, max_width=MAX_WIDTH)
|
||||
validate_string(prompt, min_length=1, max_length=MOONVALLEY_MAREY_MAX_PROMPT_LENGTH)
|
||||
validate_string(negative_prompt, field_name="negative_prompt", max_length=MOONVALLEY_MAREY_MAX_PROMPT_LENGTH)
|
||||
width_height = parse_width_height_from_res(resolution)
|
||||
|
||||
inference_params = MoonvalleyTextToVideoInferenceParams(
|
||||
negative_prompt=negative_prompt,
|
||||
steps=steps,
|
||||
seed=seed,
|
||||
guidance_scale=prompt_adherence,
|
||||
width=width_height["width"],
|
||||
height=width_height["height"],
|
||||
use_negative_prompts=True,
|
||||
)
|
||||
|
||||
# Get MIME type from tensor - assuming PNG format for image tensors
|
||||
mime_type = "image/png"
|
||||
image_url = (await upload_images_to_comfyapi(cls, image, max_images=1, mime_type=mime_type))[0]
|
||||
task_creation_response = await sync_op(
|
||||
cls,
|
||||
endpoint=ApiEndpoint(path=API_IMG2VIDEO_ENDPOINT, method="POST"),
|
||||
response_model=MoonvalleyPromptResponse,
|
||||
data=MoonvalleyTextToVideoRequest(
|
||||
image_url=image_url, prompt_text=prompt, inference_params=inference_params
|
||||
),
|
||||
)
|
||||
validate_task_creation_response(task_creation_response)
|
||||
final_response = await get_response(cls, task_creation_response.id)
|
||||
video = await download_url_to_video_output(final_response.output_url)
|
||||
return IO.NodeOutput(video)
|
||||
|
||||
|
||||
class MoonvalleyVideo2VideoNode(IO.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls) -> IO.Schema:
|
||||
return IO.Schema(
|
||||
node_id="MoonvalleyVideo2VideoNode",
|
||||
display_name="Moonvalley Marey Video to Video",
|
||||
category="api node/video/Moonvalley Marey",
|
||||
description="",
|
||||
inputs=[
|
||||
IO.String.Input(
|
||||
"prompt",
|
||||
multiline=True,
|
||||
tooltip="Describes the video to generate",
|
||||
),
|
||||
IO.String.Input(
|
||||
"negative_prompt",
|
||||
multiline=True,
|
||||
default="<synthetic> <scene cut> gopro, bright, contrast, static, overexposed, vignette, "
|
||||
"artifacts, still, noise, texture, scanlines, videogame, 360 camera, VR, transition, "
|
||||
"flare, saturation, distorted, warped, wide angle, saturated, vibrant, glowing, "
|
||||
"cross dissolve, cheesy, ugly hands, mutated hands, mutant, disfigured, extra fingers, "
|
||||
"blown out, horrible, blurry, worst quality, bad, dissolve, melt, fade in, fade out, "
|
||||
"wobbly, weird, low quality, plastic, stock footage, video camera, boring",
|
||||
tooltip="Negative prompt text",
|
||||
),
|
||||
IO.Int.Input(
|
||||
"seed",
|
||||
default=9,
|
||||
min=0,
|
||||
max=4294967295,
|
||||
step=1,
|
||||
display_mode=IO.NumberDisplay.number,
|
||||
tooltip="Random seed value",
|
||||
control_after_generate=False,
|
||||
),
|
||||
IO.Video.Input(
|
||||
"video",
|
||||
tooltip="The reference video used to generate the output video. Must be at least 5 seconds long. "
|
||||
"Videos longer than 5s will be automatically trimmed. Only MP4 format supported.",
|
||||
),
|
||||
IO.Combo.Input(
|
||||
"control_type",
|
||||
options=["Motion Transfer", "Pose Transfer"],
|
||||
default="Motion Transfer",
|
||||
optional=True,
|
||||
),
|
||||
IO.Int.Input(
|
||||
"motion_intensity",
|
||||
default=100,
|
||||
min=0,
|
||||
max=100,
|
||||
step=1,
|
||||
tooltip="Only used if control_type is 'Motion Transfer'",
|
||||
optional=True,
|
||||
),
|
||||
IO.Int.Input(
|
||||
"steps",
|
||||
default=60,
|
||||
min=60, # steps should be greater or equal to cooldown_steps(36) + warmup_steps(24)
|
||||
max=100,
|
||||
step=1,
|
||||
display_mode=IO.NumberDisplay.number,
|
||||
tooltip="Number of inference steps",
|
||||
),
|
||||
],
|
||||
outputs=[IO.Video.Output()],
|
||||
hidden=[
|
||||
IO.Hidden.auth_token_comfy_org,
|
||||
IO.Hidden.api_key_comfy_org,
|
||||
IO.Hidden.unique_id,
|
||||
],
|
||||
is_api_node=True,
|
||||
price_badge=IO.PriceBadge(
|
||||
depends_on=IO.PriceBadgeDepends(),
|
||||
expr="""{"type":"usd","usd": 2.25}""",
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(
|
||||
cls,
|
||||
prompt: str,
|
||||
negative_prompt: str,
|
||||
seed: int,
|
||||
video: Input.Video | None = None,
|
||||
control_type: str = "Motion Transfer",
|
||||
motion_intensity: int | None = 100,
|
||||
steps=60,
|
||||
prompt_adherence=4.5,
|
||||
) -> IO.NodeOutput:
|
||||
validated_video = validate_video_to_video_input(video)
|
||||
video_url = await upload_video_to_comfyapi(cls, validated_video)
|
||||
validate_string(prompt, min_length=1, max_length=MOONVALLEY_MAREY_MAX_PROMPT_LENGTH)
|
||||
validate_string(negative_prompt, field_name="negative_prompt", max_length=MOONVALLEY_MAREY_MAX_PROMPT_LENGTH)
|
||||
|
||||
# Only include motion_intensity for Motion Transfer
|
||||
control_params = {}
|
||||
if control_type == "Motion Transfer" and motion_intensity is not None:
|
||||
control_params["motion_intensity"] = motion_intensity
|
||||
|
||||
inference_params = MoonvalleyVideoToVideoInferenceParams(
|
||||
negative_prompt=negative_prompt,
|
||||
seed=seed,
|
||||
control_params=control_params,
|
||||
steps=steps,
|
||||
guidance_scale=prompt_adherence,
|
||||
)
|
||||
|
||||
task_creation_response = await sync_op(
|
||||
cls,
|
||||
endpoint=ApiEndpoint(path=API_VIDEO2VIDEO_ENDPOINT, method="POST"),
|
||||
response_model=MoonvalleyPromptResponse,
|
||||
data=MoonvalleyVideoToVideoRequest(
|
||||
control_type=parse_control_parameter(control_type),
|
||||
video_url=video_url,
|
||||
prompt_text=prompt,
|
||||
inference_params=inference_params,
|
||||
),
|
||||
)
|
||||
validate_task_creation_response(task_creation_response)
|
||||
final_response = await get_response(cls, task_creation_response.id)
|
||||
return IO.NodeOutput(await download_url_to_video_output(final_response.output_url))
|
||||
|
||||
|
||||
class MoonvalleyTxt2VideoNode(IO.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls) -> IO.Schema:
|
||||
return IO.Schema(
|
||||
node_id="MoonvalleyTxt2VideoNode",
|
||||
display_name="Moonvalley Marey Text to Video",
|
||||
category="api node/video/Moonvalley Marey",
|
||||
description="",
|
||||
inputs=[
|
||||
IO.String.Input(
|
||||
"prompt",
|
||||
multiline=True,
|
||||
),
|
||||
IO.String.Input(
|
||||
"negative_prompt",
|
||||
multiline=True,
|
||||
default="<synthetic> <scene cut> gopro, bright, contrast, static, overexposed, vignette, "
|
||||
"artifacts, still, noise, texture, scanlines, videogame, 360 camera, VR, transition, "
|
||||
"flare, saturation, distorted, warped, wide angle, saturated, vibrant, glowing, "
|
||||
"cross dissolve, cheesy, ugly hands, mutated hands, mutant, disfigured, extra fingers, "
|
||||
"blown out, horrible, blurry, worst quality, bad, dissolve, melt, fade in, fade out, "
|
||||
"wobbly, weird, low quality, plastic, stock footage, video camera, boring",
|
||||
tooltip="Negative prompt text",
|
||||
),
|
||||
IO.Combo.Input(
|
||||
"resolution",
|
||||
options=[
|
||||
"16:9 (1920 x 1080)",
|
||||
"9:16 (1080 x 1920)",
|
||||
"1:1 (1152 x 1152)",
|
||||
"4:3 (1536 x 1152)",
|
||||
"3:4 (1152 x 1536)",
|
||||
"21:9 (2560 x 1080)",
|
||||
],
|
||||
default="16:9 (1920 x 1080)",
|
||||
tooltip="Resolution of the output video",
|
||||
),
|
||||
IO.Float.Input(
|
||||
"prompt_adherence",
|
||||
default=4.0,
|
||||
min=1.0,
|
||||
max=20.0,
|
||||
step=1.0,
|
||||
tooltip="Guidance scale for generation control",
|
||||
),
|
||||
IO.Int.Input(
|
||||
"seed",
|
||||
default=9,
|
||||
min=0,
|
||||
max=4294967295,
|
||||
step=1,
|
||||
display_mode=IO.NumberDisplay.number,
|
||||
control_after_generate=True,
|
||||
tooltip="Random seed value",
|
||||
),
|
||||
IO.Int.Input(
|
||||
"steps",
|
||||
default=80,
|
||||
min=75, # steps should be greater or equal to cooldown_steps(75) + warmup_steps(0)
|
||||
max=100,
|
||||
step=1,
|
||||
tooltip="Inference steps",
|
||||
),
|
||||
],
|
||||
outputs=[IO.Video.Output()],
|
||||
hidden=[
|
||||
IO.Hidden.auth_token_comfy_org,
|
||||
IO.Hidden.api_key_comfy_org,
|
||||
IO.Hidden.unique_id,
|
||||
],
|
||||
is_api_node=True,
|
||||
price_badge=IO.PriceBadge(
|
||||
depends_on=IO.PriceBadgeDepends(),
|
||||
expr="""{"type":"usd","usd": 1.5}""",
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(
|
||||
cls,
|
||||
prompt: str,
|
||||
negative_prompt: str,
|
||||
resolution: str,
|
||||
prompt_adherence: float,
|
||||
seed: int,
|
||||
steps: int,
|
||||
) -> IO.NodeOutput:
|
||||
validate_string(prompt, min_length=1, max_length=MOONVALLEY_MAREY_MAX_PROMPT_LENGTH)
|
||||
validate_string(negative_prompt, field_name="negative_prompt", max_length=MOONVALLEY_MAREY_MAX_PROMPT_LENGTH)
|
||||
width_height = parse_width_height_from_res(resolution)
|
||||
|
||||
inference_params = MoonvalleyTextToVideoInferenceParams(
|
||||
negative_prompt=negative_prompt,
|
||||
steps=steps,
|
||||
seed=seed,
|
||||
guidance_scale=prompt_adherence,
|
||||
num_frames=128,
|
||||
width=width_height["width"],
|
||||
height=width_height["height"],
|
||||
)
|
||||
|
||||
task_creation_response = await sync_op(
|
||||
cls,
|
||||
endpoint=ApiEndpoint(path=API_TXT2VIDEO_ENDPOINT, method="POST"),
|
||||
response_model=MoonvalleyPromptResponse,
|
||||
data=MoonvalleyTextToVideoRequest(prompt_text=prompt, inference_params=inference_params),
|
||||
)
|
||||
validate_task_creation_response(task_creation_response)
|
||||
final_response = await get_response(cls, task_creation_response.id)
|
||||
return IO.NodeOutput(await download_url_to_video_output(final_response.output_url))
|
||||
|
||||
|
||||
class MoonvalleyExtension(ComfyExtension):
|
||||
@override
|
||||
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||
return [
|
||||
MoonvalleyImg2VideoNode,
|
||||
MoonvalleyTxt2VideoNode,
|
||||
MoonvalleyVideo2VideoNode,
|
||||
]
|
||||
|
||||
|
||||
async def comfy_entrypoint() -> MoonvalleyExtension:
|
||||
return MoonvalleyExtension()
|
||||
@ -453,7 +453,6 @@ class TopazVideoEnhance(IO.ComfyNode):
|
||||
progress_extractor=lambda x: getattr(x, "progress", 0),
|
||||
price_extractor=lambda x: (x.estimates.cost[0] * 0.08 if x.estimates and x.estimates.cost[0] else None),
|
||||
poll_interval=10.0,
|
||||
max_poll_attempts=320,
|
||||
)
|
||||
return IO.NodeOutput(await download_url_to_video_output(final_response.download.url))
|
||||
|
||||
|
||||
@ -38,7 +38,7 @@ async def execute_task(
|
||||
cls: type[IO.ComfyNode],
|
||||
vidu_endpoint: str,
|
||||
payload: TaskCreationRequest | TaskExtendCreationRequest | TaskMultiFrameCreationRequest,
|
||||
max_poll_attempts: int = 320,
|
||||
max_poll_attempts: int = 480,
|
||||
) -> list[TaskResult]:
|
||||
task_creation_response = await sync_op(
|
||||
cls,
|
||||
@ -1097,7 +1097,6 @@ class ViduExtendVideoNode(IO.ComfyNode):
|
||||
video_url=await upload_video_to_comfyapi(cls, video, wait_label="Uploading video"),
|
||||
images=[image_url] if image_url else None,
|
||||
),
|
||||
max_poll_attempts=480,
|
||||
)
|
||||
return IO.NodeOutput(await download_url_to_video_output(results[0].url))
|
||||
|
||||
|
||||
@ -818,7 +818,6 @@ class WanReferenceVideoApi(IO.ComfyNode):
|
||||
response_model=VideoTaskStatusResponse,
|
||||
status_extractor=lambda x: x.output.task_status,
|
||||
poll_interval=6,
|
||||
max_poll_attempts=280,
|
||||
)
|
||||
return IO.NodeOutput(await download_url_to_video_output(response.output.video_url))
|
||||
|
||||
|
||||
@ -84,7 +84,6 @@ class WavespeedFlashVSRNode(IO.ComfyNode):
|
||||
response_model=TaskResultResponse,
|
||||
status_extractor=lambda x: "failed" if x.data is None else x.data.status,
|
||||
poll_interval=10.0,
|
||||
max_poll_attempts=480,
|
||||
)
|
||||
if final_response.code != 200:
|
||||
raise ValueError(
|
||||
@ -156,7 +155,6 @@ class WavespeedImageUpscaleNode(IO.ComfyNode):
|
||||
response_model=TaskResultResponse,
|
||||
status_extractor=lambda x: "failed" if x.data is None else x.data.status,
|
||||
poll_interval=10.0,
|
||||
max_poll_attempts=480,
|
||||
)
|
||||
if final_response.code != 200:
|
||||
raise ValueError(
|
||||
|
||||
@ -148,7 +148,7 @@ async def poll_op(
|
||||
queued_statuses: list[str | int] | None = None,
|
||||
data: BaseModel | None = None,
|
||||
poll_interval: float = 5.0,
|
||||
max_poll_attempts: int = 160,
|
||||
max_poll_attempts: int = 480,
|
||||
timeout_per_poll: float = 120.0,
|
||||
max_retries_per_poll: int = 10,
|
||||
retry_delay_per_poll: float = 1.0,
|
||||
@ -254,7 +254,7 @@ async def poll_op_raw(
|
||||
queued_statuses: list[str | int] | None = None,
|
||||
data: dict[str, Any] | BaseModel | None = None,
|
||||
poll_interval: float = 5.0,
|
||||
max_poll_attempts: int = 160,
|
||||
max_poll_attempts: int = 480,
|
||||
timeout_per_poll: float = 120.0,
|
||||
max_retries_per_poll: int = 10,
|
||||
retry_delay_per_poll: float = 1.0,
|
||||
|
||||
@ -459,27 +459,23 @@ class SDPoseKeypointExtractor(io.ComfyNode):
|
||||
total_images = image.shape[0]
|
||||
captured_feat = None
|
||||
|
||||
model_h = int(head.heatmap_size[0]) * 4 # e.g. 192 * 4 = 768
|
||||
model_w = int(head.heatmap_size[1]) * 4 # e.g. 256 * 4 = 1024
|
||||
model_w = int(head.heatmap_size[0]) * 4 # 192 * 4 = 768
|
||||
model_h = int(head.heatmap_size[1]) * 4 # 256 * 4 = 1024
|
||||
|
||||
def _resize_to_model(imgs):
|
||||
"""Aspect-preserving resize + zero-pad BHWC images to (model_h, model_w). Returns (resized_bhwc, scale, pad_top, pad_left)."""
|
||||
"""Stretch BHWC images to (model_h, model_w), model expects no aspect preservation."""
|
||||
h, w = imgs.shape[-3], imgs.shape[-2]
|
||||
scale = min(model_h / h, model_w / w)
|
||||
sh, sw = int(round(h * scale)), int(round(w * scale))
|
||||
pt, pl = (model_h - sh) // 2, (model_w - sw) // 2
|
||||
method = "area" if (model_h <= h and model_w <= w) else "bilinear"
|
||||
chw = imgs.permute(0, 3, 1, 2).float()
|
||||
scaled = comfy.utils.common_upscale(chw, sw, sh, upscale_method="bilinear", crop="disabled")
|
||||
padded = torch.zeros(scaled.shape[0], scaled.shape[1], model_h, model_w, dtype=scaled.dtype, device=scaled.device)
|
||||
padded[:, :, pt:pt + sh, pl:pl + sw] = scaled
|
||||
return padded.permute(0, 2, 3, 1), scale, pt, pl
|
||||
scaled = comfy.utils.common_upscale(chw, model_w, model_h, upscale_method=method, crop="disabled")
|
||||
return scaled.permute(0, 2, 3, 1), model_w / w, model_h / h
|
||||
|
||||
def _remap_keypoints(kp, scale, pad_top, pad_left, offset_x=0, offset_y=0):
|
||||
def _remap_keypoints(kp, scale_x, scale_y, offset_x=0, offset_y=0):
|
||||
"""Remap keypoints from model space back to original image space."""
|
||||
kp = kp.copy() if isinstance(kp, np.ndarray) else np.array(kp, dtype=np.float32)
|
||||
invalid = kp[..., 0] < 0
|
||||
kp[..., 0] = (kp[..., 0] - pad_left) / scale + offset_x
|
||||
kp[..., 1] = (kp[..., 1] - pad_top) / scale + offset_y
|
||||
kp[..., 0] = kp[..., 0] / scale_x + offset_x
|
||||
kp[..., 1] = kp[..., 1] / scale_y + offset_y
|
||||
kp[invalid] = -1
|
||||
return kp
|
||||
|
||||
@ -529,18 +525,18 @@ class SDPoseKeypointExtractor(io.ComfyNode):
|
||||
continue
|
||||
|
||||
crop = img[:, y1:y2, x1:x2, :] # (1, crop_h, crop_w, C)
|
||||
crop_resized, scale, pad_top, pad_left = _resize_to_model(crop)
|
||||
crop_resized, sx, sy = _resize_to_model(crop)
|
||||
|
||||
latent_crop = vae.encode(crop_resized)
|
||||
kp_batch, sc_batch = _run_on_latent(latent_crop)
|
||||
kp = _remap_keypoints(kp_batch[0], scale, pad_top, pad_left, x1, y1)
|
||||
kp = _remap_keypoints(kp_batch[0], sx, sy, x1, y1)
|
||||
img_keypoints.append(kp)
|
||||
img_scores.append(sc_batch[0])
|
||||
else:
|
||||
img_resized, scale, pad_top, pad_left = _resize_to_model(img)
|
||||
img_resized, sx, sy = _resize_to_model(img)
|
||||
latent_img = vae.encode(img_resized)
|
||||
kp_batch, sc_batch = _run_on_latent(latent_img)
|
||||
img_keypoints.append(_remap_keypoints(kp_batch[0], scale, pad_top, pad_left))
|
||||
img_keypoints.append(_remap_keypoints(kp_batch[0], sx, sy))
|
||||
img_scores.append(sc_batch[0])
|
||||
|
||||
all_keypoints.append(img_keypoints)
|
||||
@ -549,12 +545,12 @@ class SDPoseKeypointExtractor(io.ComfyNode):
|
||||
|
||||
else: # full-image mode, batched
|
||||
for batch_start in tqdm(range(0, total_images, batch_size), desc="Extracting keypoints"):
|
||||
batch_resized, scale, pad_top, pad_left = _resize_to_model(image[batch_start:batch_start + batch_size])
|
||||
batch_resized, sx, sy = _resize_to_model(image[batch_start:batch_start + batch_size])
|
||||
latent_batch = vae.encode(batch_resized)
|
||||
kp_batch, sc_batch = _run_on_latent(latent_batch)
|
||||
|
||||
for kp, sc in zip(kp_batch, sc_batch):
|
||||
all_keypoints.append([_remap_keypoints(kp, scale, pad_top, pad_left)])
|
||||
all_keypoints.append([_remap_keypoints(kp, sx, sy)])
|
||||
all_scores.append([sc])
|
||||
|
||||
pbar.update(len(kp_batch))
|
||||
@ -727,13 +723,13 @@ class CropByBBoxes(io.ComfyNode):
|
||||
scale = min(output_width / crop_w, output_height / crop_h)
|
||||
scaled_w = int(round(crop_w * scale))
|
||||
scaled_h = int(round(crop_h * scale))
|
||||
scaled = comfy.utils.common_upscale(crop_chw, scaled_w, scaled_h, upscale_method="bilinear", crop="disabled")
|
||||
scaled = comfy.utils.common_upscale(crop_chw, scaled_w, scaled_h, upscale_method="area", crop="disabled")
|
||||
pad_left = (output_width - scaled_w) // 2
|
||||
pad_top = (output_height - scaled_h) // 2
|
||||
resized = torch.zeros(1, num_ch, output_height, output_width, dtype=image.dtype, device=image.device)
|
||||
resized[:, :, pad_top:pad_top + scaled_h, pad_left:pad_left + scaled_w] = scaled
|
||||
else: # "stretch"
|
||||
resized = comfy.utils.common_upscale(crop_chw, output_width, output_height, upscale_method="bilinear", crop="disabled")
|
||||
resized = comfy.utils.common_upscale(crop_chw, output_width, output_height, upscale_method="area", crop="disabled")
|
||||
crops.append(resized)
|
||||
|
||||
if not crops:
|
||||
|
||||
@ -721,6 +721,7 @@ class PromptExecutor:
|
||||
self.server.client_id = None
|
||||
|
||||
self.status_messages = []
|
||||
self.node_timing_ms: dict[str, dict] = {}
|
||||
self.add_message("execution_start", { "prompt_id": prompt_id}, broadcast=False)
|
||||
|
||||
self._notify_prompt_lifecycle("start", prompt_id)
|
||||
@ -767,6 +768,7 @@ class PromptExecutor:
|
||||
break
|
||||
|
||||
assert node_id is not None, "Node ID should not be None at this point"
|
||||
node_start_s = time.perf_counter() if args.benchmark_server_only else None
|
||||
result, error, ex = await execute(self.server, dynamic_prompt, self.caches, node_id, extra_data, executed, prompt_id, execution_list, pending_subgraph_results, pending_async_nodes, ui_node_outputs)
|
||||
self.success = result != ExecutionResult.FAILURE
|
||||
if result == ExecutionResult.FAILURE:
|
||||
@ -776,6 +778,12 @@ class PromptExecutor:
|
||||
execution_list.unstage_node_execution()
|
||||
else: # result == ExecutionResult.SUCCESS:
|
||||
execution_list.complete_node_execution()
|
||||
if node_start_s is not None:
|
||||
class_type = dynamic_prompt.get_node(node_id).get("class_type", "unknown")
|
||||
self.node_timing_ms[node_id] = {
|
||||
"class_type": class_type,
|
||||
"execution_ms": (time.perf_counter() - node_start_s) * 1000.0,
|
||||
}
|
||||
|
||||
if self.cache_type == CacheType.RAM_PRESSURE:
|
||||
comfy.model_management.free_memory(0, None, pins_required=ram_headroom, ram_required=ram_headroom)
|
||||
|
||||
11
main.py
11
main.py
@ -308,12 +308,19 @@ def prompt_worker(q, server_instance):
|
||||
extra_data = item[3].copy()
|
||||
for k in sensitive:
|
||||
extra_data[k] = sensitive[k]
|
||||
benchmark_mode = args.benchmark_server_only
|
||||
|
||||
asset_seeder.pause()
|
||||
e.execute(item[2], prompt_id, extra_data, item[4])
|
||||
|
||||
need_gc = True
|
||||
|
||||
if benchmark_mode:
|
||||
e.history_result["benchmark"] = {
|
||||
"execution_ms": (time.perf_counter() - execution_start_time) * 1000.0,
|
||||
"nodes": e.node_timing_ms,
|
||||
}
|
||||
|
||||
remove_sensitive = lambda prompt: prompt[:5] + prompt[6:]
|
||||
q.task_done(item_id,
|
||||
e.history_result,
|
||||
@ -329,8 +336,8 @@ def prompt_worker(q, server_instance):
|
||||
|
||||
# Log Time in a more readable way after 10 minutes
|
||||
if execution_time > 600:
|
||||
execution_time = time.strftime("%H:%M:%S", time.gmtime(execution_time))
|
||||
logging.info(f"Prompt executed in {execution_time}")
|
||||
execution_time_formatted = time.strftime("%H:%M:%S", time.gmtime(execution_time))
|
||||
logging.info(f"Prompt executed in {execution_time_formatted}")
|
||||
else:
|
||||
logging.info("Prompt executed in {:.2f} seconds".format(execution_time))
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
comfyui-frontend-package==1.42.15
|
||||
comfyui-workflow-templates==0.9.65
|
||||
comfyui-workflow-templates==0.9.66
|
||||
comfyui-embedded-docs==0.4.4
|
||||
torch
|
||||
torchsde
|
||||
|
||||
Loading…
Reference in New Issue
Block a user