Skip to content

Commit 5cf5273

Browse files
committed
Merge remote-tracking branch 'origin/master' into synap5e/fix/assets-defer-prune
# Conflicts: # app/assets/manager.py # tests-unit/assets_test/services/test_manager_isolated.py
2 parents 4c1d1b5 + 3a52aae commit 5cf5273

47 files changed

Lines changed: 1094 additions & 369 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.coderabbit.yaml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,5 +140,16 @@ knowledge_base:
140140
filePatterns:
141141
- files: "AGENTS.md"
142142
applyTo: "**"
143+
linked_repositories:
144+
- repository: "Comfy-Org/ComfyUI_frontend"
145+
instructions: |
146+
Frontend consumer of Core APIs, routes, feature flags, asset behavior,
147+
node metadata, and package-version rollouts. Check for compatibility
148+
issues and changes that require coordinated rollout or merge ordering.
149+
- repository: "Comfy-Org/comfy-kitchen"
150+
instructions: |
151+
Owns optimized inference operations and kernels used by ComfyUI. Check
152+
whether new model math or backend-specific operations duplicate an
153+
existing implementation or belong in comfy-kitchen instead of Core.
143154
learnings:
144155
scope: "auto"

‎app/assets/api/routes.py‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ async def wrapper(request: web.Request) -> web.Response:
9191
return _build_error_response(
9292
503,
9393
"SERVICE_DISABLED",
94-
"Assets system is disabled. Start the server with --enable-assets to use this feature.",
94+
"Assets system is disabled because the server was started with --disable-assets.",
9595
)
9696
return await handler(request)
9797

@@ -1036,7 +1036,7 @@ async def get_tags_refine(request: web.Request) -> web.Response:
10361036
@ROUTES.post("/api/assets/seed")
10371037
@_require_assets_feature_enabled
10381038
async def seed_assets(request: web.Request) -> web.Response:
1039-
"""Trigger asset seeding for specified roots (models, input, output).
1039+
"""Trigger asset seeding for specified roots (models, input).
10401040
10411041
Query params:
10421042
wait: If "true", block until scan completes (synchronous behavior for tests)
@@ -1048,11 +1048,11 @@ async def seed_assets(request: web.Request) -> web.Response:
10481048
"""
10491049
try:
10501050
payload = await request.json()
1051-
roots = payload.get("roots", ["models", "input", "output"])
1051+
roots = payload.get("roots", ["models", "input"])
10521052
except Exception:
1053-
roots = ["models", "input", "output"]
1053+
roots = ["models", "input"]
10541054

1055-
valid_roots = tuple(r for r in roots if r in ("models", "input", "output"))
1055+
valid_roots = tuple(r for r in roots if r in ("models", "input"))
10561056
if not valid_roots:
10571057
return _build_error_response(400, "INVALID_BODY", "No valid roots specified")
10581058

‎app/assets/lifecycle.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,12 @@ def start_asset_seeder() -> bool:
101101
from app.assets.seeder import asset_seeder
102102

103103
started = asset_seeder.start(
104-
roots=("models", "input", "output"),
104+
roots=("models", "input"),
105105
prune_first=True,
106106
compute_hashes=args.enable_asset_hashing,
107107
)
108108
if started:
109-
logging.info("Background asset scan initiated for models, input, output")
109+
logging.info("Background asset scan initiated for models, input")
110110
return started
111111

112112

‎app/assets/manager.py‎

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Selects and implements the enabled and disabled asset managers.
22
3-
``default_asset_manager`` checks database dependencies before enabling assets
4-
and chooses ``NoAssets`` when the requested mode cannot run.
3+
``default_asset_manager`` chooses ``NoAssets`` only for ``--disable-assets``; main.py
4+
stops startup before that if assets are on and the database packages are missing.
55
"""
66

77
from __future__ import annotations
@@ -13,16 +13,15 @@
1313

1414
from app.assets import mode
1515
from app.assets.lifecycle import record_hash_mode_transition_intent, run_shutdown, run_startup
16-
from app.database.db import dependencies_available, missing_dependencies
16+
from app.database.db import dependencies_available
1717
from app.user_manager import UserManager
1818
from comfy.cli_args import args
19-
from utils.install_util import get_missing_requirements_message
2019

2120
# These need the database packages. Without them only NoAssets is used, and it
2221
# does not touch these names.
2322
if dependencies_available():
2423
from app.assets.api.routes import register_assets_routes
25-
from app.assets.seeder import ScanPhase, asset_seeder
24+
from app.assets.seeder import asset_seeder
2625
from app.assets.services.ingest import (
2726
register_cached_output as ingest_register_cached_output,
2827
register_executed_output as ingest_register_executed_output,
@@ -74,7 +73,7 @@ def set_event_sink(self, sink: Callable[[str, dict[str, Any]], None] | None) ->
7473

7574

7675
class _ArgsLike(Protocol):
77-
enable_assets: bool
76+
disable_assets: bool
7877
enable_asset_hashing: bool
7978

8079

@@ -167,20 +166,14 @@ def register_routes(
167166

168167
def ensure_scan_started(self) -> None:
169168
asset_seeder.start_after_node_list(
170-
roots=("models", "input", "output"), compute_hashes=self._args.enable_asset_hashing
169+
roots=("models", "input"), compute_hashes=self._args.enable_asset_hashing
171170
)
172171

173172
def pause_background_scan(self) -> None:
174173
asset_seeder.pause()
175174

176175
def queue_output_scan(self) -> None:
177-
if not asset_seeder.is_disabled():
178-
# FULL, not ENRICH: only a walk finds outputs a node never declared. Do not downgrade without re-weighing the cost.
179-
asset_seeder.enqueue_scan(
180-
roots=("output",),
181-
phase=ScanPhase.FULL,
182-
compute_hashes=self._args.enable_asset_hashing,
183-
)
176+
return None
184177

185178
def resume_background_scan(self) -> None:
186179
asset_seeder.resume()
@@ -235,11 +228,4 @@ def set_event_sink(self, sink: Callable[[str, dict[str, Any]], None] | None) ->
235228

236229

237230
def default_asset_manager() -> AssetManager:
238-
if args.enable_assets and not dependencies_available():
239-
missing = ", ".join(missing_dependencies()) or "see the import error above"
240-
logging.error(
241-
f"--enable-assets requires packages that could not be imported: {missing}. "
242-
f"Assets are disabled.\n{get_missing_requirements_message()}"
243-
)
244-
return NoAssets(args)
245-
return AssetsEnabled(args) if args.enable_assets else NoAssets(args)
231+
return NoAssets(args) if args.disable_assets else AssetsEnabled(args)

‎app/assets/seeder.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ def is_disabled(self) -> bool:
227227

228228
def start(
229229
self,
230-
roots: tuple[RootType, ...] = ("models", "input", "output"),
230+
roots: tuple[RootType, ...] = ("models", "input"),
231231
phase: ScanPhase = ScanPhase.FULL,
232232
progress_callback: ProgressCallback | None = None,
233233
prune_first: bool = False,
@@ -282,7 +282,7 @@ def start(
282282

283283
def start_fast(
284284
self,
285-
roots: tuple[RootType, ...] = ("models", "input", "output"),
285+
roots: tuple[RootType, ...] = ("models", "input"),
286286
progress_callback: ProgressCallback | None = None,
287287
prune_first: bool = False,
288288
) -> bool:

‎app/database/db.py‎

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import time
77
from contextlib import closing
88
from app.logger import log_startup_warning
9-
from utils.install_util import get_missing_requirements_message
109
from filelock import FileLock, Timeout
1110
from comfy.cli_args import args, database_default_path
1211

@@ -23,6 +22,7 @@
2322
from alembic.runtime.migration import MigrationContext
2423
from alembic.script import ScriptDirectory
2524
from sqlalchemy import create_engine, event
25+
from sqlalchemy.engine import URL, make_url
2626
from sqlalchemy.exc import OperationalError
2727
from sqlalchemy.orm import sessionmaker
2828
from sqlalchemy.pool import StaticPool
@@ -32,16 +32,9 @@
3232
import blake3 # noqa: F401 — verify the hard dependency is importable at startup
3333

3434
_DB_AVAILABLE = True
35-
except ImportError as e:
36-
log_startup_warning(
37-
f"""
38-
------------------------------------------------------------------------
39-
Error importing dependencies: {e}
40-
{get_missing_requirements_message()}
41-
This error is happening because ComfyUI now uses a local sqlite database.
42-
------------------------------------------------------------------------
43-
""".strip()
44-
)
35+
except ImportError:
36+
# Only the assets system needs these; with it on, startup stops and says what to install.
37+
logging.debug("Database packages failed to import", exc_info=True)
4538

4639

4740
def dependencies_available():
@@ -76,8 +69,11 @@ def get_alembic_config():
7669
scripts_path = os.path.abspath(os.path.join(root_path, "alembic_db"))
7770

7871
config = Config(config_path)
79-
config.set_main_option("script_location", scripts_path)
80-
config.set_main_option("sqlalchemy.url", get_database_url())
72+
# Config values go through ConfigParser interpolation, so a literal % in a path must be doubled,
73+
# including in the %(here)s default that alembic fills in unescaped.
74+
config.file_config.set("DEFAULT", "here", os.path.dirname(config_path).replace("%", "%%"))
75+
config.set_main_option("script_location", scripts_path.replace("%", "%%"))
76+
config.set_main_option("sqlalchemy.url", get_database_url().replace("%", "%%"))
8177

8278
return config
8379

@@ -89,7 +85,9 @@ def get_database_url():
8985
import folder_paths
9086

9187
db_path = os.path.join(folder_paths.get_user_directory(), "comfyui.db")
92-
return f"sqlite:///{db_path}"
88+
# Built by SQLAlchemy so its own parser reads the path back intact: 2.1+ decodes %xx, and every version
89+
# stops the path at ? (which only 2.1+ quotes).
90+
return URL.create("sqlite", database=db_path).render_as_string()
9391

9492

9593
def get_legacy_default_db_path():
@@ -99,7 +97,7 @@ def get_legacy_default_db_path():
9997
def get_db_path():
10098
url = get_database_url()
10199
if url.startswith("sqlite:///"):
102-
return url.split("///", 1)[1]
100+
return make_url(url).database
103101
else:
104102
raise ValueError(f"Unsupported database URL '{url}'.")
105103

@@ -229,7 +227,6 @@ def _is_memory_db(db_url):
229227

230228
def init_db():
231229
db_url = get_database_url()
232-
logging.debug(f"Database URL: {db_url}")
233230

234231
if _is_memory_db(db_url):
235232
_init_memory_db(db_url)
@@ -363,7 +360,7 @@ def begin_immediate(connection):
363360
command.upgrade(config, target_rev)
364361
logging.info(f"Database upgraded from {current_rev} to {target_rev}")
365362
except Exception as e:
366-
logging.exception("Error upgrading database: ")
363+
logging.debug("Error upgrading database", exc_info=True)
367364
if backup_path:
368365
# Restore the database from backup if upgrade fails
369366
try:

‎comfy/cli_args.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,9 @@ def is_valid_directory(path: str) -> str:
275275
os.path.join(os.path.dirname(__file__), "..", "user", "comfyui.db")
276276
)
277277
parser.add_argument("--database-url", type=str, default=None, help="Specify the database URL, e.g. for an in-memory database you can use 'sqlite:///:memory:'. Defaults to 'comfyui.db' in the effective user directory.")
278-
parser.add_argument("--enable-assets", action="store_true", help="Enable the assets system (API routes, database synchronization, and background scanning).")
279-
parser.add_argument("--enable-asset-hashing", action="store_true", help="Compute blake3 content hashes when scanning assets. Hashing enables future asset-portability features (deduplication, cross-machine model resolution) but adds startup cost and per-output cost on large models directories. Off by default; enable to opt in.")
278+
parser.add_argument("--disable-assets", action="store_true", help="Disable the assets system (API routes, database synchronization, and background scanning).")
279+
parser.add_argument("--enable-assets", action="store_true", help="Deprecated: the assets system is on by default, so this does nothing. Use --disable-assets to turn it off.")
280+
parser.add_argument("--enable-asset-hashing", action="store_true", help="Compute blake3 content hashes when scanning assets. Files uploaded through the API are hashed either way. Hashing enables future asset-portability features (deduplication, cross-machine model resolution) but adds startup cost and per-output cost on large models directories. Off by default; enable to opt in.")
280281
parser.add_argument("--feature-flag", type=str, action='append', default=[], metavar="KEY[=VALUE]", help="Set a server feature flag. Use KEY=VALUE to set an explicit value, or bare KEY to set it to true. Can be specified multiple times. Boolean values (true/false) and numbers are auto-converted. Examples: --feature-flag show_signin_button=true or --feature-flag show_signin_button")
281282
parser.add_argument("--list-feature-flags", action="store_true", help="Print the registry of known CLI-settable feature flags as JSON and exit.")
282283

‎comfy/configurable.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from torch import nn
2+
3+
4+
class ConfigurableModule(nn.Module):
5+
def with_config(self, encoded_config):
6+
"""Return a new module configured from a uint8 JSON tensor, without modifying this module."""
7+
raise NotImplementedError

‎comfy/ldm/modules/attention.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from .sub_quadratic_attention import efficient_dot_product_attention
1818

1919
from comfy import model_management
20+
from comfy.configurable import ConfigurableModule
2021

2122
if model_management.xformers_enabled():
2223
import xformers
@@ -73,12 +74,17 @@ def get_attention_function(name: str, default: Any=...) -> Union[Callable, None]
7374
return REGISTERED_ATTENTION_FUNCTIONS[name]
7475

7576

76-
class ComfyAttention(nn.Module):
77+
class ComfyAttention(ConfigurableModule):
7778
def __init__(self):
7879
super().__init__()
7980
self.config = None
8081
self.function = None
8182

83+
def with_config(self, encoded_config):
84+
attention = ComfyAttention()
85+
attention.load_state_dict({"config": encoded_config})
86+
return attention
87+
8288
def _load_from_state_dict(self, state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs):
8389
self.config = None
8490
self.function = None

‎comfy/ldm/wan/model_animate2.py‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import comfy.ldm.common_dit
1313
import comfy.model_management
14+
import comfy.model_prefetch
1415
import comfy.quant_ops
1516
import comfy.utils
1617
from comfy.ldm.flux.math import apply_rope1
@@ -200,16 +201,17 @@ def prefetch(self, i, device, dtype):
200201
stream = None
201202
r = None
202203
if t.device != device:
203-
stream = comfy.model_management.get_offload_stream(device)
204-
cs = comfy.model_management.current_stream(device)
205-
if stream is not None and cs is not None:
206-
# the handed-out stream last waited on the main stream a full rotation ago, which does not cover the previous consumer's reads of this slot; wait now so the copy cannot overwrite a slot still being read
207-
stream.wait_stream(cs)
208204
# two persistent staging buffers per tensor shape instead of a fresh allocation per block (~29 GB of churn per pass at 720p); windows of different lengths get their own pair
209205
buf_key = (tuple(t.shape), cast_dtype if cast_dtype is not None else t.dtype)
210206
if buf_key not in self._staging:
211-
self._staging[buf_key] = [torch.empty(t.shape, dtype=buf_key[1], device=device) for _ in range(2)]
207+
with comfy.model_prefetch.pause_malloc_graph():
208+
self._staging[buf_key] = [torch.empty(t.shape, dtype=buf_key[1], device=device) for _ in range(2)]
212209
r = self._staging[buf_key][i % 2]
210+
stream = comfy.model_management.get_offload_stream(device)
211+
cs = comfy.model_management.current_stream(device)
212+
if stream is not None and cs is not None:
213+
# Wait for staging allocation and the previous consumer's reads before overwriting the buffer.
214+
stream.wait_stream(cs)
213215
self._pending[i] = (comfy.model_management.cast_to(t, cast_dtype, device, non_blocking=True, stream=stream, r=r), stream)
214216

215217
def take(self, i, device, dtype, batch_size):

0 commit comments

Comments
 (0)