chore: import upstream snapshot with attribution
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:16 +08:00
commit 94057c3d3e
7152 changed files with 2120455 additions and 0 deletions
@@ -0,0 +1,58 @@
# ComfyUI SGLDiffusion Plugin
A ComfyUI plugin for integrating with SGLang Diffusion server, supporting image and video generation capabilities.
## Installation
1. **Install SGLang**: Follow the [Installation Guide](../../../../../docs/diffusion/installation.md) to install `sglang[diffusion]`.
2. **Install Plugin**: Copy this entire directory (`ComfyUI_SGLDiffusion`) to your ComfyUI `custom_nodes/` folder.
3. **Restart ComfyUI**: Restart ComfyUI to load the plugin.
## Usage
The plugin supports two modes of operation: **Server Mode** (via HTTP API) and **Integrated Mode** (tight integration with ComfyUI).
### Supported Models
- **Z-Image**: High-speed image generation models (e.g., `Z-Image-Turbo`)
- **FLUX**: State-of-the-art text-to-image models (e.g., `FLUX.1-dev`)
- **Qwen-Image**: Multi-modal image generation models (e.g., `Qwen-Image`,`Qwen-Image-2512`). *Note: Image editing support is currently experimental and may have some issues.*
### Mode 1: Server Mode (HTTP API)
Connect to a standalone SGLang Diffusion server.
1. **Start SGLang Diffusion Server**: Ensure the server is running and accessible.
2. **Connect to Server**: Use the `SGLDiffusion Server Model` node to connect (default: `http://localhost:3000/v1`).
3. **Generate Content**:
- `SGLDiffusion Generate Image`: For text-to-image and image editing.
- `SGLDiffusion Generate Video`: For text-to-video and image-to-video.
4. **LoRA Support**: Use `SGLDiffusion Server Set LoRA` and `SGLDiffusion Server Unset LoRA`.
### Mode 2: Integrated Mode (Tight Integration)
Leverage SGLang's high-performance sampling directly within ComfyUI while using ComfyUI's front-end nodes (CLIP, VAE, etc.).
1. **Load Model**: Use the `SGLDiffusion UNET Loader` node to load your diffusion model.
2. **Configure Options**: Use the `SGLDiffusion Options` node to set runtime parameters like `num_gpus`, `tp_size`, `model_type`, or `enable_torch_compile`.
3. **Sample**: Connect the loaded model to standard ComfyUI samplers. SGLang will handle the sampling process efficiently.
4. **LoRA Support**: Use the `SGLDiffusion LoRA Loader` for native LoRA integration.
## Example Workflows
Reference workflow files are provided in the `workflows/` directory:
- **`flux_sgld_sp.json`**: Multi-GPU (Sequence Parallelism) workflow for FLUX models. High-performance inference across multiple cards.
- **`qwen_image_sgld.json`**: Qwen-Image generation with LoRA support. Optimized for multi-modal image tasks.
- **`z-image_sgld.json`**: High-speed image generation using Z-Image.
- **`sgld_text2img.json`**: Server-mode text-to-image generation with LoRA support.
- **`sgld_image2video.json`**: Server-mode image-to-video generation.
For other workflows supporting the models, you can easily use SGLang by replacing the official `UNET Loader` node with the `SGLDUNETLoader` node. Similarly, for LoRA support, replace the official LoRA loader with the `SGLDiffusion LoRA Loader`.
To use these workflows:
1. Open ComfyUI.
2. Load the workflow JSON file from the `workflows/` directory.
3. Adjust the parameters and model paths as needed.
4. Run the workflow.
## Current Implementation
This plugin provides a high-performance backend for diffusion models in ComfyUI. By leveraging SGLang's optimized kernels and parallelization techniques (Tensor Parallelism, TeaCache, etc.), it significantly accelerates the sampling process, especially for large models like FLUX.
@@ -0,0 +1,13 @@
"""
ComfyUI SGLang Diffusion nodes package.
"""
try:
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
except ImportError:
# ComfyUI dependencies not available (e.g., in test environment)
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
@@ -0,0 +1,14 @@
"""
Core components for SGLang Diffusion ComfyUI integration.
Provides generator, model patcher, and server API client.
"""
from .generator import SGLDiffusionGenerator
from .model_patcher import SGLDModelPatcher
from .server_api import SGLDiffusionServerAPI
__all__ = [
"SGLDiffusionGenerator",
"SGLDModelPatcher",
"SGLDiffusionServerAPI",
]
@@ -0,0 +1,231 @@
"""
Generator for SGLang Diffusion ComfyUI integration.
"""
import logging
import os
import psutil
from comfy import model_detection, model_management
from comfy.utils import (
calculate_parameters,
load_torch_file,
state_dict_prefix_replace,
unet_to_diffusers,
)
logger = logging.getLogger(__name__)
try:
from sglang.multimodal_gen import DiffGenerator
except ImportError:
logger.error(
"Error: sglang.multimodal_gen is not installed. Please install it using 'pip install sglang[diffusion]'"
)
from ..executors import (
FluxExecutor,
QwenImageEditExecutor,
QwenImageExecutor,
ZImageExecutor,
)
from .model_patcher import SGLDModelPatcher
class SGLDiffusionGenerator:
"""Generator for SGLang Diffusion models in ComfyUI."""
def __init__(self):
self.model_path = None
self.generator = None
self.executor = None
self.last_options = None
self.pipeline_class_dict = {
"flux": "ComfyUIFluxPipeline",
"lumina2": "ComfyUIZImagePipeline", # zimage
"qwen_image": "ComfyUIQwenImagePipeline",
"qwen_image_edit": "ComfyUIQwenImageEditPipeline",
}
self.executor_class_dict = {
"flux": FluxExecutor,
"lumina2": ZImageExecutor,
"qwen_image": QwenImageExecutor,
"qwen_image_edit": QwenImageEditExecutor,
}
def __del__(self):
self.close_generator()
def init_generator(
self, model_path: str, pipeline_class_name: str, kwargs: dict = None
):
"""Initialize the diffusion generator."""
if self.generator is not None:
return self.generator
if kwargs is None:
kwargs = {}
# Set comfyui_mode for ComfyUI integration
kwargs["comfyui_mode"] = True
self.generator = DiffGenerator.from_pretrained(
model_path=model_path,
pipeline_class_name=pipeline_class_name,
**kwargs,
)
return self.generator
def kill_generator(self):
"""Kill worker processes manually because generator shutdown cannot terminate them."""
current_pid = os.getpid()
worker_processes = []
for proc in psutil.process_iter(["pid", "name", "cmdline"]):
try:
# Look for sglang-diffusionWorker processes
if proc.info["cmdline"]:
cmdline = " ".join(proc.info["cmdline"])
if "sgl_diffusion::" in cmdline:
if proc.info["pid"] != current_pid:
worker_processes.append(proc)
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
if worker_processes:
logger.info(
f"Found {len(worker_processes)} worker processes to terminate..."
)
for proc in worker_processes:
try:
logger.info(
f"Terminating worker process {proc.info['pid']}: {proc.info['name']}"
)
proc.terminate()
proc.wait(timeout=5)
except psutil.TimeoutExpired:
logger.warning(
f"Process {proc.info['pid']} did not terminate, forcing kill..."
)
try:
proc.kill()
proc.wait(timeout=2)
except (psutil.NoSuchProcess, psutil.TimeoutExpired):
pass
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
def close_generator(self):
"""Close and cleanup the generator and all associated resources."""
if self.generator is not None:
self.generator.shutdown()
self.kill_generator()
# Clear other references
self.last_options = None
self.model_path = None
self.generator = None
self.executor = None
def get_comfyui_model(self, model_path: str, model_options: dict = None):
"""Get ComfyUI model from model path."""
if model_options is None:
model_options = {}
dtype = model_options.get("dtype", None)
# Allow loading unets from checkpoint files
sd = load_torch_file(model_path)
diffusion_model_prefix = model_detection.unet_prefix_from_state_dict(sd)
temp_sd = state_dict_prefix_replace(
sd, {diffusion_model_prefix: ""}, filter_keys=True
)
if len(temp_sd) > 0:
sd = temp_sd
parameters = calculate_parameters(sd)
load_device = model_management.get_torch_device()
model_detect_config = model_detection.detect_unet_config(sd, "")
model_type = model_detect_config.get("image_model", None)
if model_type is None or model_type not in self.pipeline_class_dict:
raise ValueError(f"Unsupported model type: {model_type}")
model_config = model_detection.model_config_from_unet(sd, "")
if model_config is not None:
new_sd = sd
else:
new_sd = model_detection.convert_diffusers_mmdit(sd, "")
if new_sd is not None: # diffusers mmdit
model_config = model_detection.model_config_from_unet(new_sd, "")
if model_config is None:
return None
else: # diffusers unet
model_config = model_detection.model_config_from_diffusers_unet(sd)
if model_config is None:
return None
diffusers_keys = unet_to_diffusers(model_config.unet_config)
new_sd = {}
for k in diffusers_keys:
if k in sd:
new_sd[diffusers_keys[k]] = sd.pop(k)
if dtype is None:
unet_dtype = model_management.unet_dtype(
model_params=parameters,
supported_dtypes=model_config.supported_inference_dtypes,
)
else:
unet_dtype = dtype
manual_cast_dtype = model_management.unet_manual_cast(
unet_dtype, load_device, model_config.supported_inference_dtypes
)
model_config.set_inference_dtype(unet_dtype, manual_cast_dtype)
model_config.custom_operations = model_options.get("custom_operations", None)
model_config.unet_config["disable_unet_model_creation"] = True
comfyui_model = model_config.get_model({})
return comfyui_model, model_config, model_type
def load_model(
self, model_path: str, model_options: dict = None, sgld_options: dict = None
):
"""Load model and return model patcher."""
gather_options = {
"model_path": model_path,
"model_options": model_options,
"sgld_options": sgld_options,
}
if (
self.last_options is not None
and self.last_options == gather_options
and self.generator is not None
):
return self.generator
else:
self.close_generator()
self.last_options = gather_options
self.model_path = model_path
comfyui_model, model_config, model_type = self.get_comfyui_model(
model_path, model_options
)
if model_type is None or model_type not in self.pipeline_class_dict:
raise ValueError(f"Unsupported model type: {model_type}")
set_model_type = sgld_options.pop("model_type", None) if sgld_options else None
if set_model_type is not None and set_model_type in self.pipeline_class_dict:
model_type = set_model_type
pipeline_class_name = self.pipeline_class_dict[model_type]
self.generator = self.init_generator(
model_path, pipeline_class_name, sgld_options
)
executor_class = self.executor_class_dict[model_type]
self.executor = executor_class(
self.generator, model_path, comfyui_model, model_config
)
comfyui_model.diffusion_model = self.executor
load_device = model_management.get_torch_device()
offload_device = model_management.unet_offload_device()
return SGLDModelPatcher(
comfyui_model, load_device, offload_device, model_type=model_type
)
@@ -0,0 +1,82 @@
"""
Model patcher for SGLang Diffusion ComfyUI integration.
"""
import copy
from comfy.model_patcher import ModelPatcher
class SGLDModelPatcher(ModelPatcher):
"""Model patcher for SGLang Diffusion models in ComfyUI."""
def __init__(
self,
model,
load_device,
offload_device,
size=0,
weight_inplace_update=False,
model_type=None,
):
super().__init__(
model, load_device, offload_device, size, weight_inplace_update
)
self.lora_cache = {}
self.model_type = model_type
self.model_size_dict = {
"flux": 27 * 1024 * 1024 * 1024,
"lumina2": 8 * 1024 * 1024 * 1024,
}
def clone(self):
"""Clone the model patcher."""
n = SGLDModelPatcher(
self.model,
self.load_device,
self.offload_device,
self.size,
weight_inplace_update=self.weight_inplace_update,
)
n.patches = {}
for k in self.patches:
n.patches[k] = self.patches[k][:]
n.patches_uuid = self.patches_uuid
n.object_patches = self.object_patches.copy()
n.model_options = copy.deepcopy(self.model_options)
n.backup = self.backup
n.object_patches_backup = self.object_patches_backup
n.lora_cache = copy.copy(self.lora_cache)
return n
def model_size(self):
"""Get the model size in bytes."""
if self.model_type in self.model_size_dict:
return self.model_size_dict[self.model_type]
else:
return 0
def load(
self,
device_to=None,
lowvram_model_memory=0,
force_patch_weights=False,
full_load=False,
):
"""Load model (no-op for SGLang Diffusion)."""
pass
def patch_model(
self,
device_to=None,
lowvram_model_memory=0,
load_weights=True,
force_patch_weights=False,
):
"""Patch model (no-op for SGLang Diffusion)."""
pass
def unpatch_model(self, device_to=None, unpatch_weights=True):
"""Unpatch model (no-op for SGLang Diffusion)."""
pass
@@ -0,0 +1,539 @@
"""
SGLang Diffusion Server API client.
Provides a low-level interface for interacting with SGLang Diffusion HTTP server.
"""
import base64
import io
import os
import time
from typing import Any, Dict, Optional
import requests
from PIL import Image
class SGLDiffusionServerAPI:
"""Client for SGLang Diffusion HTTP server API."""
def __init__(self, base_url: str, api_key: str = "sk-proj-1234567890"):
"""
Initialize the API client.
Args:
base_url: Base URL of the SGLang Diffusion server (e.g., "http://localhost:30010/v1")
api_key: API key for authentication (default: "sk-proj-1234567890")
"""
# Ensure base_url doesn't end with /v1 if it's already there
if base_url.endswith("/v1"):
self.base_url = base_url
elif base_url.endswith("/v1/"):
self.base_url = base_url.rstrip("/")
else:
self.base_url = f"{base_url.rstrip('/')}/v1"
self.api_key = api_key
self.headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
}
def get_model_info(self) -> Dict[str, Any]:
"""
Get information about the model served by this server.
Returns:
Dictionary containing model information including:
- model_path: Path to the model
- task_type: Type of task (e.g., "T2V", "I2I")
- pipeline_name: Name of the pipeline
- num_gpus: Number of GPUs
- dit_precision: DiT model precision
- vae_precision: VAE model precision
"""
try:
# Remove /v1 from base_url for /models endpoint
models_url = self.base_url.removesuffix("/v1") + "/models"
response = requests.get(models_url, headers=self.headers, timeout=30)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
raise RuntimeError(f"Failed to get model info: {str(e)}")
def generate_image(
self,
prompt: str,
image_path: Optional[str] = None,
mask_path: Optional[str] = None,
size: Optional[str] = None,
width: Optional[int] = None,
height: Optional[int] = None,
n: int = 1,
negative_prompt: Optional[str] = None,
guidance_scale: Optional[float] = None,
num_inference_steps: Optional[int] = None,
seed: Optional[int] = None,
enable_teacache: bool = False,
response_format: str = "b64_json",
quality: Optional[str] = "auto",
style: Optional[str] = "vivid",
background: Optional[str] = "auto",
output_format: Optional[str] = None,
generator_device: Optional[str] = "cuda",
) -> Dict[str, Any]:
"""
Generate or edit an image using SGLang Diffusion API.
If image_path is provided, calls the edit endpoint; otherwise calls the generation endpoint.
Args:
prompt: Text prompt for image generation/editing
image_path: Optional path to input image file for editing. If provided, uses edit API.
mask_path: Optional path to mask image file (only used when image_path is provided)
size: Image size in format "WIDTHxHEIGHT" (e.g., "1024x1024")
width: Image width (used if size is not provided)
height: Image height (used if size is not provided)
n: Number of images to generate (1-10)
negative_prompt: Negative prompt to avoid certain elements
guidance_scale: Classifier-free guidance scale
num_inference_steps: Number of denoising steps
seed: Random seed for reproducible generation
enable_teacache: Enable TEA cache acceleration
response_format: Response format ("b64_json" or "url")
quality: Image quality ("auto", "standard", "hd") - only for generation
style: Image style ("vivid" or "natural") - only for generation
background: Background type ("auto", "transparent", "opaque")
output_format: Output format ("png", "jpeg", "webp")
generator_device: Device for random generator ("cuda" or "cpu")
Returns:
Dictionary containing the API response with generated/edited image data
"""
if not prompt:
raise ValueError("Prompt cannot be empty")
# Determine size
if size is None:
if width is not None and height is not None:
size = f"{width}x{height}"
else:
size = "1024x1024"
# Build common parameters
common_params = self._build_image_common_params(
prompt=prompt,
size=size,
n=n,
response_format=response_format,
negative_prompt=negative_prompt,
guidance_scale=guidance_scale,
num_inference_steps=num_inference_steps,
seed=seed,
enable_teacache=enable_teacache,
background=background,
output_format=output_format,
generator_device=generator_device,
)
# If image_path is provided, use edit endpoint
if image_path:
if not os.path.exists(image_path):
raise FileNotFoundError(f"Image file not found: {image_path}")
# Prepare multipart form data for edit
files: Dict[str, Any] = {}
data = common_params.copy()
# Add image file
files["image"] = (
os.path.basename(image_path),
open(image_path, "rb"),
self._get_content_type(image_path),
)
# Add mask file if provided
if mask_path:
if not os.path.exists(mask_path):
raise FileNotFoundError(f"Mask file not found: {mask_path}")
files["mask"] = (
os.path.basename(mask_path),
open(mask_path, "rb"),
self._get_content_type(mask_path),
)
# Prepare headers for multipart form data
headers = {
"Authorization": f"Bearer {self.api_key}",
}
try:
response = requests.post(
f"{self.base_url}/images/edits",
files=files,
data=data,
headers=headers,
timeout=300, # 5 minutes timeout for generation
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
raise RuntimeError(f"Failed to edit image: {str(e)}")
finally:
# Close file handles
for file_tuple in files.values():
if isinstance(file_tuple, tuple) and len(file_tuple) > 1:
file_tuple[1].close()
else:
# Use generation endpoint - add generation-specific parameters
payload = common_params.copy()
if quality:
payload["quality"] = quality
if style:
payload["style"] = style
try:
response = requests.post(
f"{self.base_url}/images/generations",
json=payload,
headers=self.headers,
timeout=300, # 5 minutes timeout for generation
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
raise RuntimeError(f"Failed to generate image: {str(e)}")
def generate_video(
self,
prompt: str,
size: Optional[str] = None,
width: Optional[int] = None,
height: Optional[int] = None,
seconds: Optional[int] = 4,
fps: Optional[int] = None,
num_frames: Optional[int] = None,
negative_prompt: Optional[str] = None,
guidance_scale: Optional[float] = None,
num_inference_steps: Optional[int] = None,
seed: Optional[int] = None,
enable_teacache: bool = False,
generator_device: Optional[str] = "cuda",
input_reference: Optional[str] = None,
output_path: Optional[str] = None,
) -> Dict[str, Any]:
"""
Generate a video using SGLang Diffusion API and wait for completion.
Args:
prompt: Text prompt for video generation
size: Video size in format "WIDTHxHEIGHT" (e.g., "1280x720")
width: Video width (used if size is not provided)
height: Video height (used if size is not provided)
seconds: Duration of the video in seconds
fps: Frames per second
num_frames: Number of frames (overrides seconds * fps if provided)
negative_prompt: Negative prompt to avoid certain elements
guidance_scale: Classifier-free guidance scale
num_inference_steps: Number of denoising steps
seed: Random seed for reproducible generation
enable_teacache: Enable TEA cache acceleration
generator_device: Device for random generator ("cuda" or "cpu")
input_reference: Path to input reference image for image-to-video
Returns:
Dictionary containing completed video job information with file_path
"""
if not prompt:
raise ValueError("Prompt cannot be empty")
# Determine size
if size is None:
if width is not None and height is not None:
size = f"{width}x{height}"
else:
size = "720x1280"
# Prepare request payload
payload: Dict[str, Any] = {
"prompt": prompt,
"size": size,
}
# Add optional parameters
if seconds is not None:
payload["seconds"] = seconds
if fps is not None:
payload["fps"] = fps
if num_frames is not None:
payload["num_frames"] = num_frames
if negative_prompt:
payload["negative_prompt"] = negative_prompt
if guidance_scale is not None:
payload["guidance_scale"] = guidance_scale
if num_inference_steps is not None:
payload["num_inference_steps"] = num_inference_steps
if seed is not None and seed >= 0:
payload["seed"] = seed
if enable_teacache:
payload["enable_teacache"] = True
if generator_device:
payload["generator_device"] = generator_device
if input_reference:
payload["input_reference"] = input_reference
if output_path:
payload["output_path"] = output_path
try:
# Create video generation job
response = requests.post(
f"{self.base_url}/videos",
json=payload,
headers=self.headers,
timeout=30,
)
response.raise_for_status()
video_job = response.json()
video_id = video_job.get("id")
# Wait for completion with fixed polling
poll_interval = 5 # 5 seconds
max_wait_time = 3600 # 1 hour
max_consecutive_errors = 5
consecutive_errors = 0
start_time = time.time()
while time.time() - start_time < max_wait_time:
try:
status_response = requests.get(
f"{self.base_url}/videos/{video_id}",
headers=self.headers,
timeout=30,
)
status_response.raise_for_status()
status = status_response.json()
# Reset error counter on successful request
consecutive_errors = 0
if status.get("status") == "completed":
return status
elif status.get("status") == "failed":
error = status.get("error", {})
error_msg = (
error.get("message", "Unknown error")
if error
else "Unknown error"
)
raise RuntimeError(f"Video generation failed: {error_msg}")
except requests.exceptions.ConnectionError as e:
# Connection errors - likely server is down
consecutive_errors += 1
if consecutive_errors >= max_consecutive_errors:
raise RuntimeError(
f"Lost connection to server after {consecutive_errors} consecutive errors. "
f"Server may be unavailable: {str(e)}"
)
except requests.exceptions.RequestException as e:
# Other network errors - continue polling but track errors
consecutive_errors += 1
if consecutive_errors >= max_consecutive_errors:
raise RuntimeError(
f"Network error after {consecutive_errors} consecutive failures: {str(e)}"
)
time.sleep(poll_interval)
raise TimeoutError(
f"Video generation timed out after {max_wait_time} seconds"
)
except requests.exceptions.RequestException as e:
raise RuntimeError(f"Failed to generate video: {str(e)}")
def _build_image_common_params(
self,
prompt: str,
size: str,
n: int,
response_format: str,
negative_prompt: Optional[str] = None,
guidance_scale: Optional[float] = None,
num_inference_steps: Optional[int] = None,
seed: Optional[int] = None,
enable_teacache: bool = False,
background: Optional[str] = None,
output_format: Optional[str] = None,
generator_device: Optional[str] = None,
) -> Dict[str, Any]:
"""
Build common parameters for both image generation and editing.
Returns:
Dictionary containing common parameters
"""
params: Dict[str, Any] = {
"prompt": prompt,
"size": size,
"n": max(1, min(n, 10)),
"response_format": response_format,
}
# Add optional parameters
if negative_prompt:
params["negative_prompt"] = negative_prompt
if guidance_scale is not None:
params["guidance_scale"] = guidance_scale
if num_inference_steps is not None:
params["num_inference_steps"] = num_inference_steps
if seed is not None and seed >= 0:
params["seed"] = seed
if enable_teacache:
params["enable_teacache"] = True
if background:
params["background"] = background
if output_format:
params["output_format"] = output_format
if generator_device:
params["generator_device"] = generator_device
return params
def _get_content_type(self, file_path: str) -> str:
"""Get content type based on file extension."""
ext = os.path.splitext(file_path)[1].lower()
content_types = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".webp": "image/webp",
}
return content_types.get(ext, "image/png")
def decode_image_from_response(
self, response_data: Dict[str, Any], index: int = 0
) -> Image.Image:
"""
Decode base64 image from API response.
Args:
response_data: API response dictionary
index: Index of the image in the response (default: 0)
Returns:
PIL Image object
"""
if "data" not in response_data or not response_data["data"]:
raise ValueError("No image data in response")
if index >= len(response_data["data"]):
raise IndexError(f"Image index {index} out of range")
image_data = response_data["data"][index]
if "b64_json" not in image_data or not image_data["b64_json"]:
raise ValueError("No base64 image data found")
image_bytes = base64.b64decode(image_data["b64_json"])
image = Image.open(io.BytesIO(image_bytes))
# Convert to RGB if needed
if image.mode != "RGB":
image = image.convert("RGB")
return image
def set_lora(
self,
lora_nickname: str,
lora_path: Optional[str] = None,
target: str = "all",
) -> Dict[str, Any]:
"""
Set a LoRA adapter for the specified transformer(s).
Args:
lora_nickname: The nickname of the adapter (required).
lora_path: Path to the LoRA adapter (local path or HF repo id).
Required for the first load; optional if re-activating a cached nickname.
target: Which transformer(s) to apply the LoRA to. One of:
- "all": Apply to all transformers (default)
- "transformer": Apply only to the primary transformer (high noise for Wan2.2)
- "transformer_2": Apply only to transformer_2 (low noise for Wan2.2)
- "critic": Apply only to the critic model
Returns:
Dictionary containing the API response with status and message
"""
if not lora_nickname:
raise ValueError("lora_nickname cannot be empty")
# Prepare request payload
payload: Dict[str, Any] = {
"lora_nickname": lora_nickname,
"target": target,
}
# Add optional lora_path if provided
if lora_path:
payload["lora_path"] = lora_path
try:
response = requests.post(
f"{self.base_url}/set_lora",
json=payload,
headers=self.headers,
timeout=30,
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
raise RuntimeError(f"Failed to set LoRA adapter: {str(e)}")
def unset_lora(
self,
target: str = "all",
) -> Dict[str, Any]:
"""
Unset (unmerge) LoRA weights from the base model.
Args:
target: same as set_lora
Returns:
Dictionary containing the API response with status and message
"""
# Prepare request payload
payload: Dict[str, Any] = {
"target": target,
}
try:
response = requests.post(
f"{self.base_url}/unmerge_lora_weights",
json=payload,
headers=self.headers,
timeout=30,
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
raise RuntimeError(f"Failed to unset LoRA adapter: {str(e)}")
if __name__ == "__main__":
api = SGLDiffusionServerAPI(
base_url="http://localhost:30010/v1", api_key="sk-proj-1234567890"
)
model_info = api.get_model_info()
print(api.get_model_info())
if model_info.get("task_type") == "T2V" or model_info.get("task_type") == "I2V":
print(
api.generate_video(
prompt="A calico cat playing a piano on stage",
num_inference_steps=1,
size="480x480",
)
)
else:
print(
api.generate_image(
prompt="A calico cat playing a piano on stage", size="1024x1024"
)
)
@@ -0,0 +1,17 @@
"""
ComfyUI SGLang Diffusion executors package.
Provides executor classes for different model types.
"""
from .base import SGLDiffusionExecutor
from .flux import FluxExecutor
from .qwen_image import QwenImageEditExecutor, QwenImageExecutor
from .zimage import ZImageExecutor
__all__ = [
"SGLDiffusionExecutor",
"FluxExecutor",
"ZImageExecutor",
"QwenImageExecutor",
"QwenImageEditExecutor",
]
@@ -0,0 +1,56 @@
"""
Base executor class for SGLang Diffusion ComfyUI integration.
"""
import torch
class SGLDiffusionExecutor(torch.nn.Module):
"""Base executor class for SGLang Diffusion models in ComfyUI."""
def __init__(self, generator, model_path, model, config):
super(SGLDiffusionExecutor, self).__init__()
self.generator = generator
self.model_path = model_path
self.model = model
self.dtype = config.unet_config["dtype"]
self.config = config
self.loras = []
@staticmethod
def should_suppress_logs(timestep):
"""Determine if logs should be suppressed based on timestep value."""
if torch.is_tensor(timestep):
return bool((timestep < 1.0).item())
return bool(timestep < 1.0)
def set_lora(self, lora_nickname=None, lora_path=None, strength=None, target=None):
"""Set LoRA adapter using SGLang Diffusion API."""
if len(lora_nickname) > 0:
self.generator.set_lora(
lora_nickname=lora_nickname,
lora_path=lora_path,
strength=strength,
target=target,
)
def _unpack_latents(self, latents, height, width, channels):
"""Unpack latents from packed format to standard format."""
batch_size = latents.shape[0]
latents = latents.view(batch_size, height // 2, width // 2, channels, 2, 2)
latents = latents.permute(0, 3, 1, 4, 2, 5)
latents = latents.reshape(batch_size, channels, height, width)
return latents
def _pack_latents(self, latents):
"""Pack latents from standard format to packed format."""
batch_size, num_channels_latents, height, width = latents.shape
latents = latents.view(
batch_size, num_channels_latents, height // 2, 2, width // 2, 2
)
latents = latents.permute(0, 2, 4, 1, 3, 5)
latents = latents.reshape(
batch_size, (height // 2) * (width // 2), num_channels_latents * 4
)
return latents
@@ -0,0 +1,69 @@
"""
Flux executor for SGLang Diffusion ComfyUI integration.
"""
import torch
try:
from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams
from sglang.multimodal_gen.runtime.entrypoints.utils import prepare_request
except ImportError:
print(
"Error: sglang.multimodal_gen is not installed. Please install it using 'pip install sglang[diffusion]'"
)
from .base import SGLDiffusionExecutor
class FluxExecutor(SGLDiffusionExecutor):
"""Executor for Flux models in ComfyUI."""
def __init__(self, generator, model_path, model, config):
super().__init__(generator, model_path, model, config)
def forward(self, x, timestep, context, y=None, guidance=None, **kwargs):
"""Forward pass for Flux model."""
hidden_states = self._pack_latents(x)
timesteps = timestep * 1000.0
encoder_hidden_states = context
pooled_projections = y
guidance = guidance * 1000.0
B, C, H, W = x.shape
height = H * 8
width = W * 8
# Create SamplingParams
sampling_params = SamplingParams.from_user_sampling_params_args(
self.model_path,
server_args=self.generator.server_args,
prompt=" ",
guidance_scale=3.5, # Flux typically uses embedded_cfg_scale=3.5
height=height,
width=width,
num_frames=1,
num_inference_steps=1,
save_output=False,
suppress_logs=self.should_suppress_logs(timestep),
)
# Prepare request (converts SamplingParams to Req)
req = prepare_request(
server_args=self.generator.server_args,
sampling_params=sampling_params,
)
req.latents = hidden_states # Set as [B, S, D] format directly
req.timesteps = timesteps # ComfyUI's timesteps parameter
req.prompt_embeds = [pooled_projections, encoder_hidden_states] # [CLIP, T5]
req.raw_latent_shape = torch.tensor(hidden_states.shape, dtype=torch.long)
# Set pooled_projections (required by Flux)
req.pooled_embeds = [pooled_projections] # List format as per Req definition
req.do_classifier_free_guidance = False
req.generator = [
torch.Generator("cuda") for _ in range(req.num_outputs_per_prompt)
]
# Send request to scheduler
output_batch = self.generator._send_to_scheduler_and_wait_for_response([req])
noise_pred = output_batch.noise_pred
return self._unpack_latents(noise_pred, H, W, C).to(x.device)
@@ -0,0 +1,172 @@
"""
QwenImage executor for SGLang Diffusion ComfyUI integration.
"""
import torch
try:
from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams
from sglang.multimodal_gen.runtime.entrypoints.utils import prepare_request
except ImportError:
print(
"Error: sglang.multimodal_gen is not installed. Please install it using 'pip install sglang[diffusion]'"
)
import comfy.ldm.common_dit
from .base import SGLDiffusionExecutor
class QwenImageExecutor(SGLDiffusionExecutor):
"""Executor for QwenImage models in ComfyUI."""
def __init__(self, generator, model_path, model, config):
super().__init__(generator, model_path, model, config)
self.patch_size = 2
def _pack_latents(self, x):
"""Process hidden states for QwenImage model."""
latents = comfy.ldm.common_dit.pad_to_patch_size(
x, (1, self.patch_size, self.patch_size)
)
orig_shape = latents.shape
latents = latents.view(
orig_shape[0],
orig_shape[1],
orig_shape[-3],
orig_shape[-2] // 2,
2,
orig_shape[-1] // 2,
2,
)
latents = latents.permute(0, 2, 3, 5, 1, 4, 6)
latents = latents.reshape(
orig_shape[0],
orig_shape[-3] * (orig_shape[-2] // 2) * (orig_shape[-1] // 2),
orig_shape[1] * 4,
)
return latents, orig_shape
def _unpack_latents(self, latents, num_embeds, orig_shape, x):
"""Unpack hidden states from packed format to standard format."""
latents = latents[:, :num_embeds].view(
orig_shape[0],
orig_shape[-3],
orig_shape[-2] // 2,
orig_shape[-1] // 2,
orig_shape[1],
2,
2,
)
latents = latents.permute(0, 4, 1, 2, 5, 3, 6)
latents = latents.reshape(orig_shape)[:, :, :, : x.shape[-2], : x.shape[-1]]
return latents
def forward(self, x, timestep, context, **kwargs):
"""Forward pass for QwenImage model."""
latents, orig_shape = self._pack_latents(x)
num_embeds = latents.shape[1]
height = orig_shape[-2] * 8
width = orig_shape[-1] * 8
sampling_params = SamplingParams.from_user_sampling_params_args(
self.model_path,
server_args=self.generator.server_args,
prompt=" ",
guidance_scale=1.0,
height=height,
width=width,
num_frames=1,
num_inference_steps=1,
save_output=False,
suppress_logs=self.should_suppress_logs(timestep),
)
# Prepare request (converts SamplingParams to Req)
req = prepare_request(
server_args=self.generator.server_args,
sampling_params=sampling_params,
)
# Set ComfyUI-specific inputs directly on the Req object
req.latents = latents
req.timesteps = timestep * 1000.0
req.prompt_embeds = [context]
req.raw_latent_shape = torch.tensor(latents.shape, dtype=torch.long)
req.do_classifier_free_guidance = False
req.generator = [
torch.Generator("cuda") for _ in range(req.num_outputs_per_prompt)
]
output_batch = self.generator._send_to_scheduler_and_wait_for_response([req])
noise_pred = output_batch.noise_pred
return self._unpack_latents(noise_pred, num_embeds, orig_shape, x)
class QwenImageEditExecutor(QwenImageExecutor):
"""Executor for QwenImageEdit models in ComfyUI."""
def __init__(self, generator, model_path, model, config):
super().__init__(generator, model_path, model, config)
def forward(
self,
x,
timestep,
context,
attention_mask=None,
ref_latents=None,
additional_t_cond=None,
transformer_options={},
**kwargs,
):
"""Forward pass for QwenImageEdit model."""
latents, orig_shape = self._pack_latents(x)
num_embeds = latents.shape[1]
height = orig_shape[-2] * 8
width = orig_shape[-1] * 8
# Prepare vae_image_sizes for the condition image (ref_latents)
vae_image_sizes = []
pack_ref_latents = None
# TODO: sgld now don't support multiple condition images, so we only support one condition image for now.
if ref_latents is not None and len(ref_latents) > 0:
pack_ref_latents, orig_ref_shape = self._pack_latents(ref_latents[0])
vae_image_sizes = [(orig_ref_shape[-1], orig_ref_shape[-2])]
sampling_params = SamplingParams.from_user_sampling_params_args(
self.model_path,
server_args=self.generator.server_args,
prompt=" ",
guidance_scale=1.0,
image_path="",
height=height,
width=width,
num_frames=1,
num_inference_steps=1,
save_output=False,
suppress_logs=self.should_suppress_logs(timestep),
)
# Prepare request (converts SamplingParams to Req)
req = prepare_request(
server_args=self.generator.server_args,
sampling_params=sampling_params,
)
# Set ComfyUI-specific inputs directly on the Req object
req.latents = latents
req.image_latent = pack_ref_latents
req.timesteps = timestep * 1000.0
req.vae_image_sizes = vae_image_sizes
req.prompt_embeds = [context]
req.raw_latent_shape = torch.tensor(latents.shape, dtype=torch.long)
req.do_classifier_free_guidance = False
req.generator = [
torch.Generator("cuda") for _ in range(req.num_outputs_per_prompt)
]
output_batch = self.generator._send_to_scheduler_and_wait_for_response([req])
noise_pred = output_batch.noise_pred
return self._unpack_latents(noise_pred, num_embeds, orig_shape, x)
@@ -0,0 +1,64 @@
"""
ZImage executor for SGLang Diffusion ComfyUI integration.
"""
import torch
try:
from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams
from sglang.multimodal_gen.runtime.entrypoints.utils import prepare_request
except ImportError:
print(
"Error: sglang.multimodal_gen is not installed. Please install it using 'pip install sglang[diffusion]'"
)
from .base import SGLDiffusionExecutor
class ZImageExecutor(SGLDiffusionExecutor):
"""Executor for ZImage models in ComfyUI."""
def __init__(self, generator, model_path, model, config):
super().__init__(generator, model_path, model, config)
def forward(self, x, timesteps, context, **kwargs):
"""Forward pass for ZImage model."""
B, C, H, W = x.shape
height = H * 8
width = W * 8
sampling_params = SamplingParams.from_user_sampling_params_args(
self.model_path,
server_args=self.generator.server_args,
prompt=" ",
guidance_scale=1.0,
height=height,
width=width,
num_frames=1, # For images
num_inference_steps=1, # Single step for ComfyUI
save_output=False,
suppress_logs=self.should_suppress_logs(timesteps),
)
# Prepare request (converts SamplingParams to Req)
req = prepare_request(
server_args=self.generator.server_args,
sampling_params=sampling_params,
)
latents = x.unsqueeze(2)
context = context.squeeze(0)
# Set ComfyUI-specific inputs directly on the Req object
req.latents = latents # ComfyUI's x parameter
req.timesteps = timesteps * 1000.0 # ComfyUI's timesteps parameter
req.prompt_embeds = [
context
] # ComfyUI's context parameter (must be List[Tensor])
req.raw_latent_shape = torch.tensor(latents.shape, dtype=torch.long)
req.do_classifier_free_guidance = False
req.generator = [
torch.Generator("cuda") for _ in range(req.num_outputs_per_prompt)
]
output_batch = self.generator._send_to_scheduler_and_wait_for_response([req])
noise_pred = output_batch.noise_pred
return noise_pred.permute(1, 0, 2, 3).to(x.device)
@@ -0,0 +1,715 @@
"""
ComfyUI nodes for SGLang Diffusion integration.
Provides nodes for connecting to SGLang Diffusion server and generating images/videos.
"""
import os
import uuid
import folder_paths
import torch
from .core import SGLDiffusionGenerator, SGLDiffusionServerAPI
from .utils import (
convert_b64_to_tensor_image,
convert_video_to_comfy_video,
get_image_path,
is_empty_image,
)
class SGLDOptions:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {},
"optional": {
"model_type": (
["auto-detect", "qwen_image", "qwen_image_edit", "flux", "lumina2"],
{"default": "auto-detect"},
),
"enable_torch_compile": (
"BOOLEAN",
{"default": False},
),
"num_gpus": ("INT", {"default": 1, "min": 1, "step": 1}),
"tp_size": ("INT", {"default": -1, "min": -1, "step": 1}),
"sp_degree": ("INT", {"default": -1, "min": -1, "step": 1}),
"ulysses_degree": (
"INT",
{
"default": -1,
"min": -1,
"step": 1,
},
),
"ring_degree": (
"INT",
{
"default": -1,
"min": -1,
"step": 1,
},
),
"dp_size": ("INT", {"default": 1, "min": 1, "step": 1}),
"dp_degree": ("INT", {"default": 1, "min": 1, "step": 1}),
"enable_cfg_parallel": (
"BOOLEAN",
{"default": False},
),
"attention_backend": (
"STRING",
{"default": ""},
),
},
}
RETURN_TYPES = ("SGLD_OPTIONS",)
RETURN_NAMES = ("sgld_options",)
FUNCTION = "create_options"
CATEGORY = "SGLDiffusion"
def create_options(
self,
model_type: str = "auto-detect",
enable_torch_compile: bool = False,
num_gpus: int = 1,
tp_size: int = -1,
sp_degree: int = -1,
ulysses_degree: int = -1,
ring_degree: int = -1,
dp_size: int = 1,
dp_degree: int = 1,
enable_cfg_parallel: bool = False,
attention_backend: str = "",
):
"""
Build a dictionary of SGLang Diffusion runtime options.
"""
# Convert -1 to None for optional parameters (matching ServerArgs defaults)
ulysses_degree = None if ulysses_degree == -1 else ulysses_degree
ring_degree = None if ring_degree == -1 else ring_degree
attention_backend = None if attention_backend == "" else attention_backend
options = {
"model_type": model_type,
"enable_torch_compile": enable_torch_compile,
"num_gpus": num_gpus,
"tp_size": tp_size,
"sp_degree": sp_degree,
"ulysses_degree": ulysses_degree,
"ring_degree": ring_degree,
"dp_size": dp_size,
"dp_degree": dp_degree,
"enable_cfg_parallel": enable_cfg_parallel,
"attention_backend": attention_backend,
}
# Strip None to keep payload clean
options = {k: v for k, v in options.items() if v is not None}
return (options,)
class SGLDLoraLoader:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL",),
"lora_name": (folder_paths.get_filename_list("loras"),),
"strength_model": (
"FLOAT",
{"default": 1.0, "min": 0, "max": 10, "step": 0.01},
),
"nickname": ("STRING", {"default": ""}),
"target": (
["all", "transformer", "transformer_2", "critic"],
{"default": "all"},
),
},
}
RETURN_TYPES = ("MODEL",)
FUNCTION = "load_lora"
CATEGORY = "SGLDiffusion"
def load_lora(
self, model, lora_name, strength_model=1.0, nickname="", target="all"
):
"""Load LoRA adapter using SGLang Diffusion API."""
lora_path = folder_paths.get_full_path("loras", lora_name)
assert model is not None
bi = model.clone()
nickname = nickname if nickname != "" else str("lora" + str(uuid.uuid4()))
# set lora in the model
bi.patches[nickname] = (lora_path, strength_model, target)
# prepare input for the SGLang Diffusion API
lora_input = {
"lora_nickname": [],
"lora_path": [],
"strength": [],
"target": [],
}
for nickname, lora_info in bi.patches.items():
lora_input["lora_nickname"].append(nickname)
lora_input["lora_path"].append(lora_info[0])
lora_input["strength"].append(lora_info[1])
lora_input["target"].append(lora_info[2])
# call the SGLang Diffusion API
model.model.diffusion_model.set_lora(**lora_input)
return (model,)
class SGLDUNETLoader:
def __init__(self):
self.generator = SGLDiffusionGenerator()
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"unet_name": (folder_paths.get_filename_list("diffusion_models"),),
"weight_dtype": (["default", "fp8_e4m3fn", "fp8_e5m2"],),
},
"optional": {
"sgld_options": ("SGLD_OPTIONS",),
},
}
RETURN_TYPES = ("MODEL",)
FUNCTION = "load_unet"
CATEGORY = "SGLDiffusion"
def load_unet(self, unet_name, weight_dtype, sgld_options: dict = None):
model_options = {}
if weight_dtype == "fp8_e4m3fn":
model_options["dtype"] = torch.float8_e4m3fn
elif weight_dtype == "fp8_e5m2":
model_options["dtype"] = torch.float8_e5m2
unet_path = folder_paths.get_full_path("diffusion_models", unet_name)
model = self.generator.load_model(
unet_path, model_options=model_options, sgld_options=sgld_options
)
return (model,)
class SGLDiffusionServerModel:
"""Node to load and manage SGLang Diffusion server connection."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"base_url": (
"STRING",
{
"default": "http://localhost:3000/v1",
"multiline": False,
},
),
"api_key": (
"STRING",
{
"default": "sk-proj-1234567890",
"multiline": False,
},
),
}
}
RETURN_TYPES = ("SGLD_CLIENT", "STRING")
RETURN_NAMES = ("sgld_client", "model_info")
FUNCTION = "load_server"
CATEGORY = "SGLDiffusion"
def load_server(self, base_url: str, api_key: str):
"""Initialize OpenAI client for SGLang Diffusion server."""
client = SGLDiffusionServerAPI(base_url=base_url, api_key=api_key)
try:
model_info = client.get_model_info()
# Format model_info as a readable string
info_lines = ["=== SGLDiffusion Model Info ==="]
for key, value in model_info.items():
info_lines.append(f"{key}: {value}")
model_info_str = "\n".join(info_lines)
except Exception as e:
model_info_str = f"Failed to get model info: {str(e)}"
return (client, model_info_str)
class SGLDiffusionGenerateImage:
"""Node to generate images using SGLang Diffusion."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"sgld_client": ("SGLD_CLIENT",),
"positive_prompt": (
"STRING",
{
"default": "",
"tooltip": "Text prompt for image generation",
},
),
},
"optional": {
"negative_prompt": (
"STRING",
{
"default": "",
"tooltip": "Negative prompt to avoid certain elements",
},
),
"image": (
"IMAGE",
{
"default": None,
"tooltip": "input image to use for editing",
},
),
"seed": (
"INT",
{
"default": 1024,
"min": -1,
"max": 2**32 - 1,
},
),
"steps": (
"INT",
{
"default": 6,
"min": 1,
"max": 100,
"step": 1,
},
),
"cfg": (
"FLOAT",
{
"default": 7.0,
"min": 1.0,
"max": 20.0,
"step": 0.1,
},
),
"width": (
"INT",
{
"default": 1024,
"min": 256,
"max": 4096,
"step": 64,
},
),
"height": (
"INT",
{
"default": 1024,
"min": 256,
"max": 4096,
"step": 64,
},
),
"enable_teacache": (
"BOOLEAN",
{
"default": False,
},
),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "generate_image"
CATEGORY = "SGLDiffusion"
OUTPUT_NODE = False
def generate_image(
self,
sgld_client: SGLDiffusionServerAPI,
positive_prompt: str,
negative_prompt: str = "",
image: torch.Tensor = None,
seed: int = 1024,
steps: int = 6,
cfg: float = 7.0,
width: int = 1024,
height: int = 1024,
enable_teacache: bool = False,
):
"""Generate image using SGLang Diffusion API."""
if not positive_prompt:
raise ValueError("Prompt cannot be empty")
size = f"{width}x{height}"
# Prepare request parameters
request_params = {
"prompt": positive_prompt,
"size": size,
"response_format": "b64_json",
}
# Add optional parameters if provided
if negative_prompt:
request_params["negative_prompt"] = negative_prompt
if cfg is not None:
request_params["guidance_scale"] = cfg
if steps is not None:
request_params["num_inference_steps"] = steps
if seed is not None and seed >= 0:
request_params["seed"] = seed
if enable_teacache:
request_params["enable_teacache"] = True
if image is not None:
# If the image is empty, use the size of the image to generate the image
if is_empty_image(image):
width, height = image.shape[2], image.shape[1]
size = f"{width}x{height}"
request_params["size"] = size
else:
request_params["image_path"] = get_image_path(image)
# Call API
try:
response = sgld_client.generate_image(**request_params)
except Exception as e:
raise RuntimeError(f"Failed to generate image: {str(e)}")
# Decode base64 image
if not response["data"] or not response["data"][0]["b64_json"]:
raise RuntimeError("No image data in response")
image_data = response["data"][0]["b64_json"]
image = convert_b64_to_tensor_image(image_data)
return (image,)
class SGLDiffusionGenerateVideo:
"""Node to generate videos using SGLang Diffusion."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"sgld_client": ("SGLD_CLIENT",),
"positive_prompt": (
"STRING",
{
"default": "",
"tooltip": "Text prompt for video generation",
},
),
},
"optional": {
"negative_prompt": (
"STRING",
{
"default": "",
"tooltip": "Negative prompt to avoid certain elements",
},
),
"image": (
"IMAGE",
{
"default": None,
"tooltip": "input image to use for image-to-video",
},
),
"seed": (
"INT",
{
"default": 1024,
"min": -1,
"max": 2**32 - 1,
},
),
"steps": (
"INT",
{
"default": 6,
"min": 1,
"max": 100,
"step": 1,
},
),
"cfg": (
"FLOAT",
{
"default": 7.0,
"min": 1.0,
"max": 20.0,
"step": 0.1,
},
),
"width": (
"INT",
{
"default": 1280,
"min": 256,
"max": 4096,
"step": 1,
},
),
"height": (
"INT",
{
"default": 720,
"min": 256,
"max": 4096,
"step": 1,
},
),
"num_frames": (
"INT",
{
"default": 120,
"min": 1,
"max": 1000,
"step": 1,
},
),
"fps": (
"INT",
{
"default": 24,
"min": 1,
"max": 60,
"step": 1,
},
),
"seconds": (
"INT",
{
"default": 5,
"min": 1,
"max": 60,
"step": 1,
},
),
"enable_teacache": (
"BOOLEAN",
{
"default": False,
},
),
},
}
RETURN_TYPES = ("VIDEO", "STRING")
RETURN_NAMES = ("video", "video_path")
FUNCTION = "generate_video"
CATEGORY = "SGLDiffusion"
OUTPUT_NODE = False
def generate_video(
self,
sgld_client: SGLDiffusionServerAPI,
positive_prompt: str,
negative_prompt: str = "",
image: torch.Tensor = None,
seed: int = 1024,
steps: int = 6,
cfg: float = 7.0,
width: int = 1280,
height: int = 720,
num_frames: int = 120,
fps: int = 24,
seconds: int = 5,
enable_teacache: bool = False,
):
"""Generate video using SGLang Diffusion API."""
if not positive_prompt:
raise ValueError("Prompt cannot be empty")
size = f"{width}x{height}"
output_dir = folder_paths.get_temp_directory()
# Prepare request parameters
request_params = {
"prompt": positive_prompt,
"size": size,
"seconds": seconds,
"fps": fps,
"output_path": output_dir,
}
# Add optional parameters if provided
if negative_prompt:
request_params["negative_prompt"] = negative_prompt
if cfg is not None:
request_params["guidance_scale"] = cfg
if steps is not None:
request_params["num_inference_steps"] = steps
if seed is not None and seed >= 0:
request_params["seed"] = seed
if enable_teacache:
request_params["enable_teacache"] = True
if num_frames is not None:
request_params["num_frames"] = num_frames
if image is not None:
# If the image is empty, use the size of the image to generate the video
if is_empty_image(image):
width, height = image.shape[2], image.shape[1]
size = f"{width}x{height}"
request_params["size"] = size
else:
request_params["input_reference"] = get_image_path(image)
# Call API
try:
response = sgld_client.generate_video(**request_params)
video_path = response.get("file_path", "")
video = convert_video_to_comfy_video(video_path, height, width)
except Exception as e:
raise RuntimeError(f"Failed to generate video: {str(e)}")
return (video, video_path)
class SGLDiffusionServerSetLora:
"""Node to set LoRA adapter for SGLang Diffusion server."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"sgld_client": ("SGLD_CLIENT",),
"lora_name": (
"STRING",
{
"default": "",
"tooltip": "The name of the LoRA adapter",
},
),
},
"optional": {
"lora_nickname": (
"STRING",
{
"default": "",
"tooltip": "The nickname of the LoRA adapter",
},
),
"target": (
[
"all",
"transformer",
"transformer_2",
"critic",
],
{
"default": "all",
"tooltip": "Which transformer(s) to apply the LoRA to",
},
),
},
}
RETURN_TYPES = ("SGLD_CLIENT",)
RETURN_NAMES = ("sgld_client",)
FUNCTION = "set_lora"
CATEGORY = "SGLDiffusion"
OUTPUT_NODE = False
def set_lora(
self,
sgld_client: SGLDiffusionServerAPI,
lora_name: str = "",
lora_nickname: str = "",
target: str = "all",
):
"""Set LoRA adapter using SGLang Diffusion API."""
if lora_nickname == "":
lora_nickname = os.path.splitext(lora_name)[0]
# Prepare request parameters
request_params = {
"lora_nickname": lora_nickname,
"lora_path": lora_name,
"target": target,
}
# Call API
try:
sgld_client.set_lora(**request_params)
return (sgld_client,)
except Exception as e:
raise RuntimeError(f"Failed to set LoRA adapter: {str(e)}")
class SGLDiffusionServerUnsetLora:
"""Node to unset LoRA adapter for SGLang Diffusion server."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"sgld_client": ("SGLD_CLIENT",),
},
"optional": {
"target": (
[
"all",
"transformer",
"transformer_2",
"critic",
],
{
"default": "all",
"tooltip": "Which transformer(s) to unset the LoRA from",
},
),
},
}
RETURN_TYPES = ("SGLD_CLIENT",)
RETURN_NAMES = ("sgld_client",)
FUNCTION = "unset_lora"
CATEGORY = "SGLDiffusion"
OUTPUT_NODE = False
def unset_lora(
self,
sgld_client: SGLDiffusionServerAPI,
target: str = "all",
):
"""Unset LoRA adapter using SGLang Diffusion API."""
try:
sgld_client.unset_lora(target=target)
return (sgld_client,)
except Exception as e:
raise RuntimeError(f"Failed to unset LoRA adapter: {str(e)}")
# Register nodes
NODE_CLASS_MAPPINGS = {
"SGLDiffusionServerModel": SGLDiffusionServerModel,
"SGLDiffusionGenerateImage": SGLDiffusionGenerateImage,
"SGLDiffusionGenerateVideo": SGLDiffusionGenerateVideo,
"SGLDiffusionServerSetLora": SGLDiffusionServerSetLora,
"SGLDiffusionServerUnsetLora": SGLDiffusionServerUnsetLora,
"SGLDUNETLoader": SGLDUNETLoader,
"SGLDOptions": SGLDOptions,
"SGLDLoraLoader": SGLDLoraLoader,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"SGLDiffusionServerModel": "SGLDiffusion Server Model",
"SGLDiffusionGenerateImage": "SGLDiffusion Generate Image",
"SGLDiffusionGenerateVideo": "SGLDiffusion Generate Video",
"SGLDiffusionServerSetLora": "SGLDiffusion Server Set LoRA",
"SGLDiffusionServerUnsetLora": "SGLDiffusion Server Unset LoRA",
"SGLDUNETLoader": "SGLDiffusion UNET Loader",
"SGLDOptions": "SGLDiffusion Options",
"SGLDLoraLoader": "SGLDiffusion LoRA Loader",
}
@@ -0,0 +1,66 @@
# ComfyUI SGLDiffusion Pipeline Tests
This directory contains tests for each ComfyUI pipeline integration.
## Test Files
- `test_zimage_pipeline.py` - Tests for ComfyUIZImagePipeline
- `test_flux_pipeline.py` - Tests for ComfyUIFluxPipeline
- `test_qwen_image_pipeline.py` - Tests for ComfyUIQwenImagePipeline
- `test_qwen_image_edit_pipeline.py` - Tests for ComfyUIQwenImageEditPipeline (I2I/edit mode)
## Running Tests
### Run all tests
```bash
pytest python/sglang/multimodal_gen/apps/ComfyUI_SGLDiffusion/test/ -v -s
```
### Run a specific test file
```bash
pytest python/sglang/multimodal_gen/apps/ComfyUI_SGLDiffusion/test/test_zimage_pipeline.py -v -s
```
## Environment Variables
You can configure model paths via environment variables. Model paths support two formats:
- **Safetensors file**: Path to a single `.safetensors` file (e.g., `/path/to/model.safetensors`)
- **Diffusers format**: HuggingFace model ID or local diffusers directory (e.g., `Tongyi-MAI/Z-Image-Turbo`)
Environment variables:
- `SGLANG_TEST_ZIMAGE_MODEL_PATH` - Path to ZImage model (default: `Tongyi-MAI/Z-Image-Turbo`)
- `SGLANG_TEST_FLUX_MODEL_PATH` - Path to Flux model (default: `black-forest-labs/FLUX.1-dev`)
- `SGLANG_TEST_QWEN_IMAGE_MODEL_PATH` - Path to QwenImage model (default: `Qwen/Qwen-Image`)
- `SGLANG_TEST_QWEN_IMAGE_EDIT_MODEL_PATH` - Path to QwenImageEdit model (default: `Qwen/Qwen-Image-Edit-2511`)
Examples:
```bash
# Using HuggingFace model ID (diffusers format)
export SGLANG_TEST_ZIMAGE_MODEL_PATH="Tongyi-MAI/Z-Image-Turbo"
pytest python/sglang/multimodal_gen/apps/ComfyUI_SGLDiffusion/test/test_zimage_pipeline.py -v -s
# Using safetensors file
export SGLANG_TEST_ZIMAGE_MODEL_PATH="/path/to/z_image_turbo_bf16.safetensors"
pytest python/sglang/multimodal_gen/apps/ComfyUI_SGLDiffusion/test/test_zimage_pipeline.py -v -s
```
## Test Structure
Each test file follows a similar structure:
1. **Setup**: Creates a `DiffGenerator` with the appropriate pipeline class
2. **Input Preparation**: Creates dummy tensors for latents, timesteps, and embeddings
3. **Request Preparation**: Uses `prepare_request` to convert `SamplingParams` to `Req`
4. **ComfyUI Inputs**: Sets ComfyUI-specific inputs directly on the `Req` object
5. **Execution**: Sends request to scheduler and waits for response
6. **Validation**: Checks that `noise_pred` is retrieved from `OutputBatch`
## Notes
- These tests use `comfyui_mode=True` to enable ComfyUI-specific behavior
- Tests use pre-processed inputs (latents, timesteps, embeddings) as ComfyUI would provide
- The tests verify that `noise_pred` can be retrieved from the `OutputBatch` after processing
- All tests use dummy/ones tensors for simplicity - in production, these would be actual model outputs
@@ -0,0 +1,9 @@
"""
Test suite for ComfyUI SGLDiffusion pipelines.
This package contains tests for each ComfyUI pipeline integration:
- ZImagePipeline
- FluxPipeline
- QwenImagePipeline
- QwenImageEditPipeline
"""
@@ -0,0 +1,156 @@
"""Test for ComfyUIFluxPipeline with pass-through scheduler."""
import os
import sys
import pytest
import torch
from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams
from sglang.multimodal_gen.runtime.entrypoints.diffusion_generator import DiffGenerator
from sglang.multimodal_gen.runtime.entrypoints.utils import prepare_request
def test_comfyui_flux_pipeline_direct() -> None:
"""Test ComfyUIFluxPipeline with custom inputs."""
model_path = os.environ.get(
"SGLANG_TEST_FLUX_MODEL_PATH",
"black-forest-labs/FLUX.1-dev", # Supports both safetensors file and diffusers format
)
generator = DiffGenerator.from_pretrained(
model_path=model_path,
pipeline_class_name="ComfyUIFluxPipeline",
num_gpus=2,
comfyui_mode=True,
)
batch_size = 1
hidden_states_seq_len = 3600
hidden_states_dim = 64
height = 1280
width = 720
encoder_seq_len = 512
encoder_dim = 4096
pooled_dim = 768
hidden_states = torch.ones(
batch_size,
hidden_states_seq_len,
hidden_states_dim,
device="cuda",
dtype=torch.bfloat16,
)
encoder_hidden_states = torch.ones(
batch_size,
encoder_seq_len,
encoder_dim,
device="cuda",
dtype=torch.bfloat16,
)
pooled_projections = torch.ones(
batch_size,
pooled_dim,
device="cuda",
dtype=torch.bfloat16,
)
timesteps = torch.tensor([1000], dtype=torch.long, device="cuda")
sampling_params = SamplingParams.from_user_sampling_params_args(
generator.server_args.model_path,
server_args=generator.server_args,
prompt="a beautiful girl",
height=height,
width=width,
num_frames=1,
num_inference_steps=1,
save_output=True,
return_trajectory_latents=True,
)
req = prepare_request(
server_args=generator.server_args,
sampling_params=sampling_params,
)
req.latents = hidden_states
req.timesteps = timesteps
req.raw_latent_shape = torch.tensor(hidden_states.shape, dtype=torch.long)
clip_dim = 768
req.prompt_embeds = [pooled_projections, encoder_hidden_states]
if req.guidance_scale > 1.0:
dummy_neg_clip_embedding = torch.zeros(
batch_size,
77,
clip_dim,
device="cuda",
dtype=torch.bfloat16,
)
negative_encoder_hidden_states = torch.ones(
batch_size,
encoder_seq_len,
encoder_dim,
device="cuda",
dtype=torch.bfloat16,
)
req.negative_prompt_embeds = [
dummy_neg_clip_embedding,
negative_encoder_hidden_states,
]
else:
req.negative_prompt_embeds = None
req.pooled_embeds = [pooled_projections]
req.neg_pooled_embeds = []
if (
req.guidance_scale > 1.0
and req.negative_prompt_embeds is not None
and len(req.negative_prompt_embeds) > 0
):
req.do_classifier_free_guidance = True
else:
req.do_classifier_free_guidance = False
if req.seed is not None:
generator_device = req.generator_device
device_str = "cuda" if generator_device == "cuda" else "cpu"
req.generator = [
torch.Generator(device_str).manual_seed(req.seed + i)
for i in range(req.num_outputs_per_prompt)
]
else:
req.generator = [
torch.Generator("cuda") for _ in range(req.num_outputs_per_prompt)
]
output_batch = generator._send_to_scheduler_and_wait_for_response([req])
noise_pred = output_batch.noise_pred
assert noise_pred is not None, "noise_pred should not be None in OutputBatch"
assert isinstance(noise_pred, torch.Tensor), "noise_pred should be a torch.Tensor"
assert (
noise_pred.device.type == "cuda"
), f"noise_pred should be on cuda, got {noise_pred.device}"
assert (
noise_pred.dtype == torch.bfloat16
), f"noise_pred should be bfloat16, got {noise_pred.dtype}"
print("✓ Successfully retrieved noise_pred from OutputBatch!")
print(f" noise_pred shape: {noise_pred.shape}")
print(f" noise_pred dtype: {noise_pred.dtype}")
print(f" noise_pred device: {noise_pred.device}")
latents = output_batch.output if output_batch.output is not None else req.latents
assert latents is not None, "latents should not be None"
print(f"latents.shape: {latents.shape}")
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v"]))
@@ -0,0 +1,136 @@
"""Test for ComfyUIQwenImageEditPipeline with pass-through scheduler (I2I/edit mode)."""
import os
import sys
import pytest
import torch
from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams
from sglang.multimodal_gen.runtime.entrypoints.diffusion_generator import DiffGenerator
from sglang.multimodal_gen.runtime.entrypoints.utils import prepare_request
def test_comfyui_qwen_image_edit_pipeline_direct() -> None:
"""Test ComfyUIQwenImageEditPipeline with edit mode (I2I) and custom inputs."""
model_path = os.environ.get(
"SGLANG_TEST_QWEN_IMAGE_EDIT_MODEL_PATH",
"Qwen/Qwen-Image-Edit-2511", # Supports both safetensors file and diffusers format
)
generator = DiffGenerator.from_pretrained(
model_path=model_path,
pipeline_class_name="ComfyUIQwenImageEditPipeline",
num_gpus=1,
comfyui_mode=True,
dit_layerwise_offload=False,
)
batch_size = 1
noisy_image_seq_len = 3600
hidden_states_dim = 64
condition_image_seq_len = 6889
condition_image_dim = 64
encoder_seq_len = 45
encoder_dim = 3584
height = 720
width = 1280
vae_scale_factor = 8
condition_height_latent = 1328 // vae_scale_factor
condition_width_latent = 1328 // vae_scale_factor
noisy_image_latents = torch.ones(
batch_size,
noisy_image_seq_len,
hidden_states_dim,
device="cuda",
dtype=torch.bfloat16,
)
condition_image_latents = torch.ones(
batch_size,
condition_image_seq_len,
condition_image_dim,
device="cuda",
dtype=torch.bfloat16,
)
encoder_hidden_states = torch.ones(
batch_size,
encoder_seq_len,
encoder_dim,
device="cuda",
dtype=torch.bfloat16,
)
timesteps = torch.tensor([1000], dtype=torch.long, device="cuda")
sampling_params = SamplingParams.from_user_sampling_params_args(
generator.server_args.model_path,
server_args=generator.server_args,
prompt=" ",
guidance_scale=1.0,
height=height,
width=width,
image_path="",
num_frames=1,
num_inference_steps=1,
seed=42,
save_output=False,
return_frames=False,
)
req = prepare_request(
server_args=generator.server_args,
sampling_params=sampling_params,
)
req.latents = noisy_image_latents
req.image_latent = condition_image_latents
req.timesteps = timesteps
req.prompt_embeds = [encoder_hidden_states]
req.negative_prompt_embeds = None
req.vae_image_sizes = [(condition_width_latent, condition_height_latent)]
req.raw_latent_shape = torch.tensor(noisy_image_latents.shape, dtype=torch.long)
if req.guidance_scale > 1.0 and req.negative_prompt_embeds is not None:
req.do_classifier_free_guidance = True
else:
req.do_classifier_free_guidance = False
if req.seed is not None:
generator_device = req.generator_device
device_str = "cpu" if generator_device == "cpu" else "cuda"
req.generator = [
torch.Generator(device_str).manual_seed(req.seed + i)
for i in range(req.num_outputs_per_prompt)
]
else:
req.generator = [
torch.Generator("cuda") for _ in range(req.num_outputs_per_prompt)
]
output_batch = generator._send_to_scheduler_and_wait_for_response([req])
noise_pred = output_batch.noise_pred
assert noise_pred is not None, "noise_pred should not be None in OutputBatch"
assert isinstance(noise_pred, torch.Tensor), "noise_pred should be a torch.Tensor"
assert (
noise_pred.device.type == "cuda"
), f"noise_pred should be on cuda, got {noise_pred.device}"
assert (
noise_pred.dtype == torch.bfloat16
), f"noise_pred should be bfloat16, got {noise_pred.dtype}"
print("✓ Successfully retrieved noise_pred from OutputBatch (Edit Mode)!")
print(f" noise_pred shape: {noise_pred.shape}")
print(f" noise_pred dtype: {noise_pred.dtype}")
print(f" noise_pred device: {noise_pred.device}")
latents = output_batch.output if output_batch.output is not None else req.latents
assert latents is not None, "latents should not be None"
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v"]))
@@ -0,0 +1,120 @@
"""Test for ComfyUIQwenImagePipeline with pass-through scheduler."""
import os
import sys
import pytest
import torch
from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams
from sglang.multimodal_gen.runtime.entrypoints.diffusion_generator import DiffGenerator
from sglang.multimodal_gen.runtime.entrypoints.utils import prepare_request
def test_comfyui_qwen_image_pipeline_direct() -> None:
"""Test ComfyUIQwenImagePipeline with custom inputs."""
model_path = os.environ.get(
"SGLANG_TEST_QWEN_IMAGE_MODEL_PATH",
"Qwen/Qwen-Image", # Supports both safetensors file and diffusers format
)
generator = DiffGenerator.from_pretrained(
model_path=model_path,
pipeline_class_name="ComfyUIQwenImagePipeline",
num_gpus=2,
comfyui_mode=True,
dit_layerwise_offload=False,
)
batch_size = 1
hidden_states_seq_len = 6889
hidden_states_dim = 64
encoder_seq_len = 45
encoder_dim = 3584
height = 1328
width = 1328
dtype = torch.bfloat16
hidden_states = torch.ones(
batch_size,
hidden_states_seq_len,
hidden_states_dim,
device="cuda",
dtype=dtype,
)
encoder_hidden_states = torch.ones(
batch_size,
encoder_seq_len,
encoder_dim,
device="cuda",
dtype=torch.bfloat16,
)
timesteps = torch.tensor([1000], dtype=torch.long, device="cuda")
sampling_params = SamplingParams.from_user_sampling_params_args(
generator.server_args.model_path,
server_args=generator.server_args,
prompt=" ",
guidance_scale=3.0,
height=height,
width=width,
num_frames=1,
num_inference_steps=1,
seed=42,
save_output=False,
return_frames=False,
)
req = prepare_request(
server_args=generator.server_args,
sampling_params=sampling_params,
)
req.latents = hidden_states
req.timesteps = timesteps
req.prompt_embeds = [encoder_hidden_states]
req.negative_prompt_embeds = [encoder_hidden_states]
req.raw_latent_shape = torch.tensor(hidden_states.shape, dtype=torch.long)
if req.guidance_scale > 1.0 and req.negative_prompt_embeds is not None:
req.do_classifier_free_guidance = True
else:
req.do_classifier_free_guidance = False
if req.seed is not None:
generator_device = req.generator_device
device_str = "cpu" if generator_device == "cpu" else "cuda"
req.generator = [
torch.Generator(device_str).manual_seed(req.seed + i)
for i in range(req.num_outputs_per_prompt)
]
else:
req.generator = [
torch.Generator("cuda") for _ in range(req.num_outputs_per_prompt)
]
output_batch = generator._send_to_scheduler_and_wait_for_response([req])
noise_pred = output_batch.noise_pred
assert noise_pred is not None, "noise_pred should not be None in OutputBatch"
assert isinstance(noise_pred, torch.Tensor), "noise_pred should be a torch.Tensor"
assert (
noise_pred.device.type == "cuda"
), f"noise_pred should be on cuda, got {noise_pred.device}"
assert (
noise_pred.dtype == torch.bfloat16
), f"noise_pred should be bfloat16, got {noise_pred.dtype}"
print("✓ Successfully retrieved noise_pred from OutputBatch!")
print(f" noise_pred shape: {noise_pred.shape}")
print(f" noise_pred dtype: {noise_pred.dtype}")
print(f" noise_pred device: {noise_pred.device}")
latents = output_batch.output if output_batch.output is not None else req.latents
assert latents is not None, "latents should not be None"
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v"]))
@@ -0,0 +1,122 @@
"""Test for ComfyUIZImagePipeline with pass-through scheduler."""
import os
import sys
import pytest
import torch
from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams
from sglang.multimodal_gen.runtime.entrypoints.diffusion_generator import DiffGenerator
from sglang.multimodal_gen.runtime.entrypoints.utils import prepare_request
def test_comfyui_zimage_pipeline_direct() -> None:
"""Test ComfyUIZImagePipeline with custom inputs."""
model_path = os.environ.get(
"SGLANG_TEST_ZIMAGE_MODEL_PATH",
"Tongyi-MAI/Z-Image-Turbo", # Supports both safetensors file and diffusers format
)
generator = DiffGenerator.from_pretrained(
model_path=model_path,
pipeline_class_name="ComfyUIZImagePipeline",
num_gpus=1,
sp_degree=1,
comfyui_mode=True,
)
batch_size = 1
num_channels = 16
num_frames = 1
height = 720
width = 1280
latent_height = height // 8
latent_width = width // 8
latents = torch.ones(
batch_size,
num_channels,
num_frames,
latent_height,
latent_width,
device="cuda",
dtype=torch.bfloat16,
)
timesteps = torch.tensor([1000], dtype=torch.long, device="cuda")
context_seq_len = 19
context_dim = 2560
context = torch.ones(
context_seq_len,
context_dim,
device="cuda",
dtype=torch.bfloat16,
)
sampling_params = SamplingParams.from_user_sampling_params_args(
generator.server_args.model_path,
server_args=generator.server_args,
prompt="a beautiful girl",
guidance_scale=1.0,
height=height,
width=width,
num_frames=1,
num_inference_steps=1,
seed=42,
save_output=False,
return_frames=False,
)
req = prepare_request(
server_args=generator.server_args,
sampling_params=sampling_params,
)
req.latents = latents
req.timesteps = timesteps
req.prompt_embeds = [context]
req.negative_prompt_embeds = None
req.raw_latent_shape = torch.tensor(latents.shape, dtype=torch.long)
if req.guidance_scale > 1.0 and req.negative_prompt_embeds is not None:
req.do_classifier_free_guidance = True
else:
req.do_classifier_free_guidance = False
if req.seed is not None:
generator_device = req.generator_device
device_str = "cpu" if generator_device == "cpu" else "cuda"
req.generator = [
torch.Generator(device_str).manual_seed(req.seed + i)
for i in range(req.num_outputs_per_prompt)
]
else:
req.generator = [
torch.Generator("cuda") for _ in range(req.num_outputs_per_prompt)
]
output_batch = generator._send_to_scheduler_and_wait_for_response([req])
noise_pred = output_batch.noise_pred
assert noise_pred is not None, "noise_pred should not be None in OutputBatch"
assert isinstance(noise_pred, torch.Tensor), "noise_pred should be a torch.Tensor"
assert (
noise_pred.device.type == "cuda"
), f"noise_pred should be on cuda, got {noise_pred.device}"
assert (
noise_pred.dtype == torch.bfloat16
), f"noise_pred should be bfloat16, got {noise_pred.dtype}"
print("✓ Successfully retrieved noise_pred from OutputBatch!")
print(f" noise_pred shape: {noise_pred.shape}")
print(f" noise_pred dtype: {noise_pred.dtype}")
print(f" noise_pred device: {noise_pred.device}")
latents = output_batch.output if output_batch.output is not None else req.latents
assert latents is not None, "latents should not be None"
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v"]))
@@ -0,0 +1,176 @@
import base64
import io
import os
import shutil
import time
import uuid
import folder_paths
import numpy as np
import torch
from comfy_api.input import VideoInput
from PIL import Image
def _ensure_dir(path: str) -> None:
os.makedirs(path, exist_ok=True)
def _to_numpy_image(image: torch.Tensor) -> np.ndarray:
"""Convert ComfyUI image tensor to uint8 numpy array (H, W, C)."""
if image.dim() == 4:
image = image[0]
if image.dim() == 3 and image.shape[0] in (1, 3, 4):
image = image.permute(1, 2, 0)
elif image.dim() == 2:
image = image.unsqueeze(-1)
np_img = image.detach().cpu().numpy()
np_img = np.clip(np_img, 0.0, 1.0)
np_img = (np_img * 255).astype(np.uint8)
if np_img.shape[-1] == 1:
np_img = np.repeat(np_img, 3, axis=-1)
return np_img
def _to_hwc_tensor(image: torch.Tensor) -> torch.Tensor:
"""Convert ComfyUI image tensor to HWC format (normalized [0, 1])."""
img = image.clone()
if img.dim() == 4:
img = img[0]
if img.dim() == 3 and img.shape[0] in (1, 3, 4):
img = img.permute(1, 2, 0)
elif img.dim() == 2:
img = img.unsqueeze(-1)
img = torch.clamp(img, 0.0, 1.0)
if img.shape[-1] == 1:
img = img.repeat(1, 1, 3)
return img
def is_empty_image(image: torch.Tensor, tolerance: float = 1e-6) -> bool:
"""
Check if the input image is an empty/solid color image (like ComfyUI's empty image).
Args:
image: Input tensor image in ComfyUI format (BCHW, CHW, HWC, etc.)
tolerance: Tolerance for floating point comparison (default: 1e-6)
Returns:
True if the image is empty (all pixels have same color), False otherwise
"""
if image is None:
return True
# Convert to HWC format
img_hwc = _to_hwc_tensor(image)
# Get the first pixel's RGB values
first_pixel = img_hwc[0, 0, :]
h, w, c = img_hwc.shape
pixels = img_hwc.reshape(-1, c)
diff = torch.abs(pixels - first_pixel)
max_diff = torch.max(diff)
return max_diff.item() <= tolerance
def get_image_path(image: torch.Tensor) -> str:
"""
Save tensor image to ComfyUI temp directory as PNG and return the path.
"""
temp_dir = folder_paths.get_temp_directory()
# Build file name
ts = time.strftime("%Y%m%d-%H%M%S")
unique = uuid.uuid4().hex[:8]
file_name = f"sgl_output_{ts}_{unique}.png"
file_path = os.path.join(temp_dir, file_name)
# Save image
np_img = _to_numpy_image(image)
img = Image.fromarray(np_img)
img.save(file_path, format="PNG")
return file_path
def convert_b64_to_tensor_image(b64_image: str) -> torch.Tensor:
"""
Convert base64 encoded image to ComfyUI IMAGE format (torch.Tensor).
Args:
b64_image: Base64 encoded image string
Returns:
torch.Tensor with shape [batch_size, height, width, channels] (BHWC format),
values normalized to [0, 1] range, RGB format (3 channels)
"""
# Decode base64
image_bytes = base64.b64decode(b64_image)
# Open image and convert to RGB
pil_image = Image.open(io.BytesIO(image_bytes))
if pil_image.mode != "RGB":
pil_image = pil_image.convert("RGB")
# Convert to numpy array and normalize to [0, 1]
image_array = np.array(pil_image).astype(np.float32) / 255.0
# Add batch dimension: [height, width, channels] -> [1, height, width, channels]
image_array = image_array[np.newaxis, ...]
# Convert to torch.Tensor
tensor_image = torch.from_numpy(image_array)
return tensor_image
class SGLDVideoInput(VideoInput):
def __init__(self, video_path: str, height: int, width: int):
super().__init__()
self.video_path = video_path
self.height = height
self.width = width
def get_dimensions(self) -> tuple[int, int]:
"""
Returns the dimensions of the video input.
Returns:
Tuple of (width, height)
"""
return self.width, self.height
def get_components(self):
"""
Returns the components of the video input.
This is required by the VideoInput abstract base class.
"""
return [self.video_path]
def save_to(self, path: str, format=None, codec=None, metadata=None):
"""
Abstract method to save the video input to a file.
"""
save_path = path
# Copy video file from video_path to save_path
if os.path.exists(self.video_path):
# Ensure destination directory exists
save_dir = os.path.dirname(save_path)
if save_dir:
os.makedirs(save_dir, exist_ok=True)
shutil.copy2(self.video_path, save_path)
def convert_video_to_comfy_video(
video_path: str, height: int, width: int
) -> VideoInput:
"""
Convert video to ComfyUI VIDEO format (VideoInput).
"""
video_input = SGLDVideoInput(video_path, height, width)
return video_input
@@ -0,0 +1,222 @@
{
"8": {
"inputs": {
"samples": [
"40",
0
],
"vae": [
"10",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"10": {
"inputs": {
"vae_name": "ae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"11": {
"inputs": {
"clip_name1": "t5xxl_fp16.safetensors",
"clip_name2": "clip_l.safetensors",
"type": "flux",
"device": "default"
},
"class_type": "DualCLIPLoader",
"_meta": {
"title": "DualCLIPLoader"
}
},
"17": {
"inputs": {
"scheduler": "normal",
"steps": 25,
"denoise": 1,
"model": [
"46",
0
]
},
"class_type": "BasicScheduler",
"_meta": {
"title": "BasicScheduler"
}
},
"38": {
"inputs": {
"model": [
"46",
0
],
"conditioning": [
"42",
0
]
},
"class_type": "BasicGuider",
"_meta": {
"title": "BasicGuider"
}
},
"39": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"40": {
"inputs": {
"noise": [
"45",
0
],
"guider": [
"38",
0
],
"sampler": [
"47",
0
],
"sigmas": [
"17",
0
],
"latent_image": [
"44",
0
]
},
"class_type": "SamplerCustomAdvanced",
"_meta": {
"title": "SamplerCustomAdvanced"
}
},
"42": {
"inputs": {
"guidance": 3.5,
"conditioning": [
"43",
0
]
},
"class_type": "FluxGuidance",
"_meta": {
"title": "FluxGuidance"
}
},
"43": {
"inputs": {
"text": "beautiful photography of a gonger haired artist with Lots of Colorful coloursplashes in face and pn her hands, she is natural, having her hair in a casual bun, looking happily into camera, cinematic,",
"speak_and_recognation": {
"__value__": [
false,
true
]
},
"clip": [
"11",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"44": {
"inputs": {
"width": 1024,
"height": 1024,
"batch_size": 1
},
"class_type": "EmptySD3LatentImage",
"_meta": {
"title": "EmptySD3LatentImage"
}
},
"45": {
"inputs": {
"noise_seed": 747172083610812
},
"class_type": "RandomNoise",
"_meta": {
"title": "RandomNoise"
}
},
"46": {
"inputs": {
"max_shift": 1.15,
"base_shift": 0.5,
"width": 1024,
"height": 1024,
"model": [
"51",
0
]
},
"class_type": "ModelSamplingFlux",
"_meta": {
"title": "ModelSamplingFlux"
}
},
"47": {
"inputs": {
"sampler_name": "euler"
},
"class_type": "KSamplerSelect",
"_meta": {
"title": "KSamplerSelect"
}
},
"51": {
"inputs": {
"unet_name": "flux1-dev.safetensors",
"weight_dtype": "default",
"sgld_options": [
"52",
0
]
},
"class_type": "SGLDUNETLoader",
"_meta": {
"title": "SGLDiffusion UNET Loader"
}
},
"52": {
"inputs": {
"model_type": "auto-detect",
"enable_torch_compile": false,
"num_gpus": 2,
"tp_size": -1,
"sp_degree": -1,
"ulysses_degree": -1,
"ring_degree": -1,
"dp_size": 1,
"dp_degree": 1,
"enable_cfg_parallel": false,
"attention_backend": "",
"cache_strategy": "none"
},
"class_type": "SGLDOptions",
"_meta": {
"title": "SGLDiffusion Options"
}
}
}
@@ -0,0 +1,165 @@
{
"3": {
"inputs": {
"seed": 808633539418610,
"steps": 4,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "simple",
"denoise": 1,
"model": [
"66",
0
],
"positive": [
"6",
0
],
"negative": [
"7",
0
],
"latent_image": [
"58",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"6": {
"inputs": {
"text": "\"A vibrant, warm neon-lit street scene in Hong Kong at the afternoon, with a mix of colorful Chinese and English signs glowing brightly. The atmosphere is lively, cinematic, and rain-washed with reflections on the pavement. The colors are vivid, full of pink, blue, red, and green hues. Crowded buildings with overlapping neon signs. 1980s Hong Kong style. Signs include:\n\"龍鳳冰室\" \"金華燒臘\" \"HAPPY HAIR\" \"鴻運茶餐廳\" \"EASY BAR\" \"永發魚蛋粉\" \"添記粥麵\" \"SUNSHINE MOTEL\" \"美都餐室\" \"富記糖水\" \"太平館\" \"雅芳髮型屋\" \"STAR KTV\" \"銀河娛樂城\" \"百樂門舞廳\" \"BUBBLE CAFE\" \"萬豪麻雀館\" \"CITY LIGHTS BAR\" \"瑞祥香燭莊\" \"文記文具\" \"GOLDEN JADE HOTEL\" \"LOVELY BEAUTY\" \"合興百貨\" \"興旺電器\" And the background is warm yellow street and with all stores' lights on.",
"speak_and_recognation": {
"__value__": [
false,
true
]
},
"clip": [
"38",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Positive Prompt)"
}
},
"7": {
"inputs": {
"text": "",
"speak_and_recognation": {
"__value__": [
false,
true
]
},
"clip": [
"38",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Negative Prompt)"
}
},
"8": {
"inputs": {
"samples": [
"3",
0
],
"vae": [
"39",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"38": {
"inputs": {
"clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
"type": "qwen_image",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "Load CLIP"
}
},
"39": {
"inputs": {
"vae_name": "qwen_image_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"58": {
"inputs": {
"width": 1328,
"height": 1328,
"batch_size": 1
},
"class_type": "EmptySD3LatentImage",
"_meta": {
"title": "EmptySD3LatentImage"
}
},
"60": {
"inputs": {
"filename_prefix": "ComfyUI"
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"66": {
"inputs": {
"shift": 3.1000000000000005,
"model": [
"78",
0
]
},
"class_type": "ModelSamplingAuraFlow",
"_meta": {
"title": "ModelSamplingAuraFlow"
}
},
"77": {
"inputs": {
"unet_name": "qwen_image_2512_bf16.safetensors",
"weight_dtype": "default"
},
"class_type": "SGLDUNETLoader",
"_meta": {
"title": "SGLDiffusion UNET Loader"
}
},
"78": {
"inputs": {
"lora_name": "Qwen-Image-2512-Lightning-4steps-V1.0-bf16.safetensors",
"strength_model": 1,
"nickname": "",
"target": "all",
"model": [
"77",
0
]
},
"class_type": "SGLDLoraLoader",
"_meta": {
"title": "SGLDiffusion LoRA Loader"
}
}
}
@@ -0,0 +1,97 @@
{
"1": {
"inputs": {
"base_url": "http://localhost:3000/v1",
"api_key": "sk-proj-1234567890"
},
"class_type": "SGLDiffusionServerModel",
"_meta": {
"title": "SGLDiffusion Server Model"
}
},
"3": {
"inputs": {
"prompt": "The girl turn the body and spin around in place.",
"main": "none",
"lighting": "none",
"speak_and_recognation": {
"__value__": [
false,
true
]
}
},
"class_type": "easy prompt",
"_meta": {
"title": "Prompt"
}
},
"4": {
"inputs": {
"text": "",
"anything": [
"1",
1
]
},
"class_type": "easy showAnything",
"_meta": {
"title": "Show Any"
}
},
"15": {
"inputs": {
"positive_prompt": [
"3",
0
],
"negative_prompt": "",
"seed": 2435791308,
"steps": 50,
"cfg": 4,
"width": 704,
"height": 1280,
"num_frames": 16,
"fps": 16,
"seconds": 1,
"enable_teacache": false,
"sgld_client": [
"1",
0
],
"image": [
"17",
0
]
},
"class_type": "SGLDiffusionGenerateVideo",
"_meta": {
"title": "SGLDiffusion Generate Video"
}
},
"16": {
"inputs": {
"filename_prefix": "video/ComfyUI",
"format": "auto",
"codec": "auto",
"video-preview": "",
"video": [
"15",
0
]
},
"class_type": "SaveVideo",
"_meta": {
"title": "save video"
}
},
"17": {
"inputs": {
"image": "tmpe_w0bd_0.jpg"
},
"class_type": "LoadImage",
"_meta": {
"title": "load image"
}
}
}
@@ -0,0 +1,109 @@
{
"1": {
"inputs": {
"base_url": "http://localhost:3000/v1",
"api_key": "sk-proj-1234567890"
},
"class_type": "SGLDiffusionServerModel",
"_meta": {
"title": "SGLDiffusion Server Model"
}
},
"3": {
"inputs": {
"prompt": "a bicycle, illustration in the style of SMPL, thick black lines on a white background",
"main": "none",
"lighting": "none",
"speak_and_recognation": {
"__value__": [
false,
true
]
}
},
"class_type": "easy prompt",
"_meta": {
"title": "Prompt"
}
},
"4": {
"inputs": {
"text": "",
"anything": [
"1",
1
]
},
"class_type": "easy showAnything",
"_meta": {
"title": "Show Any"
}
},
"5": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"6",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "save image"
}
},
"6": {
"inputs": {
"positive_prompt": [
"3",
0
],
"negative_prompt": "",
"seed": 4215918563,
"steps": 50,
"cfg": 4,
"width": 512,
"height": 512,
"enable_teacache": false,
"sgld_client": [
"11",
0
],
"image": [
"14",
0
]
},
"class_type": "SGLDiffusionGenerateImage",
"_meta": {
"title": "SGLDiffusion Generate Image"
}
},
"11": {
"inputs": {
"lora_name": "dvyio/flux-lora-simple-illustration",
"lora_nickname": "",
"target": "all",
"sgld_client": [
"1",
0
]
},
"class_type": "SGLDiffusionSetLora",
"_meta": {
"title": "SGLDiffusion Set LoRA"
}
},
"14": {
"inputs": {
"width": 512,
"height": 512,
"batch_size": 1,
"color": 0
},
"class_type": "EmptyImage",
"_meta": {
"title": "empty image"
}
}
}
@@ -0,0 +1,140 @@
{
"3": {
"inputs": {
"seed": 3338398,
"steps": 9,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "simple",
"denoise": 1,
"model": [
"28",
0
],
"positive": [
"6",
0
],
"negative": [
"7",
0
],
"latent_image": [
"13",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"6": {
"inputs": {
"text": "cute anime style girl with massive fluffy fennec ears and a big fluffy tail blonde messy long hair blue eyes wearing a maid outfit with a long black gold leaf pattern dress and a white apron, it is a postcard held by a hand in front of a beautiful realistic city at sunset and there is cursive writing that says \"ZImage, Now in ComfyUI\"",
"speak_and_recognation": {
"__value__": [
false,
true
]
},
"clip": [
"18",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Positive Prompt)"
}
},
"7": {
"inputs": {
"text": "blurry ugly bad",
"speak_and_recognation": {
"__value__": [
false,
true
]
},
"clip": [
"18",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Negative Prompt)"
}
},
"8": {
"inputs": {
"samples": [
"3",
0
],
"vae": [
"17",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"9": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"13": {
"inputs": {
"width": 1024,
"height": 1024,
"batch_size": 1
},
"class_type": "EmptySD3LatentImage",
"_meta": {
"title": "EmptySD3LatentImage"
}
},
"17": {
"inputs": {
"vae_name": "ae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "VAE Loader"
}
},
"18": {
"inputs": {
"clip_name": "qwen_3_4b.safetensors",
"type": "lumina2",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "CLIP Loader"
}
},
"28": {
"inputs": {
"unet_name": "z_image_turbo_bf16.safetensors",
"weight_dtype": "default"
},
"class_type": "SGLDUNETLoader",
"_meta": {
"title": "SGLDiffusion UNET Loader"
}
}
}