Merge pull request #122 from pollockjj/wvw

Pull Request: Refresh WanVideoWrapper MultiGPU support to latest upstream
This commit is contained in:
John Pollock
2025-10-10 15:21:11 -05:00
committed by GitHub
21 changed files with 3633 additions and 4218 deletions
+2 -1
View File
@@ -2,4 +2,5 @@
__pycache__/
.clinerules
.vscode
memory-bank/
memory-bank/
.github/
+132 -27
View File
@@ -3,6 +3,8 @@ import logging
import weakref
import os
import copy
import json
from datetime import datetime
from pathlib import Path
import folder_paths
import comfy.model_management as mm
@@ -21,12 +23,22 @@ from .model_management_mgpu import (
)
WEB_DIRECTORY = "./web"
MGPU_MM_LOG = False
MGPU_MM_LOG = True
DEBUG_LOG = False
logger = logging.getLogger("MultiGPU")
logger.propagate = False
FOCUS_LOG_LEVEL = logging.INFO + 5
logging.addLevelName(FOCUS_LOG_LEVEL, "FOCUS")
if not hasattr(logging.Logger, "focus"):
def focus(self, message, *args, **kwargs):
if self.isEnabledFor(FOCUS_LOG_LEVEL):
self._log(FOCUS_LOG_LEVEL, message, args, **kwargs)
logging.Logger.focus = focus # type: ignore[attr-defined]
if not logger.handlers:
log_level = logging.DEBUG if DEBUG_LOG else logging.INFO
handler = logging.StreamHandler()
@@ -35,10 +47,93 @@ if not logger.handlers:
logger.addHandler(handler)
logger.setLevel(log_level)
json_log_path = os.environ.get("MGPU_JSON_LOG_PATH")
json_static_fields = {}
if json_log_path:
try:
json_static_fields = json.loads(os.environ.get("MGPU_JSON_STATIC_FIELDS", "{}"))
except json.JSONDecodeError:
json_static_fields = {}
level_aliases = {
"CRITICAL": logging.CRITICAL,
"ERROR": logging.ERROR,
"WARNING": logging.WARNING,
"FOCUS": FOCUS_LOG_LEVEL,
"INFO": logging.INFO,
"DEBUG": logging.DEBUG,
}
json_min_level = FOCUS_LOG_LEVEL
configured_min_level = os.environ.get("MGPU_JSON_MIN_LEVEL")
if configured_min_level:
value = configured_min_level.strip()
upper_value = value.upper()
if upper_value in level_aliases:
json_min_level = level_aliases[upper_value]
else:
try:
json_min_level = int(value)
except ValueError:
json_min_level = FOCUS_LOG_LEVEL
class JsonLineFileHandler(logging.Handler):
def __init__(self, path, static_fields, min_level, overwrite):
super().__init__()
self.path = Path(path)
self.path.parent.mkdir(parents=True, exist_ok=True)
self.static_fields = static_fields
self.setLevel(min_level)
if overwrite:
try:
with self.path.open("w", encoding="utf-8") as handle:
handle.write("")
except OSError:
pass
def emit(self, record):
message = record.getMessage()
category = None
if message.startswith("[") and "]" in message:
bracket_split = message.split("]", 1)
category = bracket_split[0].strip("[]")
payload = {
"timestamp": datetime.utcnow().isoformat() + "Z",
"level": record.levelname,
"name": record.name,
"message": message,
}
if category:
payload["event_category"] = category
if hasattr(record, "mgpu_context") and isinstance(record.mgpu_context, dict):
payload.update(record.mgpu_context)
workflow_id = os.environ.get("MGPU_JSON_WORKFLOW")
prompt_id = os.environ.get("MGPU_JSON_PROMPT")
if workflow_id:
payload.setdefault("workflow_id", workflow_id)
if prompt_id:
payload.setdefault("prompt_id", prompt_id)
if self.static_fields:
payload.update(self.static_fields)
try:
with self.path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(payload, ensure_ascii=True) + "\n")
except OSError:
# Fail silently for JSON logging so primary logging continues.
pass
overwrite_value = os.environ.get("MGPU_JSON_OVERWRITE", "true").strip().lower()
overwrite_enabled = overwrite_value not in {"0", "false", "no"}
logger.addHandler(JsonLineFileHandler(json_log_path, json_static_fields, json_min_level, overwrite_enabled))
def mgpu_mm_log_method(self, msg):
"""Add MultiGPU model management logging method to logger instance."""
if MGPU_MM_LOG:
self.info(f"[MultiGPU Model Management] {msg}")
self.focus(
f"[MultiGPU Model Management] {msg}",
extra={"mgpu_context": {"component": "model_management"}},
)
logger.mgpu_mm_log = mgpu_mm_log_method.__get__(logger, type(logger))
def check_module_exists(module_path):
@@ -95,8 +190,6 @@ mm.get_torch_device = get_torch_device_patched
mm.text_encoder_device = text_encoder_device_patched
from .nodes import (
DeviceSelectorMultiGPU,
HunyuanVideoEmbeddingsAdapter,
UnetLoaderGGUF,
UnetLoaderGGUFAdvanced,
CLIPLoaderGGUF,
@@ -114,21 +207,30 @@ from .nodes import (
PulidModelLoader,
PulidInsightFaceLoader,
PulidEvaClipLoader,
HyVideoModelLoader,
HyVideoVAELoader,
DownloadAndLoadHyVideoTextEncoder,
UNetLoaderLP,
)
from .wanvideo import (
WanVideoModelLoader,
WanVideoModelLoader_2,
WanVideoVAELoader,
LoadWanVideoT5TextEncoder,
LoadWanVideoClipTextEncoder,
WanVideoTextEncode,
WanVideoTextEncodeCached,
WanVideoTextEncodeSingle,
WanVideoVAELoader,
WanVideoTinyVAELoader,
WanVideoBlockSwap,
WanVideoSampler
WanVideoImageToVideoEncode,
WanVideoDecode,
WanVideoModelLoader,
WanVideoSampler,
WanVideoVACEEncode,
WanVideoEncode,
LoadWanVideoClipTextEncoder,
WanVideoClipVisionEncode,
WanVideoControlnetLoader,
FantasyTalkingModelLoader,
Wav2VecModelLoader,
WanVideoUni3C_ControlnetLoader,
DownloadAndLoadWav2VecModel,
)
from .wrappers import (
@@ -158,8 +260,6 @@ from .checkpoint_multigpu import (
)
NODE_CLASS_MAPPINGS = {
"DeviceSelectorMultiGPU": DeviceSelectorMultiGPU,
"HunyuanVideoEmbeddingsAdapter": HunyuanVideoEmbeddingsAdapter,
"CheckpointLoaderAdvancedMultiGPU": CheckpointLoaderAdvancedMultiGPU,
"CheckpointLoaderAdvancedDisTorch2MultiGPU": CheckpointLoaderAdvancedDisTorch2MultiGPU,
"UNetLoaderLP": UNetLoaderLP,
@@ -266,22 +366,27 @@ pulid_nodes = {
}
register_and_count(["PuLID_ComfyUI", "pulid_comfyui"], pulid_nodes)
hunyuan_nodes = {
"HyVideoModelLoaderMultiGPU": override_class(HyVideoModelLoader),
"HyVideoVAELoaderMultiGPU": override_class(HyVideoVAELoader),
"DownloadAndLoadHyVideoTextEncoderMultiGPU": override_class(DownloadAndLoadHyVideoTextEncoder)
}
register_and_count(["ComfyUI-HunyuanVideoWrapper", "comfyui-hunyuanvideowrapper"], hunyuan_nodes)
wanvideo_nodes = {
"WanVideoModelLoaderMultiGPU": WanVideoModelLoader,
"WanVideoModelLoaderMultiGPU_2": WanVideoModelLoader_2,
"WanVideoVAELoaderMultiGPU": WanVideoVAELoader,
"LoadWanVideoT5TextEncoderMultiGPU": LoadWanVideoT5TextEncoder,
"LoadWanVideoClipTextEncoderMultiGPU": LoadWanVideoClipTextEncoder,
"WanVideoTextEncodeMultiGPU": WanVideoTextEncode,
"WanVideoTextEncodeCachedMultiGPU": WanVideoTextEncodeCached,
"WanVideoTextEncodeSingleMultiGPU": WanVideoTextEncodeSingle,
"WanVideoVAELoaderMultiGPU": WanVideoVAELoader,
"WanVideoTinyVAELoaderMultiGPU": WanVideoTinyVAELoader,
"WanVideoBlockSwapMultiGPU": WanVideoBlockSwap,
"WanVideoSamplerMultiGPU": WanVideoSampler
"WanVideoImageToVideoEncodeMultiGPU": WanVideoImageToVideoEncode,
"WanVideoDecodeMultiGPU": WanVideoDecode,
"WanVideoModelLoaderMultiGPU": WanVideoModelLoader,
"WanVideoSamplerMultiGPU": WanVideoSampler,
"WanVideoVACEEncodeMultiGPU": WanVideoVACEEncode,
"WanVideoEncodeMultiGPU": WanVideoEncode,
"LoadWanVideoClipTextEncoderMultiGPU": LoadWanVideoClipTextEncoder,
"WanVideoClipVisionEncodeMultiGPU": WanVideoClipVisionEncode,
"WanVideoControlnetLoaderMultiGPU": WanVideoControlnetLoader,
"FantasyTalkingModelLoaderMultiGPU": FantasyTalkingModelLoader,
"Wav2VecModelLoaderMultiGPU": Wav2VecModelLoader,
"WanVideoUni3C_ControlnetLoaderMultiGPU": WanVideoUni3C_ControlnetLoader,
"DownloadAndLoadWav2VecModelMultiGPU": DownloadAndLoadWav2VecModel,
}
register_and_count(["ComfyUI-WanVideoWrapper", "comfyui-wanvideowrapper"], wanvideo_nodes)
@@ -289,4 +394,4 @@ for item in registration_data:
logger.info(fmt_reg.format(item['name'], item['found'], str(item['count'])))
logger.info(dash_line)
logger.info(f"[MultiGPU] Registration complete. Final mappings: {', '.join(NODE_CLASS_MAPPINGS.keys())}")
logger.info(f"[MultiGPU] Registration complete. Final mappings: {', '.join(NODE_CLASS_MAPPINGS.keys())}")
Binary file not shown.

After

Width:  |  Height:  |  Size: 738 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 623 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""Filter MultiGPU JSON logs for allocation summaries."""
import argparse
import json
from pathlib import Path
from typing import Iterable, Iterator, Dict, Any
def load_json_lines(path: Path) -> Iterator[Dict[str, Any]]:
with path.open("r", encoding="utf-8") as handle:
for line in handle:
line = line.strip()
if not line:
continue
try:
yield json.loads(line)
except json.JSONDecodeError:
continue
def is_allocation_event(entry: Dict[str, Any], keywords: Iterable[str]) -> bool:
message = entry.get("message", "")
return any(keyword in message for keyword in keywords)
def main() -> int:
parser = argparse.ArgumentParser(description="Extract allocation-related events from MultiGPU JSON logs")
parser.add_argument("logfile", type=Path, help="Path to JSONL log produced by MGPU_JSON_LOG_PATH")
parser.add_argument(
"--keywords",
nargs="*",
default=["Final Allocation String", "Total memory", "Virtual VRAM"],
help="Keywords that mark allocation events",
)
args = parser.parse_args()
entries = list(load_json_lines(args.logfile))
if not entries:
print("No entries found in log file.")
return 0
matched = [entry for entry in entries if is_allocation_event(entry, args.keywords)]
if not matched:
print("No allocation events matched provided keywords.")
return 0
for entry in matched:
timestamp = entry.get("timestamp", "unknown")
category = entry.get("event_category", "")
component = entry.get("component", "")
header_bits = [bit for bit in (timestamp, category, component) if bit]
header = " | ".join(header_bits) if header_bits else "allocation"
print(f"## {header}")
print(entry.get("message", ""))
print()
return 0
if __name__ == "__main__":
raise SystemExit(main())
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env python3
"""Minimal ComfyUI workflow runner for CI smoke tests."""
import argparse
import json
import os
import sys
import time
import uuid
from pathlib import Path
from typing import Iterable, Optional
import requests
import websocket
DEFAULT_HOST = os.environ.get("COMFYUI_HOST", "127.0.0.1")
DEFAULT_PORT = int(os.environ.get("COMFYUI_PORT", "8188"))
DEFAULT_CONNECT_TIMEOUT = int(os.environ.get("COMFYUI_CONNECT_TIMEOUT", "60"))
DEFAULT_WORKFLOW_TIMEOUT = int(os.environ.get("COMFYUI_WORKFLOW_TIMEOUT", "900"))
class ComfyWorkflowRunner:
def __init__(self, host: str, port: int, connect_timeout: int, workflow_timeout: int, secure: bool = False) -> None:
self.host = host
self.port = port
protocol_http = "https" if secure else "http"
protocol_ws = "wss" if secure else "ws"
self.base_http = f"{protocol_http}://{host}:{port}"
self.base_ws = f"{protocol_ws}://{host}:{port}/ws"
self.connect_timeout = connect_timeout
self.workflow_timeout = workflow_timeout
self.client_id = str(uuid.uuid4())
self.session = requests.Session()
self.websocket: Optional[websocket.WebSocket] = None
def wait_for_server(self) -> None:
deadline = time.monotonic() + self.connect_timeout
while time.monotonic() < deadline:
try:
response = self.session.get(f"{self.base_http}/system_stats", timeout=5)
if response.status_code == 200:
return
except requests.RequestException:
time.sleep(1)
raise TimeoutError(f"ComfyUI server not reachable at {self.base_http}")
def open_websocket(self) -> None:
ws = websocket.WebSocket()
ws.settimeout(5)
ws.connect(f"{self.base_ws}?clientId={self.client_id}")
self.websocket = ws
def close_websocket(self) -> None:
if self.websocket:
try:
self.websocket.close()
finally:
self.websocket = None
def queue_prompt(self, prompt: dict) -> str:
payload = {"prompt": prompt, "client_id": self.client_id}
response = self.session.post(f"{self.base_http}/prompt", json=payload, timeout=15)
response.raise_for_status()
data = response.json()
prompt_id = data.get("prompt_id")
if not prompt_id:
raise RuntimeError("No prompt_id returned from ComfyUI")
return prompt_id
def wait_for_completion(self, prompt_id: str) -> bool:
if not self.websocket:
raise RuntimeError("WebSocket connection not established")
deadline = time.monotonic() + self.workflow_timeout
ws = self.websocket
while time.monotonic() < deadline:
try:
message = ws.recv()
except websocket.WebSocketTimeoutException:
continue
except Exception as exc: # noqa: BLE001
print(f"WebSocket error: {exc}", file=sys.stderr, flush=True)
return False
if isinstance(message, bytes):
continue
try:
payload = json.loads(message)
except json.JSONDecodeError:
continue
message_type = payload.get("type")
data = payload.get("data", {})
if message_type == "execution_error":
if data.get("prompt_id") == prompt_id:
print(f"Execution error: {payload}", file=sys.stderr, flush=True)
return False
elif message_type == "status" and data.get("status") == "error":
if data.get("prompt_id") == prompt_id:
print(f"Status error: {payload}", file=sys.stderr, flush=True)
return False
elif message_type == "executing":
if data.get("prompt_id") == prompt_id and data.get("node") is None:
return True
print("Workflow timed out", file=sys.stderr, flush=True)
return False
def run_workflow(self, workflow_path: Path) -> bool:
previous_workflow = os.environ.get("MGPU_JSON_WORKFLOW")
previous_prompt = os.environ.get("MGPU_JSON_PROMPT")
def restore_env() -> None:
if previous_workflow is None:
os.environ.pop("MGPU_JSON_WORKFLOW", None)
else:
os.environ["MGPU_JSON_WORKFLOW"] = previous_workflow
if previous_prompt is None:
os.environ.pop("MGPU_JSON_PROMPT", None)
else:
os.environ["MGPU_JSON_PROMPT"] = previous_prompt
if workflow_path:
os.environ["MGPU_JSON_WORKFLOW"] = workflow_path.name
try:
with workflow_path.open("r", encoding="utf-8") as handle:
workflow = json.load(handle)
except (OSError, json.JSONDecodeError) as exc:
print(f"Failed to load workflow {workflow_path}: {exc}", file=sys.stderr, flush=True)
restore_env()
return False
print(f"Running workflow {workflow_path}", flush=True)
start = time.monotonic()
try:
prompt_id = self.queue_prompt(workflow)
os.environ["MGPU_JSON_PROMPT"] = prompt_id
except requests.HTTPError as exc:
print(f"HTTP error while queueing workflow: {exc}", file=sys.stderr, flush=True)
restore_env()
return False
except requests.RequestException as exc:
print(f"Request error while queueing workflow: {exc}", file=sys.stderr, flush=True)
restore_env()
return False
except RuntimeError as exc:
print(str(exc), file=sys.stderr, flush=True)
restore_env()
return False
try:
if not self.wait_for_completion(prompt_id):
return False
duration = time.monotonic() - start
print(f"Workflow {workflow_path} completed in {duration:.2f}s", flush=True)
return True
finally:
restore_env()
def run_suite(self, workflows: Iterable[Path], fail_fast: bool) -> bool:
self.wait_for_server()
self.open_websocket()
try:
overall = True
for workflow in workflows:
ok = self.run_workflow(workflow)
if not ok:
overall = False
if fail_fast:
break
return overall
finally:
self.close_websocket()
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Run ComfyUI workflows via the HTTP/WebSocket API")
parser.add_argument("workflows", nargs="+", type=Path, help="Workflow files in ComfyUI API JSON format")
parser.add_argument("--host", default=DEFAULT_HOST, help="ComfyUI HTTP host")
parser.add_argument("--port", type=int, default=DEFAULT_PORT, help="ComfyUI HTTP port")
parser.add_argument("--connect-timeout", type=int, default=DEFAULT_CONNECT_TIMEOUT, help="Seconds to wait for the server to come online")
parser.add_argument("--workflow-timeout", type=int, default=DEFAULT_WORKFLOW_TIMEOUT, help="Seconds to wait for each workflow to finish")
parser.add_argument("--fail-fast", action="store_true", help="Stop on first workflow failure")
parser.add_argument("--secure", action="store_true", help="Use secure HTTPS/WSS connections (default: insecure for localhost)")
return parser.parse_args()
def main() -> int:
args = parse_args()
runner = ComfyWorkflowRunner(
host=args.host,
port=args.port,
connect_timeout=args.connect_timeout,
workflow_timeout=args.workflow_timeout,
secure=args.secure,
)
success = runner.run_suite(args.workflows, fail_fast=args.fail_fast)
return 0 if success else 1
if __name__ == "__main__":
sys.exit(main())
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 1 ]]; then
echo "Usage: COMFYUI_HOME=/path/to/ComfyUI ci/smoke_test.sh <workflow.json> [<workflow.json>...]" >&2
exit 1
fi
if [[ -z "${COMFYUI_HOME:-}" ]]; then
echo "COMFYUI_HOME environment variable must point to the ComfyUI checkout" >&2
exit 1
fi
PYTHON_BIN=${PYTHON_BIN:-python3}
HOST=${COMFYUI_HOST:-127.0.0.1}
PORT=${COMFYUI_PORT:-8188}
LOG_FILE=${COMFYUI_LOG:-comfyui_ci.log}
pushd "${COMFYUI_HOME}" >/dev/null
${PYTHON_BIN} -m pip install --upgrade pip >/dev/null
${PYTHON_BIN} -m pip install -r requirements.txt >/dev/null
${PYTHON_BIN} main.py --disable-auto-launch --listen "${HOST}" --port "${PORT}" >"${LOG_FILE}" 2>&1 &
SERVER_PID=$!
trap 'kill ${SERVER_PID} >/dev/null 2>&1 || true' EXIT
popd >/dev/null
"${PYTHON_BIN}" "$(dirname "$0")/run_workflows.py" --host "${HOST}" --port "${PORT}" "$@"
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Convert MultiGPU JSON log into a Markdown summary."""
import argparse
import json
from pathlib import Path
from typing import Iterator, Dict, Any
def load_json_lines(path: Path) -> Iterator[Dict[str, Any]]:
with path.open("r", encoding="utf-8") as handle:
for line in handle:
line = line.strip()
if not line:
continue
try:
yield json.loads(line)
except json.JSONDecodeError:
continue
def main() -> int:
parser = argparse.ArgumentParser(description="Summarize MultiGPU JSON logs into Markdown")
parser.add_argument("logfile", type=Path, help="Path to JSONL log produced by MGPU_JSON_LOG_PATH")
parser.add_argument("--severity", nargs="*", help="Optional severity levels to include (e.g. INFO WARN ERROR)")
parser.add_argument(
"--component",
nargs="*",
help="Optional component names to include (matches component or event_category fields)",
)
args = parser.parse_args()
entries = list(load_json_lines(args.logfile))
if not entries:
print("No entries found in log file.")
return 0
print("| Timestamp | Level | Component | Message |")
print("| --- | --- | --- | --- |")
for entry in entries:
level = entry.get("level", "")
if args.severity and level not in args.severity:
continue
component_values = {
entry.get("component", ""),
entry.get("event_category", ""),
}
component = next((value for value in component_values if value), "")
if args.component and component not in args.component:
continue
timestamp = entry.get("timestamp", "")
message = entry.get("message", "").replace("|", "\u2502")
print(f"| {timestamp} | {level} | {component} | {message} |")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+15 -21
View File
@@ -17,7 +17,7 @@ def get_device_list():
Returns a comprehensive list of all available devices across all types:
- CPU (always available)
- CUDA devices (NVIDIA GPUs)
- CUDA devices (NVIDIA GPUs + AMD w/ ROCm GPUs)
- XPU devices (Intel GPUs)
- NPU devices (Ascend NPUs from Huawei)
- MLU devices (Cambricon MLUs)
@@ -237,32 +237,28 @@ def soft_empty_cache_distorch2_patched(force=False):
from .model_management_mgpu import multigpu_memory_log, check_cpu_memory_threshold, trigger_executor_cache_reset
from .distorch_2 import safetensor_allocation_store, create_safetensor_model_hash
multigpu_memory_log("patched_soft_empty", f"start:force={force}")
is_distorch_active = False
# Detect DisTorch2-managed models
logger.mgpu_mm_log(f"[DETECT_DEBUG] Checking DisTorch2 active status - loaded models: {len(mm.current_loaded_models)}, store entries: {len(safetensor_allocation_store)}")
# logger.mgpu_mm_log(f"[DETECT_DEBUG] Checking DisTorch2 active status - loaded models: {len(mm.current_loaded_models)}, store entries: {len(safetensor_allocation_store)}")
for i, lm in enumerate(mm.current_loaded_models):
mp = lm.model # weakref call to ModelPatcher
if mp is not None:
try:
model_hash = create_safetensor_model_hash(mp, "cache_patch_check")
in_store = model_hash in safetensor_allocation_store
alloc_value = safetensor_allocation_store.get(model_hash, "")
model_name = type(getattr(mp, 'model', mp)).__name__
unload_distorch_model = getattr(getattr(mp, 'model', None), '_mgpu_unload_distorch_model', False)
logger.mgpu_mm_log(f"[DETECT_DEBUG] Model {i}: {model_name}, hash={model_hash[:8]}, in_store={in_store}, alloc_value='{alloc_value}', unload_distorch_model={unload_distorch_model}")
if in_store and alloc_value:
is_distorch_active = True
logger.mgpu_mm_log(f"[DETECT_DEBUG] DisTorch2 ACTIVE detected on model: {model_name}")
break
except Exception as e:
logger.mgpu_mm_log(f"[DETECT_DEBUG] Model {i}: Error during detection - {e}")
model_hash = create_safetensor_model_hash(mp, "cache_patch_check")
in_store = model_hash in safetensor_allocation_store
alloc_value = safetensor_allocation_store.get(model_hash, "")
model_name = type(getattr(mp, 'model', mp)).__name__
unload_distorch_model = getattr(getattr(mp, 'model', None), '_mgpu_unload_distorch_model', False)
#logger.mgpu_mm_log(f"[DETECT_DEBUG] Model {i}: {model_name}, hash={model_hash[:8]}, in_store={in_store}, alloc_value='{alloc_value}', unload_distorch_model={unload_distorch_model}")
if in_store and alloc_value:
is_distorch_active = True
#logger.mgpu_mm_log(f"[DETECT_DEBUG] DisTorch2 ACTIVE detected on model: {model_name}")
break
logger.mgpu_mm_log(f"[DETECT_DEBUG] Final DisTorch2 active status: {is_distorch_active}")
#logger.mgpu_mm_log(f"[DETECT_DEBUG] Final DisTorch2 active status: {is_distorch_active}")
# Phase 2: adaptive CPU memory management
check_cpu_memory_threshold()
@@ -272,7 +268,6 @@ def soft_empty_cache_distorch2_patched(force=False):
logger.mgpu_mm_log("DisTorch2 active: clearing allocator caches on all devices (VRAM)")
soft_empty_cache_multigpu()
else:
logger.mgpu_mm_log("DisTorch2 not active: delegating allocator cache clear (VRAM) to original mm.soft_empty_cache")
original_soft_empty_cache(force)
# Optional: return CPU heap to OS (not part of Comfy Core)
@@ -280,7 +275,6 @@ def soft_empty_cache_distorch2_patched(force=False):
if force:
logger.mgpu_mm_log("Force flag active: triggering executor cache reset (CPU)")
trigger_executor_cache_reset(reason="forced_soft_empty", force=True)
multigpu_memory_log("patched_soft_empty", "end")
mm.soft_empty_cache = soft_empty_cache_distorch2_patched
@@ -1,717 +0,0 @@
{
"last_node_id": 51,
"last_link_id": 70,
"nodes": [
{
"id": 7,
"type": "HyVideoVAELoader",
"pos": [
-980.2922973632812,
-830.076171875
],
"size": [
379.166748046875,
82
],
"flags": {},
"order": 0,
"mode": 4,
"inputs": [
{
"name": "compile_args",
"type": "COMPILEARGS",
"link": null,
"shape": 7
}
],
"outputs": [
{
"name": "vae",
"type": "VAE",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "HyVideoVAELoader"
},
"widgets_values": [
"hunyuan_video_vae_bf16.safetensors",
"bf16"
],
"color": "#332922",
"bgcolor": "#593930"
},
{
"id": 30,
"type": "HyVideoTextEncode",
"pos": [
-194.8070831298828,
-79.95932006835938
],
"size": [
425.64068603515625,
286.85968017578125
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "text_encoders",
"type": "HYVIDTEXTENCODER",
"link": 66
},
{
"name": "custom_prompt_template",
"type": "PROMPT_TEMPLATE",
"link": null,
"shape": 7
},
{
"name": "clip_l",
"type": "CLIP",
"link": null,
"shape": 7
},
{
"name": "hyvid_cfg",
"type": "HYVID_CFG",
"link": null,
"shape": 7
}
],
"outputs": [
{
"name": "hyvid_embeds",
"type": "HYVIDEMBEDS",
"links": [
36
]
}
],
"properties": {
"Node name for S&R": "HyVideoTextEncode"
},
"widgets_values": [
"A serene Minnesota lake stretches out at sunset, the water's surface a mirror reflecting the vibrant orange and pink sky. In the foreground, a pair of loons glide gracefully across the water, their sleek black and white feathers contrasting with the warm hues of the sunset. The loons' long, slender necks curve elegantly as they dip their heads into the water, searching for fish. The camera pans slowly from left to right, capturing the tranquil scene. The shoreline is visible in the distance, lined with tall pine trees that cast long shadows across the water. The loons' haunting calls echo across the lake, adding to the peaceful atmosphere.",
false,
"video"
]
},
{
"id": 49,
"type": "DownloadAndLoadHyVideoTextEncoderMultiGPU",
"pos": [
-745.2869262695312,
-80.3648452758789
],
"size": [
516.5999755859375,
202
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "hyvid_text_encoder",
"type": "HYVIDTEXTENCODER",
"links": [
66
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "DownloadAndLoadHyVideoTextEncoderMultiGPU"
},
"widgets_values": [
"Kijai/llava-llama-3-8b-text-encoder-tokenizer",
"disabled",
"bf16",
false,
2,
"disabled",
"cuda:1"
],
"color": "#233",
"bgcolor": "#355"
},
{
"id": 48,
"type": "HyVideoModelLoaderMultiGPU",
"pos": [
-338.0295715332031,
-403.1601257324219
],
"size": [
497.3603210449219,
252.03509521484375
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "compile_args",
"type": "COMPILEARGS",
"link": null,
"shape": 7
},
{
"name": "block_swap_args",
"type": "BLOCKSWAPARGS",
"link": null,
"shape": 7
},
{
"name": "lora",
"type": "HYVIDLORA",
"link": null,
"shape": 7
}
],
"outputs": [
{
"name": "model",
"type": "HYVIDEOMODEL",
"links": [
65
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "HyVideoModelLoaderMultiGPU"
},
"widgets_values": [
"hunyuan_video_720_cfgdistill_fp8_e4m3fn.safetensors",
"fp32",
"fp8_e4m3fn",
"main_device",
"sdpa",
false,
"cuda:0"
],
"color": "#233",
"bgcolor": "#355"
},
{
"id": 16,
"type": "DownloadAndLoadHyVideoTextEncoder",
"pos": [
-1011.1117553710938,
-1076.6143798828125
],
"size": [
441,
178
],
"flags": {},
"order": 3,
"mode": 4,
"inputs": [],
"outputs": [
{
"name": "hyvid_text_encoder",
"type": "HYVIDTEXTENCODER",
"links": []
}
],
"properties": {
"Node name for S&R": "DownloadAndLoadHyVideoTextEncoder"
},
"widgets_values": [
"Kijai/llava-llama-3-8b-text-encoder-tokenizer",
"openai/clip-vit-large-patch14",
"fp16",
false,
2,
"disabled"
],
"color": "#332922",
"bgcolor": "#593930"
},
{
"id": 1,
"type": "HyVideoModelLoader",
"pos": [
-557.619384765625,
-1092.4908447265625
],
"size": [
435.37628173828125,
221.34506225585938
],
"flags": {},
"order": 4,
"mode": 4,
"inputs": [
{
"name": "compile_args",
"type": "COMPILEARGS",
"link": null,
"shape": 7
},
{
"name": "block_swap_args",
"type": "BLOCKSWAPARGS",
"link": null,
"shape": 7
},
{
"name": "lora",
"type": "HYVIDLORA",
"link": null,
"shape": 7
}
],
"outputs": [
{
"name": "model",
"type": "HYVIDEOMODEL",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "HyVideoModelLoader"
},
"widgets_values": [
"hunyuan_video_720_cfgdistill_fp8_e4m3fn.safetensors",
"bf16",
"fp8_e4m3fn",
"main_device",
"sdpa",
false
],
"color": "#332922",
"bgcolor": "#593930"
},
{
"id": 34,
"type": "VHS_VideoCombine",
"pos": [
847.0758666992188,
-415.1882629394531
],
"size": [
580.7774658203125,
698.4859008789062
],
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 69
},
{
"name": "audio",
"type": "AUDIO",
"link": null,
"shape": 7
},
{
"name": "meta_batch",
"type": "VHS_BatchManager",
"link": null,
"shape": 7
},
{
"name": "vae",
"type": "VAE",
"link": null,
"shape": 7
}
],
"outputs": [
{
"name": "Filenames",
"type": "VHS_FILENAMES",
"links": null
}
],
"properties": {
"Node name for S&R": "VHS_VideoCombine"
},
"widgets_values": {
"frame_rate": 24,
"loop_count": 0,
"filename_prefix": "HunyuanVideo",
"format": "video/h264-mp4",
"pix_fmt": "yuv420p",
"crf": 19,
"save_metadata": true,
"trim_to_audio": false,
"pingpong": false,
"save_output": true,
"videopreview": {
"hidden": false,
"paused": false,
"params": {
"filename": "HunyuanVideo_00161.mp4",
"subfolder": "",
"type": "output",
"format": "video/h264-mp4",
"frame_rate": 24,
"workflow": "HunyuanVideo_00161.png",
"fullpath": "/home/johnj/ComfyUI/output/HunyuanVideo_00161.mp4"
},
"muted": false
}
}
},
{
"id": 45,
"type": "VAEDecodeTiled",
"pos": [
532.4385986328125,
-644.8389282226562
],
"size": [
315,
150
],
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 70
},
{
"name": "vae",
"type": "VAE",
"link": 56
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
69
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecodeTiled"
},
"widgets_values": [
256,
64,
64,
8
]
},
{
"id": 44,
"type": "VAELoaderMultiGPU",
"pos": [
-230.99607849121094,
-639.45654296875
],
"size": [
315,
82
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "VAE",
"type": "VAE",
"links": [
56
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAELoaderMultiGPU"
},
"widgets_values": [
"hunyuan_video_vae_bf16.safetensors",
"cuda:1"
],
"color": "#233",
"bgcolor": "#355"
},
{
"id": 47,
"type": "HyVideoVAELoaderMultiGPU",
"pos": [
-512.9594116210938,
-822.8096313476562
],
"size": [
315,
106
],
"flags": {},
"order": 6,
"mode": 4,
"inputs": [
{
"name": "compile_args",
"type": "COMPILEARGS",
"link": null,
"shape": 7
}
],
"outputs": [
{
"name": "vae",
"type": "VAE",
"links": [
64
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "HyVideoVAELoaderMultiGPU"
},
"widgets_values": [
"hunyuan_video_vae_bf16.safetensors",
"bf16",
"cuda:1"
],
"color": "#233",
"bgcolor": "#355"
},
{
"id": 5,
"type": "HyVideoDecode",
"pos": [
-106.15780639648438,
-843.2271728515625
],
"size": [
345.4285888671875,
150
],
"flags": {},
"order": 9,
"mode": 4,
"inputs": [
{
"name": "vae",
"type": "VAE",
"link": 64
},
{
"name": "samples",
"type": "LATENT",
"link": null
}
],
"outputs": [
{
"name": "images",
"type": "IMAGE",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "HyVideoDecode"
},
"widgets_values": [
true,
86,
256,
true
]
},
{
"id": 3,
"type": "HyVideoSampler",
"pos": [
255.96482849121094,
-403.58502197265625
],
"size": [
315,
630
],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "HYVIDEOMODEL",
"link": 65
},
{
"name": "hyvid_embeds",
"type": "HYVIDEMBEDS",
"link": 36
},
{
"name": "samples",
"type": "LATENT",
"link": null,
"shape": 7
},
{
"name": "stg_args",
"type": "STGARGS",
"link": null,
"shape": 7
},
{
"name": "context_options",
"type": "COGCONTEXT",
"link": null,
"shape": 7
},
{
"name": "feta_args",
"type": "FETAARGS",
"link": null,
"shape": 7
},
{
"name": "teacache_args",
"type": "TEACACHEARGS",
"link": null,
"shape": 7
}
],
"outputs": [
{
"name": "samples",
"type": "LATENT",
"links": [
70
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "HyVideoSampler"
},
"widgets_values": [
512,
320,
85,
20,
6,
9,
5770521,
"fixed",
false,
1,
"FlowMatchDiscreteScheduler"
]
},
{
"id": 51,
"type": "Note",
"pos": [
449.2930908203125,
-955.3724975585938
],
"size": [
1160.6077880859375,
211.52166748046875
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {},
"widgets_values": [
"kijai already does an impressive amount of memory management in these nodes, so it is important for MultiGPU to \"play nice\" and vice-versa.\n\nFor this version of the workflow:\n\n• Only two of kijai's three nodes are used - model and text. For VAE we relying on Comfy's native VAE Loader node wrapped by MultiGPU.\n\n• HunyuanVideo TextEncode: \"force_offload\" is set to \"false\". Setting this option to \"true\" would defeat the purpose of selecting a different main_device to load to.\n\n• The main model and VAE devices are set to different cuda devices (cuda0 and cuda1). This is possible because the HunyuanVideo Sampler node outputs latents compatible with Comfy's native TiledVAE Decode.\n• Consequentially, now that we no longer have any other component on cuda0, we set \"force_offload\" to \"false\" on the \"HunyuanVideo Sampler\" node. This keeps the main model in VRAM, eliminating load times for the subsequent generations.\n\n**NOTE** This the **OPTIMAL** way to use MultiGPU with kijai's awesome nodes. If you want to use all three of kijai's loader nodes, please see device_selector_lowvram_flux_controlnet.json in ./examples"
],
"color": "#432",
"bgcolor": "#653"
}
],
"links": [
[
36,
30,
0,
3,
1,
"HYVIDEMBEDS"
],
[
56,
44,
0,
45,
1,
"VAE"
],
[
64,
47,
0,
5,
0,
"VAE"
],
[
65,
48,
0,
3,
0,
"HYVIDEOMODEL"
],
[
66,
49,
0,
30,
0,
"HYVIDTEXTENCODER"
],
[
69,
45,
0,
34,
0,
"IMAGE"
],
[
70,
3,
0,
45,
0,
"LATENT"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.8769226950001201,
"offset": [
895.3038892918638,
1121.109618095784
]
},
"ue_links": [],
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0
},
"version": 0.4
}
@@ -1,784 +0,0 @@
{
"last_node_id": 51,
"last_link_id": 68,
"nodes": [
{
"id": 7,
"type": "HyVideoVAELoader",
"pos": [
-980.2922973632812,
-830.076171875
],
"size": [
379.166748046875,
82
],
"flags": {},
"order": 0,
"mode": 4,
"inputs": [
{
"name": "compile_args",
"type": "COMPILEARGS",
"link": null,
"shape": 7
}
],
"outputs": [
{
"name": "vae",
"type": "VAE",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "HyVideoVAELoader"
},
"widgets_values": [
"hunyuan_video_vae_bf16.safetensors",
"bf16"
],
"color": "#332922",
"bgcolor": "#593930"
},
{
"id": 30,
"type": "HyVideoTextEncode",
"pos": [
-194.8070831298828,
-79.95932006835938
],
"size": [
425.64068603515625,
286.85968017578125
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "text_encoders",
"type": "HYVIDTEXTENCODER",
"link": 66
},
{
"name": "custom_prompt_template",
"type": "PROMPT_TEMPLATE",
"link": null,
"shape": 7
},
{
"name": "clip_l",
"type": "CLIP",
"link": null,
"shape": 7
},
{
"name": "hyvid_cfg",
"type": "HYVID_CFG",
"link": null,
"shape": 7
}
],
"outputs": [
{
"name": "hyvid_embeds",
"type": "HYVIDEMBEDS",
"links": [
36
]
}
],
"properties": {
"Node name for S&R": "HyVideoTextEncode"
},
"widgets_values": [
"A serene Minnesota lake stretches out at sunset, the water's surface a mirror reflecting the vibrant orange and pink sky. In the foreground, a pair of loons glide gracefully across the water, their sleek black and white feathers contrasting with the warm hues of the sunset. The loons' long, slender necks curve elegantly as they dip their heads into the water, searching for fish. The camera pans slowly from left to right, capturing the tranquil scene. The shoreline is visible in the distance, lined with tall pine trees that cast long shadows across the water. The loons' haunting calls echo across the lake, adding to the peaceful atmosphere.",
false,
"video"
]
},
{
"id": 49,
"type": "DownloadAndLoadHyVideoTextEncoderMultiGPU",
"pos": [
-745.2869262695312,
-80.3648452758789
],
"size": [
516.5999755859375,
202
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "hyvid_text_encoder",
"type": "HYVIDTEXTENCODER",
"links": [
66
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "DownloadAndLoadHyVideoTextEncoderMultiGPU"
},
"widgets_values": [
"Kijai/llava-llama-3-8b-text-encoder-tokenizer",
"disabled",
"bf16",
false,
2,
"disabled",
"cuda:1"
],
"color": "#233",
"bgcolor": "#355"
},
{
"id": 48,
"type": "HyVideoModelLoaderMultiGPU",
"pos": [
-338.0295715332031,
-403.1601257324219
],
"size": [
497.3603210449219,
252.03509521484375
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "compile_args",
"type": "COMPILEARGS",
"link": null,
"shape": 7
},
{
"name": "block_swap_args",
"type": "BLOCKSWAPARGS",
"link": null,
"shape": 7
},
{
"name": "lora",
"type": "HYVIDLORA",
"link": null,
"shape": 7
},
{
"name": "device",
"type": "COMBO",
"link": 67,
"widget": {
"name": "device"
},
"shape": 7
}
],
"outputs": [
{
"name": "model",
"type": "HYVIDEOMODEL",
"links": [
65
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "HyVideoModelLoaderMultiGPU"
},
"widgets_values": [
"hunyuan_video_720_cfgdistill_fp8_e4m3fn.safetensors",
"fp32",
"fp8_e4m3fn",
"main_device",
"sdpa",
false,
"cuda:0"
],
"color": "#233",
"bgcolor": "#355"
},
{
"id": 16,
"type": "DownloadAndLoadHyVideoTextEncoder",
"pos": [
-1011.1117553710938,
-1076.6143798828125
],
"size": [
441,
178
],
"flags": {},
"order": 2,
"mode": 4,
"inputs": [],
"outputs": [
{
"name": "hyvid_text_encoder",
"type": "HYVIDTEXTENCODER",
"links": []
}
],
"properties": {
"Node name for S&R": "DownloadAndLoadHyVideoTextEncoder"
},
"widgets_values": [
"Kijai/llava-llama-3-8b-text-encoder-tokenizer",
"openai/clip-vit-large-patch14",
"fp16",
false,
2,
"disabled"
],
"color": "#332922",
"bgcolor": "#593930"
},
{
"id": 1,
"type": "HyVideoModelLoader",
"pos": [
-557.619384765625,
-1092.4908447265625
],
"size": [
435.37628173828125,
221.34506225585938
],
"flags": {},
"order": 3,
"mode": 4,
"inputs": [
{
"name": "compile_args",
"type": "COMPILEARGS",
"link": null,
"shape": 7
},
{
"name": "block_swap_args",
"type": "BLOCKSWAPARGS",
"link": null,
"shape": 7
},
{
"name": "lora",
"type": "HYVIDLORA",
"link": null,
"shape": 7
}
],
"outputs": [
{
"name": "model",
"type": "HYVIDEOMODEL",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "HyVideoModelLoader"
},
"widgets_values": [
"hunyuan_video_720_cfgdistill_fp8_e4m3fn.safetensors",
"bf16",
"fp8_e4m3fn",
"main_device",
"sdpa",
false
],
"color": "#332922",
"bgcolor": "#593930"
},
{
"id": 34,
"type": "VHS_VideoCombine",
"pos": [
847.0758666992188,
-415.1882629394531
],
"size": [
580.7774658203125,
698.4859008789062
],
"flags": {},
"order": 13,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 63
},
{
"name": "audio",
"type": "AUDIO",
"link": null,
"shape": 7
},
{
"name": "meta_batch",
"type": "VHS_BatchManager",
"link": null,
"shape": 7
},
{
"name": "vae",
"type": "VAE",
"link": null,
"shape": 7
}
],
"outputs": [
{
"name": "Filenames",
"type": "VHS_FILENAMES",
"links": null
}
],
"properties": {
"Node name for S&R": "VHS_VideoCombine"
},
"widgets_values": {
"frame_rate": 24,
"loop_count": 0,
"filename_prefix": "HunyuanVideo",
"format": "video/h264-mp4",
"pix_fmt": "yuv420p",
"crf": 19,
"save_metadata": true,
"trim_to_audio": false,
"pingpong": false,
"save_output": true,
"videopreview": {
"hidden": false,
"paused": false,
"params": {
"filename": "HunyuanVideo_00161.mp4",
"subfolder": "",
"type": "output",
"format": "video/h264-mp4",
"frame_rate": 24,
"workflow": "HunyuanVideo_00161.png",
"fullpath": "/home/johnj/ComfyUI/output/HunyuanVideo_00161.mp4"
},
"muted": false
}
}
},
{
"id": 5,
"type": "HyVideoDecode",
"pos": [
538.94189453125,
-625.3562622070312
],
"size": [
345.4285888671875,
150
],
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "vae",
"type": "VAE",
"link": 64
},
{
"name": "samples",
"type": "LATENT",
"link": 4
}
],
"outputs": [
{
"name": "images",
"type": "IMAGE",
"links": [
63
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "HyVideoDecode"
},
"widgets_values": [
true,
86,
256,
true
]
},
{
"id": 50,
"type": "DeviceSelectorMultiGPU",
"pos": [
-716.0182495117188,
-492.63983154296875
],
"size": [
315,
58
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "device",
"type": "COMBO",
"links": [
67,
68
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "DeviceSelectorMultiGPU"
},
"widgets_values": [
"cuda:0"
],
"color": "#233",
"bgcolor": "#355"
},
{
"id": 47,
"type": "HyVideoVAELoaderMultiGPU",
"pos": [
-313.7779846191406,
-620.58740234375
],
"size": [
315,
106
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "compile_args",
"type": "COMPILEARGS",
"link": null,
"shape": 7
},
{
"name": "device",
"type": "COMBO",
"link": 68,
"widget": {
"name": "device"
},
"shape": 7
}
],
"outputs": [
{
"name": "vae",
"type": "VAE",
"links": [
64
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "HyVideoVAELoaderMultiGPU"
},
"widgets_values": [
"hunyuan_video_vae_bf16.safetensors",
"bf16",
"cuda:1"
],
"color": "#233",
"bgcolor": "#355"
},
{
"id": 3,
"type": "HyVideoSampler",
"pos": [
255.96482849121094,
-403.58502197265625
],
"size": [
315,
630
],
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "HYVIDEOMODEL",
"link": 65
},
{
"name": "hyvid_embeds",
"type": "HYVIDEMBEDS",
"link": 36
},
{
"name": "samples",
"type": "LATENT",
"link": null,
"shape": 7
},
{
"name": "stg_args",
"type": "STGARGS",
"link": null,
"shape": 7
},
{
"name": "context_options",
"type": "COGCONTEXT",
"link": null,
"shape": 7
},
{
"name": "feta_args",
"type": "FETAARGS",
"link": null,
"shape": 7
},
{
"name": "teacache_args",
"type": "TEACACHEARGS",
"link": null,
"shape": 7
}
],
"outputs": [
{
"name": "samples",
"type": "LATENT",
"links": [
4
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "HyVideoSampler"
},
"widgets_values": [
512,
320,
85,
20,
6,
9,
5770521,
"fixed",
true,
1,
"FlowMatchDiscreteScheduler"
]
},
{
"id": 44,
"type": "VAELoaderMultiGPU",
"pos": [
-556.3764038085938,
-809.7488403320312
],
"size": [
315,
82
],
"flags": {},
"order": 5,
"mode": 4,
"inputs": [],
"outputs": [
{
"name": "VAE",
"type": "VAE",
"links": [
56
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAELoaderMultiGPU"
},
"widgets_values": [
"hunyuan_video_vae_bf16.safetensors",
"cuda:0"
]
},
{
"id": 45,
"type": "VAEDecodeTiled",
"pos": [
-195.86590576171875,
-828.8155517578125
],
"size": [
315,
150
],
"flags": {},
"order": 10,
"mode": 4,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": null
},
{
"name": "vae",
"type": "VAE",
"link": 56
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecodeTiled"
},
"widgets_values": [
256,
64,
64,
8
]
},
{
"id": 51,
"type": "Note",
"pos": [
217.09609985351562,
-905.6065063476562
],
"size": [
1160.6077880859375,
211.52166748046875
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {},
"widgets_values": [
"kijai already does an impressive amount of memory management in these nodes, so it is important for MultiGPU to \"play nice\" and vice-versa.\n\nFor this version of the workflow:\n\n• All three of kijai's nodes are used - model, text, and VAE\n\n• HunyuanVideo TextEncode: \"force_offload\" is set to \"false\". Setting this option to \"true\" would defeat the purpose of selecting a different main_device to load to.\n\n• The main model and VAE devices are linked. This is because kijai's \"HunyuanVideo Decode\" expects both the model and the VAE to be on the same device. \n• Consequentially, to eliminate out-of-memory errors, \"force_offload\" is set to \"true\" on the \"HunyuanVideo Sampler\" node.\n\n\n**NOTE** This is not the optimial way to use MultiGPU. Please see the workflow at for an example of loading the VAE to a different cuda device using the native VAE loader and tiled decode."
],
"color": "#432",
"bgcolor": "#653"
}
],
"links": [
[
4,
3,
0,
5,
1,
"LATENT"
],
[
36,
30,
0,
3,
1,
"HYVIDEMBEDS"
],
[
56,
44,
0,
45,
1,
"VAE"
],
[
63,
5,
0,
34,
0,
"IMAGE"
],
[
64,
47,
0,
5,
0,
"VAE"
],
[
65,
48,
0,
3,
0,
"HYVIDEOMODEL"
],
[
66,
49,
0,
30,
0,
"HYVIDTEXTENCODER"
],
[
67,
50,
0,
48,
3,
"COMBO"
],
[
68,
50,
0,
47,
1,
"COMBO"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.7972024500001089,
"offset": [
1218.3831555808085,
1163.8844215880747
]
},
"ue_links": [],
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0
},
"version": 0.4
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-145
View File
@@ -5,60 +5,6 @@ from nodes import NODE_CLASS_MAPPINGS
from .device_utils import get_device_list
from .model_management_mgpu import force_full_system_cleanup
class DeviceSelectorMultiGPU:
@classmethod
def INPUT_TYPES(s):
devices = get_device_list()
return {
"required": {
"device": (devices, {"default": devices[1] if len(devices) > 1 else devices[0]})
}
}
RETURN_TYPES = (get_device_list(),)
RETURN_NAMES = ("device",)
FUNCTION = "select_device"
CATEGORY = "multigpu"
def select_device(self, device):
"""Select target device from available device list."""
return (device,)
class HunyuanVideoEmbeddingsAdapter:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"hyvid_embeds": ("HYVIDEMBEDS",),
}
}
RETURN_TYPES = ("CONDITIONING",)
FUNCTION = "adapt_embeddings"
CATEGORY = "multigpu"
def adapt_embeddings(self, hyvid_embeds):
"""Adapt HunyuanVideo embeddings to standard ComfyUI conditioning format."""
cond = hyvid_embeds["prompt_embeds"]
pooled_dict = {
"pooled_output": hyvid_embeds["prompt_embeds_2"],
"cross_attn": hyvid_embeds["prompt_embeds"],
"attention_mask": hyvid_embeds["attention_mask"],
}
if hyvid_embeds["attention_mask_2"] is not None:
pooled_dict["attention_mask_controlnet"] = hyvid_embeds["attention_mask_2"]
if hyvid_embeds["cfg"] is not None:
pooled_dict["guidance"] = float(hyvid_embeds["cfg"])
pooled_dict["start_percent"] = float(hyvid_embeds["start_percent"]) if hyvid_embeds["start_percent"] is not None else 0.0
pooled_dict["end_percent"] = float(hyvid_embeds["end_percent"]) if hyvid_embeds["end_percent"] is not None else 1.0
return ([[cond, pooled_dict]],)
class UnetLoaderGGUF:
@classmethod
def INPUT_TYPES(s):
@@ -465,97 +411,6 @@ class PulidEvaClipLoader:
original_loader = NODE_CLASS_MAPPINGS["PulidEvaClipLoader"]()
return original_loader.load_eva_clip()
class HyVideoModelLoader:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": (folder_paths.get_filename_list("diffusion_models"), {"tooltip": "These models are loaded from the 'ComfyUI/models/diffusion_models' -folder",}),
"base_precision": (["fp32", "bf16"], {"default": "bf16"}),
"quantization": (['disabled', 'fp8_e4m3fn', 'fp8_e4m3fn_fast', 'fp8_scaled', 'torchao_fp8dq', "torchao_fp8dqrow", "torchao_int8dq", "torchao_fp6", "torchao_int4", "torchao_int8"], {"default": 'disabled', "tooltip": "optional quantization method"}),
"load_device": (["main_device"], {"default": "main_device"}),
},
"optional": {
"attention_mode": ([
"sdpa",
"flash_attn_varlen",
"sageattn_varlen",
"comfy",
], {"default": "flash_attn"}),
"compile_args": ("COMPILEARGS", ),
"block_swap_args": ("BLOCKSWAPARGS", ),
"lora": ("HYVIDLORA", {"default": None}),
"auto_cpu_offload": ("BOOLEAN", {"default": False, "tooltip": "Enable auto offloading for reduced VRAM usage, implementation from DiffSynth-Studio, slightly different from block swapping and uses even less VRAM, but can be slower as you can't define how much VRAM to use"}),
}
}
RETURN_TYPES = ("HYVIDEOMODEL",)
RETURN_NAMES = ("model", )
FUNCTION = "loadmodel"
CATEGORY = "HunyuanVideoWrapper"
def loadmodel(self, model, base_precision, load_device, quantization, compile_args=None, attention_mode="sdpa", block_swap_args=None, lora=None, auto_cpu_offload=False):
"""Load HunyuanVideo model with specified precision and quantization."""
original_loader = NODE_CLASS_MAPPINGS["HyVideoModelLoader"]()
return original_loader.loadmodel(model, base_precision, load_device, quantization, compile_args, attention_mode, block_swap_args, lora, auto_cpu_offload)
class HyVideoVAELoader:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model_name": (folder_paths.get_filename_list("vae"), {"tooltip": "These models are loaded from 'ComfyUI/models/vae'"}),
},
"optional": {
"precision": (["fp16", "fp32", "bf16"],
{"default": "bf16"}
),
"compile_args":("COMPILEARGS", ),
}
}
RETURN_TYPES = ("VAE",)
RETURN_NAMES = ("vae", )
FUNCTION = "loadmodel"
CATEGORY = "HunyuanVideoWrapper"
DESCRIPTION = "Loads Hunyuan VAE model from 'ComfyUI/models/vae'"
def loadmodel(self, model_name, precision, compile_args=None):
"""Load HunyuanVideo VAE model."""
original_loader = NODE_CLASS_MAPPINGS["HyVideoVAELoader"]()
return original_loader.loadmodel(model_name, precision, compile_args)
class DownloadAndLoadHyVideoTextEncoder:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"llm_model": (["Kijai/llava-llama-3-8b-text-encoder-tokenizer","xtuner/llava-llama-3-8b-v1_1-transformers"],),
"clip_model": (["disabled","openai/clip-vit-large-patch14",],),
"precision": (["fp16", "fp32", "bf16"],
{"default": "bf16"}
),
},
"optional": {
"apply_final_norm": ("BOOLEAN", {"default": False}),
"hidden_state_skip_layer": ("INT", {"default": 2}),
"quantization": (['disabled', 'bnb_nf4', "fp8_e4m3fn"], {"default": 'disabled'}),
}
}
RETURN_TYPES = ("HYVIDTEXTENCODER",)
RETURN_NAMES = ("hyvid_text_encoder", )
FUNCTION = "loadmodel"
CATEGORY = "HunyuanVideoWrapper"
DESCRIPTION = "Loads Hunyuan text_encoder model from 'ComfyUI/models/LLM'"
def loadmodel(self, llm_model, clip_model, precision, apply_final_norm=False, hidden_state_skip_layer=2, quantization="disabled"):
"""Download and load HunyuanVideo text encoder from HuggingFace."""
original_loader = NODE_CLASS_MAPPINGS["DownloadAndLoadHyVideoTextEncoder"]()
return original_loader.loadmodel(llm_model, clip_model, precision, apply_final_norm, hidden_state_skip_layer, quantization)
class UNetLoaderLP:
"""UNet Loader (Low Precision) - sets LoRA precision to False for CPU storage optimization"""
@classmethod
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-multigpu"
description = "Provides a suite of custom nodes to manage multiple GPUs for ComfyUI, including advanced model offloading for both GGUF and Safetensor formats with DisTorch, and bespoke MultiGPU support for WanVideoWrapper and other custom nodes."
version = "2.5.1"
version = "2.5.2"
license = {file = "LICENSE"}
[project.urls]
+759 -406
View File
File diff suppressed because it is too large Load Diff