|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| """
|
|
|
| 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.")
|
|
|
|
|
| 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}.")
|
|
|
|
|
| lora_model = PeftModel.from_pretrained(base_submodule, lora_checkpoint_path)
|
| print("LoRA weights loaded successfully.")
|
|
|
|
|
| merged_submodule = lora_model.merge_and_unload()
|
| print("LoRA weights merged successfully.")
|
|
|
|
|
| setattr(model, submodule_name, merged_submodule)
|
|
|
|
|
| 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.")
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
| 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,并整体保存。"""
|
|
|
| print("加载 thinker(full finetune 权重)...")
|
| thinker = Qwen2_5OmniThinkerForConditionalGeneration.from_pretrained(
|
| saved_thinker_path,
|
| torch_dtype="auto",
|
| device_map="cpu",
|
| trust_remote_code=True,
|
| )
|
|
|
|
|
| _ensure_gate_exists(thinker)
|
| _try_load_gate_from_shards(thinker, saved_thinker_path)
|
|
|
|
|
| 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
|
|
|
|
|
| processor = AutoProcessor.from_pretrained(base_model_path, trust_remote_code=True)
|
|
|
|
|
| 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}")
|
|
|
|
|
| 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}',跳过复制。")
|
|
|
|
|
| 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})
|
|
|