234 lines
9.5 KiB
Python
234 lines
9.5 KiB
Python
"""
|
|
checkpoint_normalizer.py — Architecture-Aware Checkpoint Key Normalizer
|
|
|
|
Detects model architecture from checkpoint tensor keys and normalizes keys
|
|
to a canonical form so that checkpoints from different sources (e.g., with
|
|
or without the "model.diffusion_model." prefix) can be properly matched
|
|
and merged.
|
|
|
|
Extensible design: add new architectures to CHECKPOINT_ARCHITECTURES dict.
|
|
"""
|
|
|
|
from typing import Dict, List, Tuple, Optional
|
|
|
|
# ============================================================================
|
|
# ARCHITECTURE REGISTRY — Add new models here
|
|
# ============================================================================
|
|
# Each entry defines:
|
|
# detect: List of substrings; if ANY key in the checkpoint contains ALL
|
|
# of these (AND-logic per entry), the architecture matches.
|
|
# Multiple entries = OR logic (first matching entry wins).
|
|
# strip_prefixes: List of prefixes to remove from keys to reach canonical form.
|
|
# canonical_form: Description of canonical form for documentation.
|
|
|
|
CHECKPOINT_ARCHITECTURES = {
|
|
"anima": {
|
|
"detect": [
|
|
# Anima models use net.blocks.* pattern (FLOW-based diffusion)
|
|
["net.blocks"],
|
|
# Also detect by net.final_layer as secondary marker
|
|
["net.final_layer"],
|
|
# Detect bare blocks.* keys without net. prefix (common in fine-tuned
|
|
# checkpoints). Uses AND logic: both "blocks." and "adaln_modulation"
|
|
# must exist in the keyset. "blocks." alone would also match SDXL's
|
|
# input_blocks.* / output_blocks.* and Flux's double_blocks.*, so
|
|
# "adaln_modulation" narrows it to Anima-specific key patterns.
|
|
["blocks.", "adaln_modulation"],
|
|
],
|
|
# Strip "net." prefix so that net.blocks.* and bare blocks.* keys
|
|
# normalize to the same canonical form for matching.
|
|
"strip_prefixes": ["net."],
|
|
"canonical_form": "bare blocks. prefix — keys like blocks.0.adaln_modulation_cross_attn.1.weight",
|
|
"description": "Anima / Anime Diffusion (FLOW-based model with blocks architecture)",
|
|
},
|
|
"flux": {
|
|
"detect": ["double_blocks", "single_blocks"],
|
|
"strip_prefixes": ["model.diffusion_model."],
|
|
"canonical_form": "no_prefix — keys like double_blocks.0.img_attn.proj.weight",
|
|
"description": "FLUX / FLUX.1-dev / FLUX.1-schnell (Black Forest Labs)",
|
|
},
|
|
"lumina2": {
|
|
"detect": [
|
|
# Primary: cap_embedder is unique to NextDiT / Lumina2
|
|
["cap_embedder"],
|
|
],
|
|
"strip_prefixes": ["model.diffusion_model."],
|
|
"canonical_form": "no prefix — keys like layers.0.attention.to_q.weight, cap_embedder.0.weight",
|
|
"description": "Lumina2 / Lumina Image 2.0 / NextDiT (Alpha-VLLM, base for Z-Image Turbo)",
|
|
},
|
|
"z_image": {
|
|
"detect": [
|
|
# Primary: Z-Image has 'layers' patterns
|
|
["layers", "attention"],
|
|
# Fallback: explicit z_image marker
|
|
["z_image"],
|
|
],
|
|
"strip_prefixes": ["model.diffusion_model."],
|
|
"canonical_form": "no_prefix — keys like diffusion_model.layers.0.attention.to_q.weight",
|
|
"description": "Z-Image (Lightrick / Z-inversion models)",
|
|
},
|
|
"sdxl": {
|
|
"detect": [
|
|
# SDXL has input_blocks / middle_block / output_blocks WITH model.diffusion_model. prefix
|
|
["model.diffusion_model", "input_blocks"],
|
|
["model.diffusion_model", "middle_block"],
|
|
["model.diffusion_model", "output_blocks"],
|
|
# Catch-all: any key with model.diffusion_model. prefix is SDXL
|
|
# (FLUX and Z-Image are checked first, so they won't reach here)
|
|
["model.diffusion_model"],
|
|
],
|
|
"strip_prefixes": [],
|
|
"canonical_form": "with model.diffusion_model. prefix — keys like model.diffusion_model.input_blocks.0.0.weight",
|
|
"description": "Stable Diffusion XL",
|
|
},
|
|
"sd15": {
|
|
"detect": [
|
|
# SD1.5 has input_blocks WITHOUT model.diffusion_model prefix (but has diffusion_model)
|
|
# Only match if NOT sdxl (checked by detect order)
|
|
["diffusion_model", "input_blocks"],
|
|
["diffusion_model", "middle_block"],
|
|
["diffusion_model", "output_blocks"],
|
|
],
|
|
"strip_prefixes": [],
|
|
"canonical_form": "with diffusion_model. prefix — keys like diffusion_model.input_blocks.0.0.weight",
|
|
"description": "Stable Diffusion 1.5",
|
|
},
|
|
}
|
|
|
|
|
|
def _matches_architecture(keys: List[str], arch_config: dict) -> bool:
|
|
"""
|
|
Check if any key pattern in arch_config['detect'] matches the given keys.
|
|
Each entry in 'detect' is EITHER a string (all keys are checked for that
|
|
substring) OR a list of strings (all substrings must be present, AND-logic).
|
|
Multiple entries = OR logic.
|
|
"""
|
|
detect_patterns = arch_config.get("detect", [])
|
|
for pattern in detect_patterns:
|
|
if isinstance(pattern, str):
|
|
# Single substring — any key containing it
|
|
if any(pattern in k for k in keys):
|
|
return True
|
|
elif isinstance(pattern, list):
|
|
# List of substrings — ALL must be present in at least one key each
|
|
# (but not necessarily the same key)
|
|
if all(any(sub in k for k in keys) for sub in pattern):
|
|
return True
|
|
return False
|
|
|
|
|
|
def detect_checkpoint_architecture(keys: List[str]) -> str:
|
|
"""
|
|
Detect the model architecture from a list of tensor keys.
|
|
|
|
Checks architectures in order of specificity (most specific first):
|
|
flux → lumina2 → z_image → baked_sd15 → sdxl → sd15 → unknown
|
|
|
|
The ``baked_sd15`` pre-check catches SD1.5 checkpoints whose UNet keys
|
|
have been prefixed with ``model.diffusion_model.`` (e.g. by the baking
|
|
pipeline). Without this, such checkpoints incorrectly match the SDXL
|
|
catch-all detector (``["model.diffusion_model"]``) before the SD15
|
|
detector can fire.
|
|
|
|
Args:
|
|
keys: List of tensor key strings from the checkpoint header.
|
|
|
|
Returns:
|
|
Architecture name string: "flux", "lumina2", "z_image", "sdxl",
|
|
"sd15", or "unknown".
|
|
"""
|
|
# Ordered by specificity: Anima (net.blocks is unique) first,
|
|
# then FLUX (double_blocks are unique),
|
|
# then Lumina2 (cap_embedder is unique to NextDiT),
|
|
# then mixed patterns, then SD variants.
|
|
check_order = ["anima", "flux", "lumina2", "z_image", "sdxl", "sd15"]
|
|
|
|
# ── Pre-check: baked SD1.5 ─────────────────────────────────────────
|
|
# Baked SD1.5 checkpoints have model.diffusion_model.input_blocks.xxx
|
|
# (baker adds "model." prefix to UNet keys) which would match the SDXL
|
|
# catch-all below. Detect them by looking for SD1.5-specific CLIP keys.
|
|
# "cond_stage_model.transformer.text_model" is exclusive to SD1.5 —
|
|
# SDXL uses "model.conditioner.embedders.0.transformer.text_model" or
|
|
# "model.text_model".
|
|
if _matches_architecture(keys, {"detect": [
|
|
["cond_stage_model.transformer.text_model", "model.diffusion_model.input_blocks"],
|
|
]}):
|
|
return "sd15"
|
|
|
|
for arch_name in check_order:
|
|
arch_config = CHECKPOINT_ARCHITECTURES.get(arch_name, {})
|
|
if _matches_architecture(keys, arch_config):
|
|
return arch_name
|
|
|
|
return "unknown"
|
|
|
|
|
|
def normalize_checkpoint_key(key: str, architecture: str) -> str:
|
|
"""
|
|
Normalize a single checkpoint key to canonical form for the given architecture.
|
|
|
|
For FLUX / Z-Image: strips 'model.diffusion_model.' prefix.
|
|
For SDXL / SD1.5: keeps existing prefix as-is.
|
|
|
|
Args:
|
|
key: Original tensor key.
|
|
architecture: Architecture name from detect_checkpoint_architecture().
|
|
|
|
Returns:
|
|
Normalized key in canonical form.
|
|
"""
|
|
arch_config = CHECKPOINT_ARCHITECTURES.get(architecture, {})
|
|
strip_prefixes = arch_config.get("strip_prefixes", [])
|
|
|
|
normalized = key
|
|
for prefix in strip_prefixes:
|
|
if normalized.startswith(prefix):
|
|
normalized = normalized[len(prefix):]
|
|
break # only strip first matching prefix
|
|
|
|
return normalized
|
|
|
|
|
|
def normalize_checkpoint_header(
|
|
header: Dict[str, dict],
|
|
architecture: Optional[str] = None,
|
|
) -> Tuple[Dict[str, dict], Dict[str, str]]:
|
|
"""
|
|
Normalize all keys in a checkpoint header to canonical form.
|
|
|
|
Args:
|
|
header: Dict mapping key -> {shape, dtype, ...} as read from safetensors.
|
|
architecture: Detected architecture. If None, auto-detect.
|
|
|
|
Returns:
|
|
(normalized_header, key_mapping) where:
|
|
normalized_header: Same values, but with canonical keys.
|
|
key_mapping: Dict mapping canonical_key -> original_key.
|
|
"""
|
|
if not header:
|
|
return {}, {}
|
|
|
|
if architecture is None:
|
|
architecture = detect_checkpoint_architecture(list(header.keys()))
|
|
|
|
normalized = {}
|
|
mapping = {}
|
|
|
|
for orig_key, value in header.items():
|
|
norm_key = normalize_checkpoint_key(orig_key, architecture)
|
|
# Handle collisions: keep first occurrence with warning
|
|
if norm_key not in normalized:
|
|
normalized[norm_key] = value
|
|
mapping[norm_key] = orig_key
|
|
else:
|
|
import logging
|
|
logging.warning(
|
|
f"normalize_checkpoint_header: key collision — "
|
|
f"'{orig_key}' and '{mapping[norm_key]}' both normalize to '{norm_key}'. "
|
|
f"Keeping first occurrence."
|
|
)
|
|
|
|
return normalized, mapping
|
|
|
|
|