Files
olivv-cs-ComfyUI-FunPack/modules/sampling/prompt_nudge/tests/test_prompt_nudge.py
T

197 lines
7.2 KiB
Python

"""Taste prompt nudge: words shift along the learned direction, late, with no extra pass."""
import pytest
import torch
@pytest.fixture(autouse=True)
def _needs_comfy(comfyui):
"""Imports comfy."""
@pytest.fixture(autouse=True)
def root(tmp_path, monkeypatch):
from modules.system.taste import store
monkeypatch.setattr(store, "ROOT", tmp_path / "taste")
def _to(i, steps=4):
s = torch.linspace(1.0, 0.0, steps + 1)
return {"sample_sigmas": s, "sigmas": s[i:i + 1]}
def test_words_move_along_the_direction_by_a_share_of_their_own_size():
from modules.sampling.prompt_nudge import nudged
c = torch.ones(1, 3, 4)
d = torch.tensor([1.0, 0, 0, 0])
words = torch.tensor([True, True, False])
out = nudged(c, words, d, 0.1)
assert torch.allclose(out[0, 0, 0], c[0, 0, 0] + 0.1) # the strength is the row shift itself, as in v4
assert torch.equal(out[0, 2], c[0, 2]) # a reference image's row is left alone
assert torch.equal(out[0, :, 1:], c[0, :, 1:])
def _load(tiny, **v):
from comfy.patcher_extension import WrappersMP
from modules.sampling.modifiers.nodes import FunPackLoadModifiers
patched, status = FunPackLoadModifiers.execute(tiny.patcher, {
"taste": {"key": "fox"}, "prompt_nudge": {"enabled": True, **v}}).result
wrap = [w for ws in patched.wrappers[WrappersMP.APPLY_MODEL].values() for w in ws][-1]
for ws in patched.wrappers[WrappersMP.OUTER_SAMPLE].values():
for o in ws:
o(lambda: None)
return wrap, status
def _teach(dim):
from modules.system.taste import store
liked = torch.zeros(dim)
liked[0] = 1.0
for i in range(6):
store.capture("fox", "prompt_taste", {"pooled": liked if i % 2 else -liked}, prompt_id=f"t{i}")
store.rate(f"t{i}", "liked" if i % 2 else "disliked")
def test_late_steps_nudge_the_words_in_one_pass_and_early_ones_do_not(tiny_h3):
dim = 8
_teach(dim)
wrap, _ = _load(tiny_h3, strength=0.1)
c = torch.ones(1, 4, dim)
seen = []
def executor(x, t, c_concat=None, c_crossattn=None, control=None, transformer_options=None, **kw):
seen.append(c_crossattn.clone())
return x
x = torch.zeros(1, 1, 4)
ts = torch.linspace(1.0, 0.0, 5)
for i in (0, 3):
wrap(executor, x, ts[i:i + 1], None, c, None, _to(i))
assert len(seen) == 2 # no extra passes
assert torch.equal(seen[0], c) # first half: untouched
assert (seen[1][0, :, 0] > c[0, :, 0]).all() and torch.equal(seen[1][..., 1:], c[..., 1:])
def test_a_direction_from_another_text_width_is_said_not_silently_dropped(tiny_h3):
from core import log
_teach(8)
wrap, _ = _load(tiny_h3, strength=0.1)
log.new_run()
c = torch.ones(1, 4, 6)
seen = []
wrap(lambda x, t, *a, **k: seen.append(a[1]) or x, torch.zeros(1, 1, 4), torch.linspace(1.0, 0.0, 5)[3:4],
None, c, None, _to(3))
assert torch.equal(seen[0], c)
assert any("different model" in e["message"] for e in log.history())
def test_off_installs_nothing(tiny_h3):
from comfy.patcher_extension import WrappersMP
from modules.sampling.modifiers.nodes import FunPackLoadModifiers
patched, _ = FunPackLoadModifiers.execute(tiny_h3.patcher, {"prompt_nudge": {"enabled": False}}).result
assert not patched.wrappers.get(WrappersMP.APPLY_MODEL)
def test_the_sounds_text_channels_are_left_alone():
from modules.sampling.prompt_nudge import nudged, picture_widths
class Dm:
cross_attention_dim, audio_cross_attention_dim = 6, 2
class P:
model = type("M", (), {"diffusion_model": Dm()})()
c = torch.ones(1, 3, 8)
width = picture_widths(P())(c)
assert width == 6 and picture_widths(P())(torch.ones(1, 3, 5)) is None
out = nudged(c, torch.ones(3, dtype=torch.bool), torch.ones(8) / 8 ** 0.5, 1.0, width)
assert (out[..., :6] > 1).all() and torch.equal(out[..., 6:], c[..., 6:])
def test_a_split_negative_call_does_not_teach_the_negative_prompt(tiny_h3):
from modules.sampling.score_slider import cond_row
c = torch.ones(2, 3, 4)
assert cond_row(c, {"cond_or_uncond": [1, 0]}) == 1
assert cond_row(c[:1], {"cond_or_uncond": [1]}) is None
assert cond_row(c, {}) == 0
def test_a_run_that_never_nudged_says_so(tiny_h3):
from core import log
_teach(8)
wrap, _ = _load(tiny_h3, strength=0.1)
log.new_run()
c = torch.ones(1, 4, 8)
for i in range(2): # a two-step schedule: the late gate never opens
s = torch.linspace(1.0, 0.0, 3)
wrap(lambda x, t, *a, **k: x, torch.zeros(1, 1, 4), s[i:i + 1], None, c, None,
{"sample_sigmas": s, "sigmas": s[i:i + 1]})
assert any("Inactive" in e["message"] and "too short" in e["message"] for e in log.history())
def test_a_nudge_that_bf16_rounds_away_is_said(tiny_h3):
from core import log
_teach(8)
wrap, _ = _load(tiny_h3, strength=0.005)
log.new_run()
c = (torch.randn(1, 4, 8) * 50).to(torch.bfloat16)
s = torch.linspace(1.0, 0.0, 5)
wrap(lambda x, t, *a, **k: x, torch.zeros(1, 1, 4), s[3:4], None, c, None, _to(3))
assert any("rounding" in e["message"] for e in log.history())
def test_a_run_that_starts_with_the_negative_prompt_does_not_learn_from_it(tiny_h3):
_teach(8)
wrap, _ = _load(tiny_h3, strength=0.1, similar=True)
c = torch.ones(1, 4, 8)
s = torch.linspace(1.0, 0.0, 5)
seen = []
to = {**_to(3), "cond_or_uncond": [1]}
wrap(lambda x, t, *a, **k: seen.append(a[1]) or x, torch.zeros(1, 1, 4), s[3:4], None, c, None, to)
assert torch.equal(seen[0], c) # no positive prompt yet: nothing learned, nothing nudged
def test_strength_zero_only_learns_and_does_not_cry_wolf(tiny_h3):
from core import log
_teach(8)
wrap, status = _load(tiny_h3, strength=0.0)
log.new_run()
before = len(log.history())
c = torch.ones(1, 4, 8)
ts = torch.linspace(1.0, 0.0, 5)
seen = []
for i in range(4):
wrap(lambda x, t, *a, **k: seen.append(a[1]) or x, torch.zeros(1, 1, 4), ts[i:i + 1], None, c, None, _to(i))
assert all(torch.equal(s, c) for s in seen) and "learning only" in str(status)
assert not [e["message"] for e in log.history()[before:] if "nothing was nudged" in e["message"]]
def test_a_probe_does_not_teach_the_taste_key(tiny_h3):
from core import dit_hooks
from modules.system.taste import store
_teach(8)
wrap, _ = _load(tiny_h3, strength=0.1)
c = torch.ones(1, 4, 8)
ts = torch.linspace(1.0, 0.0, 5)
wrap(lambda x, t, *a, **k: x, torch.zeros(1, 1, 4), ts[3:4], None, c, None, {**_to(3), dit_hooks.PROBE: True})
pending = store._dir("fox") / "prompt_taste.pending.pt"
assert not pending.exists() # nothing was captured from the probe
def test_the_installed_wrapper_does_not_hold_its_patcher():
# v4 8bc7cb64: a wrapper closing over its own patcher keeps the model alive past the run ("Potential memory leak").
import gc, weakref
from modules.sampling import prompt_nudge as pn
class P:
model = None
p = P()
ref = weakref.ref(p)
fn = pn.picture_widths(p)
del p
gc.disable()
try:
assert ref() is None and fn is not None
finally:
gc.enable()