From 397f36014aae06b2bfed0561bad744a5599c29ce Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sat, 3 Oct 2026 11:34:36 +0300 Subject: [PATCH] fix: loop roll failures reach the guard (recorded, dropped, call rerun); sampler keeps an empty fault record Co-Authored-By: Claude Sonnet 5.5 --- modules/sampling/sampler/nodes.py | 7 +++- modules/sampling/temporal_style/loop.py | 38 +++++++------------ .../tests/test_temporal_style.py | 18 +++++++++ 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/modules/sampling/sampler/nodes.py b/modules/sampling/sampler/nodes.py index 534678f2..776ecd2c 100644 --- a/modules/sampling/sampler/nodes.py +++ b/modules/sampling/sampler/nodes.py @@ -102,8 +102,11 @@ class FunPackSampler(io.ComfyNode): # its cache -- so it is emptied here rather than trusted to be fresh. # Otherwise a modifier dropped in one generation stays dropped in every # later one, with nothing said. - dropped = (getattr(model, "model_options", {}).get("funpack_dropped") or getattr(model, "funpack_dropped", None) - or patching.Dropped()) + dropped = getattr(model, "model_options", {}).get("funpack_dropped") + if dropped is None: + dropped = getattr(model, "funpack_dropped", None) + if dropped is None: + dropped = patching.Dropped() dropped.clear() latent = latent.copy() diff --git a/modules/sampling/temporal_style/loop.py b/modules/sampling/temporal_style/loop.py index 3db361b1..bae2fc72 100644 --- a/modules/sampling/temporal_style/loop.py +++ b/modules/sampling/temporal_style/loop.py @@ -156,12 +156,6 @@ def _loop_audio_tail_frames(mask): return 0 -def _roll_failed(e, call, args): - log.once("temporal_style:loop_failed", log.ALERT, "FunPack Temporal style", - f"the loop roll failed ({type(e).__name__}: {e}); the calls it failed on ran without it") - return call(args) if call else None - - def make_loop_temporal_wrapper(old_wrapper): """Build the loop-style model_function_wrapper. Installed INNERMOST (closest to apply_model): prediction-modifying wrappers layered above it (dynashift, output @@ -216,25 +210,19 @@ def make_loop_temporal_wrapper(old_wrapper): state["logged"] = True tail_note = f", {v_tail} guide frame(s) pinned" if v_tail else "" log.once("temporal_style:loop", log.INFO, "FunPack Temporal style", f"loop: Mobius latent roll active (T={t_content}{tail_note})") - try: - new_c = dict(c) - if isinstance(new_c.get("denoise_mask"), torch.Tensor): - new_c["denoise_mask"] = _loop_roll_mask(new_c["denoise_mask"], frac, 1, tail=v_tail) - if isinstance(new_c.get("audio_denoise_mask"), torch.Tensor): - new_c["audio_denoise_mask"] = _loop_roll_mask( - new_c["audio_denoise_mask"], frac, 1, - tail=_loop_audio_tail_frames(new_c["audio_denoise_mask"])) - rolled = dict(args) - rolled["input"] = _loop_roll_packed(args["input"], shapes, frac, 1, tails=tails) - rolled["c"] = new_c - except Exception as e: # noqa: BLE001 -- only the roll is ours to catch - return _roll_failed(e, _call, args) - out = _call(rolled) # the model's errors are the model's: not caught here - try: - return _loop_roll_packed(out, shapes, frac, -1, tails=tails) - except Exception as e: # noqa: BLE001 - _roll_failed(e, None, None) - return out + # A roll or unroll that fails is not caught here: the guard records it, drops the loop for the + # run and reruns the call without it. The model's own errors pass through the same guard. + new_c = dict(c) + if isinstance(new_c.get("denoise_mask"), torch.Tensor): + new_c["denoise_mask"] = _loop_roll_mask(new_c["denoise_mask"], frac, 1, tail=v_tail) + if isinstance(new_c.get("audio_denoise_mask"), torch.Tensor): + new_c["audio_denoise_mask"] = _loop_roll_mask( + new_c["audio_denoise_mask"], frac, 1, + tail=_loop_audio_tail_frames(new_c["audio_denoise_mask"])) + rolled = dict(args) + rolled["input"] = _loop_roll_packed(args["input"], shapes, frac, 1, tails=tails) + rolled["c"] = new_c + return _loop_roll_packed(_call(rolled), shapes, frac, -1, tails=tails) return _loop_wrapper diff --git a/modules/sampling/temporal_style/tests/test_temporal_style.py b/modules/sampling/temporal_style/tests/test_temporal_style.py index 3f59048b..70ef29eb 100644 --- a/modules/sampling/temporal_style/tests/test_temporal_style.py +++ b/modules/sampling/temporal_style/tests/test_temporal_style.py @@ -195,3 +195,21 @@ def test_a_model_error_through_the_loop_is_not_blamed_on_the_roll_or_rerun(tiny_ with pytest.raises(RuntimeError, match="model blew up"): p.model_options["model_function_wrapper"](apply_fn, args) assert len(calls) == 1 and not dropped + + +def test_a_failing_roll_drops_the_loop_once_and_the_call_runs_unrolled(tiny_ltx, monkeypatch): + from modules.sampling.temporal_style import install, loop + monkeypatch.setattr(loop, "_loop_roll_packed", lambda *a, **k: 1 / 0) + p = tiny_ltx.patcher.clone() + dropped = patching.Dropped() + install(patching.GuardedPatcher(p, KEY, dropped), {"style": "loop"}, key=KEY) + seen = [] + + def apply_fn(x, t, **c): + seen.append(x) + return x + + x = torch.arange(8.0).view(1, 1, 8, 1, 1) + bad = {"input": x, "timestep": torch.tensor([0.5]), "c": {"denoise_mask": torch.ones(1, 1, 8, 1, 1)}} + out = p.model_options["model_function_wrapper"](apply_fn, bad) + assert KEY in dropped and torch.equal(out, x) and torch.equal(seen[-1], x)