"""Model file I/O, hashing and metadata helpers. Hosts the routines that load Stable Diffusion / Diffusers checkpoints into the in-memory training pipeline (``load_target_model`` / ``_load_target_model``), the SD-WebUI / additional-networks compatible hashes (``model_hash``, ``calculate_sha256``, ``addnet_hash_*``, ``precalculate_safetensors_hashes``), the ``ss_*`` LoRA metadata helpers (``build_minimum_network_metadata`` and the ``SS_METADATA_*`` keys), the SAI ModelSpec wrappers (``get_sai_model_spec``, ``get_sai_model_spec_dataclass``), the ``replace_unet_modules`` attention-implementation switch, the safetensors metadata reader (``load_metadata_from_safetensors``) and ``get_git_revision_hash``. Extracted from ``library.train_util`` and re-exported there for backward compatibility. """ import argparse import hashlib import json import logging import os import subprocess import time from io import BytesIO from typing import TYPE_CHECKING, Optional import safetensors import safetensors.torch import library.sai_model_spec as sai_model_spec from library.device_utils import clean_memory_on_device from library.utils import setup_logging if TYPE_CHECKING: from library.original_unet import UNet2DConditionModel # NOTE: diffusers / library.model_util / library.original_unet are imported lazily # inside the load_target_model functions: importing them here adds several seconds # of startup time to lightweight CLI tools (networks/*) that only need the # hashing / metadata helpers from this module. setup_logging() logger = logging.getLogger(__name__) def model_hash(filename): """Old model hash used by stable-diffusion-webui""" try: with open(filename, "rb") as file: m = hashlib.sha256() file.seek(0x100000) m.update(file.read(0x10000)) return m.hexdigest()[0:8] except FileNotFoundError: return "NOFILE" except IsADirectoryError: # Linux? return "IsADirectory" except PermissionError: # Windows return "IsADirectory" def calculate_sha256(filename): """New model hash used by stable-diffusion-webui""" try: hash_sha256 = hashlib.sha256() blksize = 1024 * 1024 with open(filename, "rb") as f: for chunk in iter(lambda: f.read(blksize), b""): hash_sha256.update(chunk) return hash_sha256.hexdigest() except FileNotFoundError: return "NOFILE" except IsADirectoryError: # Linux? return "IsADirectory" except PermissionError: # Windows return "IsADirectory" def precalculate_safetensors_hashes(tensors, metadata): """Precalculate the model hashes needed by sd-webui-additional-networks to save time on indexing the model later.""" # Because writing user metadata to the file can change the result of # sd_models.model_hash(), only retain the training metadata for purposes of # calculating the hash, as they are meant to be immutable metadata = {k: v for k, v in metadata.items() if k.startswith("ss_")} bytes = safetensors.torch.save(tensors, metadata) b = BytesIO(bytes) model_hash = addnet_hash_safetensors(b) legacy_hash = addnet_hash_legacy(b) return model_hash, legacy_hash def addnet_hash_legacy(b): """Old model hash used by sd-webui-additional-networks for .safetensors format files""" m = hashlib.sha256() b.seek(0x100000) m.update(b.read(0x10000)) return m.hexdigest()[0:8] def addnet_hash_safetensors(b): """New model hash used by sd-webui-additional-networks for .safetensors format files""" hash_sha256 = hashlib.sha256() blksize = 1024 * 1024 b.seek(0) header = b.read(8) n = int.from_bytes(header, "little") offset = n + 8 b.seek(offset) for chunk in iter(lambda: b.read(blksize), b""): hash_sha256.update(chunk) return hash_sha256.hexdigest() def get_git_revision_hash() -> str: try: return subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=os.path.dirname(__file__)).decode("ascii").strip() except: return "(unknown)" def replace_unet_modules(unet: "UNet2DConditionModel", mem_eff_attn, xformers, sdpa): if mem_eff_attn: logger.info("Enable memory efficient attention for U-Net") unet.set_use_memory_efficient_attention(False, True) elif xformers: logger.info("Enable xformers for U-Net") try: import xformers.ops # noqa: F401 except ImportError: raise ImportError("No xformers / xformersがインストールされていないようです") unet.set_use_memory_efficient_attention(True, False) elif sdpa: logger.info("Enable SDPA for U-Net") unet.set_use_sdpa(True) def load_metadata_from_safetensors(safetensors_file: str) -> dict: """r This method locks the file. see https://github.com/huggingface/safetensors/issues/164 If the file isn't .safetensors or doesn't have metadata, return empty dict. """ if os.path.splitext(safetensors_file)[1] != ".safetensors": return {} with safetensors.safe_open(safetensors_file, framework="pt", device="cpu") as f: metadata = f.metadata() if metadata is None: metadata = {} return metadata # this metadata is referred from train_network and various scripts, so we wrote here SS_METADATA_KEY_V2 = "ss_v2" SS_METADATA_KEY_BASE_MODEL_VERSION = "ss_base_model_version" SS_METADATA_KEY_NETWORK_MODULE = "ss_network_module" SS_METADATA_KEY_NETWORK_DIM = "ss_network_dim" SS_METADATA_KEY_NETWORK_ALPHA = "ss_network_alpha" SS_METADATA_KEY_NETWORK_ARGS = "ss_network_args" SS_METADATA_MINIMUM_KEYS = [ SS_METADATA_KEY_V2, SS_METADATA_KEY_BASE_MODEL_VERSION, SS_METADATA_KEY_NETWORK_MODULE, SS_METADATA_KEY_NETWORK_DIM, SS_METADATA_KEY_NETWORK_ALPHA, SS_METADATA_KEY_NETWORK_ARGS, ] def build_minimum_network_metadata( v2: Optional[str], base_model: Optional[str], network_module: str, network_dim: str, network_alpha: str, network_args: Optional[dict], ): # old LoRA doesn't have base_model metadata = { SS_METADATA_KEY_NETWORK_MODULE: network_module, SS_METADATA_KEY_NETWORK_DIM: network_dim, SS_METADATA_KEY_NETWORK_ALPHA: network_alpha, } if v2 is not None: metadata[SS_METADATA_KEY_V2] = v2 if base_model is not None: metadata[SS_METADATA_KEY_BASE_MODEL_VERSION] = base_model if network_args is not None: metadata[SS_METADATA_KEY_NETWORK_ARGS] = json.dumps(network_args) return metadata def get_sai_model_spec( state_dict: dict, args: argparse.Namespace, sdxl: bool, lora: bool, textual_inversion: bool, is_stable_diffusion_ckpt: Optional[bool] = None, # None for TI and LoRA sd3: str = None, flux: str = None, # "dev", "schnell" or "chroma" lumina: str = None, optional_metadata: dict[str, str] | None = None, ): timestamp = time.time() v2 = args.v2 v_parameterization = args.v_parameterization reso = args.resolution title = args.metadata_title if args.metadata_title is not None else args.output_name if args.min_timestep is not None or args.max_timestep is not None: min_time_step = args.min_timestep if args.min_timestep is not None else 0 max_time_step = args.max_timestep if args.max_timestep is not None else 1000 timesteps = (min_time_step, max_time_step) else: timesteps = None # Convert individual model parameters to model_config dict # TODO: Update calls to this function to pass in the model config model_config = {} if sd3 is not None: model_config["sd3"] = sd3 if flux is not None: model_config["flux"] = flux if lumina is not None: model_config["lumina"] = lumina # Extract metadata_* fields from args and merge with optional_metadata extracted_metadata = {} # Extract all metadata_* attributes from args for attr_name in dir(args): if attr_name.startswith("metadata_") and not attr_name.startswith("metadata___"): value = getattr(args, attr_name, None) if value is not None: # Remove metadata_ prefix and exclude already handled fields field_name = attr_name[9:] # len("metadata_") = 9 if field_name not in ["title", "author", "description", "license", "tags"]: extracted_metadata[field_name] = value # Merge extracted metadata with provided optional_metadata all_optional_metadata = {**extracted_metadata} if optional_metadata: all_optional_metadata.update(optional_metadata) metadata = sai_model_spec.build_metadata( state_dict, v2, v_parameterization, sdxl, lora, textual_inversion, timestamp, title=title, reso=reso, is_stable_diffusion_ckpt=is_stable_diffusion_ckpt, author=args.metadata_author, description=args.metadata_description, license=args.metadata_license, tags=args.metadata_tags, timesteps=timesteps, clip_skip=args.clip_skip, # None or int model_config=model_config, optional_metadata=all_optional_metadata if all_optional_metadata else None, ) return metadata def get_sai_model_spec_dataclass( state_dict: dict, args: argparse.Namespace, sdxl: bool, lora: bool, textual_inversion: bool, is_stable_diffusion_ckpt: Optional[bool] = None, sd3: str = None, flux: str = None, lumina: str = None, hunyuan_image: str = None, anima: str = None, optional_metadata: dict[str, str] | None = None, ) -> sai_model_spec.ModelSpecMetadata: """ Get ModelSpec metadata as a dataclass - preferred for new code. Automatically extracts metadata_* fields from args. """ timestamp = time.time() v2 = args.v2 v_parameterization = args.v_parameterization reso = args.resolution title = args.metadata_title if args.metadata_title is not None else args.output_name if args.min_timestep is not None or args.max_timestep is not None: min_time_step = args.min_timestep if args.min_timestep is not None else 0 max_time_step = args.max_timestep if args.max_timestep is not None else 1000 timesteps = (min_time_step, max_time_step) else: timesteps = None # Convert individual model parameters to model_config dict model_config = {} if sd3 is not None: model_config["sd3"] = sd3 if flux is not None: model_config["flux"] = flux if lumina is not None: model_config["lumina"] = lumina if hunyuan_image is not None: model_config["hunyuan_image"] = hunyuan_image if anima is not None: model_config["anima"] = anima # Use the dataclass function directly return sai_model_spec.build_metadata_dataclass( state_dict, v2, v_parameterization, sdxl, lora, textual_inversion, timestamp, title=title, reso=reso, is_stable_diffusion_ckpt=is_stable_diffusion_ckpt, author=args.metadata_author, description=args.metadata_description, license=args.metadata_license, tags=args.metadata_tags, timesteps=timesteps, clip_skip=args.clip_skip, model_config=model_config, optional_metadata=optional_metadata, ) def _load_target_model(args: argparse.Namespace, weight_dtype, device="cpu", unet_use_linear_projection_in_v2=False): from diffusers import StableDiffusionPipeline import library.model_util as model_util from library.original_unet import UNet2DConditionModel name_or_path = args.pretrained_model_name_or_path name_or_path = os.path.realpath(name_or_path) if os.path.islink(name_or_path) else name_or_path load_stable_diffusion_format = os.path.isfile(name_or_path) # determine SD or Diffusers if load_stable_diffusion_format: logger.info(f"load StableDiffusion checkpoint: {name_or_path}") text_encoder, vae, unet = model_util.load_models_from_stable_diffusion_checkpoint( args.v2, name_or_path, device, unet_use_linear_projection_in_v2=unet_use_linear_projection_in_v2 ) else: # Diffusers model is loaded to CPU logger.info(f"load Diffusers pretrained models: {name_or_path}") try: pipe = StableDiffusionPipeline.from_pretrained(name_or_path, tokenizer=None, safety_checker=None) except EnvironmentError as ex: logger.error( f"model is not found as a file or in Hugging Face, perhaps file name is wrong? / 指定したモデル名のファイル、またはHugging Faceのモデルが見つかりません。ファイル名が誤っているかもしれません: {name_or_path}" ) raise ex text_encoder = pipe.text_encoder vae = pipe.vae unet = pipe.unet del pipe # Diffusers U-Net to original U-Net # TODO *.ckpt/*.safetensorsのv2と同じ形式にここで変換すると良さそう # logger.info(f"unet config: {unet.config}") original_unet = UNet2DConditionModel( unet.config.sample_size, unet.config.attention_head_dim, unet.config.cross_attention_dim, unet.config.use_linear_projection, unet.config.upcast_attention, ) original_unet.load_state_dict(unet.state_dict()) unet = original_unet logger.info("U-Net converted to original U-Net") # VAEを読み込む if args.vae is not None: vae = model_util.load_vae(args.vae, weight_dtype) logger.info("additional VAE loaded") return text_encoder, vae, unet, load_stable_diffusion_format def load_target_model(args, weight_dtype, accelerator, unet_use_linear_projection_in_v2=False): import library.model_util as model_util for pi in range(accelerator.state.num_processes): if pi == accelerator.state.local_process_index: logger.info(f"loading model for process {accelerator.state.local_process_index}/{accelerator.state.num_processes}") text_encoder, vae, unet, load_stable_diffusion_format = _load_target_model( args, weight_dtype, accelerator.device if args.lowram else "cpu", unet_use_linear_projection_in_v2=unet_use_linear_projection_in_v2, ) # Expand 4-channel conv_in to 9 channels when training inpainting from a # standard (non-inpainting) checkpoint. if getattr(args, "train_inpainting", False) and getattr(unet, "in_channels", 4) == 4: logger.info( "train_inpainting: expanding UNet conv_in from 4 to 9 channels " "(standard checkpoint → inpainting training from scratch)" ) model_util.expand_unet_to_inpainting(unet) # work on low-ram device if args.lowram: text_encoder.to(accelerator.device) unet.to(accelerator.device) vae.to(accelerator.device) clean_memory_on_device(accelerator.device) accelerator.wait_for_everyone() return text_encoder, vae, unet, load_stable_diffusion_format