fix: H3 picture found in the packed model output; latent steering was off on every real run

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-09-27 19:01:13 +03:00
co-authored by Claude Opus 5.5
parent 169e8a6a1e
commit 0bb7e3a01a
12 changed files with 124 additions and 43 deletions
+13
View File
@@ -145,3 +145,16 @@ class TinyH3:
@pytest.fixture
def tiny_h3(comfyui):
return TinyH3()
def packed_av(video, audio):
"""[video, audio] exactly as H3's model call hands them back: comfy's
`_apply_model` packs the pair flat with `pack_latents`, and the shapes that
undo it travel with the call as `latent_shapes`. -> (x, latent_shapes)."""
import comfy.utils
return comfy.utils.pack_latents([video, audio])
def unpacked(x, latent_shapes):
import comfy.utils
return comfy.utils.unpack_latents(x, latent_shapes)
+5 -3
View File
@@ -1,14 +1,16 @@
"""The picture inside a sampling-time latent, whatever the model packs around it."""
def video_of(x):
def video_of(x, named=None):
"""-> (video [B, C, T, H, W], rebuild(new_video) -> x), or None.
A model module that packs streams answers `video_stream`; a plain 5-D
latent is its own picture. Anything else is not guessed at.
latent is its own picture. Anything else is not guessed at. `named` is the
model call's arguments (see model_args) -- a packed result is only
unpackable with the `latent_shapes` that travel with it.
"""
from . import registry
split = registry.current().ask("video_stream", x)
split = registry.current().ask("video_stream", x, named or {})
if split is not None:
return split
if getattr(x, "dim", None) and not getattr(x, "is_nested", False) and x.dim() == 5:
+34 -7
View File
@@ -1,11 +1,38 @@
"""Where H3's video lives in a sampling-time latent: the first of [video, audio]."""
"""Where H3's video lives in a sampling-time latent.
Two shapes reach a feature, and both are real:
* the model call's result (APPLY_MODEL) is PACKED: comfy's `_apply_model`
flattens [video, audio] into one [B, 1, N] tensor with `pack_latents`, and the
shapes to undo that ride along as `latent_shapes`;
* elsewhere (callbacks, the finished latent) it is a two-part NestedTensor.
"""
import math
def video_stream(x):
def video_stream(x, named=None):
"""-> (video [B, C, T, H, W], rebuild(new_video) -> x), or None when `x` is
not H3's two-branch latent."""
if not getattr(x, "is_nested", False) or len(x.tensors) != 2 or x.tensors[0].dim() != 5:
neither of H3's two shapes."""
if getattr(x, "is_nested", False):
if len(x.tensors) != 2 or x.tensors[0].dim() != 5:
return None
from comfy.nested_tensor import NestedTensor
rest = x.tensors[1:]
return x.tensors[0], lambda video: NestedTensor([video, *rest])
shapes = (named or {}).get("latent_shapes")
if not hasattr(x, "dim") or x.dim() != 3 or not shapes or len(shapes) != 2 \
or len(shapes[0]) != 5:
return None
from comfy.nested_tensor import NestedTensor
rest = x.tensors[1:]
return x.tensors[0], lambda video: NestedTensor([video, *rest])
n = math.prod(shapes[0][1:])
if x.shape[-1] != n + math.prod(shapes[1][1:]):
return None
b = x.shape[0]
video = x[..., :n].reshape(b, *shapes[0][1:])
rest = x[..., n:]
def rebuild(new_video):
import torch
return torch.cat([new_video.reshape(b, 1, n).to(rest.dtype), rest], dim=-1)
return video, rebuild
@@ -0,0 +1,36 @@
"""H3's picture, found in both shapes it reaches features in."""
import pytest
import torch
@pytest.fixture(autouse=True)
def _needs_comfy(comfyui):
"""Imports comfy."""
def test_packed_model_output_splits_and_rebuilds_exactly():
from conftest import packed_av, unpacked
from modules.models.minimax_h3.streams import video_stream
video, audio = torch.randn(2, 4, 3, 6, 6), torch.randn(2, 8, 5)
x, shapes = packed_av(video, audio)
got, rebuild = video_stream(x, {"latent_shapes": shapes})
assert torch.equal(got, video)
out = rebuild(got * 2)
assert torch.equal(unpacked(out, shapes)[0], video * 2)
assert torch.equal(unpacked(out, shapes)[1], audio)
def test_packed_without_its_shapes_is_not_guessed():
from conftest import packed_av
from modules.models.minimax_h3.streams import video_stream
x, _ = packed_av(torch.randn(1, 4, 3, 6, 6), torch.randn(1, 8, 5))
assert video_stream(x, {}) is None
def test_nested_still_works():
from comfy.nested_tensor import NestedTensor
from modules.models.minimax_h3.streams import video_stream
video, audio = torch.randn(1, 4, 3, 6, 6), torch.randn(1, 8, 5)
got, rebuild = video_stream(NestedTensor([video, audio]))
assert got is video and rebuild(video).tensors[1] is audio
+1 -1
View File
@@ -188,7 +188,7 @@ def install(patcher, values, key):
out = executor(x, t, *args, **kwargs)
named = streams.model_args(args, kwargs)
to = named.get("transformer_options")
split = streams.video_of(out)
split = streams.video_of(out, named)
if split is None:
_say("off this run: could not find the picture in this model's latent")
return out
@@ -51,7 +51,6 @@ def test_nothing_banked_at_this_resolution_changes_nothing():
def test_it_banks_the_last_step_and_steers_once_rated(tiny_h3, monkeypatch):
from comfy.nested_tensor import NestedTensor
from comfy.patcher_extension import WrappersMP
from modules.sampling.modifiers.nodes import FunPackLoadModifiers
from modules.system.taste import store
@@ -66,12 +65,13 @@ def test_it_banks_the_last_step_and_steers_once_rated(tiny_h3, monkeypatch):
torch.manual_seed(1)
bad = torch.randn(1, 4, 3, 8, 8)
audio = torch.randn(1, 8, 5)
x0 = NestedTensor([bad, audio])
from conftest import packed_av, unpacked
x0, shapes = packed_av(bad, audio)
for i, rating in enumerate(["disliked", "disliked"]):
monkeypatch.setattr(store, "current_prompt_id", lambda i=i: f"run-{i}")
wrap, outer, status = load()
for o in outer:
o(lambda: wrap(lambda *a, **k: x0, x0, None, None, None, None, _to(3)))
o(lambda: wrap(lambda *a, **k: x0, x0, None, None, None, None, _to(3), latent_shapes=shapes))
assert store.rate(f"run-{i}", rating)["recorded"] == ["dynashift"]
wrap, outer, _status = load()
@@ -80,8 +80,8 @@ def test_it_banks_the_last_step_and_steers_once_rated(tiny_h3, monkeypatch):
for o in outer:
o(lambda: None) # a run starts: the bank is read
assert any("2 disliked, 0 liked banked" in e["message"] for e in log.history())
early = wrap(lambda *a, **k: x0, x0, None, None, None, None, _to(1))
early = wrap(lambda *a, **k: x0, x0, None, None, None, None, _to(1), latent_shapes=shapes)
assert early is x0 # first half: untouched
late = wrap(lambda *a, **k: x0, x0, None, None, None, None, _to(3))
assert not torch.allclose(late.tensors[0], bad)
assert late.tensors[1] is audio # sound untouched
late = wrap(lambda *a, **k: x0, x0, None, None, None, None, _to(3), latent_shapes=shapes)
assert not torch.allclose(unpacked(late, shapes)[0], bad)
assert torch.equal(unpacked(late, shapes)[1], audio) # sound untouched
+1 -1
View File
@@ -74,7 +74,7 @@ def install(patcher, values, key):
out = executor(x, t, *args, **kwargs)
named = streams.model_args(args, kwargs)
to = named.get("transformer_options")
split = streams.video_of(out)
split = streams.video_of(out, named)
if split is None:
_say("off this run: could not find the picture in this model's latent")
return out
@@ -42,9 +42,9 @@ def _teach(kind, names):
def _x0():
from comfy.nested_tensor import NestedTensor
from conftest import packed_av
torch.manual_seed(3)
return NestedTensor([torch.randn(1, 4, 3, 8, 8), torch.randn(1, 8, 5)])
return packed_av(torch.randn(1, 4, 3, 8, 8), torch.randn(1, 8, 5))
def _start(outer):
@@ -55,16 +55,17 @@ def _start(outer):
def test_late_guidance_banks_the_final_picture_and_steers_only_late(tiny_h3, monkeypatch):
from modules.system.taste import store
monkeypatch.setattr(store, "current_prompt_id", lambda: "run-1")
x0 = _x0()
x0, shapes = _x0()
wrap, outer = _load(tiny_h3, "output_guidance")
for o in outer:
o(lambda: wrap(lambda *a, **k: x0, x0, None, None, None, None, _to(3)))
o(lambda: wrap(lambda *a, **k: x0, x0, None, None, None, None, _to(3), latent_shapes=shapes))
assert store.rate("run-1", "liked")["recorded"] == ["x0_final"]
_teach("x0_final", ["final"])
wrap, outer = _load(tiny_h3, "output_guidance")
_start(outer)
assert wrap(lambda *a, **k: x0, x0, None, None, None, None, _to(1)) is x0
late = wrap(lambda *a, **k: x0, x0, None, None, None, None, _to(3))
assert not torch.allclose(late.tensors[0], x0.tensors[0])
assert late.tensors[1] is x0.tensors[1]
from conftest import unpacked
assert wrap(lambda *a, **k: x0, x0, None, None, None, None, _to(1), latent_shapes=shapes) is x0
late = wrap(lambda *a, **k: x0, x0, None, None, None, None, _to(3), latent_shapes=shapes)
assert not torch.allclose(unpacked(late, shapes)[0], unpacked(x0, shapes)[0])
assert torch.equal(unpacked(late, shapes)[1], unpacked(x0, shapes)[1])
+3 -3
View File
@@ -120,12 +120,12 @@ def install(patcher, values, key):
return base
size = torch.linalg.vector_norm(c[:, words], dim=-1, dtype=torch.float32).mean()
step = (d.to(c.device) * STEP * size).to(c.dtype) * words.view(1, -1, 1)
split = streams.video_of(base)
split = streams.video_of(base, named)
if split is None:
_say("off this run: could not find the picture in this model's latent")
return base
plus = streams.video_of(executor(x, t, **{**named, "c_crossattn": c + step}))
minus = streams.video_of(executor(x, t, **{**named, "c_crossattn": c - step}))
plus = streams.video_of(executor(x, t, **{**named, "c_crossattn": c + step}), named)
minus = streams.video_of(executor(x, t, **{**named, "c_crossattn": c - step}), named)
video, rebuild = split
return rebuild(video + (plus[0] - minus[0]) * amount)
@@ -44,7 +44,7 @@ def test_similar_prompts_pick_their_own_direction():
def test_late_steps_combine_three_passes_on_the_picture_words_only(tiny_h3):
from comfy.nested_tensor import NestedTensor
from conftest import packed_av, unpacked
from comfy.patcher_extension import WrappersMP
from modules.sampling.modifiers.nodes import FunPackLoadModifiers
from modules.system.taste import store
@@ -68,19 +68,20 @@ def test_late_steps_combine_three_passes_on_the_picture_words_only(tiny_h3):
def executor(x, t, c_concat=None, c_crossattn=None, control=None, transformer_options=None, **kw):
seen.append(c_crossattn.clone())
return NestedTensor([torch.full((1, 2, 1, 2, 2), float(c_crossattn[0, :, 0].sum())), audio])
return packed_av(torch.full((1, 2, 1, 2, 2), float(c_crossattn[0, :, 0].sum())), audio)[0]
c = torch.ones(1, 4, dim)
tags = torch.tensor([1, 1, 0, 1]) # row 2: a reference image's token
x0 = executor(None, None, c_crossattn=c)
shapes = packed_av(torch.zeros(1, 2, 1, 2, 2), audio)[1]
seen.clear()
early = wrap(executor, x0, None, None, c, None, _to(0), minimax_payload={"text_token_tags": tags})
assert len(seen) == 1 and torch.equal(early.tensors[0], x0.tensors[0])
early = wrap(executor, x0, None, None, c, None, _to(0), minimax_payload={"text_token_tags": tags}, latent_shapes=shapes)
assert len(seen) == 1 and torch.equal(early, x0)
seen.clear()
late = wrap(executor, x0, None, None, c, None, _to(3), minimax_payload={"text_token_tags": tags})
late = wrap(executor, x0, None, None, c, None, _to(3), minimax_payload={"text_token_tags": tags}, latent_shapes=shapes)
assert len(seen) == 3
plus = seen[1]
assert torch.equal(plus[0, 2], c[0, 2]) and not torch.equal(plus[0, 0], c[0, 0])
assert not torch.equal(late.tensors[0], x0.tensors[0])
assert late.tensors[1] is audio
assert not torch.equal(unpacked(late, shapes)[0], unpacked(x0, shapes)[0])
assert torch.equal(unpacked(late, shapes)[1], audio)
@@ -80,7 +80,7 @@ def install(patcher, values, key):
out = executor(x, t, *args, **kwargs)
named = streams.model_args(args, kwargs)
q = quarter(named.get("transformer_options"))
split = streams.video_of(out)
split = streams.video_of(out, named)
if q is None or split is None:
_say("off this run: could not place this step on the schedule or find the picture")
return out
@@ -42,9 +42,9 @@ def _teach(kind, names):
def _x0():
from comfy.nested_tensor import NestedTensor
from conftest import packed_av
torch.manual_seed(3)
return NestedTensor([torch.randn(1, 4, 3, 8, 8), torch.randn(1, 8, 5)])
return packed_av(torch.randn(1, 4, 3, 8, 8), torch.randn(1, 8, 5))
def _start(outer):
@@ -56,17 +56,18 @@ def test_early_guidance_steers_in_the_first_quarter_and_banks_every_quarter(tiny
from modules.system.taste import store
_teach("x0_quarters", ["q0", "q1", "q2", "q3"])
monkeypatch.setattr(store, "current_prompt_id", lambda: "run-2")
x0 = _x0()
x0, shapes = _x0()
wrap, outer = _load(tiny_h3, "trajectory_guidance")
def run():
return [wrap(lambda *a, **k: x0, x0, None, None, None, None, _to(i)) for i in range(4)]
return [wrap(lambda *a, **k: x0, x0, None, None, None, None, _to(i), latent_shapes=shapes) for i in range(4)]
results = {}
for o in outer:
o(lambda: results.setdefault("steps", run()))
first = results["steps"][0]
assert not torch.allclose(first.tensors[0], x0.tensors[0])
assert not torch.allclose(first[..., :4 * 3 * 8 * 8], x0[..., :4 * 3 * 8 * 8])
assert torch.equal(first[..., 4 * 3 * 8 * 8:], x0[..., 4 * 3 * 8 * 8:])
pending = torch.load(store.ROOT / "fox" / "x0_quarters.pending.pt")["rows"]
assert sorted(pending) == ["q0", "q1", "q2", "q3"]