Skip to content

Commit 6421926

Browse files
authored
Fix ckpt convert for qwen35 moe model (#1233)
fix ckpt convert for qwen35 moe model and support uneven pipeline parallel
1 parent 75ad1ee commit 6421926

10 files changed

Lines changed: 491 additions & 161 deletions

File tree

tools/checkpoint/qwen35/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,19 @@ python convert_qwen35.py --direction meg2hf --meg-path ... --hf-path ... --yaml
6565
```bash
6666
python compare_two_ckpts.py --ref /path/to/ref/release --gen /path/to/gen/release --tp 2
6767
```
68+
69+
## WARNING
70+
71+
If torch version < 2.9, maybe need modify (tools/checkpoint/qwen35/qwen35/config.py)
72+
73+
```python
74+
self.use_linear_proj = cfg.get("vision_patch_embed_linear", True)
75+
```
76+
77+
to
78+
79+
```python
80+
self.use_linear_proj = cfg.get("vision_patch_embed_linear", False)
81+
```
82+
83+
because qwen_vl will use linear instead of conv3d when torch version >= 2.9.

tools/checkpoint/qwen35/convert_qwen35.py

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
"""
2222

2323
import argparse
24+
import shutil
2425
import sys
26+
import tempfile
2527

2628
from qwen35.config import Config, detect_model_type
2729
from qwen35.constants import LN_ADJUSTMENT
@@ -36,11 +38,16 @@ def parse_args():
3638
choices=["hf2meg", "meg2hf"],
3739
help="Conversion direction",
3840
)
39-
p.add_argument("--hf-path", required=True, help="Path to HF checkpoint directory")
41+
p.add_argument(
42+
"--hf-path",
43+
default=None,
44+
help="For hf2meg: input HF checkpoint directory or ModelScope model ID. "
45+
"For meg2hf: output HF checkpoint directory (optional if --ref-path is given).",
46+
)
4047
p.add_argument(
4148
"--meg-path",
42-
required=True,
43-
help="For hf2meg: output Megatron checkpoint directory. "
49+
default=None,
50+
help="For hf2meg: output Megatron checkpoint directory (optional if --ref-path is given). "
4451
"For meg2hf: input Megatron checkpoint directory.",
4552
)
4653
p.add_argument(
@@ -52,12 +59,14 @@ def parse_args():
5259
"--ref-path",
5360
default=None,
5461
help="Reference checkpoint path for validation (Megatron ref for hf2meg, "
55-
"HF ref for meg2hf).",
62+
"HF ref for meg2hf). When provided, the corresponding output path may be omitted; "
63+
"output will be written to a temporary directory, validated, and cleaned up.",
5664
)
5765
p.add_argument(
58-
"--adjust-embedding",
66+
"--ref-skip-value",
5967
action="store_true",
60-
help="Adjust embedding vocab size to match reference checkpoint (hf2meg only)",
68+
help="When validating against --ref-path, skip numerical value comparison "
69+
"and only compare structure, keys, and shapes.",
6170
)
6271
p.add_argument(
6372
"--adjust-ln",
@@ -71,6 +80,11 @@ def parse_args():
7180
action="store_true",
7281
help=argparse.SUPPRESS,
7382
)
83+
p.add_argument(
84+
"--adjust-embedding",
85+
action="store_true",
86+
help="During hf2meg, adjust vocab size to match the reference checkpoint.",
87+
)
7488
p.add_argument(
7589
"--tp",
7690
type=int,
@@ -89,11 +103,23 @@ def parse_args():
89103
default=None,
90104
help="Override expert model parallel size from YAML (MoE only)",
91105
)
92-
return p.parse_args()
106+
return p.parse_args(), p
93107

94108

95109
def main():
96-
args = parse_args()
110+
args, parser = parse_args()
111+
112+
# Validate required paths based on direction
113+
if args.direction == "hf2meg":
114+
if not args.hf_path:
115+
parser.error("--hf-path is required for hf2meg")
116+
if not args.meg_path and not args.ref_path:
117+
parser.error("--meg-path is required for hf2meg when --ref-path is not provided")
118+
else:
119+
if not args.meg_path:
120+
parser.error("--meg-path is required for meg2hf")
121+
if not args.hf_path and not args.ref_path:
122+
parser.error("--hf-path is required for meg2hf when --ref-path is not provided")
97123

98124
# Apply CLI override for layer norm adjustment
99125
if args.adjust_ln:
@@ -129,11 +155,32 @@ def main():
129155
print(f"TP={cfg.tp}, PP={cfg.pp}, EP={cfg.ep}")
130156
print(f"Layers={cfg.num_layers}, hidden={cfg.hidden_size}")
131157
print(f"LN adjustment: {LN_ADJUSTMENT}")
132-
133-
if args.direction == "hf2meg":
134-
success = converter.run_hf2meg(args.hf_path, args.meg_path, args.ref_path)
135-
else:
136-
success = converter.run_meg2hf(args.meg_path, args.hf_path, args.ref_path)
158+
print(f"Ref skip value: {args.ref_skip_value}")
159+
160+
temp_dir = None
161+
try:
162+
if args.direction == "hf2meg":
163+
out_dir = args.meg_path
164+
if not out_dir:
165+
temp_dir = tempfile.mkdtemp(prefix="qwen35_hf2meg_")
166+
out_dir = temp_dir
167+
print(f"No --meg-path provided; using temporary output: {out_dir}")
168+
success = converter.run_hf2meg(
169+
args.hf_path, out_dir, args.ref_path, skip_value=args.ref_skip_value
170+
)
171+
else:
172+
out_dir = args.hf_path
173+
if not out_dir:
174+
temp_dir = tempfile.mkdtemp(prefix="qwen35_meg2hf_")
175+
out_dir = temp_dir
176+
print(f"No --hf-path provided; using temporary output: {out_dir}")
177+
success = converter.run_meg2hf(
178+
args.meg_path, out_dir, args.ref_path, skip_value=args.ref_skip_value
179+
)
180+
finally:
181+
if temp_dir:
182+
shutil.rmtree(temp_dir, ignore_errors=True)
183+
print(f"Cleaned up temporary output directory: {temp_dir}")
137184

138185
sys.exit(0 if success else 1)
139186

tools/checkpoint/qwen35/qwen35/config.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ def __init__(self, yaml_path):
3636
self.pp = cfg.get("pipeline_model_parallel_size", 1)
3737
self.ep = cfg.get("expert_model_parallel_size", 1)
3838

39+
# Uneven PP: optional per-stage layer counts
40+
self.decoder_first_pipeline_num_layers = cfg.get("decoder_first_pipeline_num_layers", None)
41+
self.decoder_last_pipeline_num_layers = cfg.get("decoder_last_pipeline_num_layers", None)
42+
3943
self.num_layers = _require(cfg, "num_layers")
4044
self.hidden_size = _require(cfg, "hidden_size")
4145
self.num_attention_heads = _require(cfg, "num_attention_heads")
@@ -78,6 +82,71 @@ def __init__(self, yaml_path):
7882
def is_moe(self):
7983
return self.num_experts is not None and self.num_experts > 0
8084

85+
@property
86+
def pp_layer_counts(self):
87+
"""Return a list of layer counts per PP rank, supporting uneven splits.
88+
89+
Uses decoder_first/last_pipeline_num_layers if set; otherwise divides
90+
evenly. For PP=1, returns [num_layers].
91+
"""
92+
if self.pp == 1:
93+
return [self.num_layers]
94+
95+
first = self.decoder_first_pipeline_num_layers
96+
last = self.decoder_last_pipeline_num_layers
97+
98+
if first is None and last is None:
99+
# Even split
100+
base = self.num_layers // self.pp
101+
counts = [base] * self.pp
102+
# Distribute remainder to last ranks
103+
remainder = self.num_layers - base * self.pp
104+
for i in range(remainder):
105+
counts[self.pp - 1 - i] += 1
106+
return counts
107+
108+
# Uneven split: derive from first/last constraints
109+
if first is not None and last is not None:
110+
if self.pp == 2:
111+
assert first + last == self.num_layers, (
112+
f"first({first}) + last({last}) != num_layers({self.num_layers})"
113+
)
114+
return [first, last]
115+
# PP > 2: middle ranks share the remainder evenly
116+
middle_total = self.num_layers - first - last
117+
middle_ranks = self.pp - 2
118+
base = middle_total // middle_ranks
119+
counts = [first] + [base] * middle_ranks + [last]
120+
remainder = middle_total - base * middle_ranks
121+
for i in range(remainder):
122+
counts[middle_ranks - i] += 1
123+
return counts
124+
125+
if first is not None:
126+
# Only first is specified
127+
if self.pp == 2:
128+
return [first, self.num_layers - first]
129+
remaining = self.num_layers - first
130+
middle_and_last = self.pp - 1
131+
base = remaining // middle_and_last
132+
counts = [first] + [base] * middle_and_last
133+
remainder = remaining - base * middle_and_last
134+
for i in range(remainder):
135+
counts[self.pp - 1 - i] += 1
136+
return counts
137+
138+
# Only last is specified
139+
if self.pp == 2:
140+
return [self.num_layers - last, last]
141+
remaining = self.num_layers - last
142+
first_and_middle = self.pp - 1
143+
base = remaining // first_and_middle
144+
counts = [base] * first_and_middle + [last]
145+
remainder = remaining - base * first_and_middle
146+
for i in range(remainder):
147+
counts[first_and_middle - 1 - i] += 1
148+
return counts
149+
81150

82151
def _require(d, key):
83152
"""Return d[key], raising a clear error if the key is missing."""

tools/checkpoint/qwen35/qwen35/converter.py

Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -259,14 +259,6 @@ def _split_tp(self, meg_sd):
259259
vis_head_dim = vis_h // vis_heads
260260
vis_qg = vis_heads
261261

262-
num_qg = cfg.num_query_groups
263-
kv_ch = cfg.kv_channels
264-
heads_per_group = cfg.num_attention_heads // num_qg
265-
if cfg.attention_output_gate:
266-
total_hpg = 2 * heads_per_group + 2
267-
else:
268-
total_hpg = heads_per_group + 2
269-
270262
for k, v in meg_sd.items():
271263
if not isinstance(v, torch.Tensor):
272264
for r in range(tp):
@@ -367,10 +359,11 @@ def _split_tp(self, meg_sd):
367359
for r in range(tp):
368360
shards[r][k] = chunks[r]
369361
elif "linear_qkv.weight" in k:
370-
viewed = v.view(num_qg, total_hpg, kv_ch, cfg.hidden_size)
371-
chunks = viewed.chunk(tp, dim=0)
362+
# Simple dim-0 chunk / cat for TP sharding of the fused QKV weight.
363+
# This is valid for any TP size, including cases where num_query_groups < tp.
364+
chunks = v.chunk(tp, dim=0)
372365
for r in range(tp):
373-
shards[r][k] = chunks[r].reshape(-1, cfg.hidden_size)
366+
shards[r][k] = chunks[r]
374367
elif "linear_proj.weight" in k:
375368
chunks = v.chunk(tp, dim=1)
376369
for r in range(tp):
@@ -483,13 +476,9 @@ def _merge_tp(self, shards):
483476
elif "out_proj.weight" in k:
484477
merged[k] = torch.cat(vals, dim=1)
485478
elif "linear_qkv.weight" in k:
486-
heads_per_group = cfg.num_attention_heads // cfg.num_query_groups
487-
if cfg.attention_output_gate:
488-
total_hpg = 2 * heads_per_group + 2
489-
else:
490-
total_hpg = heads_per_group + 2
491-
viewed = [x.view(-1, total_hpg, cfg.kv_channels, cfg.hidden_size) for x in vals]
492-
merged[k] = torch.cat(viewed, dim=0).view(-1, cfg.hidden_size)
479+
# TP split is a simple contiguous dim-0 chunk, so merge is
480+
# just concatenation along dim 0.
481+
merged[k] = torch.cat(vals, dim=0)
493482
elif "linear_proj.weight" in k:
494483
merged[k] = torch.cat(vals, dim=1)
495484
elif "q_layernorm" in k or "k_layernorm" in k:
@@ -566,7 +555,7 @@ def _adjust_embedding(self, meg_sd, ref_dir):
566555
# -------------------------------------------------------------------------
567556
# Public run methods
568557
# -------------------------------------------------------------------------
569-
def run_hf2meg(self, hf_dir, save_dir, ref_dir=None):
558+
def run_hf2meg(self, hf_dir, save_dir, ref_dir=None, skip_value=False):
570559
"""Run HF -> Megatron conversion and save release checkpoint."""
571560
cfg = self.cfg
572561
hf_dir = ensure_hf_path(hf_dir)
@@ -596,11 +585,11 @@ def run_hf2meg(self, hf_dir, save_dir, ref_dir=None):
596585
print(f"Saved release checkpoint to {os.path.join(save_dir, 'release')}")
597586

598587
if ref_dir:
599-
success = validate_hf2meg_against_ref(shards_dict, cfg, ref_dir)
588+
success = validate_hf2meg_against_ref(shards_dict, cfg, ref_dir, skip_value=skip_value)
600589
return success
601590
return True
602591

603-
def run_meg2hf(self, meg_dir, save_dir, ref_dir=None):
592+
def run_meg2hf(self, meg_dir, save_dir, ref_dir=None, skip_value=False):
604593
"""Run Megatron -> HF conversion and save safetensors."""
605594
cfg = self.cfg
606595
pp_merged = {}
@@ -631,7 +620,7 @@ def run_meg2hf(self, meg_dir, save_dir, ref_dir=None):
631620
print(f"Saved HF checkpoint to {save_dir}")
632621

633622
if ref_dir:
634-
success = validate_meg2hf_against_ref(hf_sd, cfg, ref_dir)
623+
success = validate_meg2hf_against_ref(hf_sd, cfg, ref_dir, skip_value=skip_value)
635624
return success
636625
return True
637626

@@ -655,7 +644,7 @@ class MoEConverter(BaseConverter):
655644
def __init__(self, cfg: Config, adjust_embedding=False):
656645
super().__init__(cfg, adjust_embedding=adjust_embedding)
657646

658-
def run_hf2meg(self, hf_dir, save_dir, ref_dir=None):
647+
def run_hf2meg(self, hf_dir, save_dir, ref_dir=None, skip_value=False):
659648
cfg = self.cfg
660649
print(f"Loading HF weights from {hf_dir}...")
661650
hf_sd = load_hf_weights(hf_dir)
@@ -685,14 +674,14 @@ def run_hf2meg(self, hf_dir, save_dir, ref_dir=None):
685674
save_megatron_release_checkpoint(shards_dict, save_dir, cfg)
686675
print(f"Saved release checkpoint to {os.path.join(save_dir, 'release')}")
687676

688-
if ref_dir and cfg.ep == 1:
689-
success = validate_hf2meg_against_ref(shards_dict, cfg, ref_dir, use_ep=True)
677+
if ref_dir:
678+
success = validate_hf2meg_against_ref(
679+
shards_dict, cfg, ref_dir, use_ep=True, skip_value=skip_value
680+
)
690681
return success
691-
elif ref_dir:
692-
print("Validation against reference checkpoint is only supported for EP=1; skipping.")
693682
return True
694683

695-
def run_meg2hf(self, meg_dir, save_dir, ref_dir=None):
684+
def run_meg2hf(self, meg_dir, save_dir, ref_dir=None, skip_value=False):
696685
cfg = self.cfg
697686
pp_merged = {}
698687
for pp_rank in range(cfg.pp):
@@ -726,6 +715,6 @@ def run_meg2hf(self, meg_dir, save_dir, ref_dir=None):
726715
print(f"Saved HF checkpoint to {save_dir}")
727716

728717
if ref_dir:
729-
success = validate_meg2hf_against_ref(hf_sd, cfg, ref_dir)
718+
success = validate_meg2hf_against_ref(hf_sd, cfg, ref_dir, skip_value=skip_value)
730719
return success
731720
return True

tools/checkpoint/qwen35/qwen35/io.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,18 @@ def find_megatron_shard(meg_dir, tp_rank=0, pp_rank=0, ep_rank=None):
5252
if os.path.isdir(iter_release):
5353
candidates.append(iter_release)
5454

55-
suffixes = []
56-
full = f"{tp_rank:02d}"
57-
if pp_rank > 0:
58-
full = f"{full}_{pp_rank:03d}"
59-
if ep_rank is not None and ep_rank > 0:
60-
full = f"{full}_{ep_rank:03d}"
61-
suffixes.append(full)
62-
63-
# Fallbacks for older naming conventions
64-
if ep_rank is not None and ep_rank > 0 and pp_rank == 0:
65-
suffixes.append(f"{tp_rank:02d}_{ep_rank:03d}")
66-
if pp_rank > 0 and (ep_rank is None or ep_rank == 0):
55+
ep_rank = ep_rank if ep_rank is not None else 0
56+
suffixes = [
57+
# Standard PP>1 and EP>1 naming: tp_pp_ep
58+
f"{tp_rank:02d}_{pp_rank:03d}_{ep_rank:03d}",
59+
]
60+
# PP>1 EP=1 naming (only valid when ep_rank==0, otherwise ambiguous)
61+
if ep_rank == 0:
6762
suffixes.append(f"{tp_rank:02d}_{pp_rank:03d}")
68-
if pp_rank == 0 and (ep_rank is None or ep_rank == 0):
63+
# PP=1 EP>1 naming: tp_ep
64+
suffixes.append(f"{tp_rank:02d}_{ep_rank:03d}")
65+
# PP=1 EP=1 naming (only valid when both are 0)
66+
if pp_rank == 0 and ep_rank == 0:
6967
suffixes.append(f"{tp_rank:02d}")
7068

7169
for base in candidates:
@@ -81,13 +79,14 @@ def save_megatron_release_checkpoint(shards_dict, save_dir, cfg):
8179
release_dir = os.path.join(save_dir, "release")
8280
os.makedirs(release_dir, exist_ok=True)
8381

84-
use_ep = cfg.ep > 1
8582
for rank_tuple, shard in shards_dict.items():
86-
if use_ep:
83+
# Support both (pp, tp) and (pp, tp, ep) tuple layouts regardless of cfg.ep.
84+
if len(rank_tuple) == 3:
8785
pp_rank, tp_rank, ep_rank = rank_tuple
8886
else:
8987
pp_rank, tp_rank = rank_tuple
9088
ep_rank = None
89+
use_ep = ep_rank is not None and ep_rank >= 0 and cfg.ep > 1
9190

9291
name = f"mp_rank_{tp_rank:02d}"
9392
if cfg.pp > 1:

0 commit comments

Comments
 (0)