diff --git a/samplers.py b/samplers.py index c803cddd..7233b87c 100644 --- a/samplers.py +++ b/samplers.py @@ -3854,8 +3854,8 @@ class FunPackLTXAVSceneChainSampler: def _compiler_disabled_for_forward_patches(self, active): """Comfy's model compiler (aimdo malloc-graph + CUDA graphs) assumes every block-scope forward allocates the same pattern call to call. H3 representation - steering while actually injecting (not passive-capture) and block repeat both - patch what a block's forward allocates, which crashes it with + steering while actually injecting (not passive-capture), block repeat and shadow + negative all patch what a block's forward allocates, which crashes it with "aimdo memory compile error" -- confirmed against a real rental log where the identical workflow succeeds with REINS in passive-capture-only mode (no injection branch entered) and fails the moment strength > 0 (injection @@ -4061,19 +4061,29 @@ class FunPackLTXAVSceneChainSampler: model, refinement_key, h3_q_steer_strength, _q_steer_capture, steer_block=h3_q_steer_block) + _shadow_installed = False if self._is_h3 and h3_shadow_negative: + _model_before_shadow = model model = self._install_h3_shadow_negative( model, negative, h3_shadow_negative_video_scale, h3_shadow_negative_audio_scale, h3_shadow_negative_tau, h3_shadow_negative_alpha, h3_shadow_negative_start_percent, h3_shadow_negative_end_percent, compose=h3_shadow_negative_compose) + # Same identity test as block repeat: the installer returns the SAME model when + # it declines (no negative, both scales 1.0, non-finite knob). + _shadow_installed = model is not _model_before_shadow _phrase_probe = self._install_phrase_probe(model, positive, latent) if _phrase_probe is not None: model = _phrase_probe[0] _repr_injecting = bool(_repr_steering_installed and h3_repr_steering and float(h3_repr_steering_strength or 0.0) > 0.0) - _compiler_conflict = _block_repeat_installed or _repr_injecting + # Shadow negative replaces every hooked block's forward with its own two-stream one + # (a second attention pass, extra clones), so its allocation pattern differs from + # the stock block's -- the same "aimdo memory compile error" block repeat and REINS + # injection hit. It never tripped this before 2026-09-27 only because, until then, + # it silently never ran. + _compiler_conflict = _block_repeat_installed or _repr_injecting or _shadow_installed try: with self._compiler_disabled_for_forward_patches(_compiler_conflict): sampled = comfy.sample.sample_custom( diff --git a/tests/test_compiler_disabled_for_forward_patches.py b/tests/test_compiler_disabled_for_forward_patches.py index 369e566f..3ddfb4ef 100644 --- a/tests/test_compiler_disabled_for_forward_patches.py +++ b/tests/test_compiler_disabled_for_forward_patches.py @@ -33,7 +33,8 @@ class _FakeModel: return m -def _run_chunk_with(monkeypatch, seen, refinement_key="fake_key", model=None, **kw): +def _run_chunk_with(monkeypatch, seen, refinement_key="fake_key", model=None, negative=None, + is_h3=False, **kw): """Stubs sample_custom to record whether disable_comfy_compiler was set DURING the call, then _sample_chunk runs for real so the gating logic (h3_repr_steering / h3_repr_steering_strength / h3_block_repeat) executes exactly as it would in @@ -48,9 +49,11 @@ def _run_chunk_with(monkeypatch, seen, refinement_key="fake_key", model=None, ** monkeypatch.setattr(sys.modules["comfy.sample"], "sample_custom", _fake_sample_custom) latent = {"samples": torch.zeros(1, 2, 2, 2, 2)} sampler = type("F", (), {"extra_options": {}, "sampler_function": None})() - S()._sample_chunk(model if model is not None else object(), sampler, - torch.tensor([1.0, 0.0]), 0, 1.0, [], [], latent, - refinement_key=refinement_key, **kw) + node = S() + node._is_h3 = is_h3 + node._sample_chunk(model if model is not None else object(), sampler, + torch.tensor([1.0, 0.0]), 0, 1.0, [], negative or [], latent, + refinement_key=refinement_key, **kw) def test_reins_actively_injecting_disables_the_compiler(monkeypatch): @@ -166,3 +169,43 @@ def test_crash_survives_the_sample_call_and_still_restores(monkeypatch): except RuntimeError: pass assert not hasattr(comfy.cli_args.args, "disable_comfy_compiler") + + +class _FakeH3Model(_FakeModel): + """Enough for _install_h3_shadow_negative to really install (it needs the diffusion + model's blocks and hidden size), so the compiler test cannot pass on a declined one.""" + def __init__(self): + super().__init__() + self._dm = types.SimpleNamespace(blocks=[object(), object()], hidden_size=16) + + def clone(self): + m = _FakeH3Model() + m.model_options = dict(self.model_options) + m._dm = self._dm + return m + + def get_model_object(self, name): + return self._dm + + +_SHADOW = dict(h3_shadow_negative=True, h3_shadow_negative_video_scale=3.0, + h3_shadow_negative_audio_scale=1.0, h3_shadow_negative_tau=2.5, + h3_shadow_negative_alpha=0.35, h3_shadow_negative_start_percent=0.0, + h3_shadow_negative_end_percent=0.6) + + +def test_shadow_negative_disables_the_compiler(monkeypatch): + """It replaces every block's forward with a two-stream one: the same aimdo compile + error, first seen on a rental 2026-09-27 -- the day it started actually running.""" + seen = [] + _run_chunk_with(monkeypatch, seen, model=_FakeH3Model(), is_h3=True, + negative=[(torch.randn(1, 3, 16), {})], **_SHADOW) + assert seen == [True] + assert not hasattr(comfy.cli_args.args, "disable_comfy_compiler") + + +def test_shadow_negative_that_declines_leaves_the_compiler_alone(monkeypatch): + """No negative prompt: nothing is installed, so nothing conflicts.""" + seen = [] + _run_chunk_with(monkeypatch, seen, model=_FakeH3Model(), is_h3=True, **_SHADOW) + assert seen == ["MISSING"]