### Based on https://github.com/huggingface/diffusers/blob/main/scripts/convert_wan_to_diffusers.py import argparse import json import pathlib import shutil from typing import Any, Dict, List from safetensors.torch import load_file, save_file from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger logger = init_logger(__name__) TRANSFORMER_KEYS_RENAME_DICT = { "time_embedding.0": "condition_embedder.time_embedder.linear_1", "time_embedding.2": "condition_embedder.time_embedder.linear_2", "text_embedding.0": "condition_embedder.text_embedder.linear_1", "text_embedding.2": "condition_embedder.text_embedder.linear_2", "time_projection.1": "condition_embedder.time_proj", "head.modulation": "scale_shift_table", "head.head": "proj_out", "modulation": "scale_shift_table", "ffn.0": "ffn.net.0.proj", "ffn.2": "ffn.net.2", # Hack to swap the layer names # The original model calls the norms in following order: norm1, norm3, norm2 # We convert it to: norm1, norm2, norm3 "norm2": "norm__placeholder", "norm3": "norm2", "norm__placeholder": "norm3", # For the I2V model "img_emb.proj.0": "condition_embedder.image_embedder.norm1", "img_emb.proj.1": "condition_embedder.image_embedder.ff.net.0.proj", "img_emb.proj.3": "condition_embedder.image_embedder.ff.net.2", "img_emb.proj.4": "condition_embedder.image_embedder.norm2", # for the FLF2V model "img_emb.emb_pos": "condition_embedder.image_embedder.pos_embed", # Add attention component mappings "self_attn.q": "attn1.to_q", "self_attn.k": "attn1.to_k", "self_attn.v": "attn1.to_v", "self_attn.o": "attn1.to_out.0", "self_attn.norm_q": "attn1.norm_q", "self_attn.norm_k": "attn1.norm_k", "cross_attn.q": "attn2.to_q", "cross_attn.k": "attn2.to_k", "cross_attn.v": "attn2.to_v", "cross_attn.o": "attn2.to_out.0", "cross_attn.norm_q": "attn2.norm_q", "cross_attn.norm_k": "attn2.norm_k", "attn2.to_k_img": "attn2.add_k_proj", "attn2.to_v_img": "attn2.add_v_proj", "attn2.norm_k_img": "attn2.norm_added_k", # MXFP4 msmodelslim wraps Linear layers with a `.linear.` subpath; # strip it so keys match the SGLang model parameters. ".linear.": ".", # NonFusionSmoothQuantWrapper exports smooth quant scale as `.div.mul_scale`; # strip `.div.` so it loads as a direct parameter `mul_scale` on the linear layer. ".div.": ".", } SUPPORTED_MODEL_TYPES = ["Wan2.2-T2V-A14B", "Wan2.2-I2V-A14B", "Wan2.2-TI2V-5B"] # Cascade models have two transformers (high_noise + low_noise) CASCADE_MODEL_TYPES = {"Wan2.2-T2V-A14B", "Wan2.2-I2V-A14B"} def get_transformer_config(model_type: str) -> Dict[str, Any]: if model_type in SUPPORTED_MODEL_TYPES: return TRANSFORMER_KEYS_RENAME_DICT else: raise ValueError( f"Unsupported model_type: {model_type}. Supported: {SUPPORTED_MODEL_TYPES}" ) def get_transformer_dirs(model_type: str) -> List[str]: """Return the list of transformer directory names for a given model type.""" if model_type in CASCADE_MODEL_TYPES: return ["transformer", "transformer_2"] return ["transformer"] def get_quant_subpath( model_type: str, quant_path: pathlib.Path, transformer_dir: str ) -> pathlib.Path: """Return the quant weights subdirectory for a given transformer.""" if model_type in CASCADE_MODEL_TYPES: sub = ( "high_noise_model" if transformer_dir == "transformer" else "low_noise_model" ) return quant_path / sub return quant_path def update_dict_(d: Dict[str, Any], old_key: str, new_key: str) -> None: d[new_key] = d.pop(old_key) def load_sharded_safetensors(directory: pathlib.Path, pattern: str) -> dict: candidates = sorted(directory.glob(pattern)) if not candidates: raise FileNotFoundError(f"No file matching '{pattern}' found in {directory}") state_dict = {} for f in candidates: state_dict.update(load_file(f)) return state_dict def convert_transformer( model_type: str, model_dir: pathlib.Path, output_dir: pathlib.Path ) -> None: """Convert a single quantized transformer directory into Diffusers format.""" model_path = pathlib.Path(model_dir) out_path = pathlib.Path(output_dir) out_path.mkdir(parents=True, exist_ok=True) RENAME_DICT = get_transformer_config(model_type) state_dict = load_sharded_safetensors(model_path, "quant_model_weight*.safetensors") json_candidates = sorted(model_path.glob("quant_model_description*.json")) if not json_candidates: raise FileNotFoundError( f"No quant_model_description*.json found in {model_path}" ) with open(json_candidates[0]) as f: quant_config = json.load(f) for key in list(state_dict.keys()): new_key = key[:] for replace_key, rename_key in RENAME_DICT.items(): new_key = new_key.replace(replace_key, rename_key) if new_key != key: update_dict_(state_dict, key, new_key) # The quant JSON only covers quantized layers, not all model keys if key in quant_config: update_dict_(quant_config, key, new_key) save_file(state_dict, out_path / "diffusion_pytorch_model.safetensors") with open(out_path / "quant_model_description.json", "w") as f: json.dump(quant_config, f, indent=2) def repack( model_type: str, original_model_path: pathlib.Path, quant_path: pathlib.Path, output_path: pathlib.Path, ) -> None: """ Full one-step repack workflow: 1. Copy the original HF Diffusers model to output_path, excluding transformer dir(s). 2. For each transformer: convert quant weights and copy config.json from original. """ transformer_dirs = get_transformer_dirs(model_type) # Step 1: Copy original model, skipping transformer dirs (they will be replaced) logger.debug(f"Step 1: Copying original model to {output_path}") logger.debug(f" (skipping: {transformer_dirs})") shutil.copytree( str(original_model_path), str(output_path), ignore=shutil.ignore_patterns(*transformer_dirs), ) # Step 2+: Convert each transformer for i, tdir in enumerate(transformer_dirs): q_path = get_quant_subpath(model_type, quant_path, tdir) out_tdir = output_path / tdir logger.debug( f"\nStep {i + 2}: Converting {tdir} (quant source: {q_path.name})..." ) convert_transformer(model_type, q_path, out_tdir) # Copy config.json from the original transformer dir src_config = original_model_path / tdir / "config.json" if src_config.is_file(): shutil.copy2(str(src_config), str(out_tdir / "config.json")) logger.debug(f" Copied config.json from original {tdir}/") logger.info(f"\nDone! Repacked model saved to: {output_path}") def get_args(): parser = argparse.ArgumentParser( description="Repack msmodelslim quantized Wan2.2 weights into HF Diffusers format" ) parser.add_argument( "--model-type", type=str, required=True, choices=SUPPORTED_MODEL_TYPES, help="Model type to convert", ) parser.add_argument( "--original-model-path", type=str, required=True, help="Path to the original HF Diffusers model (e.g., /weights/Wan2.2-TI2V-5B-Diffusers)", ) parser.add_argument( "--quant-path", type=str, required=True, help="Path to msmodelslim quantized weights directory", ) parser.add_argument( "--output-path", type=str, required=True, help="Output path for the repacked model (e.g., /weights/Wan2.2-TI2V-5B-Diffusers-MXFP8)", ) return parser.parse_args() if __name__ == "__main__": args = get_args() repack( model_type=args.model_type, original_model_path=pathlib.Path(args.original_model_path), quant_path=pathlib.Path(args.quant_path), output_path=pathlib.Path(args.output_path), )