Files
olivv-cs-ComfyUI-FunPack/tests/test_scene_wrapper_hygiene.py
T
digital-garbageandClaude Fable 5 8e2ddd47a3 sampler: exception-safe wrapper lifecycle + output_guidance calibration fix
Two audit findings on the experimental feature stack:

1. Leak: an interrupt or OOM inside _sample_chunk skipped the per-feature
unwind blocks, leaking embed_guidance/score_slider/output_guidance/temporal
wrappers on the shared ModelPatcher - every later run then chained new
wrappers onto the leaked ones and double-steered (same in-process
progressive-corruption mode as the old block-hook leak).
- one base-wrapper snapshot per scene, restored in the same finally that
  removes the v2a hooks; the four conditional unwind blocks are gone
- every per-scene wrapper is tagged with its predecessor; sample() strips
  leaked tagged chains at run start (mirrors strip_funpack_block_hooks)

2. Inert feature: output_guidance applied the RAW value-fn gradient, whose
per-element magnitude is O(1/pool_window) - at video-latent sizes the
correction was ~1e-6 relative, i.e. numerically a no-op. It also fed the
full packed AV tensor to a value function trained on the video stream
alone (shifted pooling buckets + audio contamination).
- gradient now computed on the packed latent's video span only (matches
  the training domain of _save_output_value_snapshot)
- delta norm-calibrated: strength = fraction of video-stream norm per
  fully-ramped step (0.02 = 2%), same relative convention as the slider
- audio region byte-identical by construction (cat, not post-hoc mask)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 05:41:33 +03:00

145 lines
4.9 KiB
Python

"""Wrapper hygiene on the Scene Chain sampler.
Two guarantees the feature stack depends on:
1. Every per-scene model_function_wrapper (embed guidance / score slider / output
guidance) is tagged with its predecessor so a leaked chain can be unwound
(_strip_funpack_scene_wrappers) — the in-process analogue of
strip_funpack_block_hooks.
2. output_guidance's correction is norm-calibrated on the video span: strength is a
fraction of the stream norm (not a raw MLP gradient, which is numerically inert),
and the audio region of a packed AV latent is byte-identical after correction.
"""
import sys
import types
from pathlib import Path
import torch
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
for _name in (
"comfy", "comfy.k_diffusion", "comfy.k_diffusion.sampling",
"comfy.model_sampling", "comfy.nested_tensor", "comfy.sample",
"comfy.samplers", "comfy.utils",
):
sys.modules.setdefault(_name, types.ModuleType(_name))
sys.modules["comfy.nested_tensor"].NestedTensor = object
import samplers # noqa: E402
def _model():
return types.SimpleNamespace(model_options={})
def _install_stack(model):
"""Install embed -> slider -> output, the same order sample() uses."""
s = samplers.FunPackLTXAVSceneChainSampler()
liked = torch.ones(16)
s._build_embed_guidance_wrapper(model, liked, 0.02)
s._build_score_slider_wrapper(model, liked, eta=1.0)
class _VF:
def gradient(self, x):
return torch.ones_like(x)
s._build_output_guidance_wrapper(model, _VF(), 0.02)
return s
def test_installed_wrappers_are_tagged_with_prev_chain():
model = _model()
_install_stack(model)
w = model.model_options["model_function_wrapper"]
depth = 0
while w is not None and getattr(w, samplers._FUNPACK_SCENE_WRAPPER_TAG, False):
w = w._funpack_prev_wrapper
depth += 1
assert depth == 3
assert w is None
def test_strip_unwinds_leaked_stack_to_base():
model = _model()
def base_wrapper(apply_fn, args): # a non-FunPack wrapper that must survive
return apply_fn(args)
model.model_options["model_function_wrapper"] = base_wrapper
_install_stack(model)
stripped = samplers._strip_funpack_scene_wrappers(model)
assert stripped == 3
assert model.model_options["model_function_wrapper"] is base_wrapper
# Idempotent: nothing left to strip.
assert samplers._strip_funpack_scene_wrappers(model) == 0
def test_strip_removes_key_when_no_base_wrapper():
model = _model()
_install_stack(model)
assert samplers._strip_funpack_scene_wrappers(model) == 3
assert "model_function_wrapper" not in model.model_options
def test_strip_leaves_foreign_wrapper_alone():
model = _model()
def foreign(apply_fn, args):
return apply_fn(args)
model.model_options["model_function_wrapper"] = foreign
assert samplers._strip_funpack_scene_wrappers(model) == 0
assert model.model_options["model_function_wrapper"] is foreign
class _ConstGradVF:
"""Gradient of constant direction; lets the test predict the calibrated delta."""
def gradient(self, x):
return torch.ones_like(x)
def _packed_av_model(video_elems, audio_elems):
"""Model stub whose guider conds carry latent_shapes for a packed AV layout."""
ls = types.SimpleNamespace(cond=[(1, 4, video_elems // 4), (1, audio_elems)])
cond_entry = {"model_conds": {"latent_shapes": ls}}
guider = types.SimpleNamespace(conds={"positive": [cond_entry]})
return types.SimpleNamespace(model_options={}, inner_model=guider)
def test_output_guidance_delta_is_norm_calibrated_and_video_only():
video_n, audio_n = 96, 24
model = _packed_av_model(video_n, audio_n)
s = samplers.FunPackLTXAVSceneChainSampler()
s._build_output_guidance_wrapper(model, _ConstGradVF(), strength=0.02)
wrap = model.model_options["model_function_wrapper"]
denoised = torch.randn(1, 1, video_n + audio_n)
def apply_fn(inp, ts, **c):
return denoised
out = wrap(apply_fn, {"input": denoised, "timestep": torch.tensor([0.0]),
"c": {}})
# Audio region untouched (identical, not merely close).
assert torch.equal(out[..., video_n:], denoised[..., video_n:])
# Video delta norm == strength * ramp * ||video||; sigma=0 -> ramp=1.
delta = out[..., :video_n] - denoised[..., :video_n]
expected = 0.02 * float(denoised[..., :video_n].norm())
assert abs(float(delta.norm()) - expected) < 1e-5
def test_output_guidance_high_sigma_is_passthrough():
model = _packed_av_model(96, 24)
s = samplers.FunPackLTXAVSceneChainSampler()
s._build_output_guidance_wrapper(model, _ConstGradVF(), strength=0.02)
wrap = model.model_options["model_function_wrapper"]
denoised = torch.randn(1, 1, 120)
def apply_fn(inp, ts, **c):
return denoised
out = wrap(apply_fn, {"input": denoised, "timestep": torch.tensor([0.9]),
"c": {}})
assert torch.equal(out, denoised)