GGUF: port v4's gguf_support.py (backend selection, content-based .gguf detection, dequantize fallback) and wire it into both the diffusion-model and CLIP loaders. Pipeline editor: reopening the Models-and-pipeline settings window was silently discarding the whole edited pipeline (a loaded preset, every loader file, every sampler setting) because GET /api/pipeline is deliberately stateless server-side and boot.js never fed its own already-saved slots back into a freshly remounted editor -- found live while walking through the MiniMax-H3 setup flow. Fixed by re-validating the saved slots through check() on reopen instead of always replaying the raw stateless default. Also fixes, found via the extensive_testing adversarial loop over both of the above: a stale file-listing cache in the pipeline window (no way to see a newly-added model file without a full page reload), and a chain of races/leaks in its own refresh/commit error-banner handling that surfaced across five review rounds -- a rejection from a superseded request overwriting a fresher success, a failure banner never clearing on success, banners bleeding across groups on navigation, and a commit()'s refusal landing on the wrong view after the user backs out before it answers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
138 lines
6.9 KiB
Python
138 lines
6.9 KiB
Python
"""The diffusion model loader.
|
|
|
|
Picking a file and how to read it. Every option here changes what the loaded
|
|
model IS, which is why they are node widgets and not settings: nobody downstream
|
|
can supply them, and a saved workflow has to record them.
|
|
"""
|
|
|
|
import comfy.sd
|
|
import comfy.utils
|
|
import folder_paths
|
|
from comfy_api.latest import io
|
|
|
|
from ..._core import log
|
|
from .. import gguf_support
|
|
from ..common import (COMPUTE_DTYPES, WEIGHT_DTYPES, attention_choices,
|
|
attention_override, dtype_of, set_fp16_accumulation,
|
|
weight_model_options)
|
|
|
|
|
|
def _model_file_choices():
|
|
"""Diffusion model files, `.gguf` included.
|
|
|
|
Core's extension set has no `.gguf`, so those files are on disk and
|
|
invisible to every picker. Appended rather than merged in sorted order, so
|
|
an existing pipeline's saved choice keeps its position in the list and
|
|
nothing a user already picked moves.
|
|
"""
|
|
return list(folder_paths.get_filename_list("diffusion_models")) + \
|
|
gguf_support.gguf_names("diffusion_models")
|
|
|
|
|
|
class FunPackDiffusionModelLoader(io.ComfyNode):
|
|
@classmethod
|
|
def define_schema(cls) -> io.Schema:
|
|
return io.Schema(
|
|
node_id="FunPackDiffusionModelLoader",
|
|
display_name="FunPack Diffusion Model Loader",
|
|
category="FunPack/Loaders",
|
|
description="Load a diffusion model, choosing precision and attention backend.",
|
|
inputs=[
|
|
io.Combo.Input("model_name", options=_model_file_choices()),
|
|
io.Combo.Input("weight_dtype", options=WEIGHT_DTYPES, default="default",
|
|
tooltip="How the weights are stored. 'default' keeps whatever "
|
|
"the file already is."),
|
|
io.Combo.Input("compute_dtype", options=COMPUTE_DTYPES, default="default",
|
|
tooltip="What the maths runs in, independently of storage."),
|
|
io.Combo.Input("attention", options=attention_choices(), default="default",
|
|
tooltip="Attention backend. Only what this machine can run is "
|
|
"listed; 'default' keeps ComfyUI's launch choice."),
|
|
io.Boolean.Input("fp16_accumulation", default=False, optional=True,
|
|
tooltip="Faster fp16 matmuls where the torch build supports it."),
|
|
],
|
|
outputs=[
|
|
io.Model.Output(display_name="model"),
|
|
io.String.Output(display_name="status"),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(cls, model_name: str, weight_dtype: str, compute_dtype: str,
|
|
attention: str, fp16_accumulation: bool = False) -> io.NodeOutput:
|
|
notes = [f"FunPack Diffusion Model Loader | {model_name}"]
|
|
|
|
accumulation = set_fp16_accumulation(fp16_accumulation)
|
|
if fp16_accumulation and accumulation is None:
|
|
notes.append("fp16_accumulation: unsupported by this torch build, ignored")
|
|
elif accumulation is not None:
|
|
notes.append(f"fp16_accumulation: {accumulation}")
|
|
|
|
model_options = weight_model_options(weight_dtype)
|
|
notes.append(f"weight dtype: {weight_dtype}")
|
|
|
|
if gguf_support.is_gguf(model_name):
|
|
path = gguf_support.gguf_path("diffusion_models", model_name)
|
|
if not path:
|
|
raise RuntimeError(f"{model_name} is no longer where it was listed from.")
|
|
misnamed = False
|
|
else:
|
|
path = folder_paths.get_full_path_or_raise("diffusion_models", model_name)
|
|
# The CONTENT decides, not the extension. A .gguf renamed to
|
|
# .safetensors used to reach the safetensors parser and fail with
|
|
# a UTF-8 decode error from reading a binary header as JSON --
|
|
# true, and no help at all.
|
|
misnamed = gguf_support.has_gguf_magic(path)
|
|
if gguf_support.is_gguf(model_name) or misnamed:
|
|
if misnamed:
|
|
notes.append(f"{model_name} is named .safetensors but is a GGUF "
|
|
f"container -- loaded as GGUF")
|
|
state_dict, gguf_options, gguf_note = gguf_support.load_state_dict(path)
|
|
# The quantized path needs its own torch operations, and they must
|
|
# not be lost to the dtype options above -- a GGUF loaded with
|
|
# stock ops would try to matmul block-quantized storage.
|
|
model_options = {**model_options, **gguf_options}
|
|
metadata = None
|
|
notes.append(gguf_note)
|
|
else:
|
|
state_dict, metadata = comfy.utils.load_torch_file(path, return_metadata=True)
|
|
model = comfy.sd.load_diffusion_model_state_dict(
|
|
state_dict, model_options=model_options, metadata=metadata)
|
|
if model is None:
|
|
# comfy returns None rather than raising, and a None reaching the
|
|
# sampler fails much further away than the mistake.
|
|
raise RuntimeError(
|
|
f"Could not detect a diffusion model in {model_name}. Loading a text "
|
|
f"encoder or a VAE file here is the usual cause.")
|
|
|
|
dtype = dtype_of(compute_dtype)
|
|
if dtype is not None and hasattr(model, "set_model_compute_dtype"):
|
|
# Do NOT clear force_cast_weights afterwards. set_model_compute_dtype
|
|
# sets it deliberately, and it is what casts each layer's WEIGHTS to
|
|
# match. Without it the model still casts its INPUT to the requested
|
|
# dtype (_apply_model reads manual_cast_dtype), so the first Linear
|
|
# gets a bf16 activation against fp32 weights and sampling dies with
|
|
# "mat1 and mat2 must have the same dtype". v4 cleared it and shipped
|
|
# that way; it survives on constrained hardware only because the
|
|
# low-VRAM path forces the cast back on regardless.
|
|
model.set_model_compute_dtype(dtype)
|
|
notes.append(f"compute dtype: {compute_dtype}")
|
|
elif dtype is not None:
|
|
notes.append("compute dtype: unsupported by this ComfyUI, ignored")
|
|
|
|
override = attention_override(attention)
|
|
if override is not None:
|
|
model.model_options.setdefault("transformer_options", {})[
|
|
"optimized_attention_override"] = override
|
|
notes.append(f"attention: {attention}")
|
|
else:
|
|
notes.append("attention: default (as launched)")
|
|
|
|
# Never reach into the model's shape to describe it: a log line that
|
|
# assumes structure can fail the load it was only meant to narrate.
|
|
kind = type(getattr(model, "model", model)).__name__
|
|
log.info("FunPack Diffusion Model Loader",
|
|
f"{model_name} loaded as {kind}, weights "
|
|
f"{weight_dtype}, compute {compute_dtype}, attention "
|
|
f"{attention if override is not None else 'default (as launched)'}")
|
|
return io.NodeOutput(model, "\n".join(notes))
|