ROMA / scripts /merger_new_module.py
Houssem0's picture
ROMA + GH200 reproducible Docker layer
e5c09aa verified
Raw
History Blame Contribute Delete
9.37 kB
# Copyright 2025 the LlamaFactory team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import glob
import shutil
import fire
import torch
from peft import PeftModel
from transformers import AutoModel, AutoProcessor
from transformers.models.qwen2_5_omni.modeling_qwen2_5_omni import (
Qwen2_5OmniThinkerForConditionalGeneration,
)
def merge_lora(
base_model_path: str,
lora_checkpoint_path: str,
extra_file: str = "spk_dict.pt",
submodule_name: str = "thinker",
save_path: str = "./merged_model_checkpoint",
):
"""Load the original model, tokenizer, and processor configuration, merge the LoRA weights.
For a specified submodule, and save the final merged model along with its configurations.
Args:
base_model_path (str): Path to the original model directory.
lora_checkpoint_path (str): Path to the directory containing LoRA weights.
extra_file (str): Name of the extra file to be copied (default: "spk_dict.pt").
submodule_name (str): Name of the submodule to merge (default: "thinker").
save_path (str): Directory where the merged model and configurations will be saved.
"""
# 1. Load the original model, tokenizer, and processor
model = AutoModel.from_pretrained(base_model_path, torch_dtype="auto", device_map="cpu")
processor = AutoProcessor.from_pretrained(base_model_path)
print("Successfully loaded the original model and tokenizer.")
# 2. Extract the submodule to be merged (e.g., model.thinker)
if not hasattr(model, submodule_name):
raise AttributeError(f"The model does not have a submodule named '{submodule_name}'.")
base_submodule = getattr(model, submodule_name)
print(f"Successfully extracted submodule: {submodule_name}.")
# 3. Load the LoRA weights onto the extracted submodule
lora_model = PeftModel.from_pretrained(base_submodule, lora_checkpoint_path)
print("LoRA weights loaded successfully.")
# 4. Merge the LoRA weights into the submodule and unload the LoRA modules
merged_submodule = lora_model.merge_and_unload()
print("LoRA weights merged successfully.")
# 5. Replace the original submodule with the merged submodule in the model
setattr(model, submodule_name, merged_submodule)
# 6. Save the final merged model along with the tokenizer and processor configuration
model.save_pretrained(save_path)
processor.save_pretrained(save_path)
print(f"Merged model and tokenizer saved to {save_path}.")
source_file = os.path.join(base_model_path, extra_file)
target_file = os.path.join(save_path, extra_file)
if os.path.exists(source_file):
shutil.copy(source_file, target_file)
print(f"File '{extra_file}' copied from {base_model_path} to {save_path}.")
else:
print(f"File '{extra_file}' not found in {base_model_path}, skipping copy.")
# ------------------------------
# 改进后的 save_full(适用于 full+ZeRO-3)
# ------------------------------
def _ensure_gate_exists(thinker):
"""若类定义默认不带 gate_*,先补挂空模块,便于后续从分片回填权重。"""
if hasattr(thinker, "gate_head") and hasattr(thinker, "gate_mixer"):
return
H = thinker.config.get_text_config().hidden_size
device = next(thinker.parameters()).device
dtype = next(thinker.parameters()).dtype
class GateMixer(torch.nn.Module):
def __init__(self, K, device, dtype):
super().__init__()
self.logits = torch.nn.Parameter(torch.zeros(K, device=device, dtype=dtype))
def weights(self): return torch.softmax(self.logits, dim=0)
layer_ids = getattr(thinker.config, "gate_layer_ids", [-4, -3, -2, -1])
K = len(layer_ids)
thinker.gate_head = torch.nn.Linear(H, 1, bias=True).to(device=device, dtype=dtype)
thinker.gate_mixer = GateMixer(K=K, device=device, dtype=dtype)
thinker.gate_layer_ids = layer_ids
def _try_load_gate_from_shards(thinker, path):
"""从 safetensors 分片/单文件中抽取 gate_* 权重并加载到 thinker(若存在)。"""
try:
from safetensors import safe_open
except Exception:
print("⚠️ safetensors 未安装或不可用,跳过从分片回填 gate_*。")
return
# 收集可能存在的 safetensors 文件:分片或单体
files = []
single = os.path.join(path, "model.safetensors")
if os.path.exists(single):
files.append(single)
else:
files.extend(sorted(glob.glob(os.path.join(path, "model-*.safetensors"))))
if not files:
print("⚠️ 未发现 safetensors 权重文件,跳过 gate_* 回填(from_pretrained 可能已加载)。")
return
head_sd, mixer_sd = {}, {}
for fp in files:
with safe_open(fp, framework="pt", device="cpu") as f:
for k in f.keys():
if k.startswith("gate_head."):
head_sd[k.split("gate_head.", 1)[1]] = f.get_tensor(k)
elif k.startswith("gate_mixer."):
mixer_sd[k.split("gate_mixer.", 1)[1]] = f.get_tensor(k)
elif k.startswith("thinker.gate_head."):
head_sd[k.split("thinker.gate_head.", 1)[1]] = f.get_tensor(k)
elif k.startswith("thinker.gate_mixer."):
mixer_sd[k.split("thinker.gate_mixer.", 1)[1]] = f.get_tensor(k)
if head_sd:
thinker.gate_head.load_state_dict(head_sd, strict=False)
if mixer_sd:
thinker.gate_mixer.load_state_dict(mixer_sd, strict=False)
if head_sd or mixer_sd:
print("✅ 从分片/单体 safetensors 回填 gate_head / gate_mixer 完成")
else:
print("ℹ️ 未在分片中找到 gate_* 权重键,可能已由 from_pretrained 自动加载。")
def save_full_model(
saved_thinker_path: str,
base_model_path: str,
save_path: str = "./merged_model_checkpoint",
extra_file: str = "spk_dict.pt",
):
"""加载你 full 训练得到的 thinker(包含新增 gate_*),替换 base 顶层的 thinker,并整体保存。"""
# 1) 加载 full finetune 产出的 thinker
print("加载 thinker(full finetune 权重)...")
thinker = Qwen2_5OmniThinkerForConditionalGeneration.from_pretrained(
saved_thinker_path,
torch_dtype="auto",
device_map="cpu",
trust_remote_code=True, # Qwen Omni 需要
)
# 2) 兜底:若类定义默认无 gate_*,先补挂后尝试从 safetensors 回填
_ensure_gate_exists(thinker)
_try_load_gate_from_shards(thinker, saved_thinker_path)
# 3) 加载顶层 base 模型,替换其 thinker
print("加载 base 顶层模型...")
base_model = AutoModel.from_pretrained(
base_model_path,
torch_dtype="auto",
device_map="cpu",
trust_remote_code=True, # 顶层同样需要
)
if not hasattr(base_model, "thinker"):
raise AttributeError("base 模型不包含 'thinker' 子模块,检查 base_model_path 是否为 Qwen2.5-Omni 顶层。")
base_model.thinker = thinker
# 4) 处理器沿用 base(若未改 tokenizer/processor,这是最稳)
processor = AutoProcessor.from_pretrained(base_model_path, trust_remote_code=True)
# 5) 保存(safetensors 分片)
os.makedirs(save_path, exist_ok=True)
base_model.save_pretrained(
save_path,
safe_serialization=True,
max_shard_size="10GB",
)
processor.save_pretrained(save_path)
print(f"✅ 模型与处理器已保存至 {save_path}")
# 6) 复制可选文件
src = os.path.join(base_model_path, extra_file)
dst = os.path.join(save_path, extra_file)
if os.path.exists(src):
shutil.copy(src, dst)
print(f"已复制额外文件 '{extra_file}' 到保存目录。")
else:
print(f"未在 base_model_path 找到 '{extra_file}',跳过复制。")
# 7) 简单自检:索引文件中是否包含 gate_* 关键词
index_fp = os.path.join(save_path, "model.safetensors.index.json")
if os.path.exists(index_fp):
try:
text = open(index_fp, "r", encoding="utf-8").read()
hit = ("thinker.gate_head." in text) or ("thinker.gate_mixer." in text) or \
("gate_head." in text) or ("gate_mixer." in text)
print(f"自检:索引中 gate_* 路径检测 {'✅' if hit else '⚠️ 未检出(可能键名不同,建议实际加载验证)'}")
except Exception as e:
print(f"索引自检跳过:{e}")
if __name__ == "__main__":
fire.Fire({"save_full": save_full_model, "merge_lora": merge_lora})