fix(h3): the causal lane never loaded the model, so its patches never applied

Found by asking how CFG=1.0 reaches the model. It reaches it through
comfy.sample.sample_custom -> sampling_function, which sets uncond_ = None at
1.0 so the negative branch is never evaluated. Nothing wrong there.

But the causal lane bypasses sample_custom entirely, and sample_custom's
prepare_sampling is also what calls load_models_gpu — which is in turn what
applies a patcher's OBJECT PATCHES (partially_load -> patch_model). So on that
path the weights could still be offloaded AND every object patch FunPack
installs would silently not be there: the per-modality AdaLN gains and the
token-refiner edit (prompt scale + learned taste bias) would all have been
absent, with nothing reporting it.

Loaded once for the whole rollout, before the first forward — not per chunk,
which would fight Comfy's own offload decision all the way down the clip. A
failed load degrades to whatever was already resident and says the patches may
be missing, rather than turning a placement problem into a dead run.

Tests: 1719 passed (+5).
This commit is contained in:
digital-garbage
2026-08-26 18:29:01 +03:00
parent 20bd542af2
commit 070563f4d5
2 changed files with 91 additions and 0 deletions
+16
View File
@@ -344,6 +344,22 @@ def run_session(session, *, sigmas, step_rule="consistency", eta=1.0, seed=0,
layout = session["layout"]
model = session["model"]
cache = session["cache"]
# LOAD FIRST. The ordinary path reaches the model through comfy.sample.sample_custom,
# whose prepare_sampling calls load_models_gpu — and load_models_gpu is also what applies
# a patcher's OBJECT PATCHES (partially_load -> patch_model). This lane calls the DiT
# directly, so without this the weights may still be offloaded AND every object patch
# FunPack installed (the AdaLN modality gains, the token-refiner edit) would silently not
# be there. Once, for the whole rollout: the loop must not fight Comfy's offload decision
# chunk by chunk.
patcher = session.get("patcher")
if patcher is not None:
try:
import comfy.model_management
comfy.model_management.load_models_gpu([patcher], memory_required=0,
force_full_load=False)
except Exception as error: # noqa: BLE001
print(f"[FunPack H3] causal lane could not pre-load the model ({error}); "
f"the rollout continues, but object patches may not be installed.")
device = session["device"]
compute_dtype = session["compute_dtype"]
conditioning = session["conditioning"]
+75
View File
@@ -627,3 +627,78 @@ def test_the_run_says_the_sampler_and_cfg_are_not_used(monkeypatch, capsys):
out = capsys.readouterr().out
assert "CFG" in out and "not used" in out.replace("NOT used", "not used")
assert "no per-chunk redo" in out.lower()
# ── loading, and what loading also installs ─────────────────────────────────
#
# The ordinary path reaches the model through comfy.sample.sample_custom, whose
# prepare_sampling calls load_models_gpu. That call is ALSO what applies a patcher's object
# patches (partially_load -> patch_model), which is how FunPack's AdaLN modality gains and
# token-refiner edit get installed at all. This lane calls the DiT directly, so skipping the
# load would leave the weights offloaded AND both of those silently absent.
def test_the_model_is_loaded_before_the_rollout(monkeypatch):
import comfy.model_management
calls = []
monkeypatch.setattr(comfy.model_management, "load_models_gpu",
lambda models, **kw: calls.append((models, kw)), raising=False)
model = FakeCausalModel()
session = _session(model)
session["patcher"] = "the-patcher"
rc.run_session(session, sigmas=(1.0, 0.0))
assert calls and calls[0][0] == ["the-patcher"]
def test_the_load_happens_before_the_first_forward(monkeypatch):
import comfy.model_management
order = []
monkeypatch.setattr(comfy.model_management, "load_models_gpu",
lambda models, **kw: order.append("load"), raising=False)
model = FakeCausalModel()
real_forward = model.forward_chunk
def spy(**kw):
order.append("forward")
return real_forward(**kw)
model.forward_chunk = spy
session = _session(model)
session["patcher"] = "the-patcher"
rc.run_session(session, sigmas=(1.0, 0.0))
assert order[0] == "load"
def test_the_model_is_loaded_once_not_per_chunk(monkeypatch):
"""Loading per chunk would fight Comfy's own offload decision all the way down the clip."""
import comfy.model_management
calls = []
monkeypatch.setattr(comfy.model_management, "load_models_gpu",
lambda models, **kw: calls.append(models), raising=False)
session = _session()
session["patcher"] = "the-patcher"
rc.run_session(session, sigmas=(1.0, 0.5, 0.0))
assert len(calls) == 1
def test_a_failed_load_does_not_abort_the_run(monkeypatch, capsys):
"""It degrades to whatever Comfy already had resident, and says the patches may be
missing — rather than turning a recoverable placement problem into a dead run."""
import comfy.model_management
monkeypatch.setattr(
comfy.model_management, "load_models_gpu",
lambda models, **kw: (_ for _ in ()).throw(RuntimeError("out of memory")),
raising=False)
session = _session()
session["patcher"] = "the-patcher"
video, _audio = rc.run_session(session, sigmas=(1.0, 0.0))
assert video is not None
assert "object patches may not be installed" in capsys.readouterr().out
def test_no_patcher_means_no_load_attempt(monkeypatch):
import comfy.model_management
calls = []
monkeypatch.setattr(comfy.model_management, "load_models_gpu",
lambda models, **kw: calls.append(models), raising=False)
rc.run_session(_session(), sigmas=(1.0, 0.0)) # _session has no patcher
assert calls == []