mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-02-10 21:42:37 +08:00
Add RunPod Serverless support - Dockerfile, handler, and environment template
This commit is contained in:
parent
4140263be5
commit
2c77183f25
143
ComfyUI-master/.env.example
Normal file
143
ComfyUI-master/.env.example
Normal file
@ -0,0 +1,143 @@
|
||||
# RunPod Configuration
|
||||
RUNPOD_API_KEY=your_runpod_api_key_here
|
||||
COMFYUI_ENDPOINT_ID=your_endpoint_id_here
|
||||
|
||||
# Model Repository APIs
|
||||
CIVITAI_API_KEY=your_civitai_api_key_here
|
||||
HUGGINGFACE_USERNAME=your_huggingface_username_here
|
||||
HUGGINGFACE_PASSWORD=your_huggingface_password_here
|
||||
|
||||
# New API Services (from latest ComfyUI update)
|
||||
# Minimax API
|
||||
MINIMAX_API_KEY=your_minimax_api_key_here
|
||||
MINIMAX_GROUP_ID=your_minimax_group_id_here
|
||||
|
||||
# ByteDance API
|
||||
BYTEDANCE_API_KEY=your_bytedance_api_key_here
|
||||
|
||||
# Ideogram API
|
||||
IDEOGRAM_API_KEY=your_ideogram_api_key_here
|
||||
|
||||
# Kling API
|
||||
KLING_API_KEY=your_kling_api_key_here
|
||||
|
||||
# Luma API
|
||||
LUMA_API_KEY=your_luma_api_key_here
|
||||
|
||||
# OpenAI API
|
||||
OPENAI_API_KEY=your_openai_api_key_here
|
||||
|
||||
# Pixverse API
|
||||
PIXVERSE_API_KEY=your_pixverse_api_key_here
|
||||
|
||||
# Recraft API
|
||||
RECRAFT_API_KEY=your_recraft_api_key_here
|
||||
|
||||
# Runway API
|
||||
RUNWAY_API_KEY=your_runway_api_key_here
|
||||
|
||||
# Vidu API
|
||||
VIDU_API_KEY=your_vidu_api_key_here
|
||||
|
||||
# RunPod GPU Performance Settings
|
||||
# Optimized for high-end GPUs (RTX 4090, A100, H100)
|
||||
COMFYUI_FAST_MODE=true
|
||||
COMFYUI_FP16_ACCUMULATION=true
|
||||
COMFYUI_FP8_OPTIMIZATIONS=true
|
||||
COMFYUI_PINNED_MEMORY=true
|
||||
COMFYUI_CUBLAS_OPS=true
|
||||
COMFYUI_AUTOTUNE=true
|
||||
|
||||
# Cache Settings - Serverless Optimized
|
||||
COMFYUI_CACHE_TYPE=none # No cache for serverless (fresh start each time)
|
||||
COMFYUI_CACHE_LRU_SIZE=0 # Disable LRU cache for serverless
|
||||
COMFYUI_CACHE_RAM_THRESHOLD=2.0 # Lower threshold for quick startup
|
||||
|
||||
# Memory Management - Serverless Optimized
|
||||
COMFYUI_VRAM_MANAGEMENT=auto # Auto management for variable workloads
|
||||
COMFYUI_ASYNC_OFFLOAD=false # Disable for faster response times
|
||||
COMFYUI_NUM_STREAMS=2 # Fewer streams for faster startup
|
||||
|
||||
# Model Loading - GPU Optimized
|
||||
COMFYUI_DISABLE_XFORMERS=false # Keep xformers for GPU acceleration
|
||||
COMFYUI_USE_SPLIT_CROSS_ATTENTION=false # Not needed with good GPU
|
||||
COMFYUI_ATTENTION_IMPLEMENTATION=xformers # Force xformers for GPU
|
||||
COMFYUI_USE_SAGE_ATTENTION=true # New sage attention for better performance
|
||||
|
||||
# Quantization Settings - GPU Optimized
|
||||
COMFYUI_MIXED_PRECISION=true
|
||||
COMFYUI_SCALED_FP8=true
|
||||
COMFYUI_FORCE_FP16=true # Force FP16 for GPU speed
|
||||
|
||||
# GPU Specific Settings
|
||||
CUDA_VISIBLE_DEVICES=0 # Use first GPU
|
||||
PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:512,expandable_segments:True
|
||||
COMFYUI_FORCE_CUDA=true
|
||||
|
||||
# Development and Debug
|
||||
COMFYUI_VERBOSE=false
|
||||
COMFYUI_DEBUG_MODE=false
|
||||
COMFYUI_LOG_LEVEL=INFO
|
||||
|
||||
# RunPod Serverless Worker Settings
|
||||
RUNPOD_WORKER_ID=${RUNPOD_WORKER_ID}
|
||||
RUNPOD_JOB_ID=${RUNPOD_JOB_ID}
|
||||
RUNPOD_REQUEST_ID=${RUNPOD_REQUEST_ID}
|
||||
|
||||
# Serverless Worker Performance
|
||||
COMFYUI_LISTEN=0.0.0.0
|
||||
COMFYUI_PORT=8000 # Standard serverless port
|
||||
COMFYUI_ENABLE_CORS_HEADER=*
|
||||
COMFYUI_SERVERLESS_MODE=true # Enable serverless optimizations
|
||||
|
||||
# GPU Memory Optimization for RunPod
|
||||
COMFYUI_NORMALVAE_TILED=true # Use tiled VAE for memory efficiency
|
||||
COMFYUI_DISABLE_SMART_MEMORY=false # Keep smart memory management
|
||||
COMFYUI_FORCE_UPCAST_ATTENTION=false # Don't upcast on good GPUs
|
||||
|
||||
# Model Storage Paths (Serverless optimized)
|
||||
COMFYUI_MODEL_PATH=/runpod-volume/models # Network storage for serverless
|
||||
COMFYUI_OUTPUT_PATH=/tmp/outputs # Temporary output for serverless
|
||||
COMFYUI_INPUT_PATH=/tmp/inputs # Temporary input for serverless
|
||||
COMFYUI_TEMP_PATH=/tmp/comfyui
|
||||
|
||||
# Serverless Processing Optimization
|
||||
COMFYUI_BATCH_SIZE=1 # Single request processing for serverless
|
||||
COMFYUI_MAX_BATCH_AREA=2097152 # 1024x1024 max area for faster processing
|
||||
COMFYUI_TIMEOUT=300 # 5 minute timeout for serverless jobs
|
||||
|
||||
# Serverless Network Settings
|
||||
COMFYUI_EXTRA_MODEL_PATHS_CONFIG=/runpod-volume/extra_model_paths.yaml
|
||||
COMFYUI_AUTO_LAUNCH_BROWSER=false
|
||||
COMFYUI_DISABLE_METADATA=false # Keep metadata for debugging
|
||||
COMFYUI_PREVIEW_METHOD=auto # Auto preview method
|
||||
|
||||
# RunPod Serverless Specific Settings
|
||||
RUNPOD_SERVERLESS=true
|
||||
RUNPOD_WEBHOOK_GET_JOB_URL=${RUNPOD_WEBHOOK_GET_JOB_URL}
|
||||
RUNPOD_WEBHOOK_POST_OUTPUT_URL=${RUNPOD_WEBHOOK_POST_OUTPUT_URL}
|
||||
|
||||
# Worker Startup Optimization
|
||||
COMFYUI_PRELOAD_MODELS=true # Preload common models for faster startup
|
||||
COMFYUI_WARMUP_ENABLED=true # Enable warmup requests
|
||||
COMFYUI_STARTUP_TIMEOUT=120 # 2 minute startup timeout
|
||||
|
||||
# Request Handling
|
||||
COMFYUI_MAX_CONCURRENT_REQUESTS=1 # Single request at a time for serverless
|
||||
COMFYUI_REQUEST_TIMEOUT=300 # 5 minute request timeout
|
||||
COMFYUI_CLEANUP_AFTER_REQUEST=true # Clean up after each request
|
||||
|
||||
# Model Management for Serverless
|
||||
COMFYUI_MODEL_CACHE_SIZE=2 # Cache only 2 models max
|
||||
COMFYUI_UNLOAD_MODELS_AFTER_USE=true # Unload models to free VRAM
|
||||
COMFYUI_MODEL_DOWNLOAD_TIMEOUT=600 # 10 minute model download timeout
|
||||
|
||||
# Output Handling
|
||||
COMFYUI_COMPRESS_OUTPUT=true # Compress outputs for faster upload
|
||||
COMFYUI_OUTPUT_FORMAT=png # Default output format
|
||||
COMFYUI_CLEANUP_TEMP_FILES=true # Clean temp files after request
|
||||
|
||||
# Logging for Serverless
|
||||
COMFYUI_LOG_REQUESTS=true # Log all requests for debugging
|
||||
COMFYUI_LOG_PERFORMANCE=true # Log performance metrics
|
||||
COMFYUI_STRUCTURED_LOGGING=true # Use structured logging format
|
||||
89
ComfyUI-master/Dockerfile
Normal file
89
ComfyUI-master/Dockerfile
Normal file
@ -0,0 +1,89 @@
|
||||
# RunPod Serverless ComfyUI Worker
|
||||
FROM runpod/pytorch:2.2.0-py3.11-cuda12.1.1-devel-ubuntu22.04
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /workspace
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
wget \
|
||||
curl \
|
||||
unzip \
|
||||
ffmpeg \
|
||||
libsm6 \
|
||||
libxext6 \
|
||||
libxrender-dev \
|
||||
libglib2.0-0 \
|
||||
libgl1-mesa-glx \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies
|
||||
RUN pip install --no-cache-dir \
|
||||
runpod \
|
||||
requests \
|
||||
pillow \
|
||||
numpy \
|
||||
torch \
|
||||
torchvision \
|
||||
torchaudio \
|
||||
xformers \
|
||||
accelerate \
|
||||
transformers \
|
||||
diffusers \
|
||||
opencv-python \
|
||||
scipy \
|
||||
scikit-image
|
||||
|
||||
# Copy ComfyUI source code
|
||||
COPY . /workspace/ComfyUI
|
||||
|
||||
# Set ComfyUI as working directory
|
||||
WORKDIR /workspace/ComfyUI
|
||||
|
||||
# Install ComfyUI requirements
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Install additional dependencies for new features
|
||||
RUN pip install --no-cache-dir \
|
||||
safetensors \
|
||||
transformers[torch] \
|
||||
accelerate \
|
||||
bitsandbytes \
|
||||
optimum
|
||||
|
||||
# Copy environment example (users should provide their own .env)
|
||||
COPY .env.example /workspace/.env.example
|
||||
|
||||
# Create necessary directories
|
||||
RUN mkdir -p /workspace/ComfyUI/models/checkpoints \
|
||||
/workspace/ComfyUI/models/vae \
|
||||
/workspace/ComfyUI/models/loras \
|
||||
/workspace/ComfyUI/models/controlnet \
|
||||
/workspace/ComfyUI/models/clip_vision \
|
||||
/workspace/ComfyUI/models/upscale_models \
|
||||
/workspace/ComfyUI/input \
|
||||
/workspace/ComfyUI/output \
|
||||
/tmp/inputs \
|
||||
/tmp/outputs \
|
||||
/tmp/comfyui
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONPATH="/workspace/ComfyUI:${PYTHONPATH}"
|
||||
ENV COMFYUI_SERVERLESS=true
|
||||
ENV NVIDIA_VISIBLE_DEVICES=all
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||
|
||||
# Create startup script
|
||||
RUN echo '#!/bin/bash\n\
|
||||
cd /workspace/ComfyUI\n\
|
||||
python main.py --listen 0.0.0.0 --port 8000 --dont-print-server --disable-auto-launch &\n\
|
||||
sleep 10\n\
|
||||
cd /workspace/ComfyUI\n\
|
||||
python runpod_handler.py' > /workspace/start.sh && chmod +x /workspace/start.sh
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Set the command
|
||||
CMD ["/workspace/start.sh"]
|
||||
245
ComfyUI-master/runpod_handler.py
Normal file
245
ComfyUI-master/runpod_handler.py
Normal file
@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
RunPod Serverless Worker Handler for ComfyUI
|
||||
Optimized for the new ComfyUI features and performance improvements
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
import tempfile
|
||||
import requests
|
||||
from typing import Dict, Any, Optional
|
||||
import runpod
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ComfyUIServerlessHandler:
|
||||
def __init__(self):
|
||||
self.comfyui_url = "http://127.0.0.1:8000"
|
||||
self.client_id = "runpod_serverless_worker"
|
||||
self.setup_paths()
|
||||
|
||||
def setup_paths(self):
|
||||
"""Setup required paths for serverless operation"""
|
||||
os.makedirs("/tmp/inputs", exist_ok=True)
|
||||
os.makedirs("/tmp/outputs", exist_ok=True)
|
||||
os.makedirs("/tmp/comfyui", exist_ok=True)
|
||||
|
||||
def wait_for_comfyui(self, timeout: int = 120) -> bool:
|
||||
"""Wait for ComfyUI to be ready"""
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < timeout:
|
||||
try:
|
||||
response = requests.get(f"{self.comfyui_url}/system_stats")
|
||||
if response.status_code == 200:
|
||||
logger.info("ComfyUI is ready")
|
||||
return True
|
||||
except requests.exceptions.RequestException:
|
||||
pass
|
||||
time.sleep(2)
|
||||
|
||||
logger.error(f"ComfyUI not ready after {timeout} seconds")
|
||||
return False
|
||||
|
||||
def download_input_files(self, input_data: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""Download input files and return local paths"""
|
||||
local_files = {}
|
||||
|
||||
if "input_files" in input_data:
|
||||
for file_key, file_url in input_data["input_files"].items():
|
||||
try:
|
||||
response = requests.get(file_url, timeout=60)
|
||||
response.raise_for_status()
|
||||
|
||||
# Create temporary file
|
||||
with tempfile.NamedTemporaryFile(
|
||||
delete=False,
|
||||
dir="/tmp/inputs",
|
||||
suffix=os.path.splitext(file_url)[1]
|
||||
) as tmp_file:
|
||||
tmp_file.write(response.content)
|
||||
local_files[file_key] = tmp_file.name
|
||||
|
||||
logger.info(f"Downloaded {file_key} to {local_files[file_key]}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to download {file_key}: {str(e)}")
|
||||
raise
|
||||
|
||||
return local_files
|
||||
|
||||
def execute_workflow(self, workflow: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Execute ComfyUI workflow"""
|
||||
try:
|
||||
# Queue the workflow
|
||||
queue_response = requests.post(
|
||||
f"{self.comfyui_url}/prompt",
|
||||
json={
|
||||
"prompt": workflow,
|
||||
"client_id": self.client_id
|
||||
},
|
||||
timeout=30
|
||||
)
|
||||
queue_response.raise_for_status()
|
||||
|
||||
prompt_id = queue_response.json()["prompt_id"]
|
||||
logger.info(f"Queued workflow with prompt_id: {prompt_id}")
|
||||
|
||||
# Wait for completion
|
||||
return self.wait_for_completion(prompt_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to execute workflow: {str(e)}")
|
||||
raise
|
||||
|
||||
def wait_for_completion(self, prompt_id: str, timeout: int = 300) -> Dict[str, Any]:
|
||||
"""Wait for workflow completion and return results"""
|
||||
start_time = time.time()
|
||||
|
||||
while time.time() - start_time < timeout:
|
||||
try:
|
||||
# Check queue status
|
||||
queue_response = requests.get(f"{self.comfyui_url}/queue")
|
||||
queue_data = queue_response.json()
|
||||
|
||||
# Check if our job is still in queue
|
||||
running = any(item[1]["prompt_id"] == prompt_id for item in queue_data.get("queue_running", []))
|
||||
pending = any(item[1]["prompt_id"] == prompt_id for item in queue_data.get("queue_pending", []))
|
||||
|
||||
if not running and not pending:
|
||||
# Job completed, get results
|
||||
history_response = requests.get(f"{self.comfyui_url}/history/{prompt_id}")
|
||||
if history_response.status_code == 200:
|
||||
history_data = history_response.json()
|
||||
if prompt_id in history_data:
|
||||
return self.process_results(history_data[prompt_id])
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking completion: {str(e)}")
|
||||
time.sleep(5)
|
||||
|
||||
raise TimeoutError(f"Workflow {prompt_id} timed out after {timeout} seconds")
|
||||
|
||||
def process_results(self, history_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Process and upload results"""
|
||||
results = {
|
||||
"status": "completed",
|
||||
"outputs": [],
|
||||
"metadata": {}
|
||||
}
|
||||
|
||||
if "outputs" in history_data:
|
||||
for node_id, node_output in history_data["outputs"].items():
|
||||
if "images" in node_output:
|
||||
for image_info in node_output["images"]:
|
||||
# Download image from ComfyUI
|
||||
image_url = f"{self.comfyui_url}/view"
|
||||
params = {
|
||||
"filename": image_info["filename"],
|
||||
"subfolder": image_info.get("subfolder", ""),
|
||||
"type": image_info.get("type", "output")
|
||||
}
|
||||
|
||||
try:
|
||||
image_response = requests.get(image_url, params=params)
|
||||
image_response.raise_for_status()
|
||||
|
||||
# Save to temp file for upload
|
||||
output_path = f"/tmp/outputs/{image_info['filename']}"
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(image_response.content)
|
||||
|
||||
results["outputs"].append({
|
||||
"type": "image",
|
||||
"filename": image_info["filename"],
|
||||
"path": output_path,
|
||||
"node_id": node_id
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to process image {image_info['filename']}: {str(e)}")
|
||||
|
||||
return results
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up temporary files"""
|
||||
try:
|
||||
import shutil
|
||||
shutil.rmtree("/tmp/inputs", ignore_errors=True)
|
||||
shutil.rmtree("/tmp/outputs", ignore_errors=True)
|
||||
os.makedirs("/tmp/inputs", exist_ok=True)
|
||||
os.makedirs("/tmp/outputs", exist_ok=True)
|
||||
logger.info("Cleaned up temporary files")
|
||||
except Exception as e:
|
||||
logger.error(f"Cleanup failed: {str(e)}")
|
||||
|
||||
def handler(job: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Main serverless handler function"""
|
||||
handler_instance = ComfyUIServerlessHandler()
|
||||
|
||||
try:
|
||||
# Wait for ComfyUI to be ready
|
||||
if not handler_instance.wait_for_comfyui():
|
||||
return {"error": "ComfyUI failed to start"}
|
||||
|
||||
# Get job input
|
||||
job_input = job.get("input", {})
|
||||
|
||||
# Download input files if any
|
||||
local_files = handler_instance.download_input_files(job_input)
|
||||
|
||||
# Update workflow with local file paths
|
||||
workflow = job_input.get("workflow", {})
|
||||
if local_files and "file_mappings" in job_input:
|
||||
for node_id, mappings in job_input["file_mappings"].items():
|
||||
if node_id in workflow:
|
||||
for input_key, file_key in mappings.items():
|
||||
if file_key in local_files:
|
||||
workflow[node_id]["inputs"][input_key] = local_files[file_key]
|
||||
|
||||
# Execute workflow
|
||||
results = handler_instance.execute_workflow(workflow)
|
||||
|
||||
# Upload output files to RunPod storage or return base64
|
||||
output_urls = []
|
||||
for output in results.get("outputs", []):
|
||||
if output["type"] == "image":
|
||||
# For serverless, we typically return base64 or upload to storage
|
||||
with open(output["path"], "rb") as f:
|
||||
import base64
|
||||
image_data = base64.b64encode(f.read()).decode()
|
||||
output_urls.append({
|
||||
"filename": output["filename"],
|
||||
"data": image_data,
|
||||
"node_id": output["node_id"]
|
||||
})
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"outputs": output_urls,
|
||||
"execution_time": time.time() - job.get("start_time", time.time())
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Handler error: {str(e)}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"status": "failed"
|
||||
}
|
||||
|
||||
finally:
|
||||
# Always cleanup
|
||||
handler_instance.cleanup()
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Start the serverless worker
|
||||
runpod.serverless.start({"handler": handler})
|
||||
Loading…
Reference in New Issue
Block a user