Files
automatic/pipelines/native_transformer.py
T
Vladimir Mandic 0e35079f06 attempt to load incomplete model
Signed-off-by: Vladimir Mandic <mandic00@live.com>
2026-06-29 13:39:46 +02:00

647 lines
26 KiB
Python

"""Generic native loader for DiT transformers and bundled sibling components.
Loads a single-safetensors file into a diffusers (or custom) transformer class
when the user selects an override via the UNET dropdown (``shared.opts.sd_unet``).
Bypasses :func:`diffusers.loaders.FromOriginalModelMixin.from_single_file` so
sdnext owns prefix detection, optional sibling partitioning, dtype/quant/offload
handling, and explicit validation of missing/unexpected keys.
The per-arch knobs are captured in :class:`TransformerSpec`. Each pipeline
defines its spec in ``pipelines/<arch>/__init__.py`` and passes it explicitly
to :func:`load` (or to :func:`pipelines.generic.load_transformer` via the
``native_spec`` kwarg). No class-keyed registry: two pipelines may share a
transformer class but need different specs (e.g. Anima vs raw Cosmos both
use ``CosmosTransformer3DModel`` but Anima has a bundled ``llm_adapter``
sibling). Pipelines without a custom spec fall back to
:func:`make_default_spec`, which opportunistically picks up a real converter
from diffusers' ``SINGLE_FILE_LOADABLE_CLASSES`` table.
Algorithm:
1. Read the safetensors state dict (.gguf and .pth are rejected up front).
2. Detect and strip one of the spec's known prefixes (raises on mixed prefixes).
3. Check forbidden markers (catches structural mismatches like Cosmos 1.0 keys
in a Cosmos 2.0 loader).
4. Partition off sibling component keys (e.g. Anima's bundled ``llm_adapter.*``).
5. Run the spec's converter if present (else pass through unchanged).
6. Fetch ``<subfolder>/config.json`` from the base repo, instantiate via
``cls.from_config``, ``load_state_dict(strict=False)``, validate, dtype-cast,
quantize, and offload-place.
7. Repeat the build for each populated sibling (no converter, no quant by
default; sibling weights are read raw from the bundled file).
Returns ``(transformer, sibling_components_dict)``. The dict is empty for
arches with no siblings; for Anima it carries the ``llm_adapter`` if the
community file bundled one, else the empty dict and the caller falls back to
loading the adapter from the base repo.
"""
import os
import time
from dataclasses import dataclass, field
from typing import Callable
import huggingface_hub as hf
import torch
from modules import shared, devices, sd_models, model_quant, errors
from modules.logger import log, console
DEFAULT_PREFIXES: tuple[str, ...] = (
"model.diffusion_model.",
"diffusion_model.",
"net.",
)
DEFAULT_ACCEPTABLE_MISSING: tuple[str, ...] = (
"rope.",
"pos_embedder.",
"learnable_pos_embed.",
)
class OverrideArchMismatch(Exception):
"""Raised when a user-selected UNET/DiT override cannot be loaded as the
spec's transformer class: the arch-specific converter rejects its keys, or
``load_state_dict`` reports unexpected or missing keys.
:func:`pipelines.generic_transformer.load_transformer` catches this to drop
the override and load the base repo transformer instead, so a stale or
wrong-arch UNET selection degrades to the base model rather than crashing
inside a converter.
A tensor shape mismatch on otherwise-matching keys is not raised as this; it
surfaces as the native ``load_state_dict`` error, which already names the
conflicting shapes, so it stays a hard load error rather than a fall back.
"""
@dataclass(frozen=True)
class SiblingSpec:
"""Describes a non-transformer component that may ship inline in the same
safetensors as the transformer (e.g. Anima's ``llm_adapter``).
``subfolder`` names the base repo subfolder holding the canonical config and
weights when the sibling is NOT bundled inline; ``inline_prefix`` is the
key prefix that identifies the sibling's weights within the bundled file
(after the transformer's prefix has already been stripped).
"""
subfolder: str
inline_prefix: str
acceptable_missing: tuple[str, ...] = ()
@dataclass(frozen=True)
class TransformerSpec:
"""Per-arch configuration for the native loader.
Most arches only need to override ``cls`` (and rely on the default
prefixes, no converter, no siblings). Arches with bundled-sibling
components (Anima) or unusual key conventions (custom converters,
Cosmos-style structural markers) override the relevant fields.
"""
cls: type
subfolder: str = "transformer"
prefixes: tuple[str, ...] = DEFAULT_PREFIXES
converter: Callable[[dict], dict] | None = None
siblings: dict[str, SiblingSpec] = field(default_factory=dict)
acceptable_missing: tuple[str, ...] = DEFAULT_ACCEPTABLE_MISSING
forbidden_markers: tuple[tuple[str, str], ...] = ()
def make_default_spec(cls: type) -> TransformerSpec:
"""Synthesize a default spec for ``cls``: default prefixes, no siblings,
no forbidden markers, and a converter picked up automatically from
diffusers' ``SINGLE_FILE_LOADABLE_CLASSES`` table if one exists (and is
not the no-op identity lambda that ``QwenImageTransformer2DModel`` and
a few other classes register).
Used by callers (notably :func:`pipelines.generic.load_transformer`) when
a pipeline does not supply a custom ``TransformerSpec`` of its own.
"""
return TransformerSpec(cls=cls, converter=auto_pickup_converter(cls))
def auto_pickup_converter(cls: type) -> Callable[[dict], dict] | None:
"""Pull a checkpoint converter from diffusers for ``cls`` when one exists.
Skipped for the no-op identity lambda some classes register
(notably ``QwenImageTransformer2DModel``), because using it would silently
accept whatever key naming the file happens to have.
"""
try:
from diffusers.loaders.single_file_model import SINGLE_FILE_LOADABLE_CLASSES
except ImportError:
return None
entry = SINGLE_FILE_LOADABLE_CLASSES.get(cls.__name__)
if entry is None:
return None
fn = entry.get("checkpoint_mapping_fn")
if fn is None or is_noop_converter(fn):
return None
return fn
def is_noop_converter(fn: Callable) -> bool:
"""Detect ``lambda checkpoint, **kwargs: checkpoint`` and equivalents.
Strips inline ``#`` comments from the source line before inspecting the
body, so that diagnostic markers like ``# noqa`` on the lambda's source
line do not defeat the detection.
"""
try:
import inspect
src = inspect.getsource(fn).strip()
except (OSError, TypeError):
return False
if "lambda" not in src:
return False
if "#" in src:
src = src.split("#", 1)[0].rstrip()
body = src.split(":", 1)[-1].strip().rstrip(",").rstrip(")")
return body.endswith("checkpoint")
def resolve_path() -> str | None:
"""Return the absolute path of the UNET dropdown selection, or None if
no selection is active or the file is unresolvable.
"""
sel = shared.opts.sd_unet
if sel is None or sel in ("Default", "None"):
return None
from modules import sd_unet
if sel not in list(sd_unet.unet_dict):
log.error(f'Load module: type=transformer file="{sel}" not found')
return None
path = sd_unet.unet_dict[sel]
if not os.path.exists(path):
log.error(f'Load module: type=transformer path="{path}" does not exist')
return None
return path
def load(
local_file: str,
repo_id: str,
spec: TransformerSpec,
diffusers_cfg: dict | None = None,
sibling_classes: dict[str, type] | None = None,
*,
allow_quant: bool = True,
dtype=None,
modules_to_not_convert: list | None = None,
modules_dtype_dict: dict | None = None,
quant_args: dict | None = None,
quant_type: str | None = None,
**kwargs,
) -> tuple[object, dict[str, object]]:
"""Load the transformer (and any bundled siblings) from ``local_file``.
``sibling_classes`` supplies the runtime class for each sibling named in
``spec.siblings``. Required when a sibling has a dynamic class (e.g.
Anima's ``AnimaLLMAdapter`` is loaded from remote_code at runtime).
Missing sibling classes raise ``ValueError`` if the corresponding sibling
keys are present in the bundled file.
Keyword-only arguments ``allow_quant``, ``dtype``, ``modules_to_not_convert``,
and ``modules_dtype_dict`` mirror the corresponding kwargs of
:func:`pipelines.generic.load_transformer` and are forwarded unchanged.
``quant_args`` and ``quant_type`` are precomputed by the caller; when
``None`` they are derived here via ``model_quant.get_dit_args``. Extra
``**kwargs`` reach the transformer's ``cls.from_config``; siblings do not
receive them.
Returns ``(transformer, siblings_dict)``. ``siblings_dict`` is keyed by
sibling name and is empty for non-sibling specs, or for sibling specs
whose keys are absent from the bundled file.
"""
if diffusers_cfg is None:
diffusers_cfg = {}
if sibling_classes is None:
sibling_classes = {}
t0 = time.time()
if not local_file.lower().endswith(".safetensors"):
raise ValueError(
f"Load model: type={spec.cls.__name__} custom transformer requires .safetensors, "
f'got "{local_file}"'
)
if quant_args is None:
_, quant_args = model_quant.get_dit_args(
diffusers_cfg, module="Model", device_map=True,
allow_quant=allow_quant,
modules_to_not_convert=modules_to_not_convert,
modules_dtype_dict=modules_dtype_dict,
)
quant_type = model_quant.get_quant_type(quant_args)
log.debug(f' cls={spec.cls.__name__} file="{os.path.basename(local_file)}" reading state_dict')
state_dict = sd_models.read_state_dict(local_file, what="transformer")
state_dict = strip_prefix(state_dict, spec.prefixes, spec.cls.__name__)
check_forbidden_markers(state_dict, spec.forbidden_markers, spec.cls.__name__, local_file)
transformer_sd, sibling_sds = partition_siblings(state_dict, spec.siblings)
del state_dict
sibling_counts = {name: len(sd) for name, sd in sibling_sds.items() if sd}
log.info(
f' cls={spec.cls.__name__} custom="{os.path.basename(local_file)}" '
f"transformer_keys={len(transformer_sd)} siblings={sibling_counts or '{}'}"
)
effective_dtype = dtype if dtype is not None else devices.dtype
transformer_cfg = fetch_component_config(repo_id, spec.subfolder)
transformer = build_component(
component_name="transformer",
state_dict=transformer_sd,
config=transformer_cfg,
cls=spec.cls,
converter=spec.converter,
acceptable_missing=spec.acceptable_missing,
quant_args=quant_args,
quant_type=quant_type,
dtype=effective_dtype,
modules_to_not_convert=modules_to_not_convert,
modules_dtype_dict=modules_dtype_dict,
**kwargs,
)
del transformer_sd
devices.torch_gc()
loaded_siblings: dict[str, object] = {}
for name, sibling_sd in sibling_sds.items():
if not sibling_sd:
continue
sibling_spec = spec.siblings[name]
sibling_cls = sibling_classes.get(name)
if sibling_cls is None:
raise ValueError(
f"Load model: type={spec.cls.__name__} bundled sibling '{name}' present in "
f"file but no class was supplied via sibling_classes"
)
sibling_cfg = fetch_component_config(repo_id, sibling_spec.subfolder)
loaded_siblings[name] = build_component(
component_name=name,
state_dict=sibling_sd,
config=sibling_cfg,
cls=sibling_cls,
converter=None,
acceptable_missing=sibling_spec.acceptable_missing,
quant_args={},
quant_type=None,
dtype=effective_dtype,
)
sd_models.allow_post_quant = False
devices.torch_gc()
log.debug(f"Load model: type={spec.cls.__name__} native_transformer time={time.time() - t0:.2f}")
return transformer, loaded_siblings
def strip_prefix(state_dict: dict, prefixes: tuple[str, ...], type_name: str) -> dict:
"""Detect and uniformly strip the most common known prefix from every key.
Order matters: longer prefixes win over shorter ones with the same suffix
(e.g. ``model.diffusion_model.`` beats ``diffusion_model.``). If some keys
match the dominant prefix and others do not, raises ValueError because
mixed prefixes indicate a malformed file rather than a recoverable export
quirk.
"""
sorted_prefixes = sorted(prefixes, key=len, reverse=True)
counts: dict[str, int] = {}
seen = 0
for key in state_dict:
for prefix in sorted_prefixes:
if key.startswith(prefix):
counts[prefix] = counts.get(prefix, 0) + 1
seen += 1
break
total = len(state_dict)
if seen == 0:
log.debug(f"Load model: type={type_name} native_transformer prefix=bare")
return state_dict
dominant = max(counts, key=counts.get)
if counts[dominant] != total:
raise ValueError(
f"Load model: type={type_name} native_transformer has mixed prefixes "
f"(total={total} {dominant}={counts[dominant]})"
)
log.debug(f'Load model: type={type_name} native_transformer prefix="{dominant}"')
offset = len(dominant)
return {key[offset:]: value for key, value in state_dict.items()}
def check_forbidden_markers(
state_dict: dict,
forbidden_markers: tuple[tuple[str, str], ...],
type_name: str,
local_file: str,
) -> None:
"""Raise if any forbidden marker key is present in the state_dict.
Catches structural mismatches that pass prefix detection but indicate the
file is from an incompatible architecture variant (e.g. Cosmos 1.0 keys
showing up in a Cosmos 2.0 loader path).
"""
for marker, description in forbidden_markers:
if marker in state_dict:
raise ValueError(
f"Load model: type={type_name} native_transformer rejects "
f'"{os.path.basename(local_file)}" ({description}; marker key {marker!r})'
)
def partition_siblings(
state_dict: dict,
siblings: dict[str, SiblingSpec],
) -> tuple[dict, dict[str, dict]]:
"""Split state_dict into (transformer_sd, {sibling_name: sibling_sd}).
Keys matching a sibling's ``inline_prefix`` go into that sibling's dict
with the prefix stripped; everything else stays in the transformer dict.
Sibling names with no matching keys still appear in the output dict but
map to an empty dict, so the caller can iterate uniformly.
"""
sibling_sds: dict[str, dict] = {name: {} for name in siblings}
transformer_sd: dict = {}
if not siblings:
return state_dict, sibling_sds
sibling_lookups = [(name, siblings[name].inline_prefix) for name in siblings]
for key, value in state_dict.items():
matched = False
for name, prefix in sibling_lookups:
if key.startswith(prefix):
sibling_sds[name][key[len(prefix):]] = value
matched = True
break
if not matched:
transformer_sd[key] = value
return transformer_sd, sibling_sds
def fetch_component_config(repo_id: str, subfolder: str) -> dict:
"""Download and parse ``<subfolder>/config.json`` from the base repo."""
relative_path = f"{subfolder}/config.json"
try:
local = hf.hf_hub_download(repo_id, filename=relative_path, cache_dir=shared.opts.diffusers_dir)
except Exception as e:
log.error(f' path="{relative_path}" repo="{repo_id}" failed to download: {e}')
raise RuntimeError('') from e
return shared.readfile(local, as_type="dict")
def build_component_quantized(
*,
component_name: str,
state_dict: dict,
config: dict,
cls: type,
quant_args: dict,
dtype,
acceptable_missing: tuple[str, ...],
**kwargs,
) -> object:
"""Build a component with per-tensor SDNQ quantization during load.
Mirrors the per-tensor loop in
:func:`diffusers.models.model_loading_utils.load_model_dict_into_meta`:
iterates the (already-converted) state_dict and dispatches each tensor
through ``SDNQQuantizer.check_if_quantized_param`` /
``create_quantized_param`` so Linear/Conv/Embed weights are packed to
uint4 as they land, while biases, LayerNorms, and other non-quantizable
parameters go through ``accelerate.set_module_tensor_to_device``.
The component is constructed inside ``init_empty_weights(include_buffers=
False)`` so parameter slots are meta tensors (no full bf16 instantiation
upfront) while computed buffers like ``rope.freqs`` materialize normally
during ``cls.from_config(config)``. After the loop, the quantizer's
``_process_model_after_weight_loading`` hook attaches
``quantization_config`` and handles the CPU offload move.
Caller is responsible for running the converter and prefix stripping
before passing ``state_dict``.
"""
import rich.progress as rp
from accelerate import init_empty_weights
from accelerate.utils import set_module_tensor_to_device
from modules.sdnq.quantizer import SDNQQuantizer
quantization_config = quant_args.get("quantization_config")
if quantization_config is None:
raise ValueError(
f"Load model: transformer=native {component_name} "
f"per-tensor quantization requires quant_args['quantization_config']"
)
target_dtype = dtype if dtype is not None else devices.dtype
quantizer = SDNQQuantizer(quantization_config, pre_quantized=False)
quantizer.torch_dtype = target_dtype
with init_empty_weights(include_buffers=False):
component = cls.from_config(config, **kwargs)
quantizer._process_model_before_weight_loading(component, device_map=None) # pylint: disable=protected-access
target_device = (
devices.cpu if shared.opts.diffusers_offload_mode != "none"
else devices.device
)
expected_keys = set(component.state_dict().keys())
loaded_keys: set[str] = set()
unexpected: list[str] = []
total = len(state_dict)
pbar = rp.Progress(
rp.TextColumn(f'[cyan]Load {component_name}:'),
rp.BarColumn(),
rp.MofNCompleteColumn(),
rp.TaskProgressColumn(),
rp.TimeRemainingColumn(),
rp.TimeElapsedColumn(),
rp.TextColumn('[cyan]{task.description}'),
console=console,
)
with pbar:
task = pbar.add_task(total=total, description=cls.__name__)
for name, value in state_dict.items():
if name in expected_keys:
if torch.is_floating_point(value):
value = value.to(target_dtype)
if quantizer.check_if_quantized_param(component, value, name):
quantizer.create_quantized_param(component, value, name, target_device, dtype=target_dtype)
else:
set_module_tensor_to_device(component, name, target_device, value=value, dtype=target_dtype)
loaded_keys.add(name)
else:
unexpected.append(name)
pbar.update(task, advance=1)
missing = sorted(expected_keys - loaded_keys)
validate_state_dict_load(component_name, missing, unexpected, acceptable_missing)
component = quantizer._process_model_after_weight_loading(component) # pylint: disable=protected-access
return component
def build_component(
*,
component_name: str,
state_dict: dict,
config: dict,
cls: type,
converter: Callable[[dict], dict] | None,
acceptable_missing: tuple[str, ...],
quant_args: dict,
quant_type: str | None,
dtype=None,
modules_to_not_convert: list | None = None,
modules_dtype_dict: dict | None = None,
**kwargs,
) -> object:
"""Convert (if needed), instantiate, load weights, dtype-cast, quantize,
and offload-place a single component. Raises on any hard failure.
For the transformer component under SDNQ, the per-tensor pre-mode path
in :func:`build_component_quantized` is used so quantization is applied
in flight (one layer's worth of bf16 in memory at a time). All other
cases (siblings, non-quantized loads, NVIDIAModelOptConfig, layerwise
quant) go through the standard load_state_dict + post-quantize path.
``dtype`` overrides ``devices.dtype`` when supplied; otherwise the global
default is used. ``modules_to_not_convert`` and ``modules_dtype_dict``
are forwarded to :func:`apply_quant` for the post-mode path; pre-mode
receives them via the SDNQConfig in ``quant_args``. Extra ``**kwargs``
reach ``cls.from_config`` for both construction paths.
"""
try:
if converter is not None:
log.debug(f'Load model: transformer=native {component_name} converter={converter.__name__} keys={len(state_dict)}')
try:
sd = converter(state_dict)
except Exception as e:
raise OverrideArchMismatch(
f"Load model: type={cls.__name__} native_transformer converter "
f"{converter.__name__} rejected the override ({type(e).__name__}: {e}); "
f"file does not look like a {cls.__name__} checkpoint"
) from e
else:
sd = state_dict
if component_name == "transformer" and quant_type == "SDNQConfig":
component = build_component_quantized(
component_name=component_name,
state_dict=sd,
config=config,
cls=cls,
quant_args=quant_args,
dtype=dtype,
acceptable_missing=acceptable_missing,
**kwargs,
)
del sd
devices.torch_gc()
return component
log.debug(f'Load model: transformer=native {component_name} loading keys={len(sd)} cls={cls.__name__}')
component = cls.from_config(config, **kwargs)
missing, unexpected = component.load_state_dict(sd, strict=False)
validate_state_dict_load(component_name, missing, unexpected, acceptable_missing)
del sd
devices.torch_gc()
target_dtype = dtype if dtype is not None else devices.dtype
log.debug(f'Load model: transformer=native {component_name} cast dtype={target_dtype}')
component = component.to(dtype=target_dtype)
except OverrideArchMismatch:
raise
except Exception as e:
log.error(f"Load model: transformer=native {component_name} load failed: {e}")
errors.display(e, "Load")
raise
if component_name == "transformer":
apply_quant(
component,
quant_type,
modules_to_not_convert=modules_to_not_convert,
modules_dtype_dict=modules_dtype_dict,
)
if shared.opts.diffusers_offload_mode != "none":
sd_models.move_model(component, devices.cpu)
if not hasattr(component, "quantization_config"):
if hasattr(component, "config") and hasattr(component.config, "quantization_config"):
component.quantization_config = component.config.quantization_config
elif quant_type is not None and quant_args.get("quantization_config") is not None:
component.quantization_config = quant_args.get("quantization_config")
return component
def validate_state_dict_load(
component_name: str,
missing: list[str],
unexpected: list[str],
acceptable_missing: tuple[str, ...],
) -> None:
"""Raise :class:`OverrideArchMismatch` if load_state_dict produced
unexpected keys or non-acceptable missing keys, which means the override's
weights do not fit the target class. Buffer-only missing keys matching the
``acceptable_missing`` prefix list are logged at debug level and ignored.
"""
if unexpected:
sample = ", ".join(unexpected[:5])
raise OverrideArchMismatch(
f"Load model: transformer=native {component_name} has {len(unexpected)} "
f"unexpected keys (sample: {sample})"
)
hard_missing = [
k for k in missing if not any(k.startswith(p) for p in acceptable_missing)
]
if hard_missing:
sample = ", ".join(hard_missing[:5])
raise OverrideArchMismatch(
f"Load model: transformer=native {component_name} missing "
f"{len(hard_missing)} required keys (sample: {sample})"
)
if missing:
log.debug(
f"Load model: transformer=native {component_name} ignored "
f"{len(missing)} buffer-only missing keys"
)
def apply_quant(
transformer: object,
quant_type: str | None,
modules_to_not_convert: list | None = None,
modules_dtype_dict: dict | None = None,
) -> None:
"""Apply post-load quantization to a fully-loaded transformer.
Used as a fallback for cases that don't go through the per-tensor
pre-mode path in :func:`build_component_quantized`: ``layerwise_quantization``
(via ``do_post_load_quant``), and the no-quant case (no-op). SDNQ post
mode also reaches this path because pre-mode dispatch only triggers
when an SDNQConfig is present (which itself only happens under modes
``pre`` or ``auto``).
``modules_to_not_convert`` and ``modules_dtype_dict`` are forwarded
to :func:`sdnq_quantize_model` so per-call skip lists set by the
caller are honored.
"""
if quant_type == "NVIDIAModelOptConfig":
log.warning(
"Load model: transformer=native quant=TRT not supported on native path, skipping"
)
elif quant_type == "SDNQConfig":
model_quant.sdnq_quantize_model(
transformer,
op="transformer",
modules_to_not_convert=modules_to_not_convert,
modules_dtype_dict=modules_dtype_dict,
)
model_quant.do_post_load_quant(transformer, allow=False)