Files
2026-07-13 13:33:03 +08:00

282 lines
9.7 KiB
Python

import argparse
import json
import shlex
import shutil
import subprocess
import sys
from pathlib import Path
WAN_MODELS = ("text_encoder", "transformer", "vae_decoder")
def parse_args():
parser = argparse.ArgumentParser(description="Convert exported Wan ONNX models to MNN.")
parser.add_argument("onnx_path_pos", nargs="?", help="ONNX root. Kept for compatibility with older scripts.")
parser.add_argument("mnn_root_pos", nargs="?", help="MNN output root. Kept for compatibility with older scripts.")
parser.add_argument("--onnx_path", help="ONNX root produced by wan_onnx_export.py.")
parser.add_argument("--mnn_root", "--output_path", dest="mnn_root", help="Directory for Wan MNN resources.")
parser.add_argument(
"--tokenizer_path",
help="Tokenizer source directory. Defaults to <onnx_path>/tokenizer.",
)
parser.add_argument("--mnnconvert", help="Path to MNNConvert. Defaults to build/MNNConvert or mnnconvert in PATH.")
parser.add_argument(
"--extra",
nargs=argparse.REMAINDER,
default=[],
help="Extra MNNConvert arguments, for example: --extra --weightQuantBits=8",
)
args = parser.parse_args()
args.onnx_path = args.onnx_path or args.onnx_path_pos
args.mnn_root = args.mnn_root or args.mnn_root_pos
if not args.onnx_path or not args.mnn_root:
parser.error("both --onnx_path and --mnn_root are required")
return args
def find_mnnconvert(explicit_path=None):
if explicit_path:
return explicit_path
repo_root = Path(__file__).resolve().parents[4]
local = repo_root / "build" / "MNNConvert"
if local.exists():
return local.as_posix()
found = shutil.which("mnnconvert")
if found:
return found
return "mnnconvert"
def normalize_extra(extra):
args = []
for item in extra or []:
args.extend(shlex.split(item))
return args
def read_json_if_exists(path):
path = Path(path)
if not path.exists():
return None
with path.open("r", encoding="utf-8") as fp:
return json.load(fp)
def write_json(path, payload):
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as fp:
json.dump(payload, fp, indent=2, sort_keys=True)
def find_arg_value(extra_args, name):
prefix = name + "="
for index, item in enumerate(extra_args):
if item == name:
if index + 1 < len(extra_args):
return extra_args[index + 1]
return True
if item.startswith(prefix):
return item[len(prefix):]
return None
def build_flag_summary(extra_args):
weight_quant_bits = find_arg_value(extra_args, "--weightQuantBits")
weight_quant_block = find_arg_value(extra_args, "--weightQuantBlock")
return {
"fp16": bool(find_arg_value(extra_args, "--fp16")),
"hqq": bool(find_arg_value(extra_args, "--hqq")),
"transformer_fuse": bool(find_arg_value(extra_args, "--transformerFuse")),
"weight_quant": weight_quant_bits is not None,
"weight_quant_bits": int(weight_quant_bits) if weight_quant_bits not in (None, True) else weight_quant_bits,
"weight_quant_block": int(weight_quant_block) if weight_quant_block not in (None, True) else weight_quant_block,
}
def convert_one(convert_path, onnx_file, mnn_file, extra_args):
if not onnx_file.exists():
raise FileNotFoundError(f"Missing ONNX model: {onnx_file}")
mnn_file.parent.mkdir(parents=True, exist_ok=True)
cmd = [
convert_path,
"-f",
"ONNX",
"--modelFile",
onnx_file.as_posix(),
"--MNNModel",
mnn_file.as_posix(),
"--saveExternalData=1",
] + extra_args
print(" ".join(shlex.quote(x) for x in cmd))
result = subprocess.run(cmd, check=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
if result.stdout:
print(result.stdout)
if result.returncode != 0:
raise RuntimeError(f"MNNConvert failed for {onnx_file} with exit code {result.returncode}")
return {
"command": cmd,
"mnn_model": mnn_file.as_posix(),
"onnx_model": onnx_file.as_posix(),
"save_external_data": True,
}
T5_DEFAULT_QUANT_ARGS = ["--weightQuantBits=8", "--weightQuantBlock=128"]
def convert_models(onnx_root, mnn_root, convert_path, extra_args):
onnx_root = Path(onnx_root)
mnn_root = Path(mnn_root)
print("Onnx path:", onnx_root)
print("MNN path:", mnn_root)
print("Extra:", " ".join(shlex.quote(x) for x in extra_args))
records = []
for model in WAN_MODELS:
model_extra = list(extra_args)
if model == "text_encoder" and not find_arg_value(extra_args, "--weightQuantBits"):
model_extra.extend(T5_DEFAULT_QUANT_ARGS)
record = convert_one(
convert_path,
onnx_root / model / "model.onnx",
mnn_root / f"{model}.mnn",
model_extra,
)
record["name"] = model
records.append(record)
return records
def copy_tokenizer_source(src_dir, dst_dir):
src_dir = Path(src_dir)
dst_dir = Path(dst_dir)
if not src_dir.is_dir():
raise FileNotFoundError(f"Tokenizer source directory not found: {src_dir}")
dst_dir.mkdir(parents=True, exist_ok=True)
for item in src_dir.iterdir():
target = dst_dir / item.name
if item.is_dir():
if target.exists():
shutil.rmtree(target)
shutil.copytree(item, target)
elif item.is_file():
shutil.copy2(item, target)
def read_model_type(tokenizer_dir):
for name in ("config.json", "tokenizer_config.json"):
path = tokenizer_dir / name
if not path.exists():
continue
try:
with path.open("r", encoding="utf-8") as fp:
model_type = json.load(fp).get("model_type")
if model_type:
return model_type
except Exception:
pass
return "t5"
def materialize_tokenizer_json(tokenizer_dir):
tokenizer_json = tokenizer_dir / "tokenizer.json"
if tokenizer_json.exists():
return
from transformers import AutoTokenizer
errors = []
for use_fast in (True, False):
try:
tokenizer = AutoTokenizer.from_pretrained(
tokenizer_dir.as_posix(),
trust_remote_code=True,
use_fast=use_fast,
local_files_only=True,
)
tokenizer.save_pretrained(tokenizer_dir.as_posix())
if tokenizer_json.exists():
return
except Exception as e:
errors.append(f"use_fast={use_fast}: {e}")
raise RuntimeError(
f"Failed to materialize tokenizer.json under {tokenizer_dir}. "
"Provide a complete local tokenizer directory. Errors: "
+ " | ".join(errors)
)
def export_wan_mtok(tokenizer_src_root, mnn_root):
repo_root = Path(__file__).resolve().parents[4]
llm_export_dir = repo_root / "transformers" / "llm" / "export"
if str(llm_export_dir) not in sys.path:
sys.path.insert(0, str(llm_export_dir))
from utils.tokenizer import LlmTokenizer
dst_tokenizer = Path(mnn_root) / "tokenizer"
copy_tokenizer_source(tokenizer_src_root, dst_tokenizer)
materialize_tokenizer_json(dst_tokenizer)
model_type = read_model_type(dst_tokenizer)
llm_tokenizer = LlmTokenizer(dst_tokenizer.as_posix(), model_type)
out_path = llm_tokenizer.export(
dst_tokenizer.as_posix(),
model_path=dst_tokenizer.as_posix(),
model_type=model_type,
)
if not out_path.endswith("tokenizer.mtok"):
raise RuntimeError(f"Tokenizer export did not produce tokenizer.mtok: {out_path}")
print(f"Generated mtok: {out_path}")
return {
"model_type": model_type,
"mtok_path": out_path,
"source_dir": Path(tokenizer_src_root).resolve().as_posix(),
"tokenizer_dir": dst_tokenizer.as_posix(),
}
def write_convert_report(onnx_root, mnn_root, convert_path, extra_args, module_records, tokenizer_record):
report = {
"extra_args": extra_args,
"mnn_root": Path(mnn_root).resolve().as_posix(),
"mnnconvert_path": convert_path,
"modules": [
{
"module": record["name"],
"onnx_path": record["onnx_model"],
"mnn_path": record["mnn_model"],
}
for record in module_records
],
"mtok_path": tokenizer_record["mtok_path"],
"onnx_root": Path(onnx_root).resolve().as_posix(),
"saveExternalData": True,
"tokenizer": {
"model_type": tokenizer_record["model_type"],
"source_path": tokenizer_record["source_dir"],
"target_path": tokenizer_record["tokenizer_dir"],
},
"tokenizer_source_path": tokenizer_record["source_dir"],
"tokenizer_target_path": tokenizer_record["tokenizer_dir"],
"tokenizer_mtok_path": tokenizer_record["mtok_path"],
"export_report": read_json_if_exists(Path(onnx_root) / "export_report.json"),
"flags": build_flag_summary(extra_args),
}
write_json(Path(mnn_root) / "convert_report.json", report)
def main():
args = parse_args()
onnx_root = Path(args.onnx_path)
mnn_root = Path(args.mnn_root)
tokenizer_src = Path(args.tokenizer_path) if args.tokenizer_path else onnx_root / "tokenizer"
convert_path = find_mnnconvert(args.mnnconvert)
extra_args = normalize_extra(args.extra)
module_records = convert_models(onnx_root, mnn_root, convert_path, extra_args)
tokenizer_record = export_wan_mtok(tokenizer_src, mnn_root)
write_convert_report(onnx_root, mnn_root, convert_path, extra_args, module_records, tokenizer_record)
if __name__ == "__main__":
main()