woodchen7 commited on
Commit
550acd8
·
verified ·
1 Parent(s): 370f225

Upload merge_mtp.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. merge_mtp.py +136 -0
merge_mtp.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Merge the AngelSpec MTP head into a base HYV3 model.
3
+
4
+ The base model (e.g. ``Hy3_opensource``) already has the full transformer body
5
+ (layers 0-79 + embeddings + lm_head + final norm) *and* its own MTP head at
6
+ ``model.layers.80.*``. This script produces a NEW model directory that is a copy
7
+ of the base, but whose MTP head (layer 80) is replaced by the AngelSpec MTP
8
+ weights exported in ``mtp.safetensors``.
9
+
10
+ Result = base body + AngelSpec MTP head.
11
+
12
+ Only the 5 shards that contain layer-80 tensors are rewritten. The other 94
13
+ shards, together with the config / tokenizer / index files, are linked (or
14
+ copied) verbatim from the base model, so the merge is fast and cheap.
15
+
16
+ Usage:
17
+ python merge_mtp.py \
18
+ --base ORIGINAL_MODEL_PATH \
19
+ --mtp MTP_WEIGHT_PATH \
20
+ --out OUTPUT_WEIGHT_PATH \
21
+ --link copy # symlink | hardlink | copy (how to bring over the 94 unchanged shards)
22
+ """
23
+ import argparse
24
+ import json
25
+ import os
26
+ import shutil
27
+
28
+ from safetensors import safe_open
29
+ from safetensors.torch import save_file
30
+
31
+ MTP_PREFIX = "model.layers.80."
32
+
33
+
34
+ def bring_over(src, dst, mode):
35
+ """Materialise an unchanged shard in the output dir according to *mode*."""
36
+ if os.path.lexists(dst):
37
+ os.remove(dst)
38
+ if mode == "symlink":
39
+ os.symlink(os.path.abspath(src), dst)
40
+ elif mode == "hardlink":
41
+ os.link(src, dst)
42
+ elif mode == "copy":
43
+ shutil.copy2(src, dst)
44
+ else:
45
+ raise ValueError(f"unknown link mode: {mode}")
46
+
47
+
48
+ def main():
49
+ ap = argparse.ArgumentParser()
50
+ ap.add_argument("--base", required=True, help="Base full model directory (body comes from here)")
51
+ ap.add_argument("--mtp", required=True, help="MTP export dir (contains mtp.safetensors)")
52
+ ap.add_argument("--out", required=True, help="Output directory for the merged model")
53
+ ap.add_argument("--link", default="symlink", choices=["symlink", "hardlink", "copy"],
54
+ help="How to bring over the 94 unchanged shards (default: symlink)")
55
+ ap.add_argument("--prefix", default=MTP_PREFIX, help="Tensor prefix of the MTP layer")
56
+ args = ap.parse_args()
57
+
58
+ os.makedirs(args.out, exist_ok=True)
59
+
60
+ # --- 1. Load the base index and locate the MTP (layer-80) tensors ----------
61
+ base_index_path = os.path.join(args.base, "model.safetensors.index.json")
62
+ base_index = json.load(open(base_index_path))
63
+ weight_map = base_index["weight_map"]
64
+
65
+ mtp_keys = sorted(k for k in weight_map if k.startswith(args.prefix))
66
+ if not mtp_keys:
67
+ raise SystemExit(f"No {args.prefix!r} tensors found in {base_index_path}")
68
+
69
+ mtp_shards = sorted({weight_map[k] for k in mtp_keys})
70
+ all_shards = sorted(set(weight_map.values()))
71
+ print(f"MTP layer has {len(mtp_keys)} tensors across {len(mtp_shards)} shards.")
72
+ print(f"Base model has {len(all_shards)} shards total; "
73
+ f"{len(all_shards) - len(mtp_shards)} will be linked verbatim.")
74
+
75
+ # --- 2. Load replacement MTP tensors from the export -----------------------
76
+ mtp_file = os.path.join(args.mtp, "mtp.safetensors")
77
+ new_mtp = {}
78
+ with safe_open(mtp_file, framework="pt", device="cpu") as f:
79
+ exported = set(f.keys())
80
+ missing = set(mtp_keys) - exported
81
+ if missing:
82
+ raise SystemExit(f"{len(missing)} MTP tensors missing from {mtp_file}, "
83
+ f"e.g. {sorted(missing)[:3]}")
84
+ for k in mtp_keys:
85
+ new_mtp[k] = f.get_tensor(k)
86
+
87
+ # Sanity check: shapes/dtypes must match what the base expects.
88
+ for shard in mtp_shards:
89
+ with safe_open(os.path.join(args.base, shard), framework="pt", device="cpu") as f:
90
+ for k in [k for k in mtp_keys if weight_map[k] == shard]:
91
+ bt = f.get_slice(k)
92
+ bshape = tuple(bt.get_shape())
93
+ if bshape != tuple(new_mtp[k].shape):
94
+ raise SystemExit(f"Shape mismatch for {k}: base {bshape} vs mtp {tuple(new_mtp[k].shape)}")
95
+
96
+ # --- 3. Bring over the unchanged shards ------------------------------------
97
+ for shard in all_shards:
98
+ if shard in mtp_shards:
99
+ continue
100
+ bring_over(os.path.join(args.base, shard), os.path.join(args.out, shard), args.link)
101
+ print(f"Linked {len(all_shards) - len(mtp_shards)} unchanged shards ({args.link}).")
102
+
103
+ # --- 4. Rewrite the MTP-containing shards with the new head ----------------
104
+ for shard in mtp_shards:
105
+ src = os.path.join(args.base, shard)
106
+ tensors = {}
107
+ with safe_open(src, framework="pt", device="cpu") as f:
108
+ for k in f.keys():
109
+ tensors[k] = f.get_tensor(k)
110
+ replaced = 0
111
+ for k in tensors:
112
+ if k in new_mtp:
113
+ tensors[k] = new_mtp[k]
114
+ replaced += 1
115
+ save_file(tensors, os.path.join(args.out, shard), metadata={"format": "pt"})
116
+ print(f" rewrote {shard}: replaced {replaced} MTP tensors "
117
+ f"(kept {len(tensors) - replaced} base tensors)")
118
+
119
+ # --- 5. Copy every non-shard file (config, tokenizer, index, ...) ----------
120
+ for name in sorted(os.listdir(args.base)):
121
+ src = os.path.join(args.base, name)
122
+ if not os.path.isfile(src):
123
+ continue
124
+ if name.endswith(".safetensors"): # shards handled above
125
+ continue
126
+ shutil.copy2(src, os.path.join(args.out, name))
127
+ print("Copied config / tokenizer / index and other metadata files.")
128
+
129
+ print(f"\nDone. Merged model written to: {args.out}")
130
+ if args.link == "symlink":
131
+ print("NOTE: unchanged shards are symlinks into the base model. Keep the base "
132
+ "model in place, or re-run with --link copy to make a self-contained copy.")
133
+
134
+
135
+ if __name__ == "__main__":
136
+ main()