feat: GGUF loader support, and fix silent pipeline-editor state loss

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>
This commit is contained in:
digital-garbage
2026-09-11 18:14:46 +03:00
co-authored by Claude Sonnet 5
parent 4744d30087
commit 7a226830b1
9 changed files with 1191 additions and 25 deletions
+39 -3
View File
@@ -11,11 +11,24 @@ 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:
@@ -25,7 +38,7 @@ class FunPackDiffusionModelLoader(io.ComfyNode):
category="FunPack/Loaders",
description="Load a diffusion model, choosing precision and attention backend.",
inputs=[
io.Combo.Input("model_name", options=folder_paths.get_filename_list("diffusion_models")),
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."),
@@ -57,8 +70,31 @@ class FunPackDiffusionModelLoader(io.ComfyNode):
model_options = weight_model_options(weight_dtype)
notes.append(f"weight dtype: {weight_dtype}")
path = folder_paths.get_full_path_or_raise("diffusion_models", model_name)
state_dict, metadata = comfy.utils.load_torch_file(path, return_metadata=True)
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: