Files
digital-garbageandClaude Opus 5 74671329f9 log: a severity, a source, and a message
[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>
2026-08-29 17:09:33 +03:00

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