Unlimited-OCR-MLX / convert.py
LoJexLLM's picture
Initial upload from ModelScope with English translations
267d7ad verified
Raw
History Blame Contribute Delete
6.6 kB
"""Convert PaddlePaddle/Unlimited-OCR PyTorch weights to MLX format.
Usage:
python convert.py --input_dir ./Unlimited-OCR-original --output_dir ./unlimited-ocr-mlx-weights
"""
import os
import sys
import json
import argparse
import numpy as np
from pathlib import Path
from typing import Dict
import safetensors.torch
import torch
# Add current dir to path for importing the config
sys.path.insert(0, os.path.dirname(__file__))
def load_pytorch_weights(model_dir: str) -> Dict[str, np.ndarray]:
"""Load PyTorch safetensors weights."""
# Find the model directory containing safetensors
if os.path.isdir(os.path.join(model_dir, "PaddlePaddle", "Unlimited-OCR")):
model_dir = os.path.join(model_dir, "PaddlePaddle", "Unlimited-OCR")
st_path = os.path.join(model_dir, "model-00001-of-000001.safetensors")
if not os.path.exists(st_path):
raise FileNotFoundError(f"Model file not found: {st_path}")
print(f"Loading weights from {st_path}...")
weights = safetensors.torch.load_file(st_path, device="cpu")
print(f"Loaded {len(weights)} weight tensors")
return {k: v.float().numpy() for k, v in weights.items()}
def convert_weight_name(pt_name: str) -> str:
"""Convert PyTorch weight name to MLX weight name."""
# Remove model. prefix
if pt_name.startswith("model."):
name = pt_name[6:] # Remove "model."
elif pt_name.startswith("lm_head."):
name = pt_name
else:
name = pt_name
# Map embed_tokens, norm
if name == "embed_tokens.weight":
return "language_model.embed_tokens.weight"
if name == "norm.weight":
return "language_model.norm.weight"
# Map lm_head
if pt_name.startswith("lm_head."):
return pt_name
# Map layers
if name.startswith("layers."):
parts = name.split(".")
layer_idx = parts[1]
rest = ".".join(parts[2:])
if rest.startswith("self_attn."):
prefix = "self_attn."
return f"language_model.layers.{layer_idx}.self_attn.{rest[len(prefix):]}"
elif rest.startswith("input_layernorm."):
prefix = "input_layernorm."
return f"language_model.layers.{layer_idx}.input_layernorm.{rest[len(prefix):]}"
elif rest.startswith("post_attention_layernorm."):
prefix = "post_attention_layernorm."
return f"language_model.layers.{layer_idx}.post_attention_layernorm.{rest[len(prefix):]}"
elif rest.startswith("mlp."):
mlp_rest = rest[4:] # Remove "mlp."
if mlp_rest.startswith("gate.weight"):
return f"language_model.layers.{layer_idx}.mlp.gate.weight"
elif mlp_rest.startswith("shared_experts."):
return f"language_model.layers.{layer_idx}.mlp.shared_experts.{mlp_rest[15:]}"
elif mlp_rest.startswith("experts."):
return f"language_model.layers.{layer_idx}.mlp.experts.{mlp_rest[8:]}"
else:
return f"language_model.layers.{layer_idx}.mlp.{mlp_rest}"
# Map SAM model
if name.startswith("sam_model."):
return name
# Map vision model (CLIP)
if name.startswith("vision_model."):
return name
# Map projector
if name.startswith("projector."):
return name
# Map image special tokens
if name in ["image_newline", "view_seperator"]:
return name
print(f"WARNING: Unmapped weight: {pt_name}")
return pt_name
def convert_weights_to_mlx(pt_weights: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]:
"""Convert all weights to MLX-compatible format."""
mlx_weights = {}
for pt_name, value in pt_weights.items():
mlx_name = convert_weight_name(pt_name)
mlx_weights[mlx_name] = value
print(f"Converted {len(mlx_weights)} weights to MLX format")
return mlx_weights
def save_mlx_weights(weights: Dict[str, np.ndarray], output_dir: str):
"""Save MLX weights in safetensors format."""
os.makedirs(output_dir, exist_ok=True)
# Save as safetensors (MLX compatible)
output_path = os.path.join(output_dir, "model.safetensors")
torch_weights = {k: torch.from_numpy(v.copy()) for k, v in weights.items()}
safetensors.torch.save_file(torch_weights, output_path)
print(f"Saved weights to {output_path}")
# Also save config
import json as j
config = {
"model_type": "unlimited-ocr-mlx",
"architectures": ["UnlimitedOCRModel"],
"vocab_size": 129280,
"hidden_size": 1280,
"num_hidden_layers": 12,
"num_attention_heads": 10,
"num_key_value_heads": 10,
"head_dim": 128,
"intermediate_size": 6848,
"moe_intermediate_size": 896,
"n_routed_experts": 64,
"n_shared_experts": 2,
"num_experts_per_tok": 6,
"first_k_dense_replace": 1,
"max_position_embeddings": 32768,
"vision_output_dim": 2048,
}
config_path = os.path.join(output_dir, "config.json")
with open(config_path, "w") as f:
j.dump(config, f, indent=2)
print(f"Saved config to {config_path}")
def main():
parser = argparse.ArgumentParser(description="Convert Unlimited-OCR to MLX format")
parser.add_argument("--input_dir", type=str, required=True,
help="Directory containing original PyTorch model")
parser.add_argument("--output_dir", type=str, required=True,
help="Output directory for MLX weights")
args = parser.parse_args()
print("=== Unlimited-OCR: PyTorch → MLX Weight Converter ===\n")
# Load original weights
pt_weights = load_pytorch_weights(args.input_dir)
# Convert
mlx_weights = convert_weights_to_mlx(pt_weights)
# Save
save_mlx_weights(mlx_weights, args.output_dir)
# Print summary
total_params = sum(v.size for v in mlx_weights.values())
print(f"\nDone! Total parameters: {total_params:,} ({total_params * 2 / 1e9:.2f}B BF16)")
# Copy tokenizer files
input_model_dir = args.input_dir
if os.path.isdir(os.path.join(input_model_dir, "PaddlePaddle", "Unlimited-OCR")):
input_model_dir = os.path.join(input_model_dir, "PaddlePaddle", "Unlimited-OCR")
import shutil
for fname in ["tokenizer.json", "tokenizer_config.json", "special_tokens_map.json"]:
src = os.path.join(input_model_dir, fname)
if os.path.exists(src):
dst = os.path.join(args.output_dir, fname)
shutil.copy2(src, dst)
print(f"Copied {fname}")
if __name__ == "__main__":
main()