Image-Text-to-Video
Diffusion Single File
Safetensors
MiniMax H3
comfyui
ref2va
fl2va
Merge
int8
convrot
synchronized-audio-video
experimental
Instructions to use xmarre/MiniMax-H3-Pruned-Ref-Delta-Fused-r1024-ComfyUI with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusion Single File
How to use xmarre/MiniMax-H3-Pruned-Ref-Delta-Fused-r1024-ComfyUI with Diffusion Single File:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
Download tools/convert_h3_diffusers_to_comfy_v3.py from xmarre/MiniMax-H3-Pruned-Ref-Delta-Fused-r1024-ComfyUI: direct link, hf CLI and curl.
- Browser
- Download file 28.7 kB
-
https://huggingface.co/xmarre/MiniMax-H3-Pruned-Ref-Delta-Fused-r1024-ComfyUI/resolve/main/tools/convert_h3_diffusers_to_comfy_v3.py
- Command line
-
hf download hf://xmarre/MiniMax-H3-Pruned-Ref-Delta-Fused-r1024-ComfyUI/tools/convert_h3_diffusers_to_comfy_v3.py
-
curl -L -o convert_h3_diffusers_to_comfy_v3.py https://huggingface.co/xmarre/MiniMax-H3-Pruned-Ref-Delta-Fused-r1024-ComfyUI/resolve/main/tools/convert_h3_diffusers_to_comfy_v3.py
28.7 kB
| #!/usr/bin/env python3 | |
| """ | |
| Convert a Diffusers-format MiniMax-H3 transformer safetensors file back to the | |
| native/ComfyUI MiniMax-H3 state-dict layout. | |
| This is a streaming container conversion: | |
| - direct-renamed tensors are copied byte-for-byte; | |
| - Diffusers to_q/to_k/to_v tensors are concatenated into native qkv_proj; | |
| - Diffusers SwiGLU [value; gate] is restored to native [gate; value];\n- pruned `folded_bias` F32 constants are restored as native AdaLN biases; | |
| - rope.inv_freq, intentionally dropped by the Diffusers converter, is | |
| reconstructed from the MiniMax-H3 rope configuration. | |
| The full ~40 GB model is never materialized in RAM. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import hashlib | |
| import json | |
| import os | |
| import struct | |
| from collections import Counter | |
| from pathlib import Path | |
| import torch | |
| from safetensors import safe_open | |
| CHUNK_SIZE = 64 * 1024 * 1024 | |
| Q_SUFFIX = ".attn.to_q.weight" | |
| K_SUFFIX = ".attn.to_k.weight" | |
| V_SUFFIX = ".attn.to_v.weight" | |
| SWIGLU_SUFFIX = ".ff.net.0.proj.weight" | |
| DTYPE_SIZE = { | |
| "BOOL": 1, | |
| "U8": 1, | |
| "I8": 1, | |
| "F8_E4M3": 1, | |
| "F8_E5M2": 1, | |
| "I16": 2, | |
| "U16": 2, | |
| "F16": 2, | |
| "BF16": 2, | |
| "I32": 4, | |
| "U32": 4, | |
| "F32": 4, | |
| "I64": 8, | |
| "U64": 8, | |
| "F64": 8, | |
| } | |
| def product(values): | |
| out = 1 | |
| for value in values: | |
| out *= value | |
| return out | |
| def read_header(path: Path): | |
| with path.open("rb") as f: | |
| raw = f.read(8) | |
| if len(raw) != 8: | |
| raise RuntimeError(f"{path}: truncated safetensors file") | |
| header_len = struct.unpack("<Q", raw)[0] | |
| if header_len <= 0 or header_len > 1024 * 1024 * 1024: | |
| raise RuntimeError( | |
| f"{path}: implausible safetensors header length: {header_len}" | |
| ) | |
| raw_header = f.read(header_len) | |
| if len(raw_header) != header_len: | |
| raise RuntimeError(f"{path}: truncated safetensors header") | |
| try: | |
| header = json.loads(raw_header.decode("utf-8")) | |
| except Exception as exc: | |
| raise RuntimeError(f"{path}: invalid safetensors header: {exc}") from exc | |
| tensors = {k: v for k, v in header.items() if k != "__metadata__"} | |
| metadata = header.get("__metadata__", {}) | |
| if metadata is None: | |
| metadata = {} | |
| if not isinstance(metadata, dict): | |
| raise RuntimeError(f"{path}: invalid __metadata__") | |
| return { | |
| "header_len": header_len, | |
| "data_start": 8 + header_len, | |
| "tensors": tensors, | |
| "metadata": metadata, | |
| } | |
| def validate_tensor_info(path: Path, header, key: str): | |
| info = header["tensors"][key] | |
| dtype = info.get("dtype") | |
| shape = info.get("shape") | |
| offsets = info.get("data_offsets") | |
| if dtype not in DTYPE_SIZE: | |
| raise RuntimeError(f"{key}: unsupported dtype {dtype!r}") | |
| if not isinstance(shape, list) or not all( | |
| isinstance(x, int) and x >= 0 for x in shape | |
| ): | |
| raise RuntimeError(f"{key}: invalid shape {shape!r}") | |
| if ( | |
| not isinstance(offsets, list) | |
| or len(offsets) != 2 | |
| or not all(isinstance(x, int) for x in offsets) | |
| ): | |
| raise RuntimeError(f"{key}: invalid data_offsets {offsets!r}") | |
| start, end = offsets | |
| if start < 0 or end < start: | |
| raise RuntimeError(f"{key}: invalid data_offsets {offsets!r}") | |
| file_payload_size = path.stat().st_size - header["data_start"] | |
| if end > file_payload_size: | |
| raise RuntimeError( | |
| f"{key}: data_offsets extend beyond source payload " | |
| f"({end} > {file_payload_size})" | |
| ) | |
| expected = product(shape) * DTYPE_SIZE[dtype] | |
| actual = end - start | |
| if expected != actual: | |
| raise RuntimeError( | |
| f"{key}: payload byte count does not match shape/dtype " | |
| f"({actual} != {expected})" | |
| ) | |
| def tensor_size(header, key: str): | |
| start, end = header["tensors"][key]["data_offsets"] | |
| return end - start | |
| def copy_region(source_f, output_f, absolute_offset: int, size: int, digest): | |
| source_f.seek(absolute_offset) | |
| remaining = size | |
| while remaining: | |
| block = source_f.read(min(CHUNK_SIZE, remaining)) | |
| if not block: | |
| raise RuntimeError( | |
| f"Unexpected EOF while copying; {remaining:,} bytes remained" | |
| ) | |
| output_f.write(block) | |
| digest.update(block) | |
| remaining -= len(block) | |
| def copy_tensor(source_f, output_f, source_header, key: str, digest): | |
| info = source_header["tensors"][key] | |
| begin, end = info["data_offsets"] | |
| copy_region( | |
| source_f, | |
| output_f, | |
| source_header["data_start"] + begin, | |
| end - begin, | |
| digest, | |
| ) | |
| def direct_native_key(key: str): | |
| if key.endswith(Q_SUFFIX) or key.endswith(K_SUFFIX) or key.endswith(V_SUFFIX): | |
| return None | |
| if key.endswith(SWIGLU_SUFFIX): | |
| return None | |
| # The Diffusers pruned H3 representation keeps the ComfyUI/Comfy-Org | |
| # AdaLN coordinate table under time_embedder.table. Native ComfyUI detects | |
| # the pruned architecture from the top-level adaln_t_table key. | |
| if key == "time_embedder.table": | |
| return "adaln_t_table" | |
| # multimodalart/diffusers pruned H3 keeps the reduced-rank AdaLN constant | |
| # term outside the bias-free Linear as a float32 `folded_bias` buffer. | |
| # Native ComfyUI's pruned H3 Linear is float32 and expects this exact | |
| # constant as its bias. | |
| if key == "norm_out.folded_bias": | |
| return "final_layer.adaln_proj.linear.bias" | |
| # Top-level module renames must be prefix-scoped. Using unrestricted | |
| # str.replace() here would corrupt names such as | |
| # time_embedder.linear_1 -> time_embedder.proj_in -> time_embedder.video_patch_proj. | |
| top_level_prefixes = ( | |
| ("time_embedder.linear_1.", "time_embedder.proj_in."), | |
| ("time_embedder.linear_2.", "time_embedder.proj_out."), | |
| ("audio_proj_out.", "final_layer.audio_out."), | |
| ("audio_proj_in.", "audio_patch_proj."), | |
| ("proj_out.", "final_layer.video_out."), | |
| ("proj_in.", "video_patch_proj."), | |
| ("context_embedder.", "condition_proj."), | |
| ("norm_out.norm.", "final_layer.norm."), | |
| ("norm_out.linear.", "final_layer.adaln_proj.linear."), | |
| ) | |
| for old, new in top_level_prefixes: | |
| if key.startswith(old): | |
| return new + key[len(old):] | |
| out = key | |
| if out.startswith("token_refiner.refiner_blocks."): | |
| out = out.replace( | |
| "token_refiner.refiner_blocks.", "token_refiner.blocks.", 1 | |
| ) | |
| elif out.startswith("transformer_blocks."): | |
| out = out.replace("transformer_blocks.", "blocks.", 1) | |
| out = out.replace(".adaln_proj.folded_bias", ".adaln_proj.linear.bias") | |
| out = out.replace(".attn.norm_q.", ".attn.q_norm.") | |
| out = out.replace(".attn.norm_k.", ".attn.k_norm.") | |
| out = out.replace(".attn.to_out.0.", ".attn.out_proj.") | |
| out = out.replace(".ff.net.2.", ".mlp.fc2.") | |
| return out | |
| def qkv_native_key(q_key: str): | |
| base = q_key[: -len(Q_SUFFIX)] | |
| if base.startswith("transformer_blocks."): | |
| base = base.replace("transformer_blocks.", "blocks.", 1) | |
| elif base.startswith("token_refiner.refiner_blocks."): | |
| base = base.replace( | |
| "token_refiner.refiner_blocks.", "token_refiner.blocks.", 1 | |
| ) | |
| else: | |
| raise RuntimeError(f"Unexpected MiniMax-H3 Diffusers Q key: {q_key}") | |
| return base + ".attn.qkv_proj.weight" | |
| def swiglu_native_key(key: str): | |
| out = key | |
| if out.startswith("transformer_blocks."): | |
| out = out.replace("transformer_blocks.", "blocks.", 1) | |
| elif out.startswith("token_refiner.refiner_blocks."): | |
| out = out.replace( | |
| "token_refiner.refiner_blocks.", "token_refiner.blocks.", 1 | |
| ) | |
| else: | |
| raise RuntimeError(f"Unexpected MiniMax-H3 Diffusers SwiGLU key: {key}") | |
| return out.replace(".ff.net.0.proj.weight", ".mlp.fc1.weight") | |
| def make_rope_inv_freq_bytes(): | |
| # Matches the upstream MiniMax-H3/Diffusers reconstruction: | |
| # 1 / 10000 ** (arange(0, 2 * 16, 2) / (2 * 16)) | |
| inv_freq = 1.0 / ( | |
| 10000.0 | |
| ** ( | |
| torch.arange(0, 32, 2, dtype=torch.float32) | |
| / 32.0 | |
| ) | |
| ) | |
| inv_freq = inv_freq.contiguous().cpu() | |
| if tuple(inv_freq.shape) != (16,) or inv_freq.dtype != torch.float32: | |
| raise RuntimeError("Internal rope.inv_freq reconstruction invariant failed") | |
| return inv_freq.numpy().tobytes(order="C") | |
| def build_plan(source_path: Path, source_header): | |
| tensors = source_header["tensors"] | |
| keys = list(tensors) | |
| required_source_keys = ( | |
| "proj_in.weight", | |
| "audio_proj_in.weight", | |
| "context_embedder.weight", | |
| "transformer_blocks.0.attn.to_q.weight", | |
| "token_refiner.refiner_blocks.0.attn.to_q.weight", | |
| "proj_out.weight", | |
| "audio_proj_out.weight", | |
| ) | |
| missing = [key for key in required_source_keys if key not in tensors] | |
| if missing: | |
| raise RuntimeError( | |
| "Input does not look like the expected Diffusers MiniMax-H3 " | |
| "transformer. Missing:\n " + "\n ".join(missing) | |
| ) | |
| native_markers = ( | |
| "video_patch_proj.weight", | |
| "audio_patch_proj.weight", | |
| "blocks.0.attn.qkv_proj.weight", | |
| "rope.inv_freq", | |
| ) | |
| present_native = [key for key in native_markers if key in tensors] | |
| if present_native: | |
| raise RuntimeError( | |
| "Input already contains native/ComfyUI MiniMax-H3 keys:\n " | |
| + "\n ".join(present_native) | |
| ) | |
| is_pruned = "time_embedder.table" in tensors | |
| full_time_keys = { | |
| "time_embedder.linear_1.weight", | |
| "time_embedder.linear_1.bias", | |
| "time_embedder.linear_2.weight", | |
| "time_embedder.linear_2.bias", | |
| } | |
| if is_pruned: | |
| unexpected_full = sorted(full_time_keys & set(keys)) | |
| if unexpected_full: | |
| raise RuntimeError( | |
| "Checkpoint mixes the pruned AdaLN table with full timestep-MLP " | |
| "weights:\n " + "\n ".join(unexpected_full) | |
| ) | |
| table = tensors["time_embedder.table"] | |
| if table.get("shape") != [1025, 8]: | |
| raise RuntimeError( | |
| "Expected pruned MiniMax-H3 time_embedder.table shape [1025, 8], " | |
| f"got {table.get('shape')}" | |
| ) | |
| if table.get("dtype") != "F32": | |
| raise RuntimeError( | |
| "Expected pruned MiniMax-H3 time_embedder.table dtype F32, " | |
| f"got {table.get('dtype')}" | |
| ) | |
| folded_bias_keys = [ | |
| f"transformer_blocks.{i}.adaln_proj.folded_bias" for i in range(50) | |
| ] + ["norm_out.folded_bias"] | |
| missing_folded = [k for k in folded_bias_keys if k not in tensors] | |
| if missing_folded: | |
| raise RuntimeError( | |
| "Pruned Diffusers MiniMax-H3 checkpoint is missing required " | |
| "float32 folded AdaLN constant terms:\n " | |
| + "\n ".join(missing_folded) | |
| ) | |
| for i in range(50): | |
| info = tensors[f"transformer_blocks.{i}.adaln_proj.folded_bias"] | |
| if info.get("shape") != [96768] or info.get("dtype") != "F32": | |
| raise RuntimeError( | |
| f"transformer_blocks.{i}.adaln_proj.folded_bias: expected " | |
| f"[96768] F32, got {info.get('shape')} {info.get('dtype')}" | |
| ) | |
| final_bias = tensors["norm_out.folded_bias"] | |
| if final_bias.get("shape") != [10752] or final_bias.get("dtype") != "F32": | |
| raise RuntimeError( | |
| "norm_out.folded_bias: expected [10752] F32, got " | |
| f"{final_bias.get('shape')} {final_bias.get('dtype')}" | |
| ) | |
| for adaln_key in ( | |
| "transformer_blocks.0.adaln_proj.linear.weight", | |
| "norm_out.linear.weight", | |
| ): | |
| if adaln_key not in tensors: | |
| raise RuntimeError( | |
| f"Pruned MiniMax-H3 checkpoint is missing {adaln_key}" | |
| ) | |
| shape = tensors[adaln_key].get("shape") | |
| if not isinstance(shape, list) or len(shape) != 2 or shape[1] != 8: | |
| raise RuntimeError( | |
| f"{adaln_key}: expected pruned input width 8, got {shape}" | |
| ) | |
| else: | |
| missing_time = sorted(full_time_keys - set(keys)) | |
| if missing_time: | |
| raise RuntimeError( | |
| "Checkpoint has neither a complete full timestep MLP nor the " | |
| "pruned time_embedder.table. Missing:\n " | |
| + "\n ".join(missing_time) | |
| ) | |
| for key in keys: | |
| validate_tensor_info(source_path, source_header, key) | |
| q_keys = [key for key in keys if key.endswith(Q_SUFFIX)] | |
| transformer_q = [ | |
| key for key in q_keys if key.startswith("transformer_blocks.") | |
| ] | |
| refiner_q = [ | |
| key | |
| for key in q_keys | |
| if key.startswith("token_refiner.refiner_blocks.") | |
| ] | |
| if len(transformer_q) != 50: | |
| raise RuntimeError( | |
| f"Expected 50 transformer attention blocks, found {len(transformer_q)}" | |
| ) | |
| if len(refiner_q) != 2: | |
| raise RuntimeError( | |
| f"Expected 2 token-refiner attention blocks, found {len(refiner_q)}" | |
| ) | |
| used = set() | |
| plan = [] | |
| for key in keys: | |
| if key in used: | |
| continue | |
| if is_pruned and key in {"adaln_basis", "adaln_mean"}: | |
| used.add(key) | |
| continue | |
| if key.endswith(Q_SUFFIX): | |
| prefix = key[: -len(Q_SUFFIX)] | |
| k_key = prefix + K_SUFFIX | |
| v_key = prefix + V_SUFFIX | |
| if k_key not in tensors or v_key not in tensors: | |
| raise RuntimeError(f"Incomplete Q/K/V group for {key}") | |
| q_info = tensors[key] | |
| k_info = tensors[k_key] | |
| v_info = tensors[v_key] | |
| if not ( | |
| q_info["dtype"] == k_info["dtype"] == v_info["dtype"] | |
| ): | |
| raise RuntimeError(f"Q/K/V dtype mismatch for {prefix}") | |
| if not ( | |
| q_info["shape"] == k_info["shape"] == v_info["shape"] | |
| ): | |
| raise RuntimeError(f"Q/K/V shape mismatch for {prefix}") | |
| q_shape = q_info["shape"] | |
| if len(q_shape) != 2: | |
| raise RuntimeError( | |
| f"Expected 2D Q/K/V weights for {prefix}, got {q_shape}" | |
| ) | |
| plan.append( | |
| { | |
| "kind": "qkv", | |
| "out_key": qkv_native_key(key), | |
| "dtype": q_info["dtype"], | |
| "shape": [q_shape[0] * 3, q_shape[1]], | |
| "nbytes": ( | |
| tensor_size(source_header, key) | |
| + tensor_size(source_header, k_key) | |
| + tensor_size(source_header, v_key) | |
| ), | |
| "src_keys": [key, k_key, v_key], | |
| } | |
| ) | |
| used.update((key, k_key, v_key)) | |
| continue | |
| if key.endswith(K_SUFFIX) or key.endswith(V_SUFFIX): | |
| # Its Q key will consume it. If not, the unhandled check below fails. | |
| continue | |
| if key.endswith(SWIGLU_SUFFIX): | |
| info = tensors[key] | |
| shape = info["shape"] | |
| nbytes = tensor_size(source_header, key) | |
| if len(shape) != 2 or shape[0] % 2: | |
| raise RuntimeError( | |
| f"Invalid Diffusers SwiGLU tensor shape for {key}: {shape}" | |
| ) | |
| if nbytes % 2: | |
| raise RuntimeError( | |
| f"Invalid Diffusers SwiGLU byte count for {key}: {nbytes}" | |
| ) | |
| plan.append( | |
| { | |
| "kind": "swiglu", | |
| "out_key": swiglu_native_key(key), | |
| "dtype": info["dtype"], | |
| "shape": shape, | |
| "nbytes": nbytes, | |
| "src_key": key, | |
| } | |
| ) | |
| used.add(key) | |
| continue | |
| out_key = direct_native_key(key) | |
| if out_key is None: | |
| raise RuntimeError(f"Unhandled source key: {key}") | |
| info = tensors[key] | |
| plan.append( | |
| { | |
| "kind": "copy", | |
| "out_key": out_key, | |
| "dtype": info["dtype"], | |
| "shape": info["shape"], | |
| "nbytes": tensor_size(source_header, key), | |
| "src_key": key, | |
| } | |
| ) | |
| used.add(key) | |
| unhandled = sorted(set(keys) - used) | |
| if unhandled: | |
| raise RuntimeError( | |
| "Source contains keys that were not converted:\n " | |
| + "\n ".join(unhandled) | |
| ) | |
| # Diffusers intentionally drops native rope.inv_freq; restore it. | |
| rope_bytes = make_rope_inv_freq_bytes() | |
| plan.append( | |
| { | |
| "kind": "generated", | |
| "out_key": "rope.inv_freq", | |
| "dtype": "F32", | |
| "shape": [16], | |
| "nbytes": len(rope_bytes), | |
| "data": rope_bytes, | |
| } | |
| ) | |
| counts = Counter(entry["out_key"] for entry in plan) | |
| duplicates = sorted(key for key, count in counts.items() if count != 1) | |
| if duplicates: | |
| raise RuntimeError( | |
| "Conversion would create duplicate output keys:\n " | |
| + "\n ".join(duplicates) | |
| ) | |
| dropped_aux_count = sum( | |
| 1 for k in ("adaln_basis", "adaln_mean") if is_pruned and k in tensors | |
| ) | |
| expected_out_count = len(keys) - 2 * len(q_keys) + 1 - dropped_aux_count | |
| if len(plan) != expected_out_count: | |
| raise RuntimeError( | |
| f"Output tensor-count invariant failed: plan={len(plan)}, " | |
| f"expected={expected_out_count}" | |
| ) | |
| return plan, is_pruned | |
| def build_output_header(plan, source_metadata, is_pruned): | |
| offset = 0 | |
| tensors = {} | |
| for entry in plan: | |
| begin = offset | |
| end = begin + entry["nbytes"] | |
| tensors[entry["out_key"]] = { | |
| "dtype": entry["dtype"], | |
| "shape": entry["shape"], | |
| "data_offsets": [begin, end], | |
| } | |
| offset = end | |
| metadata = {} | |
| for key, value in source_metadata.items(): | |
| if not isinstance(key, str) or not isinstance(value, str): | |
| raise RuntimeError( | |
| f"Safetensors metadata must be string:string; got " | |
| f"{key!r}:{value!r}" | |
| ) | |
| metadata[key] = value | |
| # A Diffusers/full-model config can carry time_embed_dim=2688. Keeping that | |
| # metadata on a pruned ComfyUI checkpoint would override the tensor-derived | |
| # width from adaln_t_table (8) in ComfyUI model detection. | |
| if is_pruned: | |
| metadata.pop("config", None) | |
| metadata["format"] = metadata.get("format", "pt") | |
| metadata["converted_from"] = "diffusers MiniMax-H3 transformer" | |
| metadata["converted_to"] = "native ComfyUI MiniMax-H3 diffusion model" | |
| full_header = {"__metadata__": metadata, **tensors} | |
| raw = json.dumps( | |
| full_header, separators=(",", ":"), ensure_ascii=False | |
| ).encode("utf-8") | |
| raw += b" " * ((-len(raw)) % 8) | |
| return raw, offset | |
| def write_output(source_path, output_path, source_header, plan, is_pruned): | |
| raw_header, payload_size = build_output_header( | |
| plan, source_header["metadata"], is_pruned | |
| ) | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| partial_path = output_path.with_name(output_path.name + ".partial") | |
| if partial_path.exists(): | |
| partial_path.unlink() | |
| digest = hashlib.sha256() | |
| try: | |
| with source_path.open("rb") as source_f, partial_path.open("xb") as out_f: | |
| out_f.write(struct.pack("<Q", len(raw_header))) | |
| out_f.write(raw_header) | |
| total = len(plan) | |
| for index, entry in enumerate(plan, 1): | |
| kind = entry["kind"] | |
| if kind == "copy": | |
| copy_tensor( | |
| source_f, | |
| out_f, | |
| source_header, | |
| entry["src_key"], | |
| digest, | |
| ) | |
| elif kind == "qkv": | |
| # Diffusers stores three separate, contiguous [q], [k], [v] | |
| # matrices. ComfyUI's qkv_proj expects [q_all; k_all; v_all]. | |
| for key in entry["src_keys"]: | |
| copy_tensor( | |
| source_f, out_f, source_header, key, digest | |
| ) | |
| elif kind == "swiglu": | |
| # Diffusers SwiGLU is [value; gate]. | |
| # Native MiniMax-H3 is [gate; value]. | |
| info = source_header["tensors"][entry["src_key"]] | |
| begin, end = info["data_offsets"] | |
| size = end - begin | |
| half = size // 2 | |
| absolute = source_header["data_start"] + begin | |
| copy_region( | |
| source_f, out_f, absolute + half, half, digest | |
| ) | |
| copy_region(source_f, out_f, absolute, half, digest) | |
| elif kind == "generated": | |
| data = entry["data"] | |
| out_f.write(data) | |
| digest.update(data) | |
| else: | |
| raise RuntimeError(f"Unknown conversion plan kind: {kind}") | |
| if index % 25 == 0 or index == total: | |
| print( | |
| f"\rConverted tensors: {index:,}/{total:,}", | |
| end="", | |
| flush=True, | |
| ) | |
| print() | |
| out_f.flush() | |
| os.fsync(out_f.fileno()) | |
| expected_size = 8 + len(raw_header) + payload_size | |
| actual_size = partial_path.stat().st_size | |
| if actual_size != expected_size: | |
| raise RuntimeError( | |
| f"Output size mismatch: {actual_size:,} != {expected_size:,}" | |
| ) | |
| os.replace(partial_path, output_path) | |
| except Exception: | |
| try: | |
| if partial_path.exists(): | |
| partial_path.unlink() | |
| finally: | |
| raise | |
| return digest.hexdigest(), payload_size | |
| def validate_output(output_path: Path, expected_count: int, is_pruned: bool): | |
| header = read_header(output_path) | |
| keys = set(header["tensors"]) | |
| required = { | |
| "video_patch_proj.weight", | |
| "audio_patch_proj.weight", | |
| "condition_proj.weight", | |
| "blocks.0.attn.qkv_proj.weight", | |
| "blocks.49.attn.qkv_proj.weight", | |
| "token_refiner.blocks.0.attn.qkv_proj.weight", | |
| "token_refiner.blocks.1.attn.qkv_proj.weight", | |
| "final_layer.video_out.weight", | |
| "final_layer.audio_out.weight", | |
| "rope.inv_freq", | |
| } | |
| if is_pruned: | |
| required.add("adaln_t_table") | |
| required.add("final_layer.adaln_proj.linear.bias") | |
| required.update( | |
| f"blocks.{i}.adaln_proj.linear.bias" for i in range(50) | |
| ) | |
| else: | |
| required.update({ | |
| "time_embedder.proj_in.weight", | |
| "time_embedder.proj_out.weight", | |
| }) | |
| forbidden = { | |
| "proj_in.weight", | |
| "audio_proj_in.weight", | |
| "context_embedder.weight", | |
| "transformer_blocks.0.attn.to_q.weight", | |
| "token_refiner.refiner_blocks.0.attn.to_q.weight", | |
| "time_embedder.table", | |
| } | |
| if is_pruned: | |
| forbidden.update({ | |
| "time_embedder.proj_in.weight", | |
| "time_embedder.proj_in.bias", | |
| "time_embedder.proj_out.weight", | |
| "time_embedder.proj_out.bias", | |
| "norm_out.folded_bias", | |
| "adaln_basis", | |
| "adaln_mean", | |
| }) | |
| forbidden.update( | |
| f"blocks.{i}.adaln_proj.folded_bias" for i in range(50) | |
| ) | |
| else: | |
| forbidden.add("adaln_t_table") | |
| missing = sorted(required - keys) | |
| stale = sorted(forbidden & keys) | |
| if missing: | |
| raise RuntimeError( | |
| "Converted output is missing required native keys:\n " | |
| + "\n ".join(missing) | |
| ) | |
| if stale: | |
| raise RuntimeError( | |
| "Converted output still contains Diffusers keys:\n " | |
| + "\n ".join(stale) | |
| ) | |
| if len(keys) != expected_count: | |
| raise RuntimeError( | |
| f"Converted output tensor count is {len(keys)}, expected " | |
| f"{expected_count}" | |
| ) | |
| if is_pruned: | |
| table_info = header["tensors"]["adaln_t_table"] | |
| if table_info["shape"] != [1025, 8] or table_info["dtype"] != "F32": | |
| raise RuntimeError( | |
| "Converted adaln_t_table does not preserve [1025, 8] F32" | |
| ) | |
| for i in range(50): | |
| info = header["tensors"][f"blocks.{i}.adaln_proj.linear.bias"] | |
| if info["shape"] != [96768] or info["dtype"] != "F32": | |
| raise RuntimeError( | |
| f"blocks.{i}.adaln_proj.linear.bias does not preserve " | |
| "[96768] F32" | |
| ) | |
| final_bias = header["tensors"]["final_layer.adaln_proj.linear.bias"] | |
| if final_bias["shape"] != [10752] or final_bias["dtype"] != "F32": | |
| raise RuntimeError( | |
| "final_layer.adaln_proj.linear.bias does not preserve [10752] F32" | |
| ) | |
| if "config" in header["metadata"]: | |
| raise RuntimeError( | |
| "Pruned output still contains config metadata that can override " | |
| "shape-based ComfyUI detection" | |
| ) | |
| for key in header["tensors"]: | |
| validate_tensor_info(output_path, header, key) | |
| with safe_open(str(output_path), framework="pt", device="cpu") as f: | |
| safe_keys = set(f.keys()) | |
| if safe_keys != keys: | |
| raise RuntimeError( | |
| "safetensors.safe_open key set differs from parsed header" | |
| ) | |
| rope = f.get_tensor("rope.inv_freq") | |
| expected_rope = 1.0 / ( | |
| 10000.0 | |
| ** ( | |
| torch.arange(0, 32, 2, dtype=torch.float32) | |
| / 32.0 | |
| ) | |
| ) | |
| if not torch.equal(rope, expected_rope): | |
| raise RuntimeError("rope.inv_freq reconstruction mismatch") | |
| return len(keys) | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("source", type=Path) | |
| parser.add_argument("output", type=Path) | |
| args = parser.parse_args() | |
| source_path = args.source.expanduser().resolve() | |
| output_path = args.output.expanduser().resolve() | |
| if not source_path.is_file(): | |
| raise RuntimeError(f"Source file does not exist: {source_path}") | |
| if source_path == output_path: | |
| raise RuntimeError( | |
| "Source and output must be different files; validate before replacing" | |
| ) | |
| if output_path.exists(): | |
| raise RuntimeError( | |
| f"Output already exists; refusing to overwrite: {output_path}" | |
| ) | |
| source_header = read_header(source_path) | |
| print(f"Source: {source_path}") | |
| print(f"Source tensors: {len(source_header['tensors']):,}") | |
| print("Validating source and building reverse-conversion plan...") | |
| plan, is_pruned = build_plan(source_path, source_header) | |
| qkv_count = sum(entry["kind"] == "qkv" for entry in plan) | |
| swiglu_count = sum(entry["kind"] == "swiglu" for entry in plan) | |
| print(f"Checkpoint variant: {'pruned AdaLN table' if is_pruned else 'full timestep MLP'}") | |
| print(f"Native output tensors: {len(plan):,}") | |
| print(f"QKV groups recombined: {qkv_count:,}") | |
| print(f"SwiGLU tensors unswapped: {swiglu_count:,}") | |
| print("Reconstructed tensors: 1 (rope.inv_freq)") | |
| print(f"Output: {output_path}") | |
| print() | |
| payload_hash, payload_size = write_output( | |
| source_path, output_path, source_header, plan, is_pruned | |
| ) | |
| print(f"Converted payload: {payload_size / (1024**3):.3f} GiB") | |
| print(f"Converted payload SHA256: {payload_hash}") | |
| print("Validating complete output with safetensors...") | |
| count = validate_output(output_path, len(plan), is_pruned) | |
| print() | |
| print("safetensors.safe_open: OK") | |
| print(f"Native tensor count: {count:,}") | |
| print("Required native MiniMax-H3 keys: OK") | |
| if is_pruned: | |
| print("adaln_t_table mapping/shape: OK") | |
| print("Stale config metadata: removed") | |
| print("Stale Diffusers MiniMax-H3 keys: none") | |
| print("rope.inv_freq reconstruction: OK") | |
| print() | |
| print("CONVERSION SUCCESSFUL") | |
| print(output_path) | |
| if __name__ == "__main__": | |
| main() | |