Files
aigc-apps-VideoX-Fun/scripts/minimax_h3_fun/extract_control_weights.py
T

118 lines
5.2 KiB
Python

# Extract the control branch of a trained MiniMaxH3ControlTransformer3DModel checkpoint.
#
# `train_control.py` saves the whole transformer (main branch + control branch) in the diffusers layout
# (`<checkpoint>/transformer/diffusion_pytorch_model.safetensors` plus `config.json`). The control branch is
# everything that `MiniMaxH3ControlTransformer3DModel` adds on top of the base model: the `control_blocks.*`
# list (one block per `control_blocks_places` entry) and the `control_proj_in.*` patch projection. This script
# writes just those tensors to a standalone safetensors file, which can be re-applied onto a fresh base model
# with `MiniMaxH3ControlTransformer3DModel.materialize_missing_control_params(...)`.
#
# Usage:
# python scripts/minimax_h3_fun/extract_control_weights.py \
# --model_path /path/to/train_control/checkpoint-xxx/transformer \
# --output_path /path/to/control_weights.safetensors
import argparse
import json
import os
import torch
from safetensors.torch import load_file, save_file
CONTROL_PREFIXES = ("control_blocks.", "control_proj_in.")
# FSDP / DeepSpeed unwrap may leave wrapper prefixes on the keys; strip them to the bare model namespace.
WRAPPER_PREFIXES = ("_fsdp_wrapped_module.", "_fsdp_wrapped_module_", "module.", "_orig_mod.")
def parse_args():
parser = argparse.ArgumentParser(
description="Extract the control-branch weights (control_blocks / control_proj_in) of a trained "
"MiniMax-H3 control transformer into a standalone safetensors file."
)
parser.add_argument(
"--model_path", type=str, default="output_dir_minimax_h3_control_distill/checkpoint-4000/transformer/diffusion_pytorch_model.safetensors",
help="Path to the saved transformer: a directory containing diffusion_pytorch_model*.safetensors "
"(e.g. `<checkpoint>/transformer`), or a single .safetensors file.",
)
parser.add_argument(
"--output_path", type=str, default="output_dir_minimax_h3_control_distill/checkpoint-4000/transformer/diffusion_pytorch_model_control.safetensors",
help="Where to write the extracted control weights.",
)
return parser.parse_args()
def resolve_safetensor_files(model_path):
if os.path.isdir(model_path):
shards = sorted(
os.path.join(model_path, name)
for name in os.listdir(model_path)
if name.endswith(".safetensors")
)
if not shards:
raise FileNotFoundError(f"No .safetensors files found under {model_path}.")
return shards
if os.path.isfile(model_path) and model_path.endswith(".safetensors"):
return [model_path]
raise FileNotFoundError(f"--model_path must be a safetensors file or a directory of them, got {model_path}.")
def unwrap_key(key):
changed = True
while changed:
changed = False
for prefix in WRAPPER_PREFIXES:
if key.startswith(prefix):
key = key[len(prefix):]
changed = True
return key
def main():
args = parse_args()
state_dict = {}
for shard in resolve_safetensor_files(args.model_path):
state_dict.update(load_file(shard, device="cpu"))
control_state_dict = {}
for key, value in state_dict.items():
bare_key = unwrap_key(key)
if bare_key.startswith(CONTROL_PREFIXES):
control_state_dict[bare_key] = value.contiguous()
if not control_state_dict:
raise ValueError(
f"No control-branch keys (control_blocks.* / control_proj_in.*) found in {args.model_path}; "
"this checkpoint does not look like a MiniMax-H3 control training output."
)
# Carry the branch layout next to the weights so a loader can rebuild the same control model without
# inspecting the full training config. `config.json` of the saved transformer records both fields; keep
# the safetensors metadata strings-only.
metadata = {"format": "pt"}
config_path = os.path.join(args.model_path, "config.json") if os.path.isdir(args.model_path) else None
if config_path is not None and os.path.isfile(config_path):
with open(config_path, "r") as file:
config = json.load(file)
for field in ("control_blocks_places", "control_in_dim"):
if field in config:
metadata[field] = json.dumps(config[field])
os.makedirs(os.path.dirname(os.path.abspath(args.output_path)), exist_ok=True)
save_file(control_state_dict, args.output_path, metadata=metadata)
num_params = sum(value.numel() for value in control_state_dict.values())
block_ids = sorted({
int(key.split(".")[1]) for key in control_state_dict if key.startswith("control_blocks.")
})
print(f"Extracted {len(control_state_dict)} control tensors ({num_params / 1e9:.3f}B params) -> {args.output_path}")
print(f" control_blocks indices: {block_ids}")
if "control_in_dim" in metadata:
print(f" control_in_dim: {metadata['control_in_dim']}, control_blocks_places: {metadata['control_blocks_places']}")
for key in sorted(control_state_dict):
if key.endswith(".weight") and control_state_dict[key].dim() >= 2:
print(f" {key}: {list(control_state_dict[key].shape)} {control_state_dict[key].dtype}")
if __name__ == "__main__":
main()