Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
160 lines
7.2 KiB
Python
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)
|