Files
2026-03-24 13:20:34 -05:00

212 lines
7.7 KiB
Python

import hashlib
import os
import re
from dataclasses import dataclass
import safetensors.torch
import torch
from safetensors import safe_open
ALIGNMENT_VERSION = "1"
_CLIP_L_TOKEN_KEYS = (
"conditioner.embedders.0.transformer.text_model.embeddings.token_embedding.weight",
"clip_l.transformer.text_model.embeddings.token_embedding.weight",
)
_CLIP_G_TOKEN_KEYS = (
"conditioner.embedders.1.model.token_embedding.weight",
"clip_g.transformer.text_model.embeddings.token_embedding.weight",
)
@dataclass
class AlignmentTransform:
matrix: torch.Tensor
source_mean: torch.Tensor
target_mean: torch.Tensor
scale: float
fit_token_cosine: float
checkpoint_name: str
checkpoint_fingerprint: str
def map_vectors(self, vectors: torch.Tensor) -> torch.Tensor:
source_mean = self.source_mean.to(device=vectors.device, dtype=torch.float32)
target_mean = self.target_mean.to(device=vectors.device, dtype=torch.float32)
matrix = self.matrix.to(device=vectors.device, dtype=torch.float32)
vectors = vectors.to(dtype=torch.float32)
return (vectors - source_mean) @ matrix + target_mean
def compute_checkpoint_fingerprint(checkpoint_path: str) -> str:
stat = os.stat(checkpoint_path)
payload = "|".join(
[
ALIGNMENT_VERSION,
os.path.abspath(checkpoint_path),
str(stat.st_size),
str(stat.st_mtime_ns),
]
)
return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
def safe_stem(name: str) -> str:
stem = os.path.splitext(os.path.basename(name))[0]
stem = re.sub(r"[^A-Za-z0-9._-]+", "_", stem).strip("._")
return stem or "alignment"
def alignment_cache_path(cache_root: str, checkpoint_name: str, checkpoint_fingerprint: str) -> str:
filename = f"{safe_stem(checkpoint_name)}_{checkpoint_fingerprint}.safetensors"
return os.path.join(cache_root, filename)
def load_alignment_cache(cache_path: str) -> AlignmentTransform | None:
if not os.path.isfile(cache_path):
return None
try:
tensors = safetensors.torch.load_file(cache_path, device="cpu")
with safe_open(cache_path, framework="pt", device="cpu") as handle:
metadata = handle.metadata() or {}
except Exception:
return None
required_metadata = (
"embed_converter.version",
"embed_converter.scale",
"embed_converter.fit_token_cosine",
"embed_converter.checkpoint_name",
"embed_converter.checkpoint_fingerprint",
)
required_tensors = ("matrix", "source_mean", "target_mean")
if metadata.get("embed_converter.version") != ALIGNMENT_VERSION:
return None
if any(key not in metadata for key in required_metadata):
return None
if any(key not in tensors for key in required_tensors):
return None
return AlignmentTransform(
matrix=tensors["matrix"],
source_mean=tensors["source_mean"],
target_mean=tensors["target_mean"],
scale=float(metadata["embed_converter.scale"]),
fit_token_cosine=float(metadata["embed_converter.fit_token_cosine"]),
checkpoint_name=metadata["embed_converter.checkpoint_name"],
checkpoint_fingerprint=metadata["embed_converter.checkpoint_fingerprint"],
)
def save_alignment_cache(cache_path: str, alignment: AlignmentTransform) -> None:
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
metadata = {
"embed_converter.version": ALIGNMENT_VERSION,
"embed_converter.scale": f"{alignment.scale:.12f}",
"embed_converter.fit_token_cosine": f"{alignment.fit_token_cosine:.12f}",
"embed_converter.checkpoint_name": alignment.checkpoint_name,
"embed_converter.checkpoint_fingerprint": alignment.checkpoint_fingerprint,
}
tensors = {
"matrix": alignment.matrix.cpu(),
"source_mean": alignment.source_mean.cpu(),
"target_mean": alignment.target_mean.cpu(),
}
safetensors.torch.save_file(tensors, cache_path, metadata=metadata)
def fit_scaled_orthogonal_alignment(
source_embeddings: torch.Tensor,
target_embeddings: torch.Tensor,
checkpoint_name: str,
checkpoint_fingerprint: str,
) -> AlignmentTransform:
source = source_embeddings.to(dtype=torch.float32, device="cpu")
target = target_embeddings.to(dtype=torch.float32, device="cpu")
source_mean = source.mean(dim=0)
target_mean = target.mean(dim=0)
source_centered = source - source_mean
target_centered = target - target_mean
# Rectangular Procrustes via the thin SVD of the cross-covariance.
cross_covariance = source_centered.T @ target_centered
u, singular_values, vh = torch.linalg.svd(cross_covariance, full_matrices=False)
rotation = u @ vh
scale = float(singular_values.sum() / source_centered.square().sum().clamp_min(1e-12))
matrix = rotation * scale
fit_token_cosine = mean_cosine_similarity(
source,
target,
lambda batch: (batch - source_mean) @ matrix + target_mean,
)
return AlignmentTransform(
matrix=matrix.cpu(),
source_mean=source_mean.cpu(),
target_mean=target_mean.cpu(),
scale=scale,
fit_token_cosine=fit_token_cosine,
checkpoint_name=checkpoint_name,
checkpoint_fingerprint=checkpoint_fingerprint,
)
def mean_cosine_similarity(
source_embeddings: torch.Tensor,
target_embeddings: torch.Tensor,
mapper,
batch_size: int = 4096,
) -> float:
total = 0.0
count = 0
for start in range(0, source_embeddings.shape[0], batch_size):
stop = min(start + batch_size, source_embeddings.shape[0])
mapped = mapper(source_embeddings[start:stop])
target = target_embeddings[start:stop]
cosine = torch.nn.functional.cosine_similarity(mapped, target, dim=-1)
total += cosine.sum().item()
count += cosine.numel()
if count == 0:
return 0.0
return total / count
def load_checkpoint_token_tables(checkpoint_path: str) -> tuple[torch.Tensor, torch.Tensor]:
if checkpoint_path.lower().endswith((".safetensors", ".sft")):
return _load_token_tables_from_safetensors(checkpoint_path)
return _load_token_tables_from_torch_file(checkpoint_path)
def _load_token_tables_from_safetensors(checkpoint_path: str) -> tuple[torch.Tensor, torch.Tensor]:
with safe_open(checkpoint_path, framework="pt", device="cpu") as handle:
keys = set(handle.keys())
clip_l_key = next((key for key in _CLIP_L_TOKEN_KEYS if key in keys), None)
clip_g_key = next((key for key in _CLIP_G_TOKEN_KEYS if key in keys), None)
if clip_l_key is None or clip_g_key is None:
raise RuntimeError(
"The selected checkpoint does not expose SDXL CLIP-L and CLIP-G token embedding tables."
)
clip_l = handle.get_tensor(clip_l_key).clone()
clip_g = handle.get_tensor(clip_g_key).clone()
return clip_l, clip_g
def _load_token_tables_from_torch_file(checkpoint_path: str) -> tuple[torch.Tensor, torch.Tensor]:
import comfy.utils
state_dict = comfy.utils.load_torch_file(checkpoint_path, safe_load=True, device=torch.device("cpu"))
clip_l = _find_state_dict_tensor(state_dict, _CLIP_L_TOKEN_KEYS)
clip_g = _find_state_dict_tensor(state_dict, _CLIP_G_TOKEN_KEYS)
if clip_l is None or clip_g is None:
raise RuntimeError(
"The selected checkpoint does not expose SDXL CLIP-L and CLIP-G token embedding tables."
)
return clip_l.clone(), clip_g.clone()
def _find_state_dict_tensor(state_dict: dict, candidate_keys: tuple[str, ...]) -> torch.Tensor | None:
for key in candidate_keys:
tensor = state_dict.get(key)
if tensor is not None:
return tensor
return None