Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tw9fjXySkmNCh7j6UXrFYR
130 lines
5.7 KiB
Python
130 lines
5.7 KiB
Python
import json
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _needs_comfy(comfyui):
|
|
"""Imports comfy."""
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _folder(tmp_path, monkeypatch):
|
|
from modules.sampling import run_meter
|
|
monkeypatch.setattr(run_meter, "FOLDER", tmp_path / "run_meter")
|
|
|
|
|
|
def _load(tiny, **v):
|
|
from comfy.patcher_extension import WrappersMP
|
|
from modules.sampling.modifiers.nodes import FunUILoadModifiers
|
|
patched, status = FunUILoadModifiers.execute(tiny.patcher, {"run_meter": {"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][-1]
|
|
return wrap, outer, status
|
|
|
|
|
|
def _to(i, steps=4):
|
|
s = torch.linspace(1.0, 0.0, steps + 1)
|
|
return {"sample_sigmas": s, "sigmas": s[i:i + 1]}
|
|
|
|
|
|
def _run(wrap, outer, answers, seed=7):
|
|
ts = torch.linspace(1.0, 0.0, len(answers) + 1)
|
|
x = torch.zeros_like(answers[0])
|
|
|
|
class Run: # what OUTER_SAMPLE wraps: the whole sampling call
|
|
def __call__(self, *a, **k):
|
|
for i, o in enumerate(answers):
|
|
got = wrap(lambda *aa, o=o, **kk: o, x, ts[i:i + 1], None, None, None, _to(i, len(answers)))
|
|
assert got is o # the answer is never changed
|
|
return "done"
|
|
|
|
return outer(Run(), None, None, None, None, None, None, None, seed)
|
|
|
|
|
|
def test_off_installs_nothing(tiny_h3):
|
|
from comfy.patcher_extension import WrappersMP
|
|
from modules.sampling.modifiers.nodes import FunUILoadModifiers
|
|
patched, _ = FunUILoadModifiers.execute(tiny_h3.patcher, {"run_meter": {"enabled": False}}).result
|
|
assert not patched.wrappers.get(WrappersMP.APPLY_MODEL)
|
|
|
|
|
|
def test_noise_reads_grainier_than_a_smooth_picture_and_settling_falls_to_zero(tiny_h3):
|
|
from modules.sampling.run_meter import measure
|
|
smooth = torch.linspace(0, 1, 8).expand(1, 4, 3, 8, 8).contiguous()
|
|
noisy = torch.randn(1, 4, 3, 8, 8)
|
|
assert measure(smooth)[0] < 0.3 * measure(noisy)[0]
|
|
across, over, settle = measure(noisy, noisy)
|
|
assert float(settle) == 0.0 and float(over) > 0.0
|
|
|
|
|
|
def test_a_run_is_logged_saved_and_compared_with_the_previous_run_of_the_same_seed(tiny_h3, tmp_path):
|
|
from modules.sampling import run_meter
|
|
from modules._core import log
|
|
wrap, outer, status = _load(tiny_h3)
|
|
assert "run_meter" in status
|
|
base = torch.randn(1, 4, 3, 8, 8)
|
|
answers = [base * f for f in (0.9, 0.5, 0.2, 0.1)]
|
|
assert _run(wrap, outer, answers) == "done"
|
|
saved = json.loads((tmp_path / "run_meter" / "last.json").read_text())
|
|
assert saved["seed"] == 7 and saved["steps"] == 4 and saved["final"]["settle"] is not None
|
|
assert not (tmp_path / "run_meter" / "last.json.tmp").exists()
|
|
assert _run(wrap, outer, answers) == "done"
|
|
said = [r["message"] for r in log._records if r["source"] == run_meter.TITLE]
|
|
assert "nothing saved to compare with yet" in said[-2]
|
|
assert "against the previous run of this seed" in said[-1] and "same" in said[-1]
|
|
assert _run(wrap, outer, answers, seed=8) == "done"
|
|
assert "another seed" in [r["message"] for r in log._records if r["source"] == run_meter.TITLE][-1]
|
|
|
|
|
|
def test_a_bigger_picture_after_the_hand_over_has_no_settling_to_compare(tiny_h3):
|
|
from modules.sampling import run_meter
|
|
from modules._core import log
|
|
wrap, outer, _ = _load(tiny_h3)
|
|
small, big = torch.randn(1, 4, 3, 4, 4), torch.randn(1, 4, 3, 8, 8)
|
|
assert _run(wrap, outer, [small, small * 0.5, big, big * 0.5]) == "done"
|
|
text = [r["message"] for r in log._records if r["source"] == run_meter.TITLE][-1]
|
|
assert "8x8" in text and "n/a (first call at this size)" in text
|
|
|
|
|
|
def test_a_stopped_run_saves_nothing(tiny_h3, tmp_path):
|
|
wrap, outer, _ = _load(tiny_h3)
|
|
|
|
def stops(*a, **k):
|
|
wrap(lambda *aa, **kk: torch.randn(1, 4, 3, 8, 8), torch.zeros(1, 4, 3, 8, 8), torch.ones(1), None, None, None, _to(0))
|
|
raise KeyboardInterrupt
|
|
|
|
with pytest.raises(KeyboardInterrupt):
|
|
outer(stops, None, None, None, None, None, None, None, 1)
|
|
assert not (tmp_path / "run_meter").exists()
|
|
|
|
|
|
def test_a_second_call_in_a_step_does_not_replace_the_first(tiny_h3):
|
|
from modules.sampling import run_meter
|
|
from modules._core import log
|
|
wrap, outer, _ = _load(tiny_h3)
|
|
a, b = torch.randn(1, 4, 3, 8, 8), torch.randn(1, 4, 3, 8, 8)
|
|
ts = torch.linspace(1.0, 0.0, 3)
|
|
|
|
def run(*args, **k):
|
|
for i, o in ((0, a), (0, b), (1, a * 0.5)): # step 0 is asked twice, then step 1
|
|
wrap(lambda *aa, o=o, **kk: o, torch.zeros_like(a), ts[i:i + 1], None, None, None, _to(i, 2))
|
|
|
|
outer(run, None, None, None, None, None, None, None, 1)
|
|
text = [r["message"] for r in log._records if r["source"] == run_meter.TITLE][-1]
|
|
assert text.count("step 1/2") == 1 and text.count("step 2/2") == 1
|
|
|
|
|
|
def test_tensors_follow_the_input_device_and_dtype():
|
|
"""Device safety: nothing here is created on a fixed device, so a meta tensor passes through and a half one stays half."""
|
|
from modules.sampling.run_meter import measure
|
|
for dtype in (torch.float16, torch.bfloat16):
|
|
v = torch.randn(1, 4, 3, 8, 8).to(dtype)
|
|
across, over, settle = measure(v, v.float()) # the previous answer may sit in another dtype
|
|
assert across.device == v.device and across.dtype == torch.float32
|
|
assert float(settle) == 0.0 or float(settle) < 1e-2
|
|
meta = torch.empty(1, 4, 3, 8, 8, device="meta")
|
|
across, over, settle = measure(meta, torch.empty(1, 4, 3, 8, 8, device="cpu"))
|
|
assert across.device.type == "meta" and settle.device.type == "meta"
|