[Log] Info: FunPack Diffusion Model Loader | flux.safetensors loaded, attention sage
[Log] Alert: MiniMaxH3ImageToVideo node | no "first_image" input is connected
[Log] Warning: FunPack LoRA Loader | style.safetensors is empty and will not load
[Log] Critical: torch | cannot allocate 14 GB (3 GB free); the run will not start
The difference between the middle two is the one that matters. Alert is a
choice you may not have made on purpose. Warning is something that will NOT
do what its name says -- the level v4 needed and did not have, which is how
560 handlers came to swallow a failure and carry on, so a feature could be
switched on, do nothing, and never say so.
Info is not decoration either: "loaded on GPU using sage attention" is how
an attention setting that did nothing gets noticed months before the
pictures look wrong enough to investigate.
Every call site moved -- there is no note() left to be vague through -- and
the loaders now say what they actually loaded rather than only what they
were asked for. Records are kept in a bounded ring as well as printed, so a
long rental session cannot turn the log into a memory leak, and served at
/funpack/api/log so a person can read it somewhere other than a terminal
full of ComfyUI's own output. There is a page for it.
Also dropped the preflight this was traded for: min and max are already
declared and already enforced at both doors, and a second protection layer
around a check that exists is ceremony.
Two mistakes worth keeping: a log line reached into the model's shape to
describe it and could have failed the load it was only narrating; and both
new pages passed `children` to an element that takes `rows`, which the kit
accepted and silently dropped. It throws on an unknown element and not on an
unknown prop.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
117 lines
4.9 KiB
Python
117 lines
4.9 KiB
Python
"""Ordering by declared relations, never by priority numbers.
|
|
|
|
A number is a global namespace: inserting one module between two others means
|
|
renumbering everything after it, and the numbers live in files that know nothing
|
|
about each other. A relation is local -- a module names only the neighbours it
|
|
actually cares about, and everything else sorts itself.
|
|
"""
|
|
|
|
from typing import Dict, Iterable, List, Set, Tuple
|
|
|
|
from . import log
|
|
from .contract import STAGES, ModuleSpec
|
|
|
|
|
|
class CycleError(ValueError):
|
|
"""Relations that cannot be satisfied in any order."""
|
|
|
|
|
|
def order(specs: Iterable[ModuleSpec]) -> Tuple[List[ModuleSpec], List[Tuple[ModuleSpec, str]]]:
|
|
"""(ordered, rejected) -- rejected carries the reason it could not be placed.
|
|
|
|
A module in a cycle is dropped rather than placed arbitrarily: an arbitrary
|
|
order is one that works until it does not, and by then nobody remembers the
|
|
order was never real.
|
|
"""
|
|
specs = list(specs)
|
|
by_id = {spec.id: spec for spec in specs}
|
|
rejected: List[Tuple[ModuleSpec, str]] = []
|
|
|
|
stage_of = {spec.id: STAGES.index(spec.stage) for spec in specs}
|
|
|
|
# Edges point from "runs earlier" to "runs later". `before` is expressed as
|
|
# the same edge from the other end, so the two spellings cannot disagree.
|
|
edges: Dict[str, set] = {spec.id: set() for spec in specs}
|
|
|
|
def _link(later: str, earlier: str) -> None:
|
|
# Stage is the coarse order and a relation may not overturn it: a load
|
|
# module cannot meaningfully run after a conditioning one, and honouring
|
|
# that would put a whole stage out of sequence to satisfy one module.
|
|
# The relation is dropped and said out loud rather than silently obeyed.
|
|
if stage_of[later] < stage_of[earlier]:
|
|
log.alert(
|
|
"module ordering",
|
|
f"ignoring {later!r} after {earlier!r}: {later!r} is in stage "
|
|
f"{STAGES[stage_of[later]]!r}, which runs before {STAGES[stage_of[earlier]]!r}"
|
|
)
|
|
return
|
|
edges[later].add(earlier)
|
|
|
|
for spec in specs:
|
|
# A relation naming an absent module is not an error: modules come and
|
|
# go, and "after audio_clock" has nothing to wait for when audio_clock
|
|
# failed to load or was never installed.
|
|
for earlier in spec.after:
|
|
if earlier in by_id:
|
|
_link(spec.id, earlier)
|
|
for later in spec.before:
|
|
if later in by_id:
|
|
_link(later, spec.id)
|
|
ordered: List[ModuleSpec] = []
|
|
placed: set = set()
|
|
|
|
# Stage first, then declaration order, so a run without any relations is
|
|
# stable and readable rather than dependent on filesystem order.
|
|
remaining = sorted(specs, key=lambda s: (stage_of[s.id], s.id))
|
|
|
|
while remaining:
|
|
ready = [s for s in remaining if edges[s.id] <= placed]
|
|
if not ready:
|
|
# Drop only the modules actually ON a cycle. Dropping everything
|
|
# still waiting takes healthy modules with it -- one that merely
|
|
# says "after B" is not itself circular, and once B is gone its
|
|
# relation simply has nothing to wait for.
|
|
live = {s.id for s in remaining}
|
|
looping = _on_a_cycle(edges, live)
|
|
for spec in [s for s in remaining if s.id in looping]:
|
|
others = sorted(edges[spec.id] & looping)
|
|
rejected.append((spec, f"ordering cycle with {', '.join(others)}"))
|
|
remaining.remove(spec)
|
|
# A dropped module stops being something to wait for. Otherwise
|
|
# everything downstream of the cycle waits on an id that will never
|
|
# arrive, and the collateral damage spreads instead of stopping.
|
|
for waiting in edges.values():
|
|
waiting -= looping
|
|
if not looping:
|
|
# Nothing is circular and nothing is ready: unreachable, but
|
|
# looping forever would be worse than saying so.
|
|
for spec in remaining:
|
|
rejected.append((spec, "could not be ordered"))
|
|
break
|
|
continue
|
|
# Among the ready, respect stage so a later stage never jumps ahead.
|
|
nxt = min(ready, key=lambda s: (stage_of[s.id], s.id))
|
|
ordered.append(nxt)
|
|
placed.add(nxt.id)
|
|
remaining.remove(nxt)
|
|
|
|
return ordered, rejected
|
|
|
|
|
|
def _on_a_cycle(edges: Dict[str, set], live: Set[str]) -> Set[str]:
|
|
"""Ids that can reach themselves through the edges still in play."""
|
|
on_cycle: Set[str] = set()
|
|
for start in live:
|
|
seen: Set[str] = set()
|
|
stack = [n for n in edges[start] if n in live]
|
|
while stack:
|
|
node = stack.pop()
|
|
if node == start:
|
|
on_cycle.add(start)
|
|
break
|
|
if node in seen:
|
|
continue
|
|
seen.add(node)
|
|
stack.extend(n for n in edges.get(node, ()) if n in live)
|
|
return on_cycle
|