Files
olivv-cs-ComfyUI-FunPack/modules/sampling/velocity_bias/tests/test_velocity_bias.py
T

160 lines
7.2 KiB
Python

"""Velocity bias: the structure step, the reference, the size-keeping rotation, learning through ratings."""
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, sched):
return {"sample_sigmas": sched, "sigmas": sched[i:i + 1]}
def test_only_the_step_nearest_ninety_percent_of_the_noise_is_the_structure_step():
from modules.sampling.velocity_bias import structure_step
sched = torch.tensor([1.0, 0.909375, 0.725, 0.421875, 0.0]) # LTX distilled
assert [structure_step(_to(i, sched)) for i in range(4)] == [False, True, False, False]
far = torch.tensor([1.0, 0.6, 0.3, 0.0]) # nothing within the window
assert not any(structure_step(_to(i, far)) for i in range(3))
assert not structure_step({})
def test_the_reference_is_the_average_or_the_nearest_liked_prompt_at_this_shape():
from modules.sampling.velocity_bias import reference
a, b = torch.ones(2, 2), -torch.ones(2, 2)
sa, sb = torch.tensor([1.0, 0.0]), torch.tensor([0.0, 1.0])
rows = [{"reward": 1.0, "rows": {"v": a.half(), "sig": sa}},
{"reward": 1.0, "rows": {"v": b.half(), "sig": sb}},
{"reward": -1.0, "rows": {"v": a.half() * 5, "sig": sa}}, # a disliked clip never counts
{"reward": 1.0, "rows": {"v": torch.ones(3, 3).half(), "sig": sa}}] # another shape never counts
mean, how = reference(rows, (2, 2), sa, nearest=False)
assert torch.allclose(mean, torch.zeros(2, 2)) and "average of 2" in how
near, how = reference(rows, (2, 2), sa, nearest=True)
assert torch.allclose(near, a) and "nearest" in how
assert reference(rows, (5, 5), sa, False)[0] is None and "size" in reference(rows, (5, 5), sa, False)[1]
assert reference([], (2, 2), sa, False)[0] is None
def test_the_rotation_keeps_the_size_is_capped_and_fades():
from modules.sampling.velocity_bias import rotate
torch.manual_seed(0)
x, d = torch.randn(1, 4, 3, 8, 8), torch.randn(1, 4, 3, 8, 8)
out = rotate(x, d, strength=3.0, ratio=0.9)
assert torch.allclose(out.norm(), x.norm(), rtol=1e-4) # no energy added
assert float((out - x).norm() / x.norm()) < 1.0 # never wipes the clip
small = rotate(x, d, strength=0.05, ratio=0.9)
assert float((small - x).norm()) < float((out - x).norm())
assert torch.equal(rotate(x, d, strength=1.0, ratio=0.0), x) # no noise left: nothing to turn
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"}, "velocity_bias": {"enabled": True, **v}}).result
wrap = [w for ws in patched.wrappers[WrappersMP.APPLY_MODEL].values() for w in ws][-1]
outer = [w for ws in patched.wrappers[WrappersMP.OUTER_SAMPLE].values() for w in ws]
return wrap, outer
def test_a_run_banks_its_motion_and_once_liked_the_next_run_is_turned_toward_it(tiny_h3):
from conftest import packed_av
from modules.system.taste import store
sched = torch.tensor([1.0, 0.909375, 0.725, 0.421875, 0.0])
torch.manual_seed(0)
video, audio = torch.randn(1, 4, 3, 8, 8), torch.randn(1, 8, 5)
x0, shapes = packed_av(video, audio)
c = torch.ones(1, 4, 8)
answer = x0 * 0.6 + torch.randn_like(x0) # a direction that is not just x itself
def run(pid, rate=None):
wrap, outer = _load(tiny_h3, strength=1.0)
seen = []
import core.log as log
log.new_run()
def body():
for i in range(4):
wrap(lambda x, *a, **k: seen.append(x) or answer, x0, sched[i:i + 1], None, c, None,
_to(i, sched), latent_shapes=shapes)
store.current_prompt_id = lambda: pid
def nest(i): # the wrappers wrap each other, the body runs once inside them all
return body() if i == len(outer) else outer[i](lambda: nest(i + 1))
nest(0)
if rate:
store.rate(pid, rate)
return seen
first = run("r1", "liked")
assert all(torch.equal(s, x0) for s in first) # nothing banked yet: untouched
second = run("r2")
assert torch.equal(second[0], x0) and torch.equal(second[2], x0) and torch.equal(second[3], x0)
assert not torch.equal(second[1], x0) # only the structure step is turned
from conftest import unpacked
assert torch.equal(unpacked(second[1], shapes)[1], unpacked(x0, shapes)[1]) # sound untouched
def test_strength_zero_only_learns(tiny_h3):
wrap, _ = _load(tiny_h3, strength=0.0)
from conftest import packed_av
x0, shapes = packed_av(torch.randn(1, 4, 3, 8, 8), torch.randn(1, 8, 5))
sched = torch.tensor([1.0, 0.909375, 0.725, 0.421875, 0.0])
seen = []
wrap(lambda x, *a, **k: seen.append(x) or x0, x0, sched[1:2], None, None, None, _to(1, sched), latent_shapes=shapes)
assert torch.equal(seen[0], x0)
def _packed(shapes_seed=0):
from conftest import packed_av
torch.manual_seed(shapes_seed)
return packed_av(torch.randn(1, 4, 3, 8, 8), torch.randn(1, 8, 5))
def test_a_schedule_with_no_structure_step_says_so_on_its_last_step(tiny_h3):
from core import log
wrap, _ = _load(tiny_h3, strength=1.0)
x0, shapes = _packed()
sched = torch.tensor([1.0, 0.75, 0.5, 0.25, 0.0])
log.new_run()
before = len(log.history())
for i in range(4):
wrap(lambda x, *a, **k: x0, x0, sched[i:i + 1], None, None, None, _to(i, sched), latent_shapes=shapes)
assert any("no step of this schedule is near 90%" in e["message"] for e in log.history()[before:])
def test_context_windows_and_second_order_calls_are_refused_in_words(tiny_h3):
from core import log
x0, shapes = _packed()
sched = torch.tensor([1.0, 0.909375, 0.725, 0.421875, 0.0])
wrap, _ = _load(tiny_h3, strength=1.0)
log.new_run()
wrap(lambda x, *a, **k: x0, x0, sched[1:2], None, None, None, {**_to(1, sched), "context_window": object()},
latent_shapes=shapes)
assert any("context windows" in e["message"] for e in log.history())
wrap2, outer = _load(tiny_h3, strength=1.0)
log.new_run()
for o in outer[:1]:
o(lambda: None) # a run starts: the state is fresh
for _ in range(2): # the same structure step twice: a corrector call
wrap2(lambda x, *a, **k: x0, x0, sched[1:2], None, None, None, _to(1, sched), latent_shapes=shapes)
assert any("more than once per step" in e["message"] for e in log.history())
def test_a_call_that_carries_only_the_negative_prompt_neither_biases_nor_learns(tiny_h3):
wrap, _ = _load(tiny_h3, strength=1.0)
x0, shapes = _packed()
sched = torch.tensor([1.0, 0.909375, 0.725, 0.421875, 0.0])
seen = []
to = {**_to(1, sched), "cond_or_uncond": [1]}
wrap(lambda x, *a, **k: seen.append(x) or x0, x0, sched[1:2], None, torch.ones(1, 4, 8), None, to, latent_shapes=shapes)
assert torch.equal(seen[0], x0)