Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
182 lines
7.9 KiB
Python
182 lines
7.9 KiB
Python
"""Late-branch guidance on the real tiny H3: which blocks the weak copy runs, and where the push goes."""
|
|
|
|
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")
|
|
monkeypatch.setattr(store, "current_prompt_id", lambda: "run-1")
|
|
|
|
|
|
def _load(tiny, **values):
|
|
from comfy.patcher_extension import WrappersMP
|
|
from modules.sampling.modifiers.nodes import FunPackLoadModifiers
|
|
settings = {"taste": {"key": "fox"}, "late_branch": {"enabled": True, "block": 2, **values}}
|
|
patched, status = FunPackLoadModifiers.execute(tiny.patcher, settings).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 patched, wrap, outer, status
|
|
|
|
|
|
def _executor(tiny, patched, shapes):
|
|
"""The real tiny H3 forward, packed the way comfy hands it back."""
|
|
from conftest import packed_av
|
|
calls = []
|
|
|
|
def executor(x, t, c_concat=None, c_crossattn=None, control=None, transformer_options=None, **kw):
|
|
calls.append(dict(transformer_options or {}))
|
|
sigma = float(t.max())
|
|
video, audio = tiny.run(patched, sigma=sigma, sigmas=torch.tensor([1.0, 0.75, 0.5, 0.25, 0.0]),
|
|
options={k: v for k, v in (transformer_options or {}).items()
|
|
if k in ("funpack_weak_branch", "funpack_probe")})
|
|
return packed_av(video, audio)[0]
|
|
return executor, calls
|
|
|
|
|
|
def _run(outer, fn):
|
|
call = fn
|
|
for o in reversed(outer):
|
|
call = (lambda w, inner: (lambda: w(lambda: inner())))(o, call)
|
|
return call()
|
|
|
|
|
|
def _to(i, steps=4, **extra):
|
|
s = torch.tensor([1.0, 0.75, 0.5, 0.25, 0.0])
|
|
return {"sample_sigmas": s, "sigmas": s[i:i + 1], "cond_or_uncond": [0], **extra}
|
|
|
|
|
|
def _setup(tiny_h3, **values):
|
|
from conftest import packed_av
|
|
patched, wrap, outer, status = _load(tiny_h3, **values)
|
|
x0, shapes = packed_av(tiny_h3.video, tiny_h3.audio)
|
|
executor, calls = _executor(tiny_h3, patched, shapes)
|
|
return patched, wrap, outer, x0, shapes, executor, calls
|
|
|
|
|
|
def test_the_weak_copy_runs_only_the_blocks_after_the_branch_and_the_answer_stays_the_models(tiny_h3):
|
|
patched, wrap, outer, x0, shapes, executor, _calls = _setup(tiny_h3, mode="manual", strength=1.0)
|
|
ran = []
|
|
blocks = patched.model.diffusion_model.blocks
|
|
for i, b in enumerate(blocks):
|
|
orig = b.forward
|
|
b.forward = (lambda orig, i: (lambda *a, **k: (ran.append(i), orig(*a, **k))[1]))(orig, i)
|
|
|
|
def go(i):
|
|
t = torch.tensor([[1.0, 0.75, 0.5, 0.25][i]])
|
|
return wrap(executor, x0, t, None, None, None, _to(i), latent_shapes=shapes)
|
|
|
|
answers = _run(outer, lambda: [go(i) for i in range(4)])
|
|
n = len(blocks)
|
|
plain = [i for i in ran]
|
|
# steps 0-2 each: all blocks once (normal) + only blocks after the branch (weak); step 3 is final: normal only
|
|
assert plain.count(n - 1) == 3 * 2 + 1 and plain.count(0) == 4
|
|
assert plain.count(2) == 4 # the branch block runs only in the normal pass
|
|
assert all(a.shape == x0.shape for a in answers)
|
|
|
|
|
|
def test_the_push_rides_into_the_next_input_in_the_picture_rows_only(tiny_h3):
|
|
patched, wrap, outer, x0, shapes, executor, _ = _setup(tiny_h3, mode="manual", strength=1.0)
|
|
seen = []
|
|
|
|
def spy(x, t, *a, **k):
|
|
seen.append(x.clone())
|
|
return executor(x, t, *a, **k)
|
|
|
|
def go(i):
|
|
t = torch.tensor([[1.0, 0.75, 0.5, 0.25][i]])
|
|
return wrap(spy, x0, t, None, None, None, _to(i), latent_shapes=shapes)
|
|
|
|
_run(outer, lambda: [go(i) for i in range(4)])
|
|
# normal inputs of steps 0..3 are the 1st, 3rd, 5th, 7th spy entries (weak passes in between)
|
|
inputs = [seen[0], seen[2], seen[4], seen[6]]
|
|
assert torch.equal(inputs[0], x0) # step 1: nothing pushed yet
|
|
from conftest import unpacked
|
|
for i in (1, 2, 3): # every step but the last makes a push
|
|
push = unpacked(inputs[i] - x0, shapes)
|
|
assert not torch.allclose(push[0], torch.zeros_like(push[0]))
|
|
assert torch.equal(push[1], torch.zeros_like(push[1])) # sound untouched
|
|
|
|
|
|
def test_probe_calls_and_negative_calls_are_left_unguided_and_weak_calls_are_marked(tiny_h3):
|
|
from core import dit_hooks
|
|
patched, wrap, outer, x0, shapes, executor, calls = _setup(tiny_h3, mode="manual", strength=1.0)
|
|
|
|
def go(to):
|
|
return wrap(executor, x0, torch.tensor([0.5]), None, None, None, to, latent_shapes=shapes)
|
|
|
|
_run(outer, lambda: go(_to(1, **{dit_hooks.PROBE: True})))
|
|
assert len(calls) == 1
|
|
calls.clear()
|
|
_run(outer, lambda: go(_to(1, cond_or_uncond=[0, 1])))
|
|
assert len(calls) == 1
|
|
calls.clear()
|
|
_run(outer, lambda: go(_to(1)))
|
|
assert len(calls) == 2 and not calls[0].get("funpack_weak_branch")
|
|
assert calls[1]["funpack_weak_branch"] and dit_hooks.probing(calls[1])
|
|
|
|
|
|
def test_learned_mode_banks_strength_effect_and_branch_after_a_felt_push_only(tiny_h3, monkeypatch):
|
|
from core import input_steer
|
|
from modules.system.taste import store
|
|
monkeypatch.setattr(input_steer, "FLOOR", 0.0)
|
|
patched, wrap, outer, x0, shapes, executor, _ = _setup(tiny_h3)
|
|
|
|
def go(i):
|
|
t = torch.tensor([[1.0, 0.75, 0.5, 0.25][i]])
|
|
return wrap(executor, x0, t, None, None, None, _to(i), latent_shapes=shapes)
|
|
|
|
_run(outer, lambda: [go(i) for i in range(4)])
|
|
assert store.rate("run-1", "liked")["recorded"] == ["late_branch"]
|
|
row = store.load("fox", "late_branch")["rows"][0]["rows"]
|
|
assert 0.0 <= float(row["v"]) <= 1.5 and float(row["e"]) > 0 and int(row["b"]) == 2
|
|
|
|
|
|
def test_a_push_too_small_to_feel_teaches_nothing(tiny_h3, monkeypatch):
|
|
from core import input_steer
|
|
from modules.system.taste import store
|
|
monkeypatch.setattr(input_steer, "FLOOR", 10.0) # nothing can be big enough
|
|
patched, wrap, outer, x0, shapes, executor, _ = _setup(tiny_h3)
|
|
_run(outer, lambda: [wrap(executor, x0, torch.tensor([[1.0, 0.75, 0.5, 0.25][i]]), None, None, None,
|
|
_to(i), latent_shapes=shapes) for i in range(4)])
|
|
assert store.rate("run-1", "liked")["why"] # nothing was captured
|
|
|
|
|
|
def test_strength_zero_runs_no_weak_copy_and_the_mix_pushes_away_from_the_weak_one():
|
|
from modules.sampling.late_branch import mix
|
|
n, w = torch.tensor([2.0]), torch.tensor([1.0])
|
|
assert float(mix(n, w, 0.5)) == 2.5 and float(mix(n, w, 0.0)) == 2.0
|
|
|
|
|
|
def test_manual_strength_zero_makes_one_call_per_step(tiny_h3):
|
|
patched, wrap, outer, x0, shapes, executor, calls = _setup(tiny_h3, mode="manual", strength=0.0)
|
|
_run(outer, lambda: wrap(executor, x0, torch.tensor([0.5]), None, None, None, _to(1), latent_shapes=shapes))
|
|
assert len(calls) == 1
|
|
|
|
|
|
def test_a_branch_block_outside_the_model_is_refused_and_said(tiny_h3):
|
|
from comfy.patcher_extension import WrappersMP
|
|
from core import log
|
|
from modules.sampling.modifiers.nodes import FunPackLoadModifiers
|
|
log.new_run()
|
|
patched, _ = FunPackLoadModifiers.execute(tiny_h3.patcher, {
|
|
"taste": {"key": "fox"}, "late_branch": {"enabled": True, "block": 43}}).result
|
|
assert any("not a branch point" in e["message"] for e in log.history())
|
|
assert not patched.wrappers.get(WrappersMP.APPLY_MODEL)
|
|
|
|
|
|
def test_a_failure_after_the_weak_copy_returns_the_models_own_answer_not_the_weak_one(tiny_h3, monkeypatch):
|
|
from modules.sampling import late_branch
|
|
patched, wrap, outer, x0, shapes, executor, calls = _setup(tiny_h3, mode="manual", strength=1.0)
|
|
plain = executor(x0, torch.tensor([0.5]))
|
|
monkeypatch.setattr(late_branch, "mix", lambda *a: (_ for _ in ()).throw(RuntimeError("boom")))
|
|
out = _run(outer, lambda: wrap(executor, x0, torch.tensor([0.5]), None, None, None, _to(1), latent_shapes=shapes))
|
|
assert torch.equal(out, plain)
|