diff --git a/modules/conditioning/prompt_enhancer/__init__.py b/modules/conditioning/prompt_enhancer/__init__.py index 288d295d..17b1bac3 100644 --- a/modules/conditioning/prompt_enhancer/__init__.py +++ b/modules/conditioning/prompt_enhancer/__init__.py @@ -45,8 +45,7 @@ def routes(table, base, web): async def _encoders(_req): """Encoder files and families the Enhance panel can offer (read fresh: files come and go).""" from ...loaders.clip import nodes as clip_nodes - from ...loaders import gguf_support - files = [f for f in clip_nodes.encoder_files() if gguf_support.backend() == "pack" or not gguf_support.is_gguf(f)] # only the pack can read a text encoder's .gguf + files = clip_nodes.encoder_files() return web.json_response({"files": files, "families": clip_nodes.clip_types()}) diff --git a/modules/loaders/clip/nodes.py b/modules/loaders/clip/nodes.py index b9c6d68c..5be2472f 100644 --- a/modules/loaders/clip/nodes.py +++ b/modules/loaders/clip/nodes.py @@ -43,10 +43,9 @@ def clip_types(): def encoder_files(): - """Text encoder files, `.gguf` included. Same reasoning as the diffusion - model loader's own file-list helper.""" - return list(folder_paths.get_filename_list("text_encoders")) + \ - gguf_support.gguf_names("text_encoders") + """Text encoder files; `.gguf` ones only when the ComfyUI-GGUF pack is there to read them.""" + ggufs = gguf_support.gguf_names("text_encoders") if gguf_support.backend() == "pack" else [] + return list(folder_paths.get_filename_list("text_encoders")) + ggufs class FunPackCLIPLoader(io.ComfyNode): diff --git a/modules/loaders/gguf_support.py b/modules/loaders/gguf_support.py index 73e0b356..39a00894 100644 --- a/modules/loaders/gguf_support.py +++ b/modules/loaders/gguf_support.py @@ -115,6 +115,8 @@ def gguf_path(folder: str, name: str) -> Optional[str]: pass for root in roots: cand = os.path.join(root or "", name) + if not os.path.realpath(cand).startswith(os.path.realpath(root or ".") + os.sep): + continue # a name that climbs out of the model folder if os.path.isfile(cand): return cand return None @@ -176,6 +178,8 @@ def _pack_dir() -> Optional[str]: if not os.path.isdir(base): return None for entry in sorted(os.listdir(base)): + if entry.endswith(".disabled"): # Manager's way of switching a pack off + continue d = os.path.join(base, entry) loader = os.path.join(d, "loader.py") if not os.path.isfile(loader) or not os.path.isfile(os.path.join(d, "ops.py")): @@ -248,7 +252,8 @@ def _load(path: str, clip: bool) -> tuple[dict, dict, str]: # Only the pack maps llama.cpp's tensor names back to a text encoder's; the native reader would hand over # a state dict that looks loaded and is the wrong model. raise RuntimeError("a text-encoder .gguf needs the ComfyUI-GGUF node pack to be read, and it is not installed. " - "Install it, or use a .safetensors encoder of the same model.") + "Installing it helps only for architectures it handles (not Gemma 4, not vision files); " + "otherwise use a .safetensors encoder of the same model.") if which is None: raise RuntimeError(UNAVAILABLE) if which == "pack": @@ -281,8 +286,8 @@ def _load(path: str, clip: bool) -> tuple[dict, dict, str]: # The native reader keeps llama.cpp's tensor names; only the pack maps them back to a text encoder's. # Loading them anyway gives a model that looks loaded and is the wrong one (a Gemma 4 .gguf did). raise RuntimeError(f"this text-encoder .gguf could not be loaded as one: {e}. " - "Architectures ComfyUI-GGUF does not know (Gemma 4) and vision files are not supported; " - "if the file should work, update ComfyUI-GGUF. Or use a .safetensors encoder of the same model.") from e + "If the file is intact, its architecture is not one ComfyUI-GGUF handles (Gemma 4, vision files) " + "or ComfyUI-GGUF is out of date. Use a .safetensors encoder of the same model.") from e if no_gguf: raise sd, options, note = _load_native(path, pack=True) @@ -330,7 +335,9 @@ def _load_native(path: str, pack: bool = False) -> tuple[dict, dict, str]: f"is not laid out the way the gguf package describes it.") if shape: t = t.reshape(shape) - sd[name] = t.to(torch.float16) if t.dtype == torch.float32 else t + if t.dtype == torch.float32 and t.numel(): # fp16 halves the size; a tensor too big for its range goes bf16 instead of inf + t = t.to(torch.float16) if t.abs().max() < 60000 else t.to(torch.bfloat16) + sd[name] = t note = (f"gguf: dequantized at load ({quantized} quantized tensors expanded) -- the " f"file loads, but it occupies its full size in VRAM." + ("" if pack else " Install ComfyUI-GGUF to keep it quantized.")) diff --git a/modules/loaders/tests/test_gguf_support.py b/modules/loaders/tests/test_gguf_support.py index a72ffadb..b64a28a6 100644 --- a/modules/loaders/tests/test_gguf_support.py +++ b/modules/loaders/tests/test_gguf_support.py @@ -330,3 +330,34 @@ def test_a_pack_whose_gguf_library_is_missing_says_how_to_fix_it_not_an_import_e gguf_support.load_state_dict("/x.gguf") with pytest.raises(RuntimeError, match="installed, but the `gguf` package"): gguf_support.load_clip_state_dict("/x.gguf") + + +def test_a_name_that_climbs_out_of_the_model_folder_is_not_found(fake_folders): + (fake_folders / "secret.gguf").write_bytes(b"x") + assert gguf_support.gguf_path("text_encoders", "../secret.gguf") is None + assert gguf_support.gguf_path("text_encoders", "gemma-Q5_K.gguf") + + +def test_a_pack_switched_off_by_manager_is_not_used(fake_folders): + pack = fake_folders / "custom_nodes" / "ComfyUI-GGUF.disabled" + pack.mkdir(parents=True) + (pack / "ops.py").write_text("") + (pack / "loader.py").write_text("def gguf_sd_loader(p): pass\n") + assert gguf_support._pack_dir() is None + (fake_folders / "custom_nodes" / "ComfyUI-GGUF").mkdir() + (fake_folders / "custom_nodes" / "ComfyUI-GGUF" / "ops.py").write_text("") + (fake_folders / "custom_nodes" / "ComfyUI-GGUF" / "loader.py").write_text("def gguf_sd_loader(p): pass\n") + assert gguf_support._pack_dir().endswith("ComfyUI-GGUF") + + +def test_native_load_keeps_an_f32_value_too_big_for_fp16_finite(tmp_path): + gguf = pytest.importorskip("gguf") + torch = pytest.importorskip("torch") + import numpy as np + f = tmp_path / "t.gguf" + w = gguf.GGUFWriter(str(f), "llama") + w.add_tensor("a", np.array([[70000.0, 1.0]], dtype=np.float32)) + w.write_header_to_file(); w.write_kv_data_to_file(); w.write_tensors_to_file(); w.close() + sd, _o, _n = gguf_support._load_native(str(f)) + assert torch.isfinite(sd["a"].float()).all() +