Every one of them was a real test of a live feature. 18 failed only in a full-suite run and passed alone, 4 asserted behaviour that was deliberately changed and never got updated. Ordering (18): each test module built its own partial `comfy` stub and registered it with `sys.modules.setdefault(...)`, so the first module to import won and every later module's richer stub was silently discarded — the scene chain sampler then died on `comfy` having no `.sample`, and the mask-safe attention install bailed at its first guard because `import comfy.ldm.modules.attention` resolves through the PARENT's attribute, which one module set and another did not. tests/_comfy_stubs.py now builds that package tree once, wiring every parent attribute, and test modules attach their fakes as attributes on the shared modules (per-test, via fixtures, where the fake is only theirs — the idiom test_detailing.py already used). Same file also stubs `server.PromptServer`, whose absence made every module that imports templates.py (i.e. anything importing conditioning) fail on `No module named 'server'`. Stale assertions (4): * the "every learning rating" loop excluded non-learning ratings by hardcoded LABEL, so the later-added `Wrong appearance|loved` and the two control sentinels were asserted to train phrase memory. Now filtered by profile key, with the reason per key. * three split_by_transitions tests still used "scene ten" / "cut to" as cut markers. The unified splitter cuts on a real transition trigger or the generic `scene <N>` label, on purpose — an incidental phrase must never split a prompt. Rewritten on the current contract, and they now also pin that the delimiter is dropped rather than encoded. Verified: 584 pass, and every file passes alone, in reverse order, and with movie_editor's suite standalone. Mutation-checked the rewritten split tests (disabling the scene-label pattern fails 4 of 5) so they are not passing vacuously. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
136 lines
5.5 KiB
Python
136 lines
5.5 KiB
Python
"""Unit tests for the Blackwell (sm_120) xformers masked-attention fallback in samplers.py.
|
|
|
|
On GPUs newer than capability (9, 0) xformers has no kernel that accepts a tensor attn_bias,
|
|
so i2v ANCHOR scenes (unmasked attention) generate fine but GUIDE scenes (LTX guide path
|
|
passes a tensor mask) crash. _funpack_install_mask_safe_attention installs a per-call override
|
|
(ComfyUI wrap_attn) that routes only MASKED attention to SDPA when that exact combo is present.
|
|
|
|
Covers: override routing (masked→pytorch, unmasked→original, mask as kwarg or positional),
|
|
and the install gating (only xformers + too-new GPU; respects/ignores other backends, existing
|
|
overrides, missing CUDA; idempotent).
|
|
"""
|
|
import sys
|
|
import types
|
|
from pathlib import Path
|
|
|
|
import _comfy_stubs
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
|
import samplers # noqa: E402
|
|
|
|
|
|
def _install_attn_module(*, backend="xformers", with_pytorch_sentinel=True):
|
|
"""Point comfy.ldm.modules.attention at a fake with a selectable active backend.
|
|
|
|
Installed through _comfy_stubs.install_module so the PARENT attribute moves too. Registering
|
|
only the sys.modules entry is not enough: `import comfy.ldm.modules.attention as _am`
|
|
(what samplers.py does) resolves through `comfy.ldm.modules.attention` the attribute, so
|
|
a stub left behind by another test module kept winning here and the install under test
|
|
bailed out at its first guard.
|
|
"""
|
|
def attention_xformers(*a, **kw):
|
|
return "xformers"
|
|
|
|
def attention_pytorch(*a, **kw):
|
|
return "pytorch"
|
|
|
|
return _comfy_stubs.install_module(
|
|
"comfy.ldm.modules.attention",
|
|
attention_xformers=attention_xformers,
|
|
attention_pytorch=attention_pytorch,
|
|
optimized_attention=attention_xformers if backend == "xformers" else attention_pytorch,
|
|
)
|
|
|
|
|
|
def _fake_model():
|
|
return types.SimpleNamespace(model_options={})
|
|
|
|
|
|
def _force_capability(monkeypatch, cap, available=True):
|
|
cuda = types.SimpleNamespace(
|
|
is_available=lambda: available,
|
|
get_device_capability=lambda *a, **k: cap,
|
|
)
|
|
monkeypatch.setattr(samplers.torch, "cuda", cuda, raising=False)
|
|
|
|
|
|
# ── override routing ──────────────────────────────────────────────────────────
|
|
def test_override_routes_masked_to_pytorch_kwarg():
|
|
_install_attn_module()
|
|
sentinel = object()
|
|
out = samplers._funpack_mask_safe_attention_override(
|
|
lambda *a, **k: "xformers", "q", "k", "v", 8, mask=sentinel,
|
|
)
|
|
assert out == "pytorch"
|
|
|
|
|
|
def test_override_routes_masked_to_pytorch_positional():
|
|
_install_attn_module()
|
|
# (q, k, v, heads, mask) — mask as the 5th positional arg.
|
|
out = samplers._funpack_mask_safe_attention_override(
|
|
lambda *a, **k: "xformers", "q", "k", "v", 8, object(),
|
|
)
|
|
assert out == "pytorch"
|
|
|
|
|
|
def test_override_passes_unmasked_to_original():
|
|
_install_attn_module()
|
|
out = samplers._funpack_mask_safe_attention_override(
|
|
lambda *a, **k: "xformers", "q", "k", "v", 8, mask=None,
|
|
)
|
|
assert out == "xformers"
|
|
|
|
|
|
# ── install gating ────────────────────────────────────────────────────────────
|
|
def test_installs_on_xformers_and_blackwell(monkeypatch):
|
|
_install_attn_module(backend="xformers")
|
|
_force_capability(monkeypatch, (12, 0))
|
|
m = _fake_model()
|
|
samplers._funpack_install_mask_safe_attention(m)
|
|
assert (m.model_options["transformer_options"]["optimized_attention_override"]
|
|
is samplers._funpack_mask_safe_attention_override)
|
|
|
|
|
|
def test_no_install_on_supported_capability(monkeypatch):
|
|
_install_attn_module(backend="xformers")
|
|
_force_capability(monkeypatch, (9, 0)) # Hopper — xformers tensor-bias kernels OK
|
|
m = _fake_model()
|
|
samplers._funpack_install_mask_safe_attention(m)
|
|
assert "optimized_attention_override" not in m.model_options.get("transformer_options", {})
|
|
|
|
|
|
def test_no_install_when_backend_not_xformers(monkeypatch):
|
|
_install_attn_module(backend="pytorch") # user already on SDPA
|
|
_force_capability(monkeypatch, (12, 0))
|
|
m = _fake_model()
|
|
samplers._funpack_install_mask_safe_attention(m)
|
|
assert "optimized_attention_override" not in m.model_options.get("transformer_options", {})
|
|
|
|
|
|
def test_no_install_without_cuda(monkeypatch):
|
|
_install_attn_module(backend="xformers")
|
|
_force_capability(monkeypatch, (12, 0), available=False)
|
|
m = _fake_model()
|
|
samplers._funpack_install_mask_safe_attention(m)
|
|
assert "optimized_attention_override" not in m.model_options.get("transformer_options", {})
|
|
|
|
|
|
def test_does_not_stomp_existing_override(monkeypatch):
|
|
_install_attn_module(backend="xformers")
|
|
_force_capability(monkeypatch, (12, 0))
|
|
other = lambda func, *a, **k: func(*a, **k)
|
|
m = types.SimpleNamespace(model_options={"transformer_options": {"optimized_attention_override": other}})
|
|
samplers._funpack_install_mask_safe_attention(m)
|
|
assert m.model_options["transformer_options"]["optimized_attention_override"] is other
|
|
|
|
|
|
def test_idempotent(monkeypatch):
|
|
_install_attn_module(backend="xformers")
|
|
_force_capability(monkeypatch, (12, 0))
|
|
m = _fake_model()
|
|
samplers._funpack_install_mask_safe_attention(m)
|
|
samplers._funpack_install_mask_safe_attention(m)
|
|
assert (m.model_options["transformer_options"]["optimized_attention_override"]
|
|
is samplers._funpack_mask_safe_attention_override)
|