mirror of
https://github.com/Comfy-Org/ComfyUI.git
synced 2026-10-03 03:17:58 -05:00
* chore(assets): drop the unused asset_meta table from migration 0007 * fix(assets): drop asset_meta when downgrading a database that already created it * docs(assets): clarify asset schema docstring * test(assets): assert alembic and ORM index parity for the surviving asset tables * test(assets): cover asset system state index parity * refactor(tests): hoist migration-0007 test imports to module scope * fix(assets): guard the hashing dependency and chain the real import error * fix(assets): always resume background scanning when prompt handling fails * fix(api): derive the assets feature flag from the selected manager * fix(assets): paginate enrichment by id cursor so failures cannot starve or overflow the query * refactor(assets): extract prompt_worker so its resume contract is testable in-process * refactor(assets): test the blake3 import guard in-process instead of via subprocess * fix(assets): advance the enrichment cursor only past rows the batch attempted * fix(assets): track the scan pause across prompt worker iterations * chore(assets): address review follow-ups in the hashing guard, feature flags, and pagination pin * docs(assets): describe enrichment rows as attempted rather than selected The cursor holds at the last row a batch actually attempted, so a pause ends a batch early and the rows behind it are selected again when the scan resumes. Only the attempt is capped at once per pass. * test(api): derive the expected assets flag from the manager under test The assertion asked for the flag with no argument, so it read the parameter default rather than anything the manager reported - in a test whose subject is the two agreeing. Passing the manager's own state keeps it honest if the setup ever yields an enabled manager. * test(assets): let a broken prompt worker import fail instead of skipping The fixture wrapped importlib.import_module in a bare except that called pytest.skip, so a circular import, a missing dependency or a syntax error in app/prompt_worker.py would retire all four resume-contract tests while CI stayed green. The whole premise of extracting the module is that main.py can import it, so an import failure has to be a collection error. The CPU guard is genuinely load-bearing — comfy.model_management selects its device at import time and a CUDA build with no driver raises there — so it is kept, but as a precondition rather than an exception handler, matching the args.cpu-before-import convention already used by the comfy_test and comfy_api_nodes_test modules. Nothing is caught now. * docs(assets): name the unattempted rows instead of the ones behind the cursor The cursor moves forward through ascending ids, so 'the rows behind it' reads as the rows already passed - the opposite of what is selected again. * fix(assets): only absorb duplicate-path races when seeding scanned assets * fix(db): copy the legacy database inside the process lock * fix(assets): drop watch-list entries on stat errors instead of aborting the scan * fix(assets): clean up temp uploads on validation failures Multipart parsing writes the uploaded bytes to a temporary file before it validates the remaining form fields, so a request rejected after its file part had already been read left the temp file and its uuid directory on disk. Routing those removals through delete_temp_file_if_exists also changes the success path. The previous helper returned early when the temp file was already gone, so it never reached the parent rmdir; the shared helper attempts the rmdir unconditionally. Moving the upload to its destination leaves the temp path absent, so a successful upload now also discards its empty uuid directory, closing a pre-existing leak. * fix(assets): keep updated_at stable on no-op renames * docs(assets): make module docstrings and the rebuild warning truthful * fix(assets): correct event-log status snapshots and failure telemetry * test(assets): make the keyset tie-breaker and temp-exclusion tests falsifiable * chore: comment cleanup Comment-Gate: 3 quarantined * fix(assets): keep unreadable filesystem metadata from failing a whole scan batch * fix(assets): remove temporary uploads on non-UploadError failures * fix(assets): preserve successful specs when a scan batch fault propagates * test(db): drop the inert legacy-copy patch from the path preparation tests prepare_file_db_path no longer copies the legacy database - that moved inside the process lock in _init_file_db - so patching copy_legacy_default_db here did nothing. Leaving it implied a side effect the function does not have, and would have masked one if it were reintroduced. * fix(assets): report specs committed before a batch fault and preserve the fault itself * fix(assets): distinguish partial batch insert failures * refactor(assets): collapse the duplicated batch fault deferral into seed_asset_specs * fix(assets): reject duplicate file parts instead of stranding the first upload * refactor(assets): reap empty upload directories without importing the API layer * fix(assets): emit the invalid-mtime event once per scan Every other per-file emit on this scan path is gated -- mark_emitted( "stat_failed:enrich"), "hash_discarded_modified", "hash_failed", "enrich_failed" -- but scanner.invalid_mtime fired per file, so a restored archive or a FAT volume of pre-epoch mtimes put one structured event per file into the stream the closed vocabulary exists to keep parseable. Counted and emitted once, carrying the count. seed_asset_specs receives no _ScanProgress object and neither does insert_asset_specs above it, so routing this through mark_emitted would mean changing both signatures plus the seeder call site; the count form needs neither and the event is now strictly more informative than N identical fieldless lines. The per-file logging.warning is unchanged, and the emit stays inside seed_asset_specs so the static call-site manifest still matches. test_seed_skips_negative_fresh_mtime_with_warning_and_telemetry now pins the full list of invalid_mtime lines to exactly ["... count=1"] instead of asserting one such line exists -- a strictly stronger assertion, and the only change the new field required. * fix(assets): keep spec construction failures from wedging the watch list get_name_and_tags_from_asset_path raises ValueError by contract when a path stops resolving to a configured root, and it sat outside the guard, as did compute_loader_path and mimetypes.guess_type. An escape skipped the _WATCH_LIST[:] = remaining write at the end, so drained entries stayed on the list and were re-attempted every tick while entries past the fault never reached the increment _WATCH_SCAN_RETRIES needs to retire them. The list wedged permanently. Spec construction is now inside a guard that drops just the offending entry, and the list write moved into a finally so no future escape can skip it. The loop walks an iterator rather than the list, so the finally can put back the entries it never reached instead of discarding them. New event name rather than reusing one: scanner.watch_seed_failed is emitted only when seed_asset_specs returns an error, and widening it to also mean "never got as far as seeding" would make it lie -- a consumer treating it as a database-health signal would get false positives from what is really a path layout problem. scanner.watch_spec_failed is registered in ALLOWED_EVENTS and in the static call-site manifest. * refactor(assets): export the live-path conflict check as public API scanner.py reached past the package's own re-export surface to import _is_live_path_conflict directly out of records.py. The underscore said module-private while the import said otherwise, and records.py deliberately publishes its public names through app.assets.database.queries -- which the same import block three lines above was already using. The use is correct and unchanged; only the name and the route change. Renamed to is_live_path_conflict, listed in the package __init__ import and __all__ alongside its siblings, and scanner.py now takes it from the package like everything else it imports from there. * docs(db): restore the rationale for locking before migration Commit1dbcdcd7and the comment-cleanup pass8205022freduced this to "All database reads and writes, including the legacy import, run under the lock", dropping the part that did the work: upstream master locks after migrating and justifies it with "Alembic uses its own connection, so we must wait until it's done before locking -- otherwise our own lock blocks the migration". That is false, the lock is on a separate <db>.lock file, and the surviving sentence said nothing to stop a contributor "fixing" the ordering back. Restored and adapted rather than pasted: the legacy copy and the db_exists probe now happen inside the lock, which the original text predates, so both are named in the list of things the ordering makes mutually exclusive. * fix(assets): stop a scan on memory exhaustion instead of deferring it MemoryError is an Exception, so the per-spec and per-batch handlers stored it alongside ordinary faults and carried on - allocating for every remaining spec and then every remaining batch while the process was already out of memory. Both handlers now let it through, and the scan records a failure and stops. * docs(assets): document the prune failure response and its None result The route gained a 500 PRUNE_FAILED branch and the seeder method gained a None return, both so a prune that did not run cannot be reported as a clean one. Neither contract was written down. * test(assets): assert the surviving spec count after a propagated fault * docs(db): shorten the lock-ordering comment while keeping its rationale * docs(tests): drop the cross-module justification from the import-order comment * test(assets): restore the cpu flag after the guarded prompt worker import * test(assets): restore the cpu flag even when the prompt worker import fails * fix(assets): drop asset_meta in a new migration instead of editing 0007 0007 shipped in v0.36.0, v0.37.0 and v0.37.1, so editing it would leave two installs at that revision with different schemas depending on when they upgraded. Restore 0007 to its released form and drop the unused asset_meta table in 0008 instead. Nothing reads or writes asset_meta; asset metadata lives in the JSON columns on assets. 0008 downgrades by recreating the table and its four indexes exactly as 0007 creates them. * refactor(assets): keep prompt_worker in main.py Custom nodes may reference main.prompt_worker, and tests can already import main (test_db_init_locking does), so the function stays where it was. Its body keeps the resume-on-failure handling and the pause flag that persists across loop iterations; the tests now call main.prompt_worker. * fix(assets): report the selected manager through the existing feature flags * fix(assets): record a failed prune in the scan status A failed prune left the scan's error list empty, so the run looked clean. Record it instead and let discovery continue as before. * test(assets): test the feature flag API directly instead of copying the startup write Both parity tests wrote SERVER_FEATURE_FLAGS["assets"] themselves, so they passed whatever startup did. Pin the one real claim - a no-argument get_server_features() reports the flag - in the feature flags tests, and drop the disabled case, which default_asset_manager already covers. * Bring #16486's watch-list batching and seed logging into this branch The merge before this commit is `git merge -X ours origin/master`: in conflicting hunks it keeps this branch's side. This commit ports what #16486 changed in those hunks. - tick_watch_list takes no session. It collects settled entries and seeds them in one batch through insert_asset_specs, keeping this branch's per-entry stat and spec handling and the finally that always rewrites the watch list. A failed seed is reported once per batch, since the batch only returns its first error. - seed_asset_specs no longer warns again for a skipped spec; observe_asset_specs already logged why. - Tests call tick_watch_list() without a session, bind the write session where they seed for real, and fake insert_asset_specs with its (created, error) return. The mid-drain fault now comes from stat, because seeding runs after the loop. * Note where the assets core flag is set and why prompt_worker catches BaseException --------- Co-authored-by: guill <jacob.e.segal@gmail.com>
600 lines
25 KiB
Python
600 lines
25 KiB
Python
import comfy.options
|
|
comfy.options.enable_args_parsing()
|
|
|
|
from comfy.cli_args import args
|
|
from comfy.cli_args import get_console_log_level, get_file_log_outputs
|
|
|
|
if args.list_feature_flags:
|
|
import json
|
|
from comfy_api.feature_flags import CLI_FEATURE_FLAG_REGISTRY
|
|
print(json.dumps(CLI_FEATURE_FLAG_REGISTRY, indent=2)) # noqa: T201
|
|
raise SystemExit(0)
|
|
|
|
import os
|
|
import importlib.util
|
|
import shutil
|
|
import importlib.metadata
|
|
import folder_paths
|
|
import time
|
|
from comfy.cli_args import enables_dynamic_vram
|
|
from app.logger import setup_logger
|
|
console_log_level = get_console_log_level(args.verbose)
|
|
file_log_outputs = get_file_log_outputs(args.verbose)
|
|
setup_logger(log_level=console_log_level, file_outputs=file_log_outputs, use_stdout=args.log_stdout)
|
|
|
|
from app.database.db import dependencies_available, init_db
|
|
from app.assets.lifecycle import cleanup_temp_filesystem
|
|
from app.assets.manager import AssetManager, default_asset_manager
|
|
import itertools
|
|
import utils.extra_config
|
|
from utils.mime_types import init_mime_types
|
|
import faulthandler
|
|
import logging
|
|
import signal
|
|
import sys
|
|
from comfy_execution.progress import get_progress_state
|
|
from comfy_execution.utils import get_executing_context
|
|
from comfy_api import feature_flags
|
|
|
|
if __name__ == "__main__":
|
|
#NOTE: These do not do anything on core ComfyUI, they are for custom nodes.
|
|
os.environ['HF_HUB_DISABLE_TELEMETRY'] = '1'
|
|
os.environ['DO_NOT_TRACK'] = '1'
|
|
|
|
import cuda_malloc
|
|
|
|
if (
|
|
os.name == "nt"
|
|
and args.cuda_device is None
|
|
and args.default_device is None
|
|
and os.environ.get("CUDA_VISIBLE_DEVICES") is None
|
|
):
|
|
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
|
|
logging.warning("On windows we are currently forcing single GPU mode in ComfyUI due to a Nvidia related issue, if you want to disable this use: --cuda-device all")
|
|
|
|
faulthandler.enable(file=sys.stderr, all_threads=args.debug_hang)
|
|
if __name__ == "__main__" and args.debug_hang:
|
|
dumping_traceback = False
|
|
|
|
def dump_traceback_on_sigint(signum, frame):
|
|
global dumping_traceback
|
|
if dumping_traceback:
|
|
raise KeyboardInterrupt
|
|
dumping_traceback = True
|
|
faulthandler.dump_traceback(file=sys.stderr, all_threads=True)
|
|
raise KeyboardInterrupt
|
|
|
|
signal.signal(signal.SIGINT, dump_traceback_on_sigint)
|
|
|
|
import comfy_aimdo.control
|
|
|
|
if enables_dynamic_vram():
|
|
simple_vram_headroom = None if args.reserve_vram is None else int(args.reserve_vram * 1024 ** 3)
|
|
try:
|
|
comfy_aimdo.control.init(simple_vram_headroom=simple_vram_headroom, nvml_pressure=not args.disable_nvml_pressure)
|
|
except TypeError:
|
|
# comfy-aimdo 0.4.10 protocol.
|
|
try:
|
|
comfy_aimdo.control.init(simple_vram_headroom=simple_vram_headroom)
|
|
except TypeError:
|
|
# comfy-aimdo 0.4.9 protocol.
|
|
comfy_aimdo.control.init()
|
|
|
|
if os.name == "nt":
|
|
os.environ['MIMALLOC_PURGE_DELAY'] = '0'
|
|
|
|
if __name__ == "__main__":
|
|
os.environ['TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL'] = '1'
|
|
if args.default_device is not None and args.cuda_device != "all":
|
|
default_dev = args.default_device
|
|
devices = list(range(32))
|
|
devices.remove(default_dev)
|
|
devices.insert(0, default_dev)
|
|
devices = ','.join(map(str, devices))
|
|
os.environ['CUDA_VISIBLE_DEVICES'] = str(devices)
|
|
os.environ['HIP_VISIBLE_DEVICES'] = str(devices)
|
|
|
|
if args.cuda_device == "all":
|
|
logging.info("Set cuda devices to all")
|
|
elif args.cuda_device is not None:
|
|
os.environ['CUDA_VISIBLE_DEVICES'] = str(args.cuda_device)
|
|
os.environ['HIP_VISIBLE_DEVICES'] = str(args.cuda_device)
|
|
os.environ["ASCEND_RT_VISIBLE_DEVICES"] = str(args.cuda_device)
|
|
logging.info("Set cuda device to: {}".format(args.cuda_device))
|
|
|
|
if args.oneapi_device_selector is not None:
|
|
os.environ['ONEAPI_DEVICE_SELECTOR'] = args.oneapi_device_selector
|
|
logging.info("Set oneapi device selector to: {}".format(args.oneapi_device_selector))
|
|
|
|
if args.deterministic:
|
|
if 'CUBLAS_WORKSPACE_CONFIG' not in os.environ:
|
|
os.environ['CUBLAS_WORKSPACE_CONFIG'] = ":4096:8"
|
|
|
|
if "rocm" in cuda_malloc.get_torch_version_noimport():
|
|
os.environ['OCL_SET_SVM_SIZE'] = '4194304' # 4TB. Much larger than the ROCM 64GB/256GB defaults for Aimdos liberal VA use
|
|
|
|
|
|
def handle_comfyui_manager_unavailable():
|
|
manager_req_path = os.path.join(os.path.dirname(os.path.abspath(folder_paths.__file__)), "manager_requirements.txt")
|
|
uv_available = shutil.which("uv") is not None
|
|
|
|
pip_cmd = f"{sys.executable} -m pip install -r {manager_req_path}"
|
|
msg = f"\n\nTo use the `--enable-manager` feature, the `comfyui-manager` package must be installed first.\ncommand:\n\t{pip_cmd}"
|
|
if uv_available:
|
|
msg += f"\nor using uv:\n\tuv pip install -r {manager_req_path}"
|
|
msg += "\n"
|
|
logging.warning(msg)
|
|
args.enable_manager = False
|
|
|
|
|
|
if args.enable_manager:
|
|
if importlib.util.find_spec("comfyui_manager"):
|
|
import comfyui_manager
|
|
|
|
if not comfyui_manager.__file__ or not comfyui_manager.__file__.endswith('__init__.py'):
|
|
handle_comfyui_manager_unavailable()
|
|
else:
|
|
handle_comfyui_manager_unavailable()
|
|
|
|
|
|
def apply_custom_paths():
|
|
# extra model paths
|
|
extra_model_paths_config_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "extra_model_paths.yaml")
|
|
if os.path.isfile(extra_model_paths_config_path):
|
|
utils.extra_config.load_extra_path_config(extra_model_paths_config_path)
|
|
|
|
if args.extra_model_paths_config:
|
|
for config_path in itertools.chain(*args.extra_model_paths_config):
|
|
utils.extra_config.load_extra_path_config(config_path)
|
|
|
|
# --base-directory
|
|
if args.base_directory:
|
|
logging.info(f"Setting base directory to: {folder_paths.base_path}")
|
|
|
|
# --models-directory
|
|
if args.models_directory:
|
|
logging.info(f"Setting models directory to: {folder_paths.models_dir}")
|
|
|
|
# --output-directory, --input-directory, --user-directory
|
|
if args.output_directory:
|
|
output_dir = os.path.abspath(args.output_directory)
|
|
logging.info(f"Setting output directory to: {output_dir}")
|
|
folder_paths.set_output_directory(output_dir)
|
|
|
|
# These are the default folders that checkpoints, clip and vae models will be saved to when using CheckpointSave, etc.. nodes
|
|
folder_paths.add_model_folder_path("checkpoints", os.path.join(folder_paths.get_output_directory(), "checkpoints"))
|
|
folder_paths.add_model_folder_path("clip", os.path.join(folder_paths.get_output_directory(), "clip"))
|
|
folder_paths.add_model_folder_path("vae", os.path.join(folder_paths.get_output_directory(), "vae"))
|
|
folder_paths.add_model_folder_path("diffusion_models",
|
|
os.path.join(folder_paths.get_output_directory(), "diffusion_models"))
|
|
folder_paths.add_model_folder_path("loras", os.path.join(folder_paths.get_output_directory(), "loras"))
|
|
|
|
if args.input_directory:
|
|
input_dir = os.path.abspath(args.input_directory)
|
|
logging.info(f"Setting input directory to: {input_dir}")
|
|
folder_paths.set_input_directory(input_dir)
|
|
|
|
if args.user_directory:
|
|
user_dir = os.path.abspath(args.user_directory)
|
|
logging.info(f"Setting user directory to: {user_dir}")
|
|
folder_paths.set_user_directory(user_dir)
|
|
|
|
|
|
def execute_prestartup_script():
|
|
if args.disable_all_custom_nodes and len(args.whitelist_custom_nodes) == 0:
|
|
return
|
|
|
|
def execute_script(script_path):
|
|
module_name = os.path.splitext(script_path)[0]
|
|
try:
|
|
spec = importlib.util.spec_from_file_location(module_name, script_path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return True
|
|
except Exception as e:
|
|
logging.error(f"Failed to execute startup-script: {script_path} / {e}")
|
|
return False
|
|
|
|
node_paths = folder_paths.get_folder_paths("custom_nodes")
|
|
node_prestartup_times = []
|
|
for custom_node_path in node_paths:
|
|
possible_modules = os.listdir(custom_node_path)
|
|
|
|
for possible_module in possible_modules:
|
|
module_path = os.path.join(custom_node_path, possible_module)
|
|
|
|
if args.enable_manager:
|
|
if comfyui_manager.should_be_disabled(module_path):
|
|
continue
|
|
|
|
if os.path.isfile(module_path) or module_path.endswith(".disabled") or module_path == "__pycache__":
|
|
continue
|
|
|
|
script_path = os.path.join(module_path, "prestartup_script.py")
|
|
if os.path.exists(script_path):
|
|
if args.disable_all_custom_nodes and possible_module not in args.whitelist_custom_nodes:
|
|
logging.info(f"Prestartup Skipping {possible_module} due to disable_all_custom_nodes and whitelist_custom_nodes")
|
|
continue
|
|
time_before = time.perf_counter()
|
|
success = execute_script(script_path)
|
|
node_prestartup_times.append((time.perf_counter() - time_before, module_path, success))
|
|
if len(node_prestartup_times) > 0:
|
|
logging.info("\nPrestartup times for custom nodes:")
|
|
for n in sorted(node_prestartup_times):
|
|
if n[2]:
|
|
import_message = ""
|
|
else:
|
|
import_message = " (PRESTARTUP FAILED)"
|
|
logging.info("{:6.1f} seconds{}: {}".format(n[0], import_message, n[1]))
|
|
logging.info("")
|
|
|
|
apply_custom_paths()
|
|
init_mime_types()
|
|
|
|
if args.enable_manager:
|
|
comfyui_manager.prestartup()
|
|
|
|
execute_prestartup_script()
|
|
|
|
|
|
# Main code
|
|
import asyncio
|
|
import threading
|
|
import gc
|
|
|
|
if 'torch' in sys.modules:
|
|
logging.warning("WARNING: Potential Error in code: Torch already imported, torch should never be imported before this point.")
|
|
|
|
|
|
import comfy.utils
|
|
|
|
import execution
|
|
import server
|
|
from protocol import BinaryEventTypes
|
|
import nodes
|
|
import comfy.model_management
|
|
import comfyui_version
|
|
import app.logger
|
|
import hook_breaker_ac10a0
|
|
|
|
import comfy.memory_management
|
|
import comfy.model_patcher
|
|
|
|
|
|
def dynamic_vram_supported():
|
|
if comfy.model_management.is_nvidia():
|
|
return True
|
|
if comfy.model_management.is_amd():
|
|
if comfy.model_management.rocm_version >= (7, 14):
|
|
return True
|
|
return False
|
|
|
|
|
|
if args.enable_dynamic_vram or (enables_dynamic_vram() and dynamic_vram_supported()):
|
|
if (not args.enable_dynamic_vram) and (comfy.model_management.torch_version_numeric < (2, 8)):
|
|
logging.warning("Unsupported Pytorch detected. DynamicVRAM support requires Pytorch version 2.8 or later (2.12+ is recommended). Falling back to legacy ModelPatcher. VRAM estimates may be unreliable especially on Windows")
|
|
else:
|
|
try:
|
|
aimdo_initialized = comfy_aimdo.control.init_devices((d.index, int(args.vram_headroom * 1024 ** 3)) for d in comfy.model_management.get_all_torch_devices())
|
|
except TypeError:
|
|
# comfy-aimdo 0.4.9 protocol.
|
|
aimdo_initialized = comfy_aimdo.control.init_devices(d.index for d in comfy.model_management.get_all_torch_devices())
|
|
|
|
if aimdo_initialized:
|
|
if console_log_level == 'DEBUG':
|
|
comfy_aimdo.control.set_log_debug()
|
|
elif console_log_level == 'DETAIL':
|
|
try:
|
|
comfy_aimdo.control.set_log_detail()
|
|
except AttributeError:
|
|
comfy_aimdo.control.set_log_info()
|
|
elif console_log_level == 'CRITICAL':
|
|
comfy_aimdo.control.set_log_critical()
|
|
elif console_log_level == 'ERROR':
|
|
comfy_aimdo.control.set_log_error()
|
|
elif console_log_level == 'WARNING':
|
|
comfy_aimdo.control.set_log_warning()
|
|
else: #INFO
|
|
comfy_aimdo.control.set_log_info()
|
|
|
|
comfy.model_patcher.CoreModelPatcher = comfy.model_patcher.ModelPatcherDynamic
|
|
comfy.memory_management.aimdo_enabled = True
|
|
logging.info("DynamicVRAM support detected and enabled")
|
|
else:
|
|
logging.warning("No working comfy-aimdo install detected. DynamicVRAM support disabled. Falling back to legacy ModelPatcher. VRAM estimates may be unreliable especially on Windows")
|
|
|
|
|
|
def cuda_malloc_warning():
|
|
device = comfy.model_management.get_torch_device()
|
|
device_name = comfy.model_management.get_torch_device_name(device)
|
|
cuda_malloc_warning = False
|
|
if "cudaMallocAsync" in device_name:
|
|
for b in cuda_malloc.blacklist:
|
|
if b in device_name:
|
|
cuda_malloc_warning = True
|
|
if cuda_malloc_warning:
|
|
logging.warning("\nWARNING: this card most likely does not support cuda-malloc, if you get \"CUDA error\" please run ComfyUI with: --disable-cuda-malloc\n")
|
|
|
|
|
|
def prompt_worker(q, server_instance, asset_manager):
|
|
current_time: float = 0.0
|
|
cache_ram = 0
|
|
cache_ram_inactive = 0
|
|
if not args.cache_classic and not args.cache_none and args.cache_lru <= 0:
|
|
cache_ram = min(10.0, max(2.0, comfy.model_management.total_ram * 0.10 / 1024.0))
|
|
cache_ram_inactive = min(128.0, comfy.model_management.total_ram / 1024.0)
|
|
if len(args.cache_ram) > 0:
|
|
cache_ram = args.cache_ram[0]
|
|
if len(args.cache_ram) > 1:
|
|
cache_ram_inactive = args.cache_ram[1]
|
|
|
|
cache_type = execution.CacheType.RAM_PRESSURE
|
|
if args.cache_classic:
|
|
cache_type = execution.CacheType.CLASSIC
|
|
elif args.cache_lru > 0:
|
|
cache_type = execution.CacheType.LRU
|
|
elif args.cache_none:
|
|
cache_type = execution.CacheType.NONE
|
|
|
|
e = execution.PromptExecutor(server_instance, cache_type=cache_type, cache_args={ "lru" : args.cache_lru, "ram" : cache_ram, "ram_inactive" : cache_ram_inactive }, asset_manager=asset_manager )
|
|
last_gc_collect = 0
|
|
need_gc = False
|
|
gc_collect_interval = 10.0
|
|
background_scan_paused = False
|
|
|
|
while True:
|
|
try:
|
|
timeout = 1000.0
|
|
if need_gc:
|
|
timeout = max(gc_collect_interval - (current_time - last_gc_collect), 0.0)
|
|
|
|
queue_item = q.get(timeout=timeout)
|
|
if queue_item is not None:
|
|
item, item_id = queue_item
|
|
execution_start_time = time.perf_counter()
|
|
prompt_id = item[1]
|
|
server_instance.last_prompt_id = prompt_id
|
|
|
|
sensitive = item[5]
|
|
extra_data = item[3].copy()
|
|
for k in sensitive:
|
|
extra_data[k] = sensitive[k]
|
|
|
|
asset_manager.pause_background_scan()
|
|
background_scan_paused = True
|
|
e.execute(item[2], prompt_id, extra_data, item[4])
|
|
|
|
need_gc = True
|
|
|
|
remove_sensitive = lambda prompt: prompt[:5] + prompt[6:]
|
|
q.task_done(item_id,
|
|
e.history_result,
|
|
status=execution.PromptQueue.ExecutionStatus(
|
|
status_str='success' if e.success else 'error',
|
|
completed=e.success,
|
|
messages=e.status_messages), process_item=remove_sensitive)
|
|
if server_instance.client_id is not None:
|
|
server_instance.send_sync("executing", {"node": None, "prompt_id": prompt_id}, server_instance.client_id)
|
|
|
|
current_time = time.perf_counter()
|
|
execution_time = current_time - execution_start_time
|
|
|
|
# Log Time in a more readable way after 10 minutes
|
|
if execution_time > 600:
|
|
execution_time = time.strftime("%H:%M:%S", time.gmtime(execution_time))
|
|
logging.info(f"Prompt executed in {execution_time}", extra={'color': 'green'})
|
|
else:
|
|
logging.info("Prompt executed in {:.2f} seconds".format(execution_time), extra={'color': 'green'})
|
|
|
|
flags = q.get_flags()
|
|
free_memory = flags.get("free_memory", False)
|
|
|
|
if flags.get("unload_models", free_memory):
|
|
comfy.model_management.unload_all_models()
|
|
need_gc = True
|
|
last_gc_collect = 0
|
|
|
|
if free_memory:
|
|
e.reset()
|
|
need_gc = True
|
|
last_gc_collect = 0
|
|
|
|
if need_gc:
|
|
current_time = time.perf_counter()
|
|
if (current_time - last_gc_collect) > gc_collect_interval:
|
|
gc.collect()
|
|
comfy.model_management.soft_empty_cache()
|
|
last_gc_collect = current_time
|
|
need_gc = False
|
|
hook_breaker_ac10a0.restore_functions()
|
|
|
|
asset_manager.queue_output_scan()
|
|
asset_manager.resume_background_scan()
|
|
background_scan_paused = False
|
|
# BaseException is deliberate. This runs on the worker thread, so Ctrl-C lands in
|
|
# the main thread instead, and resume only flips the seeder's pause state.
|
|
except BaseException:
|
|
if background_scan_paused:
|
|
try:
|
|
asset_manager.resume_background_scan()
|
|
except Exception:
|
|
logging.exception("Failed to resume background asset scanning after prompt worker failure")
|
|
raise
|
|
|
|
|
|
async def run(server_instance, address='', port=8188, verbose=True, call_on_start=None):
|
|
addresses = []
|
|
for addr in address.split(","):
|
|
addresses.append((addr, port))
|
|
await asyncio.gather(
|
|
server_instance.start_multi_address(addresses, call_on_start, verbose), server_instance.publish_loop()
|
|
)
|
|
|
|
def hijack_progress(server_instance):
|
|
def hook(value, total, preview_image, prompt_id=None, node_id=None):
|
|
executing_context = get_executing_context()
|
|
if prompt_id is None and executing_context is not None:
|
|
prompt_id = executing_context.prompt_id
|
|
if node_id is None and executing_context is not None:
|
|
node_id = executing_context.node_id
|
|
comfy.model_management.throw_exception_if_processing_interrupted()
|
|
if prompt_id is None:
|
|
prompt_id = server_instance.last_prompt_id
|
|
if node_id is None:
|
|
node_id = server_instance.last_node_id
|
|
progress = {"value": value, "max": total, "prompt_id": prompt_id, "node": node_id}
|
|
get_progress_state().update_progress(node_id, value, total, preview_image)
|
|
|
|
server_instance.send_sync("progress", progress, server_instance.client_id)
|
|
if preview_image is not None:
|
|
# Only send old method if client doesn't support preview metadata
|
|
if not feature_flags.supports_feature(
|
|
server_instance.sockets_metadata,
|
|
server_instance.client_id,
|
|
"supports_preview_metadata",
|
|
):
|
|
server_instance.send_sync(
|
|
BinaryEventTypes.UNENCODED_PREVIEW_IMAGE,
|
|
preview_image,
|
|
server_instance.client_id,
|
|
)
|
|
|
|
comfy.utils.set_progress_bar_global_hook(hook)
|
|
|
|
|
|
def setup_database(asset_manager):
|
|
if not dependencies_available():
|
|
return
|
|
|
|
try:
|
|
init_db()
|
|
asset_manager.startup()
|
|
except Exception as e:
|
|
if "database is locked" in str(e):
|
|
logging.error(
|
|
"Database is locked. Another ComfyUI process is already using this database.\n"
|
|
"To resolve this, specify a separate database file for this instance:\n"
|
|
" --database-url sqlite:///path/to/another.db"
|
|
)
|
|
sys.exit(1)
|
|
if "Could not acquire lock on database" in str(e):
|
|
logging.error(
|
|
"Database is locked. Another ComfyUI process is already using this database.\n"
|
|
"To resolve this, specify a separate database file for this instance:\n"
|
|
" --database-url sqlite:///path/to/another.db"
|
|
)
|
|
if args.enable_assets:
|
|
sys.exit(1)
|
|
return
|
|
if args.enable_assets:
|
|
logging.error(
|
|
f"Failed to initialize database: {e}\n"
|
|
"The --enable-assets flag requires a working database connection.\n"
|
|
"To resolve this, try one of the following:\n"
|
|
" 1. Install the latest requirements: pip install -r requirements.txt\n"
|
|
" 2. Specify an alternative database URL: --database-url sqlite:///path/to/your.db\n"
|
|
" 3. Use an in-memory database: --database-url sqlite:///:memory:"
|
|
)
|
|
sys.exit(1)
|
|
logging.error(f"Failed to initialize database. Please ensure you have installed the latest requirements. If the error persists, please report this as in future the database will be required: {e}")
|
|
|
|
|
|
def start_comfyui(asyncio_loop=None):
|
|
"""
|
|
Starts the ComfyUI server using the provided asyncio event loop or creates a new one.
|
|
Returns the event loop, server instance, and a function to start the server asynchronously.
|
|
"""
|
|
if args.temp_directory:
|
|
temp_dir = os.path.join(os.path.abspath(args.temp_directory), "temp")
|
|
logging.info(f"Setting temp directory to: {temp_dir}")
|
|
folder_paths.set_temp_directory(temp_dir)
|
|
|
|
asset_manager: AssetManager = default_asset_manager()
|
|
feature_flags.SERVER_FEATURE_FLAGS["assets"] = asset_manager.enabled
|
|
if not asset_manager.enabled:
|
|
cleanup_temp_filesystem()
|
|
|
|
if not asyncio_loop:
|
|
asyncio_loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(asyncio_loop)
|
|
prompt_server = server.PromptServer(asyncio_loop, asset_manager)
|
|
|
|
if args.enable_manager and not args.disable_manager_ui:
|
|
comfyui_manager.start()
|
|
|
|
hook_breaker_ac10a0.save_functions()
|
|
asyncio_loop.run_until_complete(nodes.init_extra_nodes(
|
|
init_custom_nodes=(not args.disable_all_custom_nodes) or len(args.whitelist_custom_nodes) > 0,
|
|
init_api_nodes=not args.disable_api_nodes
|
|
))
|
|
|
|
# Re-apply Comfy's cuDNN benchmark policy after custom-node imports. Benchmark
|
|
# mode can request near-card-sized autotune workspaces, and some custom nodes set it at import time.
|
|
comfy.model_management.set_cudnn_benchmark()
|
|
|
|
hook_breaker_ac10a0.restore_functions()
|
|
|
|
cuda_malloc_warning()
|
|
setup_database(asset_manager)
|
|
|
|
prompt_server.add_routes()
|
|
hijack_progress(prompt_server)
|
|
|
|
threading.Thread(target=prompt_worker, daemon=True, args=(prompt_server.prompt_queue, prompt_server, asset_manager)).start()
|
|
|
|
if args.quick_test_for_ci:
|
|
exit(0)
|
|
|
|
os.makedirs(folder_paths.get_temp_directory(), exist_ok=True)
|
|
call_on_start = None
|
|
if args.auto_launch:
|
|
def startup_server(scheme, address, port):
|
|
import webbrowser
|
|
if os.name == 'nt' and address == '0.0.0.0':
|
|
address = '127.0.0.1'
|
|
if ':' in address:
|
|
address = "[{}]".format(address)
|
|
webbrowser.open(f"{scheme}://{address}:{port}")
|
|
call_on_start = startup_server
|
|
|
|
async def start_all():
|
|
await prompt_server.setup()
|
|
await run(prompt_server, address=args.listen, port=args.port, verbose=not args.dont_print_server, call_on_start=call_on_start)
|
|
|
|
# Returning these so that other code can integrate with the ComfyUI loop and server
|
|
return asyncio_loop, prompt_server, start_all
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Running directly, just start ComfyUI.
|
|
logging.info("Python version: {}".format(sys.version))
|
|
logging.info("ComfyUI version: {}".format(comfyui_version.__version__))
|
|
for package in ("comfy-aimdo", "comfy-kitchen"):
|
|
try:
|
|
logging.info("{} version: {}".format(package, importlib.metadata.version(package)))
|
|
except:
|
|
pass
|
|
|
|
if sys.version_info.major == 3 and sys.version_info.minor < 10:
|
|
logging.warning("WARNING: You are using a python version older than 3.10, please upgrade to a newer one. 3.12 and above is recommended.")
|
|
if sys.version_info.major == 3 and sys.version_info.minor == 10:
|
|
logging.warning("WARNING: Python 3.10 will be EOL on October 31 2026, please consider upgrading to a newer version.")
|
|
|
|
if args.disable_dynamic_vram:
|
|
logging.warning(
|
|
"Dynamic vram disabled with argument. If you have any issues with "
|
|
"dynamic vram enabled please give us a detailed reports as this "
|
|
"argument will be removed soon. If you use gguf we recommend keeping "
|
|
"dynamic vram enabled and using native ComfyUI model formats instead. "
|
|
"ComfyUI native formats like fp8, int8 and w4a8 will be faster even if they are larger than your memory."
|
|
)
|
|
event_loop, prompt_server, start_all_func = start_comfyui()
|
|
try:
|
|
x = start_all_func()
|
|
app.logger.print_startup_warnings()
|
|
event_loop.run_until_complete(x)
|
|
except KeyboardInterrupt:
|
|
logging.info("\nStopped server")
|
|
finally:
|
|
prompt_server.asset_manager.shutdown()
|