REINS and Block repeat work on LTX's (video, audio) blocks: new block_rows trait

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tw9fjYZhHqk196LykrWy3T
This commit is contained in:
Claude
2026-10-27 10:26:44 +00:00
parent ee56a282d9
commit 2f4a291b2e
8 changed files with 134 additions and 17 deletions
+3
View File
@@ -241,6 +241,9 @@ class TinyLTX:
torch.tensor([sigma * 1000.0]), self.context, frame_rate=25, transformer_options=to)
TinyLTX.sample = TinyH3.sample # `run` inside the patcher's OUTER_SAMPLE wrappers, the same way
_TINY_LTX = []
+4 -2
View File
@@ -67,7 +67,7 @@ def probe_traits(keys) -> list:
"""What `traits()` would say, read before anything loads."""
if not detect(keys):
return []
return ["audio_stream", "temporal_latent", "temporal_compression", "ltx_av", "block_scope"]
return ["audio_stream", "temporal_latent", "temporal_compression", "ltx_av", "block_scope", "block_rows"]
def traits(model):
@@ -79,7 +79,9 @@ def traits(model):
# through optimized_attention with the block's transformer_options, so a hook that only SCOPES a
# block (hands it changed transformer_options) works. Not dit_block_hooks: a block here takes and
# returns (video, audio), where the hooks that edit rows expect one packed tensor.
return ["audio_stream", "ltx_av", "block_scope"]
# block_rows: the hooks that read or push the video rows of a block's output (REINS, Block repeat)
# handle the pair too (see those modules).
return ["audio_stream", "ltx_av", "block_scope", "block_rows"]
def frames_for(length: int) -> int:
+2 -2
View File
@@ -122,7 +122,7 @@ def probe_traits(keys) -> list:
return []
from comfy.latent_formats import MiniMaxH3AV
found = ["audio_stream", "dit_block_hooks", "block_scope", "adaln_modalities", *H3_ONLY]
found = ["audio_stream", "dit_block_hooks", "block_scope", "block_rows", "adaln_modalities", *H3_ONLY]
rank = _traits.LATENT_RANK.get(MiniMaxH3AV.latent_dimensions)
if rank:
found.append(rank)
@@ -138,7 +138,7 @@ def traits(model):
# dit_block_hooks: its forward honours patches_replace["dit"][("double_block",
# i)] and routes every attention call through optimized_attention, which is
# what core/dit_hooks.py hooks.
found = ["audio_stream", "dit_block_hooks", "block_scope", *H3_ONLY]
found = ["audio_stream", "dit_block_hooks", "block_scope", "block_rows", *H3_ONLY]
if has_block(model, ADALN_CLASS):
found.append("adaln_modalities")
return found
@@ -30,7 +30,7 @@ def test_blocks_past_the_models_last_are_said_not_silently_dropped(tiny_ltx):
assert f"0-{n - 1}" in status
def test_row_editing_modules_stay_off_ltx(tiny_ltx):
def test_modules_written_for_h3s_packed_rows_stay_off_ltx(tiny_ltx):
from modules.sampling.modifiers.nodes import FunUILoadModifiers
_p, status = FunUILoadModifiers.execute(tiny_ltx.patcher, {"reins": {"enabled": True}}).result
assert "reins: needs dit_block_hooks" in status
_p, status = FunUILoadModifiers.execute(tiny_ltx.patcher, {"stas": {"enabled": True}}).result
assert "stas: needs dit_block_hooks" in status
+18 -1
View File
@@ -35,7 +35,7 @@ MOUNT = "generation.sampling"
STAGE = "sampling"
CATEGORY = "sampling"
STATUS = "experimental"
REQUIRES = ["dit_block_hooks"]
REQUIRES = ["block_rows"]
SETTINGS = {
"enabled": {
@@ -121,6 +121,10 @@ def install(patcher, values, key):
steps = int(values.get("steps", 0))
video_only = bool(values.get("video_only"))
span = values.get("shape") == "span"
paired = hasattr(getattr(patcher.model, "diffusion_model", None), "transformer_blocks") # audio+video blocks: (video, audio) in and out
if span and paired:
_say("this model's blocks take picture and sound as a pair, so 'whole span' runs as 'each block'")
span = False
if span:
lo, hi = blocks[0], blocks[-1]
@@ -154,7 +158,20 @@ def install(patcher, values, key):
dit_hooks.add_block_hook(patcher, key, i, skip)
what = f"blocks {lo}-{hi} loop {times + 1}x as one span"
else:
def repeat_pair(args, extra):
once = extra["original_block"](args)["img"]
if not _in_window(args, steps):
return {"img": once}
if video_only: # these blocks add their residual into their input: keep the single pass whole
once = tuple(t.clone() for t in once)
h = once
for _ in range(times):
h = extra["original_block"]({**args, "img": h})["img"]
return {"img": (h[0], once[1]) if video_only else h}
def repeat(args, extra):
if paired:
return repeat_pair(args, extra)
once = extra["original_block"](args)["img"]
if not _in_window(args, steps):
return {"img": once}
@@ -0,0 +1,49 @@
"""Block repeat on a (tiny) LTX forward: picture and sound go through each block as a pair."""
import pytest
import torch
@pytest.fixture(autouse=True)
def _needs_comfy(comfyui):
"""Imports comfy."""
def _load(tiny, **values):
from modules.sampling.modifiers.nodes import FunUILoadModifiers
return FunUILoadModifiers.execute(tiny.patcher, {"block_repeat": {"enabled": True, **values}}).result
def _calls(tiny, patched):
counts, handles = {}, []
for i, block in enumerate(tiny.patcher.model.diffusion_model.transformer_blocks):
handles.append(block.register_forward_pre_hook(lambda m, a, kw=None, i=i: counts.__setitem__(i, counts.get(i, 0) + 1)))
try:
tiny.run(patched)
finally:
for h in handles:
h.remove()
return counts
def test_each_named_block_runs_times_plus_one(tiny_ltx):
patched, status = _load(tiny_ltx, blocks="1", times=2)
assert "block_repeat: blocks 1 run 3x each" in status
counts = _calls(tiny_ltx, patched)
assert counts[1] == 3 and counts[0] == 1
def test_it_changes_the_picture_and_video_only_leaves_the_sound_single_pass(tiny_ltx):
base_v, base_a = tiny_ltx.run()
both, _ = _load(tiny_ltx, blocks="1")
v, a = tiny_ltx.run(both)
assert not torch.allclose(v, base_v) and not torch.allclose(a, base_a)
only, _ = _load(tiny_ltx, blocks="1", video_only=True)
v2, a2 = tiny_ltx.run(only)
assert not torch.allclose(v2, base_v)
assert v2.shape == base_v.shape and a2.shape == base_a.shape
def test_whole_span_runs_as_each_block_here(tiny_ltx):
patched, status = _load(tiny_ltx, blocks="1-2", shape="span")
assert "run 2x each" in status
+14 -9
View File
@@ -23,7 +23,7 @@ MOUNT = "generation.sampling"
STAGE = "guidance"
CATEGORY = "guidance"
STATUS = "experimental"
REQUIRES = ["dit_block_hooks"]
REQUIRES = ["block_rows"]
USES = ["taste_store"]
SETTINGS = {
@@ -87,27 +87,32 @@ def install(patcher, values, key):
def make(block):
def hook(args, extra):
out = extra["original_block"](args)["img"]
span = video_span(args, out)
if span is None:
return {"img": out}
rows = out[span[0]:span[1]]
if isinstance(out, (tuple, list)): # an audio+video block hands back (video, audio): the video is all picture
rows, width = out[0].reshape(-1, out[0].shape[-1]), out[0].shape[-1]
else:
span = video_span(args, out)
if span is None:
return {"img": out}
rows, width = out[span[0]:span[1]], out.shape[-1]
if dit_hooks.last_step(args.get("transformer_options")):
captured[block] = rows.mean(0, dtype=torch.float32).detach()
direction = live["dirs"].get(block)
if direction is None or strength <= 0.0:
return {"img": out}
if direction.numel() != out.shape[-1]:
if direction.numel() != width:
_say(f"block {block}'s learned direction is from a different model; "
"learning only there")
return {"img": out}
d = cast.get((block, out.dtype))
d = cast.get((block, rows.dtype))
if d is None:
d = cast[(block, out.dtype)] = direction.to(device=out.device, dtype=out.dtype)
d = cast[(block, rows.dtype)] = direction.to(device=rows.device, dtype=rows.dtype)
norm = torch.linalg.vector_norm(rows, dim=-1, dtype=torch.float32).mean()
# No host sync: a bad norm makes the push zero instead of a branch.
scale = torch.where(torch.isfinite(norm) & (norm > 0), norm * strength,
torch.zeros_like(norm))
rows += d * scale.to(out.dtype)
rows += d * scale.to(rows.dtype)
if isinstance(out, (tuple, list)) and rows.data_ptr() != out[0].data_ptr():
out[0].copy_(rows.view_as(out[0])) # the reshape had to copy: put the push back
return {"img": out}
return hook
@@ -0,0 +1,41 @@
"""Taste steering on a (tiny) LTX forward: its blocks hand back (video, audio)."""
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 _width(tiny):
return tiny.patcher.model.diffusion_model.transformer_blocks[0].attn1.to_out[0].weight.shape[0]
def _load(tiny, **values):
from modules.sampling.modifiers.nodes import FunUILoadModifiers
return FunUILoadModifiers.execute(
tiny.patcher, {"reins": {"enabled": True, **values}, "taste": {"key": "fox"}}).result
def test_it_learns_at_every_block_and_steers_only_the_picture(tiny_ltx):
from modules.system.taste import store
liked = torch.zeros(_width(tiny_ltx))
liked[0] = 1.0
for i, (vec, rating) in enumerate([(liked, "liked"), (-liked, "disliked")] * 2):
store.capture("fox", "reins", {1: vec}, prompt_id=f"t{i}")
store.rate(f"t{i}", rating)
base_v, base_a = tiny_ltx.run()
patched, status = _load(tiny_ltx, blocks="1", strength=0.5)
assert "reins: learning at every block; steers 1" in status
v, a = tiny_ltx.sample(patched)
assert not torch.allclose(v, base_v), "the learned direction changed nothing"
assert v.shape == base_v.shape and a.shape == base_a.shape