Files
olivv-cs-ComfyUI-FunPack/modules/sampling/block_repeat/tests/test_block_repeat.py
T
Claude 3adebb731f Rename FunPack to FunUI (5.0.3 Classy Chopper): /fun endpoint, wink logo, load-time migration of old names
Old project/workflow names and browser settings migrate on load; /funpack redirects to /fun.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016acRss69eH1235eRdFTZHY
2026-10-25 13:11:03 +00:00

95 lines
3.5 KiB
Python

"""Block repeat on a real H3 forward."""
import pytest
import torch
@pytest.fixture(autouse=True)
def _needs_comfy(comfyui):
"""Imports comfy."""
def _load(tiny, patcher=None, **values):
from modules.sampling.modifiers.nodes import FunUILoadModifiers
return FunUILoadModifiers.execute(
patcher or tiny.patcher, {"block_repeat": {"enabled": True, **values}}).result
def _count_block_calls(tiny, patched, **run):
"""How many times each real block ran during one forward."""
counts = {}
handles = []
for i, block in enumerate(tiny.patcher.model.diffusion_model.blocks):
handles.append(block.register_forward_pre_hook(
lambda m, a, i=i: counts.__setitem__(i, counts.get(i, 0) + 1)))
try:
tiny.run(patched, **run)
finally:
for h in handles:
h.remove()
return counts
def test_each_runs_every_named_block_times_plus_one(tiny_h3):
patched, status = _load(tiny_h3, blocks="1-2", times=2)
assert "block_repeat: blocks 1,2 run 3x each" in status
assert _count_block_calls(tiny_h3, patched) == {0: 1, 1: 3, 2: 3, 3: 1}
def test_span_loops_the_range_and_runs_each_block_once_per_loop(tiny_h3):
patched, status = _load(tiny_h3, blocks="1-2", shape="span", times=1)
assert "blocks 1-2 loop 2x as one span" in status
assert _count_block_calls(tiny_h3, patched) == {0: 1, 1: 2, 2: 2, 3: 1}
def test_span_output_equals_running_the_span_twice_by_hand(tiny_h3):
"""The loop really feeds block 2's output back into block 1."""
patched, _ = _load(tiny_h3, blocks="1-2", shape="span")
looped = tiny_h3.run(patched)
# By hand: blocks 0,1,2,1,2,3 in order.
from core import dit_hooks
manual = tiny_h3.patcher.clone()
real = tiny_h3.patcher.model.diffusion_model.blocks
def twice(args, extra):
h = args["img"]
for i in (1, 2, 1, 2):
h = real[i](h, args["t_emb"], args["mod_segments"], args["rope_freqs"],
transformer_options=args["transformer_options"])
return {"img": h}
dit_hooks.add_block_hook(manual, "t", 1, twice)
dit_hooks.add_block_hook(manual, "t", 2, lambda a, e: {"img": a["img"]})
assert torch.allclose(looped[0], tiny_h3.run(manual)[0])
def test_a_scattered_list_is_refused_for_span(tiny_h3):
_p, status = _load(tiny_h3, blocks="0,2", shape="span")
assert "block_repeat: failed to install" in status
def test_step_window_repeats_only_on_the_last_steps(tiny_h3):
patched, _ = _load(tiny_h3, blocks="1", steps=1)
sched = torch.tensor([1.0, 0.7, 0.3, 0.0])
early = _count_block_calls(tiny_h3, patched, sigma=0.7, sigmas=sched)
last = _count_block_calls(tiny_h3, patched, sigma=0.3, sigmas=sched)
assert early[1] == 1 and last[1] == 2
def test_video_only_leaves_audio_as_a_single_pass_would(tiny_h3):
"""Repeating the LAST block with video only: the audio branch must match a
run with no repeat at all, the video branch must not."""
base = tiny_h3.run()
patched, _ = _load(tiny_h3, blocks="3", video_only=True)
out = tiny_h3.run(patched)
assert torch.allclose(out[1], base[1], atol=1e-6), "audio got the repeat"
assert not torch.allclose(out[0], base[0]), "video did not"
def test_video_only_span_leaves_audio_as_a_single_pass_would(tiny_h3):
base = tiny_h3.run()
patched, _ = _load(tiny_h3, blocks="2-3", shape="span", video_only=True)
out = tiny_h3.run(patched)
assert torch.allclose(out[1], base[1], atol=1e-6), "audio got the loop"
assert not torch.allclose(out[0], base[0])