Capture H3 adaLN sidecars & loader hook

Add h3lora/sidecars.py to peel adaln_basis, adaln_mean, and silu_t_emb_grid from checkpoint state dicts and stash them on model/patcher objects so ComfyUI doesn't drop them. Install a load_model_weights wrapper (ensure_load_hook) in __init__.py and call it from h3lora.apply; use sidecar collect when creating AdalnContext. Include unit tests (tests/test_sidecars.py) for peel/attach/collect and the wrapper. Also update README with no attribution by proxy. Thanks pl0x
This commit is contained in:
doggeddalle
2026-09-06 18:34:25 +10:00
parent 4ef4de0a5d
commit 3f7bf0d762
5 changed files with 408 additions and 3 deletions
+38
View File
@@ -304,6 +304,44 @@ The package requires Python 3.10 or newer and a ComfyUI revision providing the
MiniMax H3 model, `QuantizedTensor`, weight adapters, and patcher wrappers used
by this node.
<details>
<summary>(Funny Find)</summary>
• I found overlap, but the evidence does not establish that PlagueKind took the AdaLN fix from your PowerLoraStack. The strongest code matches actually appear earlier in their Git history.
I compared both local repositories, historical versions, tests, and attribution, and verified that the original AdaLN files match their public GitHub snapshots.
| Date (2026) | Evidence |
| :--- | :--- |
| August 8 | Your commit 28ac439 introduces AdaLN conversion, including least-squares fitting, reverse conversion, and bias correction. |
| August 20 | Their commit 7e5743f imports the standalone fix, explicitly credited “by pl0x.” |
| August 27 | Your commit f38d084 introduces the helpers that most closely match their code. |
### What matches
Two current helpers have identical Python syntax trees after removing docstrings—meaning their executable implementation matches despite differences in comments:
- `_safetensors_header`: yours (`h3lora/adaln.py:155`) / theirs (`C:/comfycli/custom_nodes/ComfyUI-PlagueKind-Nodes/ComfyUI-H3-AdaLN-LoRA-Fix/adaln.py:205`).
- `_time_embedder_prefix`: yours (`h3lora/adaln.py:166`) / theirs (`C:/comfycli/custom_nodes/ComfyUI-PlagueKind-Nodes/ComfyUI-H3-AdaLN-LoRA-Fix/adaln.py:217`).
Hashing and time-embedding grid helpers also closely resemble each other. These helpers existed in their August 20 import before entering your repository on August 27. That chronology cannot support them copying those helpers from your published version. It also does not establish who originally wrote them.
### What differs
Your earlier implementation converts LoRA state-dictionary entries before loading, using `AdalnContext`, `port_adaln_pairs`, and `.diff_b`. Their implementation repairs already-attached model patches through a separate node.
Comparing your last committed version before their import against all five imported Python files, I found no substantive matching sequence of three or more consecutive nonblank lines after trimming whitespace; matches were import boilerplate. This does not rule out a rewrite or conceptual influence, but it weakens a direct-copy claim.
The shared grid filename also appears in another public H3 project, so that filename alone is not evidence of copying yours. Example
### Attribution and conclusion
I found no credit to you or PowerLoraStack in their current README or fix files. Their import credits “pl0x”; the repository declares MIT, while yours declares Apache-2.0. Those facts alone do not establish a licensing violation.
Your repository records the core solution earlier. Possible influence remains unproven; direct copying from your original implementation is not demonstrated. The most useful next evidence would be the original package supplied by “pl0x” and the development context behind your August 27 changes. Git dates establish recorded chronology, not private authorship or first publication.
</details>
## Development checks
From the ComfyUI root, make the package and ComfyUI import roots visible before
+2
View File
@@ -2,9 +2,11 @@
from .h3lora import server as _server
from .h3lora.nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
from .h3lora.sidecars import ensure_load_hook
WEB_DIRECTORY = "./web"
_server.register() # /h3_power_lora_stack/balance, for the auto-balance button
ensure_load_hook() # peel adaln_basis/adaln_mean before Comfy drops them
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
+3 -3
View File
@@ -20,8 +20,10 @@ from . import keymap
from . import modality as modality_mod
from . import pdd as pdd_mod
from . import schedule as schedule_mod
from . import sidecars as sidecar_mod
LOG = logging.getLogger("h3.powerlorastack")
sidecar_mod.ensure_load_hook()
# Layers whose forward is bypassed by a fused kernel, per weight layout. A
# runtime branch on these would be silently dropped, so they must merge.
@@ -135,11 +137,9 @@ def apply_stack(model, entries, mode="auto", adaln_mode="auto", grid_path="",
else:
if not grid_path:
grid_path = adaln_mod.find_silu_grid()
cfg = getattr(getattr(patcher, "model", None), "model_config", None)
sidecars = getattr(cfg, "adaln_sidecars", None) if cfg is not None else None
adaln_ctx = adaln_mod.AdalnContext(
target_dim, table, grid_path,
sidecars=sidecars,
sidecars=sidecar_mod.collect(patcher, diffusion_model),
time_embedder=getattr(diffusion_model, "time_embedder", None),
)
+203
View File
@@ -0,0 +1,203 @@
"""Capture MiniMax H3 adaLN sidecar tensors without touching native ComfyUI.
Curve H3 checkpoints often bake ``adaln_basis`` + ``adaln_mean`` (or a
``silu_t_emb_grid``) next to the DiT weights. ComfyUI does not register those
keys on the UNet, so ``load_model_weights`` logs them as unexpected and drops
them. The Power LoRA Stack needs them to port dense-trained adaLN pairs onto
the loaded curve basis.
We never read missing attributes off Comfy's model-config object: its
``__getattr__`` warns ``Please fix your code`` instead of raising. Sidecars are
peeled from the checkpoint state dict (via a ``BaseModel.load_model_weights``
wrapper installed by this pack) and stashed on the H3 model instance.
"""
from __future__ import annotations
import logging
import torch
LOG = logging.getLogger("h3.powerlorastack")
SIDECAR_NAMES = ("adaln_basis", "adaln_mean", "silu_t_emb_grid")
STASH_ATTR = "_h3_adaln_sidecars"
PATCHER_ATTACHMENT = "h3_adaln_sidecars"
def _as_mapping(value):
return value if isinstance(value, dict) else None
def _instance_dict(obj):
try:
return vars(obj)
except TypeError:
return None
def unet_config_of(model):
"""``unet_config`` from a BaseModel, never via model-config ``__getattr__``."""
cfg = getattr(model, "model_config", None)
data = _instance_dict(cfg) if cfg is not None else None
if not data:
return None
unet = data.get("unet_config")
return unet if isinstance(unet, dict) else None
def is_h3_base(model) -> bool:
if model is None:
return False
if type(model).__name__ == "MiniMaxH3":
return True
diffusion = getattr(model, "diffusion_model", None)
if diffusion is not None and type(diffusion).__name__ == "MiniMaxH3Model":
return True
unet = unet_config_of(model)
return bool(unet) and unet.get("image_model") == "minimax_h3"
def _key_for(sd, prefix: str, name: str):
prefix = prefix or ""
for key in (prefix + name if prefix else None, name,
None if prefix else "diffusion_model." + name):
if key and key in sd:
return key
return None
def peel(sd, prefix: str = "") -> dict:
"""Pop H3 adaLN sidecar tensors out of a checkpoint state dict.
Mutates ``sd``. Non-tensor values are ignored and left in place.
"""
if not isinstance(sd, dict) or not sd:
return {}
found = {}
for name in SIDECAR_NAMES:
key = _key_for(sd, prefix, name)
if key is None:
continue
value = sd[key]
if torch.is_tensor(value):
found[name] = sd.pop(key)
return found
def attach(model, sidecars: dict):
"""Stash sidecars on the H3 BaseModel and its DiT. Survives ModelPatcher.clone."""
if not sidecars or model is None:
return
existing = _as_mapping(getattr(model, STASH_ATTR, None)) or {}
merged = {**existing, **sidecars}
try:
object.__setattr__(model, STASH_ATTR, merged)
except Exception:
pass
diffusion = getattr(model, "diffusion_model", None)
if diffusion is not None and diffusion is not model:
try:
object.__setattr__(diffusion, STASH_ATTR, merged)
except Exception:
pass
patcher = getattr(model, "current_patcher", None)
setter = getattr(patcher, "set_attachments", None) if patcher is not None else None
if callable(setter):
try:
setter(PATCHER_ATTACHMENT, merged)
except Exception:
pass
LOG.info("H3 PowerLoraStack: captured checkpoint adaLN sidecars %s",
", ".join(sorted(merged)))
def _take_named_tensors(obj, found: dict):
extra = _as_mapping(getattr(obj, STASH_ATTR, None))
if extra:
found.update(extra)
for name in SIDECAR_NAMES:
try:
value = object.__getattribute__(obj, name)
except AttributeError:
continue
if torch.is_tensor(value):
found.setdefault(name, value)
def collect(patcher=None, diffusion_model=None) -> dict | None:
"""Gather baked adaLN sidecars without touching model-config ``__getattr__``."""
found: dict = {}
objects = []
model = None
if patcher is not None:
objects.append(patcher)
getter = getattr(patcher, "get_attachment", None)
if callable(getter):
extra = _as_mapping(getter(PATCHER_ATTACHMENT))
if extra:
found.update(extra)
model = getattr(patcher, "model", None)
if model is not None:
objects.append(model)
cfg = getattr(model, "model_config", None)
data = _instance_dict(cfg) if cfg is not None else None
if data:
extra = _as_mapping(data.get("adaln_sidecars"))
if extra:
found.update(extra)
if diffusion_model is None:
diffusion_model = getattr(model, "diffusion_model", None)
if diffusion_model is not None:
objects.append(diffusion_model)
for obj in objects:
if obj is None:
continue
_take_named_tensors(obj, found)
return found or None
def wrap_load_model_weights(cls) -> bool:
"""Wrap ``cls.load_model_weights`` so H3 sidecar keys never become unexpected."""
orig = getattr(cls, "load_model_weights", None)
if not callable(orig) or getattr(orig, "_h3_adaln_hook", False):
return bool(getattr(orig, "_h3_adaln_hook", False))
def wrapped(self, sd, *args, **kwargs):
prefix = kwargs["unet_prefix"] if "unet_prefix" in kwargs else (
args[0] if args else "")
if isinstance(sd, dict) and is_h3_base(self):
found = peel(sd, prefix)
if found:
attach(self, found)
return orig(self, sd, *args, **kwargs)
wrapped._h3_adaln_hook = True
wrapped._h3_adaln_orig = orig
cls.load_model_weights = wrapped
return True
def ensure_load_hook() -> bool:
"""Install the loader wrap on Comfy's BaseModel. No-op if Comfy is absent.
Also wraps ``MiniMaxH3`` when that subclass defines its own
``load_model_weights``, so a native override cannot bypass the peel.
"""
try:
from comfy import model_base
except Exception:
return False
installed = False
for name in ("BaseModel", "MiniMaxH3"):
cls = getattr(model_base, name, None)
if cls is None:
continue
if name != "BaseModel" and "load_model_weights" not in vars(cls):
continue
try:
installed = wrap_load_model_weights(cls) or installed
except Exception:
LOG.debug("H3 PowerLoraStack: could not wrap %s.load_model_weights",
name, exc_info=True)
return installed
+162
View File
@@ -0,0 +1,162 @@
import unittest
from types import SimpleNamespace
import torch
from h3lora.sidecars import (
STASH_ATTR,
attach,
collect,
is_h3_base,
peel,
wrap_load_model_weights,
)
class _WarnConfig:
"""Mimics Comfy ``supported_models_base.BASE.__getattr__``."""
def __init__(self, unet_config=None, sidecars=None):
self.unet_config = unet_config or {"image_model": "minimax_h3"}
if sidecars is not None:
self.adaln_sidecars = sidecars
self.accessed = []
def __getattr__(self, name):
self.accessed.append(name)
raise AssertionError(
"model config __getattr__(%r) must not be used" % (name,)
)
class _H3Base:
def __init__(self, config=None, leftover_capture=None):
self.model_config = config or _WarnConfig()
self.diffusion_model = SimpleNamespace()
self.leftover_capture = leftover_capture if leftover_capture is not None else []
self.current_patcher = None
def load_model_weights(self, sd, unet_prefix="", assign=False):
self.leftover_capture.extend(
k[len(unet_prefix):] for k in list(sd) if k.startswith(unet_prefix)
)
return self
class SidecarPeelTests(unittest.TestCase):
def test_peel_pops_prefixed_tensors(self):
basis = torch.randn(8, 3)
mean = torch.randn(8)
sd = {
"diffusion_model.adaln_basis": basis,
"diffusion_model.adaln_mean": mean,
"diffusion_model.adaln_t_table": torch.randn(16, 3),
"diffusion_model.blocks.0.weight": torch.randn(2, 2),
}
got = peel(sd, "diffusion_model.")
self.assertTrue(torch.equal(got["adaln_basis"], basis))
self.assertTrue(torch.equal(got["adaln_mean"], mean))
self.assertNotIn("silu_t_emb_grid", got)
self.assertEqual(sorted(sd), [
"diffusion_model.adaln_t_table",
"diffusion_model.blocks.0.weight",
])
def test_peel_bare_keys_and_grid(self):
grid = torch.randn(4, 8)
sd = {"adaln_basis": torch.randn(8, 2), "silu_t_emb_grid": grid}
got = peel(sd, "")
self.assertIn("adaln_basis", got)
self.assertTrue(torch.equal(got["silu_t_emb_grid"], grid))
self.assertFalse(sd)
def test_peel_ignores_non_tensors(self):
sd = {"adaln_basis": "not-a-tensor", "adaln_mean": torch.randn(3)}
got = peel(sd)
self.assertEqual(list(got), ["adaln_mean"])
self.assertIn("adaln_basis", sd)
class SidecarCollectTests(unittest.TestCase):
def test_collect_uses_stashed_model_attr(self):
basis = torch.randn(8, 3)
mean = torch.randn(8)
model = _H3Base()
attach(model, {"adaln_basis": basis, "adaln_mean": mean})
patcher = SimpleNamespace(model=model, get_attachment=lambda key: None)
got = collect(patcher, model.diffusion_model)
self.assertTrue(torch.equal(got["adaln_basis"], basis))
self.assertTrue(torch.equal(got["adaln_mean"], mean))
self.assertEqual(model.model_config.accessed, [])
def test_collect_reads_config_dict_without_getattr(self):
sidecars = {"adaln_basis": torch.randn(4, 2), "adaln_mean": torch.randn(4)}
cfg = _WarnConfig(sidecars=sidecars)
model = SimpleNamespace(model_config=cfg, diffusion_model=SimpleNamespace())
patcher = SimpleNamespace(model=model, get_attachment=lambda key: None)
got = collect(patcher)
self.assertTrue(torch.equal(got["adaln_basis"], sidecars["adaln_basis"]))
self.assertEqual(cfg.accessed, [])
def test_collect_skips_missing_config_attr(self):
cfg = _WarnConfig()
model = SimpleNamespace(model_config=cfg, diffusion_model=SimpleNamespace())
patcher = SimpleNamespace(model=model)
self.assertIsNone(collect(patcher))
self.assertEqual(cfg.accessed, [])
class SidecarHookTests(unittest.TestCase):
def test_is_h3_from_unet_config_without_getattr(self):
model = _H3Base()
self.assertTrue(is_h3_base(model))
self.assertEqual(model.model_config.accessed, [])
other = SimpleNamespace(
model_config=_WarnConfig({"image_model": "flux"}),
diffusion_model=SimpleNamespace(),
)
self.assertFalse(is_h3_base(other))
self.assertEqual(other.model_config.accessed, [])
def test_hook_peels_before_load_and_is_idempotent(self):
leftover = []
wrap_load_model_weights(_H3Base)
wrap_load_model_weights(_H3Base)
model = _H3Base(leftover_capture=leftover)
basis = torch.randn(8, 3)
mean = torch.randn(8)
sd = {
"adaln_basis": basis,
"adaln_mean": mean,
"blocks.0.weight": torch.randn(2, 2),
}
model.load_model_weights(sd, unet_prefix="")
self.assertEqual(leftover, ["blocks.0.weight"])
self.assertNotIn("adaln_basis", sd)
stashed = getattr(model, STASH_ATTR)
self.assertTrue(torch.equal(stashed["adaln_basis"], basis))
self.assertTrue(torch.equal(stashed["adaln_mean"], mean))
self.assertTrue(torch.equal(
getattr(model.diffusion_model, STASH_ATTR)["adaln_basis"], basis))
self.assertEqual(model.model_config.accessed, [])
def test_hook_leaves_non_h3_state_dict_alone(self):
class Other:
def __init__(self):
self.model_config = _WarnConfig({"image_model": "flux"})
self.seen = None
def load_model_weights(self, sd, unet_prefix="", assign=False):
self.seen = dict(sd)
return self
wrap_load_model_weights(Other)
model = Other()
sd = {"adaln_basis": torch.randn(2, 2), "w": torch.randn(1)}
model.load_model_weights(sd)
self.assertIn("adaln_basis", model.seen)
self.assertFalse(hasattr(model, STASH_ATTR))
if __name__ == "__main__":
unittest.main()