Old project/workflow names and browser settings migrate on load; /funpack redirects to /fun. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016acRss69eH1235eRdFTZHY
157 lines
6.5 KiB
Python
157 lines
6.5 KiB
Python
"""The attention override, carried all the way to a real attention call.
|
|
|
|
This is the seam the loader is most likely to get silently wrong: a bad override
|
|
loads perfectly and then fails at the first attention call, which is sampling.
|
|
None of it needs model weights, so there is no excuse for not running it.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def torch_and_attention(comfyui):
|
|
import torch
|
|
import comfy.ldm.modules.attention as attention
|
|
return torch, attention
|
|
|
|
|
|
@pytest.fixture
|
|
def qkv(torch_and_attention):
|
|
torch, _ = torch_and_attention
|
|
torch.manual_seed(0)
|
|
heads, dim = 2, 16
|
|
shape = (1, 8, heads * dim)
|
|
return torch.randn(shape), torch.randn(shape), torch.randn(shape), heads
|
|
|
|
|
|
def test_every_backend_this_machine_offers_survives_the_override(torch_and_attention, qkv):
|
|
"""The override is handed the WRAPPED function plus the wrapper's own kwargs
|
|
(`_inside_attn_wrapper` among them). If the unwrapped backend chokes on any
|
|
of that, choosing a backend breaks sampling and nothing else."""
|
|
torch, attention = torch_and_attention
|
|
from modules.loaders.common import attention_override
|
|
|
|
q, k, v, heads = qkv
|
|
reference = attention.get_attention_function("pytorch")(q, k, v, heads=heads)
|
|
|
|
for name in sorted(attention.REGISTERED_ATTENTION_FUNCTIONS):
|
|
override = attention_override(name)
|
|
assert override is not None, f"{name} is registered but produced no override"
|
|
out = override(attention.get_attention_function(name), q, k, v, heads=heads,
|
|
transformer_options={}, _inside_attn_wrapper=True)
|
|
assert out.shape == reference.shape, f"{name} changed the output shape"
|
|
|
|
|
|
def test_an_installed_override_is_actually_reached_by_comfys_dispatch(torch_and_attention, qkv):
|
|
"""Proves the KEY the loader writes is the key comfy reads. A typo here is
|
|
invisible: the model loads, sampling runs, and the chosen backend is simply
|
|
never used."""
|
|
torch, attention = torch_and_attention
|
|
from modules.loaders.common import attention_override
|
|
|
|
q, k, v, heads = qkv
|
|
inner = attention_override("pytorch")
|
|
reached = []
|
|
|
|
def spy(func, *args, **kwargs):
|
|
reached.append(True)
|
|
return inner(func, *args, **kwargs)
|
|
|
|
# Exactly the dict the diffusion loader writes into model_options.
|
|
transformer_options = {"optimized_attention_override": spy}
|
|
out = attention.optimized_attention(q, k, v, heads=heads,
|
|
transformer_options=transformer_options)
|
|
|
|
assert reached, "the override was installed where nothing reads it"
|
|
plain = attention.optimized_attention(q, k, v, heads=heads, transformer_options={})
|
|
assert torch.allclose(out, plain, atol=1e-5), (
|
|
"routing through the override changed the result")
|
|
|
|
|
|
def test_the_loader_writes_the_override_where_dispatch_looks(comfyui, monkeypatch):
|
|
"""The loader's own installation step, without loading a model: the key name
|
|
and its nesting under model_options are what this pins."""
|
|
import comfy.sd
|
|
import comfy.utils
|
|
import folder_paths
|
|
from comfy.ldm.modules import attention as attn
|
|
from modules.loaders.diffusion_model import nodes
|
|
|
|
class FakeModel:
|
|
def __init__(self):
|
|
self.model_options = {}
|
|
|
|
def wrapped(*args, **kwargs):
|
|
raise AssertionError("called the wrapped function, re-entering wrap_attn")
|
|
|
|
wrapped.__wrapped__ = lambda *a, **k: "attended"
|
|
monkeypatch.setattr(attn, "get_attention_function", lambda name, default: wrapped)
|
|
monkeypatch.setattr(folder_paths, "get_full_path_or_raise", lambda kind, name: f"/fake/{name}")
|
|
monkeypatch.setattr(comfy.utils, "load_torch_file", lambda p, **kw: ({}, None))
|
|
monkeypatch.setattr(comfy.sd, "load_diffusion_model_state_dict",
|
|
lambda sd, **kw: FakeModel())
|
|
|
|
out = nodes.FunUIDiffusionModelLoader.execute(
|
|
model_name="m.safetensors", weight_dtype="default",
|
|
compute_dtype="default", attention="pytorch")
|
|
|
|
model = out.result[0]
|
|
installed = model.model_options["transformer_options"]["optimized_attention_override"]
|
|
assert callable(installed)
|
|
assert installed(wrapped, "q") == "attended"
|
|
|
|
|
|
def test_a_model_comfy_could_not_identify_fails_at_the_loader(comfyui, monkeypatch):
|
|
"""comfy returns None instead of raising, and a None model reaches the
|
|
sampler before anything complains."""
|
|
import comfy.sd
|
|
import comfy.utils
|
|
import folder_paths
|
|
from modules.loaders.diffusion_model import nodes
|
|
|
|
monkeypatch.setattr(folder_paths, "get_full_path_or_raise", lambda kind, name: f"/fake/{name}")
|
|
monkeypatch.setattr(comfy.utils, "load_torch_file", lambda p, **kw: ({}, None))
|
|
monkeypatch.setattr(comfy.sd, "load_diffusion_model_state_dict", lambda sd, **kw: None)
|
|
|
|
with pytest.raises(RuntimeError, match="Could not detect a diffusion model"):
|
|
nodes.FunUIDiffusionModelLoader.execute(
|
|
model_name="actually_a_vae.safetensors", weight_dtype="default",
|
|
compute_dtype="default", attention="default")
|
|
|
|
|
|
def test_a_backends_zero_copy_path_is_forwarded_to_the_override(comfyui, monkeypatch):
|
|
"""`wrap_attn` offers the container fast path only when the OVERRIDE carries
|
|
`container_function`. Dropping it is invisible -- the backend is selected and
|
|
the maths is right, while the path it exists for is never used. ComfyUI's own
|
|
set_model_optimized_attention forwards it for exactly this reason."""
|
|
from comfy.ldm.modules import attention as attn
|
|
from modules.loaders import common
|
|
|
|
def backend(*args, **kwargs):
|
|
raise AssertionError("took the slow path")
|
|
|
|
backend.__wrapped__ = lambda *a, **k: "slow"
|
|
backend.container_function = lambda *a, **k: "fast"
|
|
|
|
monkeypatch.setattr(attn, "get_attention_function", lambda name, default: backend)
|
|
override = common.attention_override("kitchen")
|
|
|
|
assert getattr(override, "container_function", None) is not None
|
|
assert override.container_function() == "fast"
|
|
|
|
|
|
def test_a_backend_without_a_container_path_does_not_grow_one(comfyui, monkeypatch):
|
|
from comfy.ldm.modules import attention as attn
|
|
from modules.loaders import common
|
|
|
|
def backend(*args, **kwargs):
|
|
return "x"
|
|
|
|
backend.__wrapped__ = lambda *a, **k: "x"
|
|
monkeypatch.setattr(attn, "get_attention_function", lambda name, default: backend)
|
|
|
|
override = common.attention_override("plain")
|
|
# hasattr is what wrap_attn tests, so an attribute set to None would still
|
|
# divert every call into a container path that does not exist.
|
|
assert not hasattr(override, "container_function")
|