[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>
133 lines
5.5 KiB
Python
133 lines
5.5 KiB
Python
"""Shared loading options.
|
|
|
|
Not a module: it announces nothing and ships no node. `registry.scan` only walks
|
|
two levels of DIRECTORIES, so a plain file at the domain level is a helper the
|
|
loaders import and nothing else.
|
|
|
|
These options are on the loader node rather than in a settings payload, and the
|
|
line is worth stating: a loader widget describes the FILE and how it is read,
|
|
which the loader cannot be handed by anyone else. Feature knobs -- strengths,
|
|
sigma windows, anything a modifier owns -- are settings, and never appear here.
|
|
"""
|
|
|
|
import torch
|
|
|
|
from .._core import log
|
|
|
|
# Named exactly as core's UNETLoader names them where they overlap, so a value
|
|
# copied from a stock workflow means the same thing here.
|
|
WEIGHT_DTYPES = ["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2", "fp16", "bf16", "fp32"]
|
|
COMPUTE_DTYPES = ["default", "fp16", "bf16", "fp32"]
|
|
VAE_DTYPES = ["bfloat16", "float16", "float32"]
|
|
|
|
_BY_NAME = {
|
|
"fp16": torch.float16,
|
|
"bf16": torch.bfloat16,
|
|
"fp32": torch.float32,
|
|
"float16": torch.float16,
|
|
"bfloat16": torch.bfloat16,
|
|
"float32": torch.float32,
|
|
"fp8_e4m3fn": getattr(torch, "float8_e4m3fn", None),
|
|
"fp8_e5m2": getattr(torch, "float8_e5m2", None),
|
|
# fp8_e4m3fn plus comfy's fp8 matmul path; see weight_model_options.
|
|
"fp8_e4m3fn_fast": getattr(torch, "float8_e4m3fn", None),
|
|
}
|
|
|
|
|
|
def dtype_of(name):
|
|
"""The torch dtype a choice names, or None for 'default'/an unsupported build."""
|
|
return _BY_NAME.get(str(name or "default"))
|
|
|
|
|
|
def weight_model_options(weight_dtype):
|
|
"""comfy `model_options` for a weight dtype. The mapping core's UNETLoader uses."""
|
|
options = {}
|
|
dtype = dtype_of(weight_dtype)
|
|
if dtype is not None:
|
|
options["dtype"] = dtype
|
|
if weight_dtype == "fp8_e4m3fn_fast":
|
|
options["fp8_optimizations"] = True
|
|
return options
|
|
|
|
|
|
def attention_choices():
|
|
"""Backends this ComfyUI can actually run.
|
|
|
|
Read from ComfyUI's own registry rather than a private import, so the list
|
|
offers exactly what the machine has -- sage where SageAttention is installed,
|
|
flash where flash-attn is -- and never lies about the rest. 'default' leaves
|
|
whatever ComfyUI itself selected from its launch flags.
|
|
"""
|
|
names = []
|
|
try:
|
|
from comfy.ldm.modules.attention import REGISTERED_ATTENTION_FUNCTIONS
|
|
names = sorted(REGISTERED_ATTENTION_FUNCTIONS.keys())
|
|
except Exception: # noqa: BLE001 -- older ComfyUI
|
|
pass
|
|
return ["default"] + names
|
|
|
|
|
|
def attention_override(name):
|
|
"""A transformer_options override routing every attention call to `name`.
|
|
|
|
ComfyUI wraps each implementation with `wrap_attn`, which hands the override
|
|
the original function plus the call's arguments -- so calling the WRAPPED
|
|
replacement would re-enter that machinery. The unwrapped one is what gets
|
|
called.
|
|
"""
|
|
if not name or name == "default":
|
|
return None
|
|
try:
|
|
from comfy.ldm.modules.attention import get_attention_function
|
|
except ImportError:
|
|
log.warning("attention", "this ComfyUI has no attention registry, so the "
|
|
"chosen backend cannot be applied")
|
|
return None
|
|
chosen = get_attention_function(name, None)
|
|
if chosen is None:
|
|
log.warning("attention", f"backend {name!r} is not available on this machine, "
|
|
f"so it was not applied")
|
|
return None
|
|
inner = getattr(chosen, "__wrapped__", chosen)
|
|
|
|
def override(_func, *args, **kwargs):
|
|
return inner(*args, **kwargs)
|
|
|
|
# Some backends carry a zero-copy path that takes the attention tensors still
|
|
# in their containers, and `wrap_attn` only offers it when the OVERRIDE has
|
|
# the attribute -- otherwise it unpacks each container and calls the slow way.
|
|
# Forgetting to forward it is invisible: the backend is selected, the maths is
|
|
# right, and the fast path it exists for is silently never used. ComfyUI's own
|
|
# `ModelPatcher.set_model_optimized_attention` forwards it for this reason.
|
|
container_function = getattr(chosen, "container_function", None)
|
|
if container_function is not None:
|
|
override.container_function = container_function
|
|
|
|
return override
|
|
|
|
|
|
def set_fp16_accumulation(enabled):
|
|
"""torch's fp16 accumulation switch, or None where the build has no such knob.
|
|
|
|
PROCESS-WIDE, and there is no scoped version of it: torch has one flag for the
|
|
whole interpreter. So this cannot mean "for this model" however it is spelled
|
|
on a node -- a second loader setting it differently changes the first model's
|
|
maths, and because ComfyUI caches loader outputs the last value set outlives
|
|
the run that set it, reaching models loaded by core's own nodes.
|
|
|
|
It is therefore never changed silently. Every transition says so once, which
|
|
is the difference between a documented global and an invisible one. The real
|
|
fix is to move this to a machine-level setting, where its scope is honest.
|
|
"""
|
|
matmul = getattr(getattr(torch.backends, "cuda", None), "matmul", None)
|
|
if matmul is None or not hasattr(matmul, "allow_fp16_accumulation"):
|
|
return None
|
|
wanted = bool(enabled)
|
|
before = bool(getattr(matmul, "allow_fp16_accumulation", False))
|
|
matmul.allow_fp16_accumulation = wanted
|
|
if before != wanted:
|
|
log.alert("fp16 accumulation",
|
|
f"now {wanted} for EVERY model in this process, not just this one -- "
|
|
f"torch has a single process-wide flag")
|
|
return wanted
|